From 36c94e86ae9833839e5711e302c29ae709c2d249 Mon Sep 17 00:00:00 2001 From: Ondra Pelech Date: Tue, 13 Oct 2026 21:23:35 +0200 Subject: [PATCH] Remove the multi-file Scala.js linked output directory recursively With the Wasm backend (or JS module splitting) the linker emits several files, so Package.linkJs turns the temporary destination into a directory. The cleanup used os.remove, which throws DirectoryNotEmptyException: `scala-cli test --js-emit-wasm` exited 1 after all tests passed, and `run` leaked the directory. Co-Authored-By: Claude Opus 5.5 --- .../scala/scala/cli/commands/run/Run.scala | 11 +++++++--- .../cli/integration/TestTestDefinitions.scala | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala index b03b8cbedc..6a87b3c05d 100644 --- a/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala +++ b/modules/cli/src/main/scala/scala/cli/commands/run/Run.scala @@ -556,7 +556,7 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { ) } } - process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) + process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove.all(jsDest)) Right((process, None)) } } @@ -613,7 +613,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { esModule = esModule ) } - process.onExit().thenApply(_ => if os.exists(jsDest) then os.remove(jsDest)) + process.onExit().thenApply(_ => + if os.exists(jsDest) then os.remove.all(jsDest) + ) Right((process, None)) } } @@ -842,7 +844,10 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers { noOpt = noOpt, logger = logger ).map(outputPath => f(outputPath)) - finally if os.exists(dest) then os.remove(dest) + // `dest` starts as a file, but `linkJs` replaces it with a directory when the linker emits + // several files (Wasm, or JS module splitting), so a plain `os.remove` would throw + // `DirectoryNotEmptyException` and fail the command after it already succeeded. + finally if os.exists(dest) then os.remove.all(dest) } def withNativeLauncher[T]( diff --git a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala index da821e2d98..31c64af20e 100644 --- a/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala +++ b/modules/integration/src/test/scala/scala/cli/integration/TestTestDefinitions.scala @@ -377,6 +377,26 @@ abstract class TestTestDefinitions extends ScalaCliSuite with TestScalaVersionAr } } + test("successful test Wasm") { + successfulTestInputs().fromRoot { root => + // The Wasm backend emits several files (main.js, __loader.js, main.wasm), so the linked + // output is a directory: the command must still exit 0 after cleaning it up. + val output = os.proc( + TestUtil.cli, + "--power", + "test", + extraOptions, + ".", + "--js-emit-wasm", + "--js-module-kind", + "es" + ) + .call(cwd = root) + .out.text() + expect(output.contains("Hello from tests")) + } + } + test("successful test esmodule import JS") { successfulESModuleTestInputs.fromRoot { root => val output = os.proc(TestUtil.cli, "test", extraOptions, ".", "--js")