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))