@@ -189,6 +189,60 @@ object Runner {
189189 run(command, logger, cwd = cwd, extraEnv = extraEnv)
190190 }
191191
192+ // Detects the major version of Node.js on PATH; cached for the JVM lifetime (lazy val).
193+ // Returns None if node is not found or version cannot be parsed.
194+ private lazy val nodeMajorVersion : Option [Int ] =
195+ try {
196+ val process = new ProcessBuilder (" node" , " --version" )
197+ .redirectErrorStream(true )
198+ .start()
199+ val output = new String (process.getInputStream.readAllBytes()).trim
200+ process.waitFor()
201+ // Node version format: "v22.5.0" -> extract 22
202+ if (output.startsWith(" v" ))
203+ output.drop(1 ).takeWhile(_.isDigit) match {
204+ case s if s.nonEmpty => Some (s.toInt)
205+ case _ => None
206+ }
207+ else None
208+ }
209+ catch {
210+ case _ : Exception => None
211+ }
212+
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 ] =
220+ try {
221+ val process = new ProcessBuilder (" deno" , " --version" )
222+ .redirectErrorStream(true )
223+ .start()
224+ val output = new String (process.getInputStream.readAllBytes()).trim
225+ 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+ case s if s.nonEmpty => Some (s.toInt)
232+ case _ => None
233+ }
234+ }
235+ catch {
236+ case _ : Exception => None
237+ }
238+
239+ // Deno 2.x+ bundles V8 13+ which has wasm-exnref enabled by default; no flag needed.
240+ private def denoNeedsWasmFlag : Boolean =
241+ denoMajorVersion.flatMap { major =>
242+ if (major >= 2 ) Some (false ) // Deno 2.x+ has V8 13+ with wasm-exnref by default
243+ else Some (true )
244+ }.getOrElse(true ) // true if unknown
245+
192246 private def endsWithCaseInsensitive (s : String , suffix : String ): Boolean =
193247 s.length >= suffix.length &&
194248 s.regionMatches(true , s.length - suffix.length, suffix, 0 , suffix.length)
@@ -221,11 +275,13 @@ object Runner {
221275 def jsCommand (
222276 entrypoint : File ,
223277 args : Seq [String ],
224- jsDom : Boolean = false
278+ jsDom : Boolean = false ,
279+ emitWasm : Boolean = false
225280 ): Seq [String ] = {
226281
227- val nodePath = findInPath(" node" ).fold(" node" )(_.toString)
228- val command = Seq (nodePath, entrypoint.getAbsolutePath) ++ args
282+ val nodePath = findInPath(" node" ).fold(" node" )(_.toString)
283+ val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List (" --experimental-wasm-exnref" ) else Nil
284+ val command = Seq (nodePath) ++ nodeFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
229285
230286 if (jsDom)
231287 // FIXME We'd need to replicate what JSDOMNodeJSEnv does under-the-hood to get the command in that case.
@@ -242,14 +298,16 @@ object Runner {
242298 allowExecve : Boolean = false ,
243299 jsDom : Boolean = false ,
244300 sourceMap : Boolean = false ,
245- esModule : Boolean = false
301+ esModule : Boolean = false ,
302+ emitWasm : Boolean = false
246303 ): Either [BuildException , Process ] = either {
247304 val nodePath : String =
248305 value(findInPath(" node" )
249306 .map(_.toString)
250307 .toRight(NodeNotFoundError ()))
308+ val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List (" --experimental-wasm-exnref" ) else Nil
251309 if ! jsDom && allowExecve && Execve .available() then {
252- val command = Seq (nodePath, entrypoint.getAbsolutePath) ++ args
310+ val command = Seq (nodePath) ++ nodeFlags ++ Seq ( entrypoint.getAbsolutePath) ++ args
253311
254312 logger.log(
255313 s " Running ${command.mkString(" " )}" ,
@@ -265,12 +323,25 @@ object Runner {
265323 )
266324 sys.error(" should not happen" )
267325 }
326+ else if (emitWasm) {
327+ // For WASM mode with ES modules, run node directly instead of NodeJSEnv.
328+ // NodeJSEnv's stdin piping with "-" doesn't work with Input.ESModule.
329+ val command = Seq (nodePath) ++ nodeFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
330+
331+ logger.log(
332+ s " Running ${command.mkString(" " )}" ,
333+ " Running" + System .lineSeparator() +
334+ command.iterator.map(_ + System .lineSeparator()).mkString
335+ )
336+
337+ new ProcessBuilder (command : _* ).inheritIO().start()
338+ }
268339 else {
269340 val nodeArgs =
270341 // Scala.js runs apps by piping JS to node.
271342 // If we need to pass arguments, we must first make the piped input explicit
272343 // with "-", and we pass the user's arguments after that.
273- if args.isEmpty then Nil else " -" :: args.toList
344+ nodeFlags ++ ( if args.isEmpty then Nil else " -" :: args.toList)
274345 val envJs =
275346 if jsDom then
276347 new JSDOMNodeJSEnv (
@@ -307,6 +378,69 @@ object Runner {
307378 }
308379 }
309380
381+ def denoCommand (
382+ entrypoint : File ,
383+ args : Seq [String ],
384+ denoPathOpt : Option [String ] = None
385+ ): Seq [String ] = {
386+ val denoPath = denoPathOpt.getOrElse(findInPath(" deno" ).fold(" deno" )(_.toString))
387+ val denoFlags = Seq (" run" , " --allow-read" )
388+ Seq (denoPath) ++ denoFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
389+ }
390+
391+ def runDeno (
392+ entrypoint : File ,
393+ args : Seq [String ],
394+ logger : Logger ,
395+ allowExecve : Boolean = false ,
396+ emitWasm : Boolean = false ,
397+ denoPathOpt : Option [String ] = None
398+ ): Either [BuildException , Process ] = either {
399+ val denoPath : String = denoPathOpt.getOrElse {
400+ value(findInPath(" deno" )
401+ .map(_.toString)
402+ .toRight(DenoNotFoundError ()))
403+ }
404+ val denoFlags = Seq (" run" , " --allow-read" )
405+ val extraEnv =
406+ if (emitWasm && denoNeedsWasmFlag) Map (" DENO_V8_FLAGS" -> " --experimental-wasm-exnref" )
407+ else Map .empty
408+
409+ if (allowExecve && Execve .available()) {
410+ val command = Seq (denoPath) ++ denoFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
411+
412+ logger.log(
413+ s " Running ${command.mkString(" " )}" ,
414+ " Running" + System .lineSeparator() +
415+ command.iterator.map(_ + System .lineSeparator()).mkString
416+ )
417+
418+ logger.debug(" execve available" )
419+ Execve .execve(
420+ command.head,
421+ " deno" +: command.tail.toArray,
422+ (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s " $k= $v" }
423+ )
424+ sys.error(" should not happen" )
425+ }
426+ else {
427+ val command = Seq (denoPath) ++ denoFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
428+
429+ logger.log(
430+ s " Running ${command.mkString(" " )}" ,
431+ " Running" + System .lineSeparator() +
432+ command.iterator.map(_ + System .lineSeparator()).mkString
433+ )
434+
435+ val builder = new ProcessBuilder (command* )
436+ .inheritIO()
437+ val env = builder.environment()
438+ for ((k, v) <- extraEnv)
439+ env.put(k, v)
440+ builder.start()
441+ }
442+ }
443+
310444 def runNative (
311445 launcher : File ,
312446 args : Seq [String ],
0 commit comments