From 9282ea459d0e9e1a6c35aa9c9a1abe7d92f8c226 Mon Sep 17 00:00:00 2001 From: Richie Caputo Date: Wed, 26 Aug 2026 17:52:23 -0400 Subject: [PATCH] Make bare test modules actually reuse the outer install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit BunTypeScriptTests extends upstream Mill's TypeScriptTests, so the unqualified this.tsDeps() inside bunTestPackageJson resolved to the Node toolchain defaults (ts-node, tsconfig-paths, @types/node) that the outer trait deliberately replaced. Those names always survived the outer-name filter, so a bare test module's package.json never equaled the outer's and the reusesOuterInstall branch was unreachable — with bunRequireLockfile on, every bare test module demanded its own lockfile, contradicting the documented contract. The trait now pins tsDeps to the outer module's. The guard test passed anyway because both of its assertions also hold in the broken standalone branch (the suite relaxes the lockfile requirement, and installs never write into the source tree). It now also asserts the returned install path is the outer module's npmInstall.dest — confirmed failing before this fix and passing after. Also: dev() on BunTypeScriptWebModule served straight out of the cached webStage output while its sync thread mirrored live edits (but never deletions) into it, and bundle builds from that same stage — a file created and deleted during a dev session would ship in the next production bundle. dev() now serves from a private copy in its own command dest; copyTree preserves the node_modules symlink, so the copy is cheap. Co-Authored-By: Claude Fable 5 --- .../src/mill/bun/BunTypeScriptIntegrationTests.scala | 4 ++++ .../mill/javascriptlib/bun/BunTypeScriptModule.scala | 12 ++++++++++++ .../javascriptlib/bun/BunTypeScriptWebModule.scala | 6 +++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala b/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala index 0aa008e..45eb959 100644 --- a/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala +++ b/millbun/integration/src/mill/bun/BunTypeScriptIntegrationTests.scala @@ -243,6 +243,10 @@ object BunTypeScriptIntegrationTests extends TestSuite { val tester = this.tester("typescript-tests") assert(tester.eval("app.test.npmInstall").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") } test("bunEnv") { diff --git a/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala b/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala index 44644f0..6ff9dbc 100644 --- a/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala +++ b/millbun/src/mill/javascriptlib/bun/BunTypeScriptModule.scala @@ -409,6 +409,18 @@ trait BunTypeScriptModule extends TypeScriptModule with BunToolchainModule with */ trait BunTypeScriptTests extends TypeScriptTests { + /** + * The outer module's Bun-specific TS toolchain, not upstream Mill's ts-node defaults. + * + * This trait extends upstream `TypeScriptTests`, so an unqualified `tsDeps()` resolves to + * the Node toolchain (`ts-node`, `tsconfig-paths`, `@types/node`) that the outer trait + * deliberately replaced. Those names always survived the outer-name filter in + * [[bunTestPackageJson]], so a bare test module's package.json never matched the outer's + * and the install-reuse path in [[npmInstall]] was unreachable — with `bunRequireLockfile` + * on, every bare test module demanded its own lockfile. + */ + override def tsDeps: T[Seq[String]] = Task { outer.tsDeps() } + /** Test timeout in milliseconds. 0 means no timeout. */ def bunTestTimeout: T[Int] = Task { 0 } diff --git a/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala b/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala index b66d9e9..22c9ac6 100644 --- a/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala +++ b/millbun/src/mill/javascriptlib/bun/BunTypeScriptWebModule.scala @@ -74,7 +74,11 @@ trait BunTypeScriptWebModule extends BunTypeScriptModule: /** Start Bun's HTML development server with source mirroring for native HMR. */ def dev(): Command[Unit] = Task.Command { - val stage = webStage().path + // Serve from a private copy: the sync thread mirrors live edits (but never deletions) into + // the serving root, and `bundle` builds from the same cached stage — mutating it in place + // would let a file created and deleted during a dev session ship in the production bundle. + val stage = Task.dest / "stage" + mill.bun.BunToolchainModule.copyTree(webStage().path, stage) val entries = BunWebSupport.htmlEntries(webEntryPoints(), moduleDir, stage) val syncRoots = (sources() ++ generatedSources() ++ resources() ++ webEntryPoints() ++ webPublicSources()) .filter(ref => os.exists(ref.path) && ref.path.startsWith(moduleDir))