Skip to content

Commit 714e11a

Browse files
committed
Always pass --experimental-wasm-exnref when emitting WASM
Node 24 still ships V8 12.x where wasm-exnref is gated behind --experimental-wasm-exnref; the flag only flips to default in V8 13.x (Node 25+). The previous nodeMajorVersion < 24 guard therefore left Node 24 (the version pinned in CI) without the flag, which made any Scala.js WASM code using exception bytecodes, runtime throws, JS interop or Scala 3 @main fail at runtime. Same reasoning applies to Deno (Deno 2.x = V8 12.x). Until V8 13.x is the default everywhere, just always set the flag, there is no any overhead
1 parent 5fc340a commit 714e11a

6 files changed

Lines changed: 50 additions & 28 deletions

File tree

modules/build/src/main/scala/scala/build/internal/Runner.scala

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -210,24 +210,26 @@ object Runner {
210210
case _: Exception => None
211211
}
212212

213-
// Node 24+ (V8 13+) has wasm-exnref enabled by default; older versions need --experimental-wasm-exnref.
214-
private def nodeNeedsWasmFlag: Boolean =
215-
nodeMajorVersion.forall(_ < 24) // true if unknown or < 24
216-
217-
// Detects the major version of Deno on PATH; cached for the JVM lifetime (lazy val).
218-
// Returns None if deno is not found or version cannot be parsed.
219-
private lazy val denoMajorVersion: Option[Int] =
213+
// Pre-V8 13.x runtimes need --experimental-wasm-exnref for the Scala.js WASM exception model.
214+
// V8 13.x ships in Node 25+ (Node 24 is still on V8 12.x where exnref is gated behind the flag),
215+
// so until the default flips we always pass the flag when emitWasm is true.
216+
private def nodeNeedsWasmFlag: Boolean = true
217+
218+
// Deno 2.x bundles V8 12.x where wasm-exnref is gated behind a flag; symmetrical reasoning to Node.
219+
// We always set DENO_V8_FLAGS=--experimental-wasm-exnref on emitWasm until V8 13.x lands in Deno.
220+
private def denoNeedsWasmFlag: Boolean = true
221+
222+
// Detects the major version of Bun on PATH; cached for the JVM lifetime (lazy val).
223+
// Returns None if bun is not found or version cannot be parsed.
224+
private lazy val bunMajorVersion: Option[Int] =
220225
try {
221-
val process = new ProcessBuilder("deno", "--version")
226+
val process = new ProcessBuilder("bun", "--version")
222227
.redirectErrorStream(true)
223228
.start()
224229
val output = new String(process.getInputStream.readAllBytes()).trim
225230
process.waitFor()
226-
// Deno version format: "deno 2.1.0 (release, aarch64-apple-darwin)\nv8 13.x\ntypescript 5.x"
227-
// Extract major from first line
228-
val firstLine = output.linesIterator.nextOption().getOrElse("")
229-
val versionStr = firstLine.stripPrefix("deno ").takeWhile(c => c.isDigit || c == '.')
230-
versionStr.takeWhile(_.isDigit) match {
231+
// Bun version format: "1.1.30" -> extract 1
232+
output.takeWhile(_.isDigit) match {
231233
case s if s.nonEmpty => Some(s.toInt)
232234
case _ => None
233235
}
@@ -236,10 +238,6 @@ object Runner {
236238
case _: Exception => None
237239
}
238240

239-
// Deno 2.x+ bundles V8 13+ which has wasm-exnref enabled by default; no flag needed.
240-
private def denoNeedsWasmFlag: Boolean =
241-
denoMajorVersion.forall(_ < 2) // true if unknown or < 2
242-
243241
private def endsWithCaseInsensitive(s: String, suffix: String): Boolean =
244242
s.length >= suffix.length &&
245243
s.regionMatches(true, s.length - suffix.length, suffix, 0, suffix.length)
@@ -302,6 +300,10 @@ object Runner {
302300
value(findInPath("node")
303301
.map(_.toString)
304302
.toRight(NodeNotFoundError()))
303+
if (emitWasm)
304+
nodeMajorVersion.foreach { v =>
305+
if (v < 22) value(Left(new NodeVersionTooOldForWasmError(v)))
306+
}
305307
val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List("--experimental-wasm-exnref") else Nil
306308
if !jsDom && allowExecve && Execve.available() then {
307309
val command = Seq(nodePath) ++ nodeFlags ++ Seq(entrypoint.getAbsolutePath) ++ args
@@ -453,6 +455,9 @@ object Runner {
453455
value(findInPath("bun")
454456
.map(_.toString)
455457
.toRight(BunNotFoundError()))
458+
bunMajorVersion.foreach { v =>
459+
if (v < 1) value(Left(new BunVersionTooOldForWasmError(v)))
460+
}
456461

457462
val command = Seq(bunPath, "run", entrypoint.getAbsolutePath) ++ args
458463

modules/cli/src/main/scala/scala/cli/commands/run/Run.scala

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,9 @@ object Run extends ScalaCommand[RunOptions] with BuildCommandHelpers {
552552
build.options.platform.value match {
553553
case Platform.JS =>
554554
val esModule =
555-
build.options.scalaJsOptions.moduleKindStr.exists(m => m == "es" || m == "esmodule")
555+
build.options.scalaJsOptions.moduleKindStr.exists(m =>
556+
m == "es" || m == "esmodule"
557+
)
556558

557559
val linkerConfig = build.options.scalaJsOptions.linkerConfig(logger)
558560
val jsDest = {

modules/core/src/main/scala/scala/build/errors/BunNotFoundError.scala

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ package scala.build.errors
22

33
final class BunNotFoundError extends BuildException(
44
"Bun was not found on the PATH. Install Bun from https://bun.sh/ or use --wasm-runtime node"
5-
)
5+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package scala.build.errors
2+
3+
final class BunVersionTooOldForWasmError(found: Int)
4+
extends BuildException(
5+
s"Scala.js WASM backend requires Bun >= 1, but found Bun $found. " +
6+
"Upgrade Bun (https://bun.sh/) or switch runtime via --wasm-runtime node|deno."
7+
)
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package scala.build.errors
2+
3+
final class NodeVersionTooOldForWasmError(found: Int)
4+
extends BuildException(
5+
s"Scala.js WASM backend requires Node.js >= 22, but found Node.js $found. " +
6+
"Upgrade Node (https://nodejs.org/) or switch runtime via --wasm-runtime deno|bun."
7+
)

project/deps/package.mill

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@ object Cli {
1313
}
1414

1515
object Scala {
16-
def scala212 = "2.12.21"
17-
def scala213 = "2.13.18"
18-
def scala3LtsPrefix = "3.3" // used for the LTS version tags
19-
def scala3Lts = s"$scala3LtsPrefix.7" // the LTS version currently used in the build
20-
def runnerScala3 = scala3Lts
21-
def scala3NextPrefix = "3.8"
22-
def scala3Next = s"$scala3NextPrefix.3" // the newest/next version of Scala
23-
def scala3NextAnnounced = s"$scala3NextPrefix.2" // the newest/next version of Scala that's been announced
24-
def scala3NextRc = "3.8.4-RC1" // the latest RC version of Scala Next
16+
def scala212 = "2.12.21"
17+
def scala213 = "2.13.18"
18+
def scala3LtsPrefix = "3.3" // used for the LTS version tags
19+
def scala3Lts = s"$scala3LtsPrefix.7" // the LTS version currently used in the build
20+
def runnerScala3 = scala3Lts
21+
def scala3NextPrefix = "3.8"
22+
def scala3Next = s"$scala3NextPrefix.3" // the newest/next version of Scala
23+
def scala3NextAnnounced =
24+
s"$scala3NextPrefix.2" // the newest/next version of Scala that's been announced
25+
def scala3NextRc = "3.8.4-RC1" // the latest RC version of Scala Next
2526
def scala3NextRcAnnounced = "3.8.4-RC1" // the latest announced RC version of Scala Next
2627

2728
// The Scala version used to build the CLI itself.

0 commit comments

Comments
 (0)