diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1694ba3..1e89ccd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,9 +7,14 @@ on: jobs: build: strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + defaults: + run: + # One wrapper invocation everywhere: git-bash on the Windows runner runs ./mill too. + shell: bash steps: - uses: actions/checkout@v4 @@ -18,15 +23,26 @@ jobs: distribution: temurin java-version: '21' + - uses: actions/cache@v4 + with: + path: ~/.cache/mill-bun + key: mill-bun-${{ runner.os }}-${{ hashFiles('millbun/src/mill/bun/BunToolchainModule.scala') }} + + - name: Select Mill launcher + # The sh wrapper fetches Mill's native launcher, which has no Windows build + # ("This native mill launcher supports only Linux and macOS") — Windows goes + # through the committed mill.bat instead. + run: echo "MILL=./mill$([ "$RUNNER_OS" = "Windows" ] && echo .bat)" >> "$GITHUB_ENV" + - uses: oven-sh/setup-bun@v2 with: bun-version: 1.4.0 - name: Compile - run: ./mill --no-server millbun.compile + run: $MILL --no-server millbun.compile - name: Unit Tests - run: ./mill --no-server millbun.test + run: $MILL --no-server millbun.test # The suite defaults to the managed toolchain; this job pins system Bun to the same # version so `findOnPath` resolution and version verification keep CI coverage, while @@ -34,13 +50,17 @@ jobs: - name: Integration Tests env: MILL_BUN_USE_SYSTEM: 'true' - run: ./mill --no-server millbun.integration + run: $MILL --no-server millbun.integration managed-bun: strategy: + fail-fast: false matrix: - os: [ubuntu-latest, macos-latest] + os: [ubuntu-latest, macos-latest, windows-latest] runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash steps: - uses: actions/checkout@v4 @@ -49,7 +69,18 @@ jobs: distribution: temurin java-version: '21' + - uses: actions/cache@v4 + with: + path: ~/.cache/mill-bun + key: mill-bun-${{ runner.os }}-${{ hashFiles('millbun/src/mill/bun/BunToolchainModule.scala') }} + + - name: Select Mill launcher + # The sh wrapper fetches Mill's native launcher, which has no Windows build + # ("This native mill launcher supports only Linux and macOS") — Windows goes + # through the committed mill.bat instead. + run: echo "MILL=./mill$([ "$RUNNER_OS" = "Windows" ] && echo .bat)" >> "$GITHUB_ENV" + - name: Managed Bun smoke test env: MILL_BUN_USE_SYSTEM: 'false' - run: ./mill --no-server millbun.integration.testOnly mill.bun.BunManagedToolchainIntegrationTests + run: $MILL --no-server millbun.integration.testOnly mill.bun.BunManagedToolchainIntegrationTests diff --git a/CHANGELOG.md b/CHANGELOG.md index 1418fd4..281e213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `BunWorkspaceModule` for one install and lockfile across mixed Scala.js/TypeScript packages. - Published dependency manifest schema v2 with runtime, optional, and peer requirements. - `bunDoctor` diagnostics and managed-toolchain CI smoke coverage. +- A committed lockfile written by a newer Bun fails frozen installs with regeneration guidance, + instead of surfacing bun's raw `UnknownLockfileVersion`. ### Changed @@ -29,6 +31,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Development dependencies are local tooling inputs and are no longer published transitively. - `bunPackageJsonExtras` rejects dependency fields now represented by typed settings. - Missing dependency versions are represented as `latest` instead of an empty package.json value. +- TypeScript `bunBundleFormat` is `Option[String]`, matching Scala.js; `None` lets `bun build` infer. +- `unmanagedDeps` entries are staged into `vendor/` and declared as `file:./vendor/` + dependencies, so local packages install under frozen lockfiles and locks stay portable. +- The TypeScript install task is canonically `bunInstall`; Mill's inherited `npmInstall` delegates to it. +- `bunToolEnv` is defined once on `BunToolchainModule` for all toolchain subprocesses, and the + TypeScript `bunRuntimeEnv` is public. ### Deprecated @@ -36,6 +44,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `bunOptionalDeps` in favor of `npmOptionalDeps`. - `managedBunExecutable` in favor of `bunExecutableOverride`. - The TypeScript `bunCompileExecutable: Boolean` switch in favor of the `compileExecutable` task. +- The TypeScript test command `test` and the Scala.js test command `bunTest`, both in favor of `testForked`. ## [0.2.1] - Overridable test-time JS env (2026-04-17) diff --git a/README.md b/README.md index aefe80d..eef6df6 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ Scala.js versions are explicit: choose the version your application tests agains ./mill app.run ./mill app.bundle ./mill app.compileExecutable -./mill app.test.test +./mill app.test.testForked ``` `BunScalaJSModule` delegates `fastLinkJS`, `fullLinkJS`, and test linking to Mill's standard `ScalaJSModule` hooks. That keeps the plugin compatible with Mill's linker lifecycle and removes its former private linker-worker coupling. @@ -76,7 +76,7 @@ object app extends BunTypeScriptModule { ./mill app.run ./mill app.bundle ./mill app.compileExecutable -./mill app.test.test +./mill app.test.testForked ``` ## Web applications diff --git a/build.mill b/build.mill index 0219c36..ff46997 100644 --- a/build.mill +++ b/build.mill @@ -41,8 +41,10 @@ object millbun extends ScalaModule, PublishModule { // pins rather than whatever the developer happens to have on PATH. The shared // download cache means all fixtures reuse one archive. Export MILL_BUN_USE_SYSTEM=true // to run against a local Bun instead. - "MILL_BUN_USE_SYSTEM" -> Task.env.getOrElse("MILL_BUN_USE_SYSTEM", "false"), - "MILL_BUN_REQUIRE_LOCKFILE" -> Task.env.getOrElse("MILL_BUN_REQUIRE_LOCKFILE", "false") + "MILL_BUN_USE_SYSTEM" -> Task.env.getOrElse("MILL_BUN_USE_SYSTEM", "false") + // The lockfile requirement runs the SHIPPED default (strict): dependency-bearing + // fixtures commit a bun.lock, regenerated via mill.bun.RegenerateFixtureLocks, and + // fixtures that deliberately test the relaxed path opt out in their own build.mill. ) } diff --git a/docs/MIGRATING-0.3.md b/docs/MIGRATING-0.3.md index e38fa53..6dcd494 100644 --- a/docs/MIGRATING-0.3.md +++ b/docs/MIGRATING-0.3.md @@ -29,6 +29,25 @@ Subsequent installs are frozen. During a staged migration only, set `MILL_BUN_RE Use `npmDeps`, `npmDevDeps`, `npmOptionalDeps`, `npmPeerDeps`, and `npmOverrides`. `bunPackageJsonExtras` remains available for unmodeled fields such as `scripts`, but now rejects dependency sections so task invalidation and published metadata remain correct. +### `bunBundleFormat` is `Option[String]` on TypeScript modules + +Scala.js and TypeScript previously disagreed on this member's type — `Option[String]` versus +`String` — which no deprecation alias can bridge, so 0.3 takes the one-time break. Both are now +`T[Option[String]]`; `None` lets `bun build` infer the format. + +```scala +// 0.2 +override def bunBundleFormat = Task { "esm" } +// 0.3 +override def bunBundleFormat = Task { Some("esm") } +``` + +### Local packages via `unmanagedDeps` must be directories with a `package.json` + +Entries are staged into `vendor/` beside the generated package.json and declared as +`file:./vendor/` dependencies, so they now work under frozen lockfile installs and the +lockfile stays independent of the checkout path. Tarballs are no longer accepted — unpack them. + ## Renamed APIs | 0.2 name | 0.3 name | Status | @@ -39,9 +58,17 @@ Use `npmDeps`, `npmDevDeps`, `npmOptionalDeps`, `npmPeerDeps`, and `npmOverrides | `bunCompileExecutables` | `compileExecutables` | Compatibility alias retained | | `bunOptionalDeps` | `npmOptionalDeps` | Deprecated compatibility setting | | `managedBunExecutable` | `bunExecutableOverride` | Deprecated compatibility setting | +| `npmInstall` (TypeScript) | `bunInstall` | Mill's inherited name delegates to `bunInstall` and stays usable | +| `test` (TypeScript test modules) | `testForked` | Deprecated compatibility command | +| `bunTest` (Scala.js test modules) | `testForked` (inherited from Mill) | Deprecated compatibility command | The old names are planned for removal at 1.0. +Environment hooks share one vocabulary: `bunToolEnv` (toolchain subprocesses — install, lock, +build) is defined on `BunToolchainModule` for every module kind, and the TypeScript +`bunRuntimeEnv` (program and test processes) is now public. Scala.js keeps `bunJsEnv` / +`bunJsEnvArgs` / `bunTestJsEnv` for its Scala.js-test JS environment, unchanged. + ## Toolchain behavior The default is now checksum-verified managed Bun 1.4.0. To preserve the old PATH behavior: diff --git a/examples/build.mill b/examples/build.mill index a904533..12e20a9 100644 --- a/examples/build.mill +++ b/examples/build.mill @@ -79,7 +79,7 @@ object fullstack extends Module { // No moduleDir or sources overrides needed. // // ./mill tsLib.compile # TypeScript type-check -// ./mill tsLib.test.test # Bun-native tests +// ./mill tsLib.test.testForked # Bun-native tests // ./mill client.bundle # TS browser bundle (imports tsLib) // ./mill scalaClient.bundle # Scala.js browser bundle // ./mill backend.run # JVM Cask server on :8080 diff --git a/mill.bat b/mill.bat new file mode 100644 index 0000000..948392f --- /dev/null +++ b/mill.bat @@ -0,0 +1,296 @@ +@echo off + +setlocal enabledelayedexpansion + +if [!DEFAULT_MILL_VERSION!]==[] ( set "DEFAULT_MILL_VERSION=1.1.5" ) + +if [!MILL_GITHUB_RELEASE_CDN!]==[] ( set "MILL_GITHUB_RELEASE_CDN=" ) + +if [!MILL_MAIN_CLI!]==[] ( set "MILL_MAIN_CLI=%~f0" ) + +set "MILL_REPO_URL=https://github.com/com-lihaoyi/mill" + +SET MILL_BUILD_SCRIPT= + +if exist "build.mill" ( + set MILL_BUILD_SCRIPT=build.mill +) else ( + if exist "build.mill.scala" ( + set MILL_BUILD_SCRIPT=build.mill.scala + ) else ( + if exist "build.sc" ( + set MILL_BUILD_SCRIPT=build.sc + ) else ( + rem no-op + ) + ) +) + +if [!MILL_VERSION!]==[] ( + if exist .mill-version ( + set /p MILL_VERSION=<.mill-version + ) else ( + if exist .config\mill-version ( + set /p MILL_VERSION=<.config\mill-version + ) else ( + rem Determine which config file to use for version extraction + set "MILL_VERSION_CONFIG_FILE=" + set "MILL_VERSION_SEARCH_PATTERN=" + + if exist build.mill.yaml ( + set "MILL_VERSION_CONFIG_FILE=build.mill.yaml" + set "MILL_VERSION_SEARCH_PATTERN=mill-version:" + ) else ( + if not "%MILL_BUILD_SCRIPT%"=="" ( + set "MILL_VERSION_CONFIG_FILE=%MILL_BUILD_SCRIPT%" + set "MILL_VERSION_SEARCH_PATTERN=//\|.*mill-version" + ) + ) + + rem Process the config file if found + if not "!MILL_VERSION_CONFIG_FILE!"=="" ( + rem Find the line and process it + for /f "tokens=*" %%a in ('findstr /R /C:"!MILL_VERSION_SEARCH_PATTERN!" "!MILL_VERSION_CONFIG_FILE!"') do ( + set "line=%%a" + + rem --- 1. Replicate sed 's/.*://' --- + rem This removes everything up to and including the first colon + set "line=!line:*:=!" + + rem --- 2. Replicate sed 's/#.*//' --- + rem Split on '#' and keep the first part + for /f "tokens=1 delims=#" %%b in ("!line!") do ( + set "line=%%b" + ) + + rem --- 3. Replicate sed 's/['"]//g' --- + rem Remove all quotes + set "line=!line:'=!" + set "line=!line:"=!" + + rem --- 4. Replicate sed's trim/space removal --- + rem Remove all space characters from the result. This is more robust. + set "MILL_VERSION=!line: =!" + + rem We found the version, so we can exit the loop + goto :version_found + ) + + :version_found + rem no-op + ) + ) + ) +) + +if [!MILL_VERSION!]==[] ( + set MILL_VERSION=%DEFAULT_MILL_VERSION% +) + +if [!MILL_FINAL_DOWNLOAD_FOLDER!]==[] set MILL_FINAL_DOWNLOAD_FOLDER=%USERPROFILE%\.cache\mill\download + +rem without bat file extension, cmd doesn't seem to be able to run it + +set "MILL_NATIVE_SUFFIX=-native" +set "MILL_JVM_SUFFIX=-jvm" +set "MILL_FULL_VERSION=%MILL_VERSION%" +set "MILL_DOWNLOAD_EXT=.bat" +set "ARTIFACT_SUFFIX=" +REM Check if MILL_VERSION contains MILL_NATIVE_SUFFIX +echo !MILL_VERSION! | findstr /C:"%MILL_NATIVE_SUFFIX%" >nul +if !errorlevel! equ 0 ( + set "MILL_VERSION=%MILL_VERSION:-native=%" + REM -native images compiled with graal do not support windows-arm + REM https://github.com/oracle/graal/issues/9215 + IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "ARTIFACT_SUFFIX=-native-windows-amd64" + set "MILL_DOWNLOAD_EXT=.exe" + ) else ( + rem no-op + ) +) else ( + echo !MILL_VERSION! | findstr /C:"%MILL_JVM_SUFFIX%" >nul + if !errorlevel! equ 0 ( + set "MILL_VERSION=%MILL_VERSION:-jvm=%" + ) else ( + set "SKIP_VERSION=false" + set "MILL_PREFIX=%MILL_VERSION:~0,4%" + if "!MILL_PREFIX!"=="0.1." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.2." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.3." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.4." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.5." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.6." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.7." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.8." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.9." set "SKIP_VERSION=true" + set "MILL_PREFIX=%MILL_VERSION:~0,5%" + if "!MILL_PREFIX!"=="0.10." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.11." set "SKIP_VERSION=true" + if "!MILL_PREFIX!"=="0.12." set "SKIP_VERSION=true" + + if "!SKIP_VERSION!"=="false" ( + IF /I NOT "%PROCESSOR_ARCHITECTURE%"=="ARM64" ( + set "ARTIFACT_SUFFIX=-native-windows-amd64" + set "MILL_DOWNLOAD_EXT=.exe" + ) + ) else ( + rem no-op + ) + ) +) + +set MILL=%MILL_FINAL_DOWNLOAD_FOLDER%\!MILL_FULL_VERSION!!MILL_DOWNLOAD_EXT! + +set MILL_RESOLVE_DOWNLOAD= + +if not exist "%MILL%" ( + set MILL_RESOLVE_DOWNLOAD=true +) else ( + if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( + set MILL_RESOLVE_DOWNLOAD=true + ) else ( + rem no-op + ) +) + + +if [!MILL_RESOLVE_DOWNLOAD!]==[true] ( + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,4% + set MILL_SHORT_VERSION_PREFIX=%MILL_VERSION:~0,2% + rem Since 0.5.0 + set MILL_DOWNLOAD_SUFFIX=-assembly + rem Since 0.11.0 + set MILL_DOWNLOAD_FROM_MAVEN=1 + if [!MILL_VERSION_PREFIX!]==[0.0.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.1.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.2.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.3.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.4.] ( + set MILL_DOWNLOAD_SUFFIX= + set MILL_DOWNLOAD_FROM_MAVEN=0 + ) + if [!MILL_VERSION_PREFIX!]==[0.5.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.6.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.7.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.8.] set MILL_DOWNLOAD_FROM_MAVEN=0 + if [!MILL_VERSION_PREFIX!]==[0.9.] set MILL_DOWNLOAD_FROM_MAVEN=0 + + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% + if [!MILL_VERSION_PREFIX!]==[0.10.] set MILL_DOWNLOAD_FROM_MAVEN=0 + + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,8% + if [!MILL_VERSION_PREFIX!]==[0.11.0-M] set MILL_DOWNLOAD_FROM_MAVEN=0 + + set MILL_VERSION_PREFIX=%MILL_VERSION:~0,5% + set DOWNLOAD_EXT=exe + if [!MILL_SHORT_VERSION_PREFIX!]==[0.] set DOWNLOAD_EXT=jar + if [!MILL_VERSION_PREFIX!]==[0.12.] set DOWNLOAD_EXT=exe + if [!MILL_VERSION!]==[0.12.0] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.1] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.2] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.3] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.4] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.5] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.6] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.7] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.8] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.9] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.10] set DOWNLOAD_EXT=jar + if [!MILL_VERSION!]==[0.12.11] set DOWNLOAD_EXT=jar + + set MILL_VERSION_PREFIX= + set MILL_SHORT_VERSION_PREFIX= + + for /F "delims=- tokens=1" %%A in ("!MILL_VERSION!") do set MILL_VERSION_BASE=%%A + set MILL_VERSION_MILESTONE= + for /F "delims=- tokens=2" %%A in ("!MILL_VERSION!") do set MILL_VERSION_MILESTONE=%%A + set MILL_VERSION_MILESTONE_START=!MILL_VERSION_MILESTONE:~0,1! + if [!MILL_VERSION_MILESTONE_START!]==[M] ( + set MILL_VERSION_TAG=!MILL_VERSION_BASE!-!MILL_VERSION_MILESTONE! + ) else ( + set MILL_VERSION_TAG=!MILL_VERSION_BASE! + ) + if [!MILL_DOWNLOAD_FROM_MAVEN!]==[1] ( + set MILL_DOWNLOAD_URL=https://repo1.maven.org/maven2/com/lihaoyi/mill-dist!ARTIFACT_SUFFIX!/!MILL_VERSION!/mill-dist!ARTIFACT_SUFFIX!-!MILL_VERSION!.!DOWNLOAD_EXT! + ) else ( + set MILL_DOWNLOAD_URL=!MILL_GITHUB_RELEASE_CDN!%MILL_REPO_URL%/releases/download/!MILL_VERSION_TAG!/!MILL_VERSION!!MILL_DOWNLOAD_SUFFIX! + ) + + if defined MILL_TEST_DRY_RUN_LAUNCHER_SCRIPT ( + echo !MILL_DOWNLOAD_URL! + echo !MILL! + exit /b 0 + ) + + rem there seems to be no way to generate a unique temporary file path (on native Windows) + if defined MILL_OUTPUT_DIR ( + set MILL_TEMP_DOWNLOAD_FILE=%MILL_OUTPUT_DIR%\mill-temp-download + if not exist "%MILL_OUTPUT_DIR%" mkdir "%MILL_OUTPUT_DIR%" + ) else ( + set MILL_TEMP_DOWNLOAD_FILE=out\mill-bootstrap-download + if not exist "out" mkdir "out" + ) + + echo Downloading mill !MILL_VERSION! from !MILL_DOWNLOAD_URL! ... 1>&2 + + curl -f -L "!MILL_DOWNLOAD_URL!" -o "!MILL_TEMP_DOWNLOAD_FILE!" + + if not exist "%MILL_FINAL_DOWNLOAD_FOLDER%" mkdir "%MILL_FINAL_DOWNLOAD_FOLDER%" + move /y "!MILL_TEMP_DOWNLOAD_FILE!" "%MILL%" + + set MILL_TEMP_DOWNLOAD_FILE= + set MILL_DOWNLOAD_SUFFIX= +) + +set MILL_FINAL_DOWNLOAD_FOLDER= +set MILL_VERSION= +set MILL_REPO_URL= + +rem Need to preserve the first position of those listed options +set MILL_FIRST_ARG= +if [%~1%]==[--bsp] ( + set MILL_FIRST_ARG=%1% +) else ( + if [%~1%]==[-i] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--interactive] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--no-server] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--no-daemon] ( + set MILL_FIRST_ARG=%1% + ) else ( + if [%~1%]==[--help] ( + set MILL_FIRST_ARG=%1% + ) + ) + ) + ) + ) +) +set "MILL_PARAMS=%*%" + +if not [!MILL_FIRST_ARG!]==[] ( + for /f "tokens=1*" %%a in ("%*") do ( + set "MILL_PARAMS=%%b" + ) +) + +rem -D mill.main.cli is for compatibility with Mill 0.10.9 - 0.13.0-M2 +"%MILL%" %MILL_FIRST_ARG% -D "mill.main.cli=%MILL_MAIN_CLI%" %MILL_PARAMS% diff --git a/millbun/integration/resources/scalajs-bundle/bun.lock b/millbun/integration/resources/scalajs-bundle/bun.lock new file mode 100644 index 0000000..5b62cfe --- /dev/null +++ b/millbun/integration/resources/scalajs-bundle/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "dependencies": { + "lodash": "4.17.21", + }, + }, + }, + "packages": { + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + } +} diff --git a/millbun/integration/resources/scalajs-dependency-manifests/build.mill b/millbun/integration/resources/scalajs-dependency-manifests/build.mill index d9b0660..1646f15 100644 --- a/millbun/integration/resources/scalajs-dependency-manifests/build.mill +++ b/millbun/integration/resources/scalajs-dependency-manifests/build.mill @@ -17,6 +17,8 @@ trait StubBunModule extends BunScalaJSModule { def scalaJSVersion = "1.22.0" override def moduleKind = Task { ModuleKind.CommonJSModule } override def bunExecutable = Task { "stub-bun" } + // The stub never produces a real lockfile; the strict path is covered by real-bun fixtures. + override def bunRequireLockfile = Task { false } override protected def runBun( bunExe: String, diff --git a/millbun/integration/resources/scalajs-transitive/build.mill b/millbun/integration/resources/scalajs-transitive/build.mill index 059a350..1cbbf76 100644 --- a/millbun/integration/resources/scalajs-transitive/build.mill +++ b/millbun/integration/resources/scalajs-transitive/build.mill @@ -10,13 +10,13 @@ import mill.scalajslib.* import mill.scalajslib.api.* import mill.scalajslib.bun.* +// lib keeps its own moduleDir (lib/): sharing the root with app would make both modules +// resolve the same bun.lock, and only one dependency set can be recorded there. object lib extends BunScalaJSModule { - override def moduleDir = build.moduleDir def scalaVersion = "3.8.2" def scalaJSVersion = "1.22.0" override def moduleKind = Task { ModuleKind.ESModule } - override def sources = Task.Sources(moduleDir / "lib") override def npmDeps = Task { Seq("lodash@4.17.21") } } diff --git a/millbun/integration/resources/scalajs-transitive/bun.lock b/millbun/integration/resources/scalajs-transitive/bun.lock new file mode 100644 index 0000000..5b62cfe --- /dev/null +++ b/millbun/integration/resources/scalajs-transitive/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "dependencies": { + "lodash": "4.17.21", + }, + }, + }, + "packages": { + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + } +} diff --git a/millbun/integration/resources/scalajs-transitive/lib/LibWords.scala b/millbun/integration/resources/scalajs-transitive/lib/src/LibWords.scala similarity index 100% rename from millbun/integration/resources/scalajs-transitive/lib/LibWords.scala rename to millbun/integration/resources/scalajs-transitive/lib/src/LibWords.scala diff --git a/millbun/integration/resources/scalajs-web/bun.lock b/millbun/integration/resources/scalajs-web/bun.lock new file mode 100644 index 0000000..5b62cfe --- /dev/null +++ b/millbun/integration/resources/scalajs-web/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "dependencies": { + "lodash": "4.17.21", + }, + }, + }, + "packages": { + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + } +} diff --git a/millbun/integration/resources/typescript-browser/bun.lock b/millbun/integration/resources/typescript-browser/bun.lock new file mode 100644 index 0000000..23524fb --- /dev/null +++ b/millbun/integration/resources/typescript-browser/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + } +} diff --git a/millbun/integration/resources/typescript-bundle/build.mill b/millbun/integration/resources/typescript-bundle/build.mill index 02c1ede..3b296da 100644 --- a/millbun/integration/resources/typescript-bundle/build.mill +++ b/millbun/integration/resources/typescript-bundle/build.mill @@ -18,5 +18,5 @@ object app extends BunTypeScriptModule { ) } override def bunBundleTarget = Task { "bun" } - override def bunBundleFormat = Task { "esm" } + override def bunBundleFormat = Task { Some("esm") } } diff --git a/millbun/integration/resources/typescript-bundle/bun.lock b/millbun/integration/resources/typescript-bundle/bun.lock new file mode 100644 index 0000000..4c189bf --- /dev/null +++ b/millbun/integration/resources/typescript-bundle/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-bunfig/app/bun.lock b/millbun/integration/resources/typescript-bunfig/app/bun.lock new file mode 100644 index 0000000..4c189bf --- /dev/null +++ b/millbun/integration/resources/typescript-bunfig/app/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-compile/bun.lock b/millbun/integration/resources/typescript-compile/bun.lock new file mode 100644 index 0000000..4c189bf --- /dev/null +++ b/millbun/integration/resources/typescript-compile/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-env/build.mill b/millbun/integration/resources/typescript-env/build.mill index 22ed549..686ac00 100644 --- a/millbun/integration/resources/typescript-env/build.mill +++ b/millbun/integration/resources/typescript-env/build.mill @@ -11,7 +11,10 @@ import mill.javascriptlib.bun.* object app extends BunTypeScriptModule { override def moduleDir = build.moduleDir override def npmDevDeps = Task { Seq("typescript@5.7.3") } - def bunProxy = Task.Source(moduleDir / "bun-proxy") + // The sh proxy cannot execute on Windows, so the fixture carries a .cmd twin. + def bunProxy = Task.Source( + if (scala.util.Properties.isWin) moduleDir / "bun-proxy.cmd" else moduleDir / "bun-proxy" + ) override def managedBunExecutable = Task { Some(PathRef(bunProxy().path)) } // The proxy execs whatever Bun is on PATH, so its --version reports the developer's Bun rather // than bunVersion. This fixture tests env propagation, not the version pin. diff --git a/millbun/integration/resources/typescript-env/bun-proxy.cmd b/millbun/integration/resources/typescript-env/bun-proxy.cmd new file mode 100644 index 0000000..3e105e3 --- /dev/null +++ b/millbun/integration/resources/typescript-env/bun-proxy.cmd @@ -0,0 +1,9 @@ +@echo off +setlocal +set "FIRST=%~1" +if "%FIRST%"=="" set "FIRST=unknown" +set "MARKER=%BUN_PROXY_MARKER%" +if "%MARKER%"=="" set "MARKER=missing" +>> "%CD%\.bun-env-log" echo %FIRST%:%MARKER% +bun %* +exit /b %ERRORLEVEL% diff --git a/millbun/integration/resources/typescript-env/bun.lock b/millbun/integration/resources/typescript-env/bun.lock new file mode 100644 index 0000000..a7b8336 --- /dev/null +++ b/millbun/integration/resources/typescript-env/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-simple/bun.lock b/millbun/integration/resources/typescript-simple/bun.lock new file mode 100644 index 0000000..4c189bf --- /dev/null +++ b/millbun/integration/resources/typescript-simple/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-stale-lock/build.mill b/millbun/integration/resources/typescript-stale-lock/build.mill new file mode 100644 index 0000000..b60587c --- /dev/null +++ b/millbun/integration/resources/typescript-stale-lock/build.mill @@ -0,0 +1,30 @@ +//| mill-version: 1.1.5 +//| mill-jvm-version: system +//| mvnDeps: +//| - com.tjclp::mill-bun_mill1:0.0.0-NIGHTLY + +package build + +import mill.* +import mill.javascriptlib.bun.* + +// Pins a Bun that cannot read the committed lockfileVersion (1.3.14 reads only v1; the lock +// says v2). The install must fail with regeneration guidance before bun ever runs — the stub +// proves no subprocess is reached. +object app extends BunTypeScriptModule { + override def moduleDir = build.moduleDir + override def bunVersion = Task { "1.3.14" } + override def bunVerifyVersion = Task { false } + override def bunExecutable = Task { "stub-bun" } + override def npmDeps = Task { Seq("is-even@1.0.0") } + + override protected def runBun( + bunExe: String, + args: Seq[String], + cwd: os.Path, + env: Map[String, String] + ): os.CommandResult = { + os.write.over(cwd / ".stub-bun-ran", args.mkString(" ")) + os.call(Seq("true"), cwd = cwd, env = env) + } +} diff --git a/millbun/integration/resources/typescript-stale-lock/bun.lock b/millbun/integration/resources/typescript-stale-lock/bun.lock new file mode 100644 index 0000000..4eed9da --- /dev/null +++ b/millbun/integration/resources/typescript-stale-lock/bun.lock @@ -0,0 +1,11 @@ +{ + "lockfileVersion": 2, + "workspaces": { + "": { + "name": "app", + "dependencies": { + "is-even": "1.0.0", + }, + }, + }, +} diff --git a/millbun/integration/resources/typescript-stale-lock/src/main.ts b/millbun/integration/resources/typescript-stale-lock/src/main.ts new file mode 100644 index 0000000..8b179f2 --- /dev/null +++ b/millbun/integration/resources/typescript-stale-lock/src/main.ts @@ -0,0 +1 @@ +console.log("never installs"); diff --git a/millbun/integration/resources/typescript-tests/bun.lock b/millbun/integration/resources/typescript-tests/bun.lock new file mode 100644 index 0000000..4c189bf --- /dev/null +++ b/millbun/integration/resources/typescript-tests/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-tsx/bun.lock b/millbun/integration/resources/typescript-tsx/bun.lock new file mode 100644 index 0000000..4c189bf --- /dev/null +++ b/millbun/integration/resources/typescript-tsx/bun.lock @@ -0,0 +1,24 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "@types/bun": "1.4.0", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/resources/typescript-web/bun.lock b/millbun/integration/resources/typescript-web/bun.lock new file mode 100644 index 0000000..23524fb --- /dev/null +++ b/millbun/integration/resources/typescript-web/bun.lock @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "devDependencies": { + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + } +} diff --git a/millbun/integration/resources/typescript-workers/bun.lock b/millbun/integration/resources/typescript-workers/bun.lock new file mode 100644 index 0000000..1f06ab3 --- /dev/null +++ b/millbun/integration/resources/typescript-workers/bun.lock @@ -0,0 +1,32 @@ +{ + "lockfileVersion": 2, + "configVersion": 1, + "workspaces": { + "": { + "name": "app", + "dependencies": { + "lodash": "4.17.21", + }, + "devDependencies": { + "@types/bun": "1.4.0", + "@types/lodash": "4.17.20", + "typescript": "5.7.3", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.4.0", "", { "dependencies": { "bun-types": "1.4.0" } }, "sha512-K+lZULY23vRgK/CfTjFIV+tyifaNdSMlPh9j+6mQ/cLfpOznLyAuzgV/JQysyECpkBQLVMSyvjlr2fBUSA9wFQ=="], + + "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], + + "@types/node": ["@types/node@26.3.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-L3fgrnchriRC2ExBflb8j4uZZURHZfQsmQeyVzhjcHW4kkwVyo8/0h1B2MVzMTrYUJYu6G7EWs14hW/L9putqw=="], + + "bun-types": ["bun-types@1.4.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-iIKw23BspnQQYd3prITOBxeUsxBHnwzX6YJfGMuNOZzeNcMmVqzIIVGRm1l69ogaPQmb4wB6BN8mA5bE9YuC5Q=="], + + "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], + } +} diff --git a/millbun/integration/src/mill/bun/BunScalaJSIntegrationTests.scala b/millbun/integration/src/mill/bun/BunScalaJSIntegrationTests.scala index da59704..0836c27 100644 --- a/millbun/integration/src/mill/bun/BunScalaJSIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunScalaJSIntegrationTests.scala @@ -123,10 +123,12 @@ object BunScalaJSIntegrationTests extends TestSuite { assert(runBundledScript(mainJs) == "Hello from transitive scala.js bun") } - test("bunTest") { + test("testForked runs Scala.js tests on Bun") { val tester = this.tester("scalajs-test") - val res = tester.eval("app.test.bunTest") + val res = tester.eval("app.test.testForked") assert(res.isSuccess) + // The deprecated alias must keep resolving until removal. + assert(tester.eval("app.test.bunTest").isSuccess) } test("bunfig propagates to Scala.js workspaces without leaking .npmrc") { @@ -150,7 +152,7 @@ object BunScalaJSIntegrationTests extends TestSuite { assert(os.exists(compileWorkspace / "bunfig.toml")) assert(!os.exists(compileWorkspace / ".npmrc")) - val testRes = tester.eval("app.test.bunTest") + val testRes = tester.eval("app.test.testForked") assert(testRes.isSuccess) val testRoot = tester.workspacePath / "out" / "app" / "test" assert(os.exists(testRoot)) diff --git a/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala b/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala index f4854a8..d4d2bcc 100644 --- a/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala @@ -86,25 +86,37 @@ object BunTypeScriptIntegrationTests extends TestSuite { test("strict installs require a source lock and bunLock creates it") { val tester = this.tester("typescript-lock") - val missingLock = tester.eval("app.npmInstall") + val missingLock = tester.eval("app.bunInstall") assert(!missingLock.isSuccess) val lockResult = tester.eval("app.bunLock") assert(lockResult.isSuccess) assert(os.exists(tester.workspacePath / "bun.lock")) - val installResult = tester.eval("app.npmInstall") + val installResult = tester.eval("app.bunInstall") assert(installResult.isSuccess) - val args = os.read(tester.workspacePath / "out" / "app" / "npmInstall.dest" / ".bun-args") + val args = os.read(tester.workspacePath / "out" / "app" / "bunInstall.dest" / ".bun-args") assert(args.contains("--frozen-lockfile")) } + test("a lockfile from a newer Bun fails with regeneration guidance") { + // bun.lock is forward- but not backward-compatible; bun's own failure is a raw + // UnknownLockfileVersion that never mentions bunLock. The fixture's stub proves the + // guard fires before any bun subprocess is reached. + val tester = this.tester("typescript-stale-lock") + val res = tester.eval("app.bunInstall") + assert(!res.isSuccess) + // The message text is asserted at unit level (lockfileSkewError); in-memory evals expose + // no readable error stream, so here the contract is: fail, and never reach bun. + assert(!os.exists(tester.workspacePath / "out" / "app" / "bunInstall.dest" / ".stub-bun-ran")) + } + test("bun target ambient types are pinned") { val tester = this.tester("typescript-simple") - val res = tester.eval("app.npmInstall") + val res = tester.eval("app.bunInstall") assert(res.isSuccess) - val packageJson = ujson.read(os.read(tester.workspacePath / "out" / "app" / "npmInstall.dest" / "package.json")) + val packageJson = ujson.read(os.read(tester.workspacePath / "out" / "app" / "bunInstall.dest" / "package.json")) val devDeps = packageJson("devDependencies").obj // Pinned, and pinned to the Bun we ship: @types/bun is published in lockstep with Bun. @@ -118,7 +130,7 @@ object BunTypeScriptIntegrationTests extends TestSuite { val res = tester.eval("app.compile") assert(res.isSuccess) - val packageJson = ujson.read(os.read(tester.workspacePath / "out" / "app" / "npmInstall.dest" / "package.json")) + val packageJson = ujson.read(os.read(tester.workspacePath / "out" / "app" / "bunInstall.dest" / "package.json")) val devDeps = packageJson("devDependencies").obj assert(devDeps("typescript").str == "5.7.3") @@ -128,8 +140,12 @@ object BunTypeScriptIntegrationTests extends TestSuite { test("bun test module") { val tester = this.tester("typescript-tests") - val res = tester.eval("app.test.test") + val res = tester.eval("app.test.testForked") assert(res.isSuccess) + // Deprecated and inherited aliases must keep resolving until removal. + assert(tester.eval("app.test.test").isSuccess) + assert(tester.eval("app.npmInstall").isSuccess) + assert(outputPath(tester, "app.npmInstall") == tester.workspacePath / "out" / "app" / "bunInstall.dest") } test("bundle workers") { @@ -165,7 +181,7 @@ object BunTypeScriptIntegrationTests extends TestSuite { val res = tester.eval("app.compile") assert(res.isSuccess) - val installDir = tester.workspacePath / "out" / "app" / "npmInstall.dest" + val installDir = tester.workspacePath / "out" / "app" / "bunInstall.dest" val compileDir = tester.workspacePath / "out" / "app" / "compile.dest" // Install workspace keeps both configs. @@ -183,16 +199,16 @@ object BunTypeScriptIntegrationTests extends TestSuite { assert(tester.eval("app.test.bunLock").isSuccess) // Outer module should have is-even in dependencies - val outerRes = tester.eval("app.npmInstall") + val outerRes = tester.eval("app.bunInstall") assert(outerRes.isSuccess) - val outerPkg = ujson.read(os.read(tester.workspacePath / "out" / "app" / "npmInstall.dest" / "package.json")) + val outerPkg = ujson.read(os.read(tester.workspacePath / "out" / "app" / "bunInstall.dest" / "package.json")) assert(outerPkg("dependencies").obj.contains("is-even")) assert(!outerPkg("dependencies").obj.contains("is-odd")) // Test module should have is-odd in devDependencies (not dependencies) - val testRes = tester.eval("app.test.npmInstall") + val testRes = tester.eval("app.test.bunInstall") assert(testRes.isSuccess) - val testPkg = ujson.read(os.read(tester.workspacePath / "out" / "app" / "test" / "npmInstall.dest" / "package.json")) + val testPkg = ujson.read(os.read(tester.workspacePath / "out" / "app" / "test" / "bunInstall.dest" / "package.json")) assert(testPkg("devDependencies").obj.contains("is-odd")) assert(!testPkg("dependencies").obj.contains("is-odd")) assert(!testPkg("devDependencies").obj.contains("is-even")) @@ -200,7 +216,7 @@ object BunTypeScriptIntegrationTests extends TestSuite { assert(testPkg("dependencies").obj.contains("is-even")) // Tests should actually run (both deps available) - val runRes = tester.eval("app.test.test") + val runRes = tester.eval("app.test.testForked") assert(runRes.isSuccess) } @@ -211,7 +227,7 @@ object BunTypeScriptIntegrationTests extends TestSuite { val tester = this.tester("typescript-test-deps") // Without any lock, the install refuses and names the test module's own path. - val unlocked = tester.eval("app.test.npmInstall") + val unlocked = tester.eval("app.test.bunInstall") assert(!unlocked.isSuccess) assert(tester.eval("app.bunLock").isSuccess) @@ -234,8 +250,8 @@ object BunTypeScriptIntegrationTests extends TestSuite { assert(declaredDeps(testLock).contains("is-odd")) // The frozen install now succeeds against the test module's own lock. - assert(tester.eval("app.test.npmInstall").isSuccess) - assert(tester.eval("app.test.test").isSuccess) + assert(tester.eval("app.test.bunInstall").isSuccess) + assert(tester.eval("app.test.testForked").isSuccess) } test("unmanaged local packages install under a frozen lockfile") { @@ -248,8 +264,8 @@ object BunTypeScriptIntegrationTests extends TestSuite { // The lock must not record where this repository happens to be checked out. assert(!lock.contains(tester.workspacePath.toString)) - assert(tester.eval("app.npmInstall").isSuccess) - val installed = outputPath(tester, "app.npmInstall") + assert(tester.eval("app.bunInstall").isSuccess) + val installed = outputPath(tester, "app.bunInstall") assert(os.exists(installed / "node_modules" / "local-lib" / "package.json")) assert(tester.eval("app.bundle").isSuccess) @@ -261,12 +277,12 @@ object BunTypeScriptIntegrationTests extends TestSuite { test("test modules adding nothing reuse the outer install") { // A bare test module must not demand a second lockfile. val tester = this.tester("typescript-tests") - assert(tester.eval("app.test.npmInstall").isSuccess) + assert(tester.eval("app.test.bunInstall").isSuccess) assert(!os.exists(tester.workspacePath / "test" / "bun.lock")) // And must actually reuse the outer install — not run a second one that merely succeeds // because the lockfile requirement happens to be off in this suite. - val installPath = outputPath(tester, "app.test.npmInstall") - assert(installPath == tester.workspacePath / "out" / "app" / "npmInstall.dest") + val installPath = outputPath(tester, "app.test.bunInstall") + assert(installPath == tester.workspacePath / "out" / "app" / "bunInstall.dest") } test("bunEnv") { @@ -274,7 +290,7 @@ object BunTypeScriptIntegrationTests extends TestSuite { val res = tester.eval("app.bundle") assert(res.isSuccess) - val installLog = os.read(tester.workspacePath / "out" / "app" / "npmInstall.dest" / ".bun-env-log") + val installLog = os.read(tester.workspacePath / "out" / "app" / "bunInstall.dest" / ".bun-env-log") val compileLog = os.read(tester.workspacePath / "out" / "app" / "compile.dest" / ".bun-env-log") val bundleLog = os.read(tester.workspacePath / "out" / "app" / "bundle.dest" / ".bun-env-log") diff --git a/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala b/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala index 51aee0f..6475eef 100644 --- a/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunWorkspaceIntegrationTests.scala @@ -48,8 +48,8 @@ object BunWorkspaceIntegrationTests extends TestSuite: assert(!os.read(workspaceInstall / ".workspace-installed").contains("shared-local")) val scalaResult = tester.eval("scalaApp.bunInstall") - val typescriptResult = tester.eval("typescriptApp.npmInstall") + val typescriptResult = tester.eval("typescriptApp.bunInstall") assert(scalaResult.isSuccess) assert(typescriptResult.isSuccess) assert(os.isLink(outputPath(tester, "scalaApp.bunInstall") / "node_modules")) - assert(os.isLink(outputPath(tester, "typescriptApp.npmInstall") / "node_modules")) + assert(os.isLink(outputPath(tester, "typescriptApp.bunInstall") / "node_modules")) diff --git a/millbun/integration/src/mill/bun/RegenerateFixtureLocks.scala b/millbun/integration/src/mill/bun/RegenerateFixtureLocks.scala new file mode 100644 index 0000000..4f698c5 --- /dev/null +++ b/millbun/integration/src/mill/bun/RegenerateFixtureLocks.scala @@ -0,0 +1,64 @@ +package mill.bun + +import mill.testkit.IntegrationTester +import utest.* + +/** + * Maintenance entry point, not a test: regenerates every committed fixture lockfile with the + * pinned Bun, so a dependency-default change or a `bunVersion` bump is one command instead of a + * hunt across fixtures. Without the env guard it reports what would run and does nothing. + * + * {{{ + * MILL_BUN_REGENERATE_LOCKS=1 ./mill millbun.integration.testOnly mill.bun.RegenerateFixtureLocks + * }}} + */ +object RegenerateFixtureLocks extends TestSuite: + val resourceDir: os.Path = os.Path(sys.env("MILL_WORKSPACE_ROOT")) / "millbun" / "integration" / "resources" + val millExe: os.Path = os.Path(sys.env("MILL_EXECUTABLE_PATH")) + + /** Fixture -> the bunLock commands that produce its committed lockfiles. */ + val lockedFixtures: Seq[(String, Seq[String])] = Seq( + "scalajs-bundle" -> Seq("app.bunLock"), + "scalajs-transitive" -> Seq("app.bunLock"), + "scalajs-web" -> Seq("app.bunLock"), + "typescript-browser" -> Seq("app.bunLock"), + "typescript-bundle" -> Seq("app.bunLock"), + "typescript-bunfig" -> Seq("app.bunLock"), + "typescript-compile" -> Seq("app.bunLock"), + "typescript-env" -> Seq("app.bunLock"), + "typescript-simple" -> Seq("app.bunLock"), + "typescript-tests" -> Seq("app.bunLock"), + "typescript-tsx" -> Seq("app.bunLock"), + "typescript-web" -> Seq("app.bunLock"), + "typescript-workers" -> Seq("app.bunLock") + ) + + def tests: Tests = Tests: + test("regenerate"): + if !sys.env.get("MILL_BUN_REGENERATE_LOCKS").exists(_.nonEmpty) then + println( + s"MILL_BUN_REGENERATE_LOCKS is not set; would regenerate locks for " + + s"${lockedFixtures.size} fixtures. Set it to 1 to actually rewrite them." + ) + else + lockedFixtures.foreach { case (fixture, commands) => + val tester = new IntegrationTester( + daemonMode = false, + workspaceSourcePath = resourceDir / fixture, + millExecutable = millExe, + useInMemory = true + ) + commands.foreach { command => + val result = tester.eval(command) + Predef.assert(result.isSuccess, s"$fixture: $command failed") + } + val generated = os + .walk(tester.workspacePath, skip = p => p.last == "out" || p.last == "node_modules") + .filter(_.last == "bun.lock") + Predef.assert(generated.nonEmpty, s"$fixture: no bun.lock produced") + generated.foreach { lock => + val target = resourceDir / fixture / lock.relativeTo(tester.workspacePath) + os.copy.over(lock, target, createFolders = true) + println(s"regenerated ${target.relativeTo(resourceDir)}") + } + } diff --git a/millbun/src/mill/bun/BunToolchainModule.scala b/millbun/src/mill/bun/BunToolchainModule.scala index 5cb1f34..83491bd 100644 --- a/millbun/src/mill/bun/BunToolchainModule.scala +++ b/millbun/src/mill/bun/BunToolchainModule.scala @@ -75,6 +75,33 @@ object BunToolchainModule { /** Versions with a complete bundled checksum table, for diagnostics. */ private[bun] def bundledVersions: Seq[String] = BundledChecksums.keys.toSeq.sorted + /** Highest `lockfileVersion` each bundled Bun can read. 1.4.0 writes v2; 1.3.14 fails on v2. */ + private val SupportedLockfileVersions: Map[String, Int] = Map( + "1.3.14" -> 1, + "1.4.0" -> 2 + ) + + private[bun] def supportedLockfileVersion(bunVersion: String): Option[Int] = + SupportedLockfileVersions.get(bunVersion) + + /** bun.lock is JSONC (trailing commas), so extract the version lexically rather than parsing. */ + private[bun] def lockfileVersion(lockText: String): Option[Int] = + """"lockfileVersion"\s*:\s*(\d+)""".r.findFirstMatchIn(lockText).map(_.group(1).toInt) + + /** Error text when the committed lock was written by a newer Bun; None when readable. */ + private[bun] def lockfileSkewError( + lockText: String, + lockPath: os.Path, + pinnedBunVersion: String + ): Option[String] = + for { + version <- lockfileVersion(lockText) + supported <- supportedLockfileVersion(pinnedBunVersion) + if version > supported + } yield s"$lockPath has lockfileVersion $version, which Bun $pinnedBunVersion cannot read " + + s"(it supports up to $supported). Regenerate the lockfile with this module's bunLock " + + "command, or raise bunVersion." + /** * Compose a Bun release asset name from the platform axes. * @@ -535,6 +562,9 @@ trait BunToolchainModule extends Module { /** Environment passed to Bun subprocesses. */ def bunEnv: T[Map[String, String]] = Task { Map.empty } + /** Environment for Bun toolchain subprocesses: install, lock, build, and bundle. */ + def bunToolEnv: T[Map[String, String]] = Task { bunEnv() } + /** Explicit resolutions for conflicting transitive npm dependency declarations. */ def npmOverrides: T[Map[String, String]] = Task { Map.empty } @@ -597,6 +627,7 @@ trait BunToolchainModule extends Module { hasInstallInputs: Boolean, lockfile: Option[PathRef], required: Boolean, + pinnedBunVersion: String, lockfilePath: os.Path = moduleDir / "bun.lock" ): Unit = { if (hasInstallInputs && required && lockfile.isEmpty) { @@ -604,6 +635,12 @@ trait BunToolchainModule extends Module { s"Missing $lockfilePath. Run this module's bunLock command and commit the generated lockfile." ) } + // bun.lock is forward- but not backward-compatible: a lock written by a newer Bun makes an + // older one fail with a raw UnknownLockfileVersion that never mentions how to recover. + lockfile.foreach { lock => + BunToolchainModule.lockfileSkewError(os.read(lock.path), lock.path, pinnedBunVersion) + .foreach(message => throw new RuntimeException(message)) + } } /** diff --git a/millbun/src/mill/bun/BunWorkersModule.scala b/millbun/src/mill/bun/BunWorkersModule.scala index cb50d8a..db095c9 100644 --- a/millbun/src/mill/bun/BunWorkersModule.scala +++ b/millbun/src/mill/bun/BunWorkersModule.scala @@ -33,7 +33,7 @@ trait BunWorkersModule extends BunToolchainModule { this: BunTypeScriptModule => def workerBundleTarget: T[String] = Task { bunBundleTarget() } /** Output format for worker bundles. Defaults to the module format. */ - def workerBundleFormat: T[Option[String]] = Task { Some(bunBundleFormat()) } + def workerBundleFormat: T[Option[String]] = Task { bunBundleFormat() } /** Extra raw flags for worker bundling. */ def workerBundleArgs: T[Seq[String]] = Task { Seq.empty } @@ -61,7 +61,7 @@ trait BunWorkersModule extends BunToolchainModule { this: BunTypeScriptModule => val outDir = Task.dest / "workers" // Declared explicitly: the staged tree carries a node_modules symlink into this install, // and Mill's filesystem checker only permits reading a dest we depend on. - npmInstall() + bunInstall() BunToolchainModule.copyWorkspace(compile().path, workspace) os.makeDir.all(outDir) @@ -94,7 +94,7 @@ trait BunWorkersModule extends BunToolchainModule { this: BunTypeScriptModule => target ) ++ formatArgs ++ extraArgs, cwd = workspace, - env = bunEnv() + env = bunToolEnv() ) } diff --git a/millbun/src/mill/bun/BunWorkspaceModule.scala b/millbun/src/mill/bun/BunWorkspaceModule.scala index fd5b4d5..f9eb152 100644 --- a/millbun/src/mill/bun/BunWorkspaceModule.scala +++ b/millbun/src/mill/bun/BunWorkspaceModule.scala @@ -115,7 +115,7 @@ trait BunWorkspaceModule extends BunToolchainModule: copyConfigs(Task.dest, npmRc().path, bunfigFiles()) val lockfile = bunLockfile() - requireBunLockfile(hasDependencyInputs(packages), lockfile, bunRequireLockfile()) + requireBunLockfile(hasDependencyInputs(packages), lockfile, bunRequireLockfile(), bunVersion()) copyBunLockfile(lockfile, Task.dest) runBun( @@ -127,7 +127,7 @@ trait BunWorkspaceModule extends BunToolchainModule: updateLockfile = false ), cwd = Task.dest, - env = bunEnv() + env = bunToolEnv() ) PathRef(Task.dest) } @@ -147,7 +147,7 @@ trait BunWorkspaceModule extends BunToolchainModule: updateLockfile = true ), cwd = Task.dest, - env = bunEnv() + env = bunToolEnv() ) val generated = Task.dest / "bun.lock" diff --git a/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala b/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala index 6fcedde..caed8b2 100644 --- a/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala +++ b/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala @@ -24,8 +24,13 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with /** Target used by `bun build`: browser | bun | node. */ def bunBundleTarget: T[String] = Task { "bun" } - /** Output format used by `bun build`. */ - def bunBundleFormat: T[String] = Task { if (enableEsm()) "esm" else "cjs" } + /** Output format passed to `bun build --format`; `None` lets bun infer. + * + * `Option[String]` to match the Scala.js module's member of the same name — a `T[String]` + * here made the two module kinds' shared vocabulary diverge on type, which no alias can + * bridge. 0.3.0 takes the one-time break. + */ + def bunBundleFormat: T[Option[String]] = Task { Some(if (enableEsm()) "esm" else "cjs") } /** Emit a standalone executable instead of a JS bundle. */ @deprecated("Use the compileExecutable task", "0.3.0") @@ -49,11 +54,8 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with /** Bun-only package.json fields not modeled by Mill's typed PackageJson. */ def bunPackageJsonExtras: T[ujson.Obj] = Task { ujson.Obj() } - /** Environment for Bun toolchain subprocesses such as install/build/test. */ - protected def bunToolEnv: T[Map[String, String]] = Task { bunEnv() } - /** Runtime environment for Bun-executed programs and tests. */ - protected def bunRuntimeEnv: T[Map[String, String]] = Task { bunEnv() ++ forkEnv() } + def bunRuntimeEnv: T[Map[String, String]] = Task { bunEnv() ++ forkEnv() } /** TypeScript version used for `bun x tsc`. */ def typeScriptVersion: T[String] = Task { "5.7.3" } @@ -146,8 +148,8 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with } } - /** Replace npm install with bun install. */ - override def npmInstall: T[PathRef] = Task { + /** Install dependencies with Bun. The canonical name; Mill's [[npmInstall]] delegates here. */ + def bunInstall: T[PathRef] = Task { val dest = Task.dest os.makeDir.all(dest) mkBunPackageJson() @@ -164,7 +166,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with } case None => val lockfile = bunLockfile() - requireBunLockfile(true, lockfile, bunRequireLockfile()) + requireBunLockfile(true, lockfile, bunRequireLockfile(), bunVersion()) copyBunLockfile(lockfile, dest) BunToolchainModule.stageUnmanagedDeps(transitiveUnmanagedDeps(), dest) @@ -183,6 +185,9 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with PathRef(dest) } + /** Mill's inherited install name; delegates to [[bunInstall]]. */ + override def npmInstall: T[PathRef] = Task { bunInstall() } + /** Resolve dependencies and update the source-controlled `bun.lock`. */ def bunLock(): Command[PathRef] = Task.Command { if bunWorkspaceInstall().nonEmpty then @@ -222,7 +227,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with tscCopyGenSources() tscLinkResources() BunTypeScriptModule.removeInstallOnlyConfigs(Task.dest) - ensureInstallArtifacts(Task.dest, npmInstall().path, bunLockfiles()) + ensureInstallArtifacts(Task.dest, bunInstall().path, bunLockfiles()) BunTypeScriptModule.copyBunfigsTo(Task.dest, resolvedBunfigs()) mkTsconfig() @@ -237,7 +242,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with } override def createNodeModulesSymlink: Task[Unit] = Task.Anon { - ensureInstallArtifacts(Task.dest, npmInstall().path, bunLockfiles()) + ensureInstallArtifacts(Task.dest, bunInstall().path, bunLockfiles()) } /** Run the entrypoint directly with Bun. */ @@ -291,7 +296,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with // Declared explicitly: the staged tree carries a node_modules symlink into this // install, and Mill's filesystem checker only permits reading a dest we depend on. - npmInstall() + bunInstall() BunToolchainModule.copyWorkspace(compileDir, buildDir) BunTypeScriptModule.removeInstallOnlyConfigs(buildDir) BunTypeScriptModule.copyBunfigsTo(buildDir, resolvedBunfigs()) @@ -309,10 +314,9 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with "--outfile", outFile.toString, "--target", - bunBundleTarget(), - "--format", - bunBundleFormat() - ) ++ packagesExternal ++ externalArgs ++ compileArgs ++ bunBuildArgs(), + bunBundleTarget() + ) ++ bunBundleFormat().toSeq.flatMap(format => Seq("--format", format)) + ++ packagesExternal ++ externalArgs ++ compileArgs ++ bunBuildArgs(), cwd = buildDir, env = bunToolEnv() ) @@ -329,7 +333,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with // Declared explicitly: the staged tree carries a node_modules symlink into this // install, and Mill's filesystem checker only permits reading a dest we depend on. - npmInstall() + bunInstall() BunToolchainModule.copyWorkspace(compileDir, buildDir) BunTypeScriptModule.removeInstallOnlyConfigs(buildDir) BunTypeScriptModule.copyBunfigsTo(buildDir, resolvedBunfigs()) @@ -369,7 +373,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with val mainFile = resolvedEntrypoint(mainFilePath(), compileDir).relativeTo(compileDir).toString // Declared explicitly: the staged tree carries a node_modules symlink into this // install, and Mill's filesystem checker only permits reading a dest we depend on. - npmInstall() + bunInstall() BunToolchainModule.copyWorkspace(compileDir, buildDir) BunTypeScriptModule.removeInstallOnlyConfigs(buildDir) BunTypeScriptModule.copyBunfigsTo(buildDir, resolvedBunfigs()) @@ -499,8 +503,9 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with bunTestPackageJson().render() == outer.bunWorkspacePackageJson().render() } - override def npmInstall: T[PathRef] = Task { - if (reusesOuterInstall()) outer.npmInstall() + /** Install this test module's dependencies with Bun; reuses the outer install when equal. */ + def bunInstall: T[PathRef] = Task { + if (reusesOuterInstall()) outer.bunInstall() else { val dest = Task.dest os.makeDir.all(dest) @@ -516,6 +521,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with hasInstallInputs = true, lockfile = lockfile, required = outer.bunRequireLockfile(), + pinnedBunVersion = outer.bunVersion(), lockfilePath = moduleDir / "bun.lock" ) outer.copyBunLockfile(lockfile, dest) @@ -540,6 +546,9 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with } } + /** Mill's inherited install name; delegates to [[bunInstall]]. */ + override def npmInstall: T[PathRef] = Task { bunInstall() } + /** * Resolve this test module's dependencies and update its own `bun.lock`. * @@ -584,7 +593,7 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with val dest = Task.dest BunToolchainModule.copyWorkspace(this.compile().path, dest) BunTypeScriptModule.removeInstallOnlyConfigs(dest) - outer.ensureInstallArtifacts(dest, npmInstall().path, bunLockfiles()) + outer.ensureInstallArtifacts(dest, bunInstall().path, bunLockfiles()) BunTypeScriptModule.copyBunfigsTo(dest, outer.resolvedBunfigs()) PathRef(dest) } @@ -601,6 +610,18 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with bunTestArgs() ++ timeoutArgs ++ reporterArgs } + /** Run `bun test`. Named after Mill's standard test entrypoint, so both module kinds share it. */ + def testForked(args: mill.api.Args): Command[CommandResult] = Task.Command { + os.call( + Seq(bunExecutable(), "test") ++ resolvedTestFlags() ++ args.value, + cwd = preparedTestWorkspace().path, + env = outer.bunRuntimeEnv(), + stdout = os.Inherit, + stderr = os.Inherit + ) + } + + @deprecated("Use testForked", "0.3.0") def test(args: mill.api.Args): Command[CommandResult] = Task.Command { os.call( Seq(bunExecutable(), "test") ++ resolvedTestFlags() ++ args.value, diff --git a/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala b/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala index 22c9ac6..98de0c3 100644 --- a/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala +++ b/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala @@ -65,7 +65,7 @@ trait BunTypeScriptWebModule extends BunTypeScriptModule: sources() ++ generatedSources() ++ resources(), webEntryPoints(), webPublicSources(), - npmInstall().path, + bunInstall().path, bunfigFiles(), webScriptEntryPoint().path ) diff --git a/millbun/src/mill/scalajslib/bun/BunPublishModule.scala b/millbun/src/mill/scalajslib/bun/BunPublishModule.scala index 74025de..8956b06 100644 --- a/millbun/src/mill/scalajslib/bun/BunPublishModule.scala +++ b/millbun/src/mill/scalajslib/bun/BunPublishModule.scala @@ -85,7 +85,7 @@ trait BunPublishModule extends BunScalaJSModule { val hasRuntimeInputs = deps.nonEmpty || optional.nonEmpty || unmanagedDeps().nonEmpty val lockfile = bunLockfile() - requireBunLockfile(hasRuntimeInputs, lockfile, bunRequireLockfile()) + requireBunLockfile(hasRuntimeInputs, lockfile, bunRequireLockfile(), bunVersion()) copyBunLockfile(lockfile, dest) if hasRuntimeInputs then BunToolchainModule.stageUnmanagedDeps(unmanagedDeps(), dest) @@ -98,7 +98,7 @@ trait BunPublishModule extends BunScalaJSModule { updateLockfile = false ), cwd = dest, - env = bunEnv() + env = bunToolEnv() ) PathRef(dest) diff --git a/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala b/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala index 18316ba..eb0b739 100644 --- a/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala +++ b/millbun/src/mill/scalajslib/bun/BunScalaJSModule.scala @@ -287,7 +287,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac } case None => val lockfile = bunLockfile() - requireBunLockfile(hasInstallInputs, lockfile, bunRequireLockfile()) + requireBunLockfile(hasInstallInputs, lockfile, bunRequireLockfile(), bunVersion()) copyBunLockfile(lockfile, dest) if hasInstallInputs then @@ -301,7 +301,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac updateLockfile = false ), cwd = dest, - env = bunEnv() + env = bunToolEnv() ) mergeVendoredNodeModules(vendoredEntries, dest / "node_modules") @@ -332,7 +332,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac updateLockfile = true ), cwd = dest, - env = bunEnv() + env = bunToolEnv() ) val generated = dest / "bun.lock" @@ -449,7 +449,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac bytecodeArgs ++ bunBundleArgs(), cwd = linked.dest.path, - env = bunEnv() + env = bunToolEnv() ) PathRef(outDir) @@ -481,7 +481,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac splittingArgs ++ bunBundleArgs(), cwd = linked.dest.path, - env = bunEnv() + env = bunToolEnv() ) PathRef(outDir) @@ -518,7 +518,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac bytecodeArgs ++ bunBundleArgs(), cwd = buildDir, - env = bunEnv() + env = bunToolEnv() ) PathRef(outFile) @@ -560,7 +560,7 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac bytecodeArgs ++ bunBundleArgs(), cwd = buildDir, - env = bunEnv() + env = bunToolEnv() ) target -> PathRef(outFile) @@ -624,7 +624,12 @@ trait BunScalaJSModule extends ScalaJSModule with BunToolchainModule with BunPac linked } - /** Run Scala.js tests through Mill's test bridge with Bun as the JS runtime. */ + /** Run Scala.js tests through Mill's test bridge with Bun as the JS runtime. + * + * The inherited `testForked` already does exactly this — the overridden [[jsEnvConfig]] and + * [[testLinkTask]] put every test run on Bun — so this alias adds nothing over it. + */ + @deprecated("Use the inherited testForked", "0.3.0") def bunTest(args: mill.api.Args): Command[(msg: String, results: Seq[mill.javalib.testrunner.TestResult])] = Task.Command { testTask( diff --git a/millbun/src/mill/scalajslib/bun/BunScalaJSWebModule.scala b/millbun/src/mill/scalajslib/bun/BunScalaJSWebModule.scala index 68a2f34..e7dfb84 100644 --- a/millbun/src/mill/scalajslib/bun/BunScalaJSWebModule.scala +++ b/millbun/src/mill/scalajslib/bun/BunScalaJSWebModule.scala @@ -94,7 +94,7 @@ trait BunScalaJSWebModule extends BunScalaJSModule: bunExecutable(), Seq("build") ++ entries.map(_.toString) ++ Seq("--minify", "--outdir", destination.toString) ++ bunBundleArgs(), cwd = stage, - env = bunEnv() + env = bunToolEnv() ) PathRef(destination) } diff --git a/millbun/test/src/mill/bun/BunToolchainTests.scala b/millbun/test/src/mill/bun/BunToolchainTests.scala index 94d9c12..df23cb5 100644 --- a/millbun/test/src/mill/bun/BunToolchainTests.scala +++ b/millbun/test/src/mill/bun/BunToolchainTests.scala @@ -131,11 +131,14 @@ object BunToolchainTests extends TestSuite: val cached = root / "cache" / "def456" / "bun" val staged = root / "staged" / "bun" os.write(staged, "bun-binary", createFolders = true) - os.perms.set(staged, "rwxr-xr-x") + // NTFS has no POSIX permissions and os.perms throws there; Windows bun.exe needs no + // executable bit, so the permission half of this contract is POSIX-only. + val posix = !scala.util.Properties.isWin + if posix then os.perms.set(staged, "rwxr-xr-x") assert(BunToolchainModule.publishViaCopy(staged, cached) == cached) assert(os.read(cached) == "bun-binary") - assert(os.perms(cached).toString == "rwxr-xr-x") + if posix then assert(os.perms(cached).toString == "rwxr-xr-x") // The bytes must travel under a temp name and arrive by rename: a crash mid-publish can // never leave a partial file at the published path, and success leaves nothing behind. assert(os.list(cached / os.up) == Seq(cached)) @@ -151,6 +154,29 @@ object BunToolchainTests extends TestSuite: assert(os.read(cached) == "bun-binary") assert(os.list(cached / os.up) == Seq(cached)) + test("lockfile version is extracted lexically from JSONC"): + // bun.lock has trailing commas, so a JSON parser cannot be the extraction mechanism. + assert(BunToolchainModule.lockfileVersion("{\n \"lockfileVersion\": 2,\n}") == Some(2)) + assert(BunToolchainModule.lockfileVersion("not a lockfile") == None) + + test("a newer lock against an older Bun explains how to recover"): + val lock = "{\n \"lockfileVersion\": 2,\n}" + val error = BunToolchainModule.lockfileSkewError(lock, os.root / "bun.lock", "1.3.14") + assert(error.exists(_.contains("Regenerate the lockfile"))) + assert(error.exists(_.contains("lockfileVersion 2"))) + // Readable combinations stay silent: same version, older lock, or an unbundled Bun. + assert(BunToolchainModule.lockfileSkewError(lock, os.root / "bun.lock", "1.4.0").isEmpty) + val v1 = "{\n \"lockfileVersion\": 1,\n}" + assert(BunToolchainModule.lockfileSkewError(v1, os.root / "bun.lock", "1.3.14").isEmpty) + assert(BunToolchainModule.lockfileSkewError(lock, os.root / "bun.lock", "9.9.9").isEmpty) + + test("every bundled Bun declares its supported lockfile version"): + // Committed fixture locks make the pin load-bearing: a version added to the checksum + // table without a lockfile-version entry silently disables the skew guard for it. + BunToolchainModule.bundledVersions.foreach { version => + assert(BunToolchainModule.supportedLockfileVersion(version).nonEmpty) + } + test("unmanaged deps become file: specifiers under vendor"): val dep = os.temp.dir() / "local-lib" os.write(dep / "package.json", """{"name":"local-lib","version":"1.0.0"}""", createFolders = true)