@@ -186,6 +186,60 @@ object Runner {
186186 run(command, logger, cwd = cwd, extraEnv = extraEnv)
187187 }
188188
189+ // Detects the major version of Node.js on PATH; cached for the JVM lifetime (lazy val).
190+ // Returns None if node is not found or version cannot be parsed.
191+ private lazy val nodeMajorVersion : Option [Int ] =
192+ try {
193+ val process = new ProcessBuilder (" node" , " --version" )
194+ .redirectErrorStream(true )
195+ .start()
196+ val output = new String (process.getInputStream.readAllBytes()).trim
197+ process.waitFor()
198+ // Node version format: "v22.5.0" -> extract 22
199+ if (output.startsWith(" v" ))
200+ output.drop(1 ).takeWhile(_.isDigit) match {
201+ case s if s.nonEmpty => Some (s.toInt)
202+ case _ => None
203+ }
204+ else None
205+ }
206+ catch {
207+ case _ : Exception => None
208+ }
209+
210+ // Node 24+ (V8 13+) has wasm-exnref enabled by default; older versions need --experimental-wasm-exnref.
211+ private def nodeNeedsWasmFlag : Boolean =
212+ nodeMajorVersion.forall(_ < 24 ) // true if unknown or < 24
213+
214+ // Detects the major version of Deno on PATH; cached for the JVM lifetime (lazy val).
215+ // Returns None if deno is not found or version cannot be parsed.
216+ private lazy val denoMajorVersion : Option [Int ] =
217+ try {
218+ val process = new ProcessBuilder (" deno" , " --version" )
219+ .redirectErrorStream(true )
220+ .start()
221+ val output = new String (process.getInputStream.readAllBytes()).trim
222+ process.waitFor()
223+ // Deno version format: "deno 2.1.0 (release, aarch64-apple-darwin)\nv8 13.x\ntypescript 5.x"
224+ // Extract major from first line
225+ val firstLine = output.linesIterator.nextOption().getOrElse(" " )
226+ val versionStr = firstLine.stripPrefix(" deno " ).takeWhile(c => c.isDigit || c == '.' )
227+ versionStr.takeWhile(_.isDigit) match {
228+ case s if s.nonEmpty => Some (s.toInt)
229+ case _ => None
230+ }
231+ }
232+ catch {
233+ case _ : Exception => None
234+ }
235+
236+ // Deno 2.x+ bundles V8 13+ which has wasm-exnref enabled by default; no flag needed.
237+ private def denoNeedsWasmFlag : Boolean =
238+ denoMajorVersion.flatMap { major =>
239+ if (major >= 2 ) Some (false ) // Deno 2.x+ has V8 13+ with wasm-exnref by default
240+ else Some (true )
241+ }.getOrElse(true ) // true if unknown
242+
189243 private def endsWithCaseInsensitive (s : String , suffix : String ): Boolean =
190244 s.length >= suffix.length &&
191245 s.regionMatches(true , s.length - suffix.length, suffix, 0 , suffix.length)
@@ -218,11 +272,13 @@ object Runner {
218272 def jsCommand (
219273 entrypoint : File ,
220274 args : Seq [String ],
221- jsDom : Boolean = false
275+ jsDom : Boolean = false ,
276+ emitWasm : Boolean = false
222277 ): Seq [String ] = {
223278
224- val nodePath = findInPath(" node" ).fold(" node" )(_.toString)
225- val command = Seq (nodePath, entrypoint.getAbsolutePath) ++ args
279+ val nodePath = findInPath(" node" ).fold(" node" )(_.toString)
280+ val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List (" --experimental-wasm-exnref" ) else Nil
281+ val command = Seq (nodePath) ++ nodeFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
226282
227283 if (jsDom)
228284 // FIXME We'd need to replicate what JSDOMNodeJSEnv does under-the-hood to get the command in that case.
@@ -239,14 +295,16 @@ object Runner {
239295 allowExecve : Boolean = false ,
240296 jsDom : Boolean = false ,
241297 sourceMap : Boolean = false ,
242- esModule : Boolean = false
298+ esModule : Boolean = false ,
299+ emitWasm : Boolean = false
243300 ): Either [BuildException , Process ] = either {
244301 val nodePath : String =
245302 value(findInPath(" node" )
246303 .map(_.toString)
247304 .toRight(NodeNotFoundError ()))
305+ val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List (" --experimental-wasm-exnref" ) else Nil
248306 if ! jsDom && allowExecve && Execve .available() then {
249- val command = Seq (nodePath, entrypoint.getAbsolutePath) ++ args
307+ val command = Seq (nodePath) ++ nodeFlags ++ Seq ( entrypoint.getAbsolutePath) ++ args
250308
251309 logger.log(
252310 s " Running ${command.mkString(" " )}" ,
@@ -262,12 +320,25 @@ object Runner {
262320 )
263321 sys.error(" should not happen" )
264322 }
323+ else if (emitWasm) {
324+ // For WASM mode with ES modules, run node directly instead of NodeJSEnv.
325+ // NodeJSEnv's stdin piping with "-" doesn't work with Input.ESModule.
326+ val command = Seq (nodePath) ++ nodeFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
327+
328+ logger.log(
329+ s " Running ${command.mkString(" " )}" ,
330+ " Running" + System .lineSeparator() +
331+ command.iterator.map(_ + System .lineSeparator()).mkString
332+ )
333+
334+ new ProcessBuilder (command : _* ).inheritIO().start()
335+ }
265336 else {
266337 val nodeArgs =
267338 // Scala.js runs apps by piping JS to node.
268339 // If we need to pass arguments, we must first make the piped input explicit
269340 // with "-", and we pass the user's arguments after that.
270- if args.isEmpty then Nil else " -" :: args.toList
341+ nodeFlags ++ ( if args.isEmpty then Nil else " -" :: args.toList)
271342 val envJs =
272343 if jsDom then
273344 new JSDOMNodeJSEnv (
@@ -304,6 +375,69 @@ object Runner {
304375 }
305376 }
306377
378+ def denoCommand (
379+ entrypoint : File ,
380+ args : Seq [String ],
381+ denoPathOpt : Option [String ] = None
382+ ): Seq [String ] = {
383+ val denoPath = denoPathOpt.getOrElse(findInPath(" deno" ).fold(" deno" )(_.toString))
384+ val denoFlags = Seq (" run" , " --allow-read" )
385+ Seq (denoPath) ++ denoFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
386+ }
387+
388+ def runDeno (
389+ entrypoint : File ,
390+ args : Seq [String ],
391+ logger : Logger ,
392+ allowExecve : Boolean = false ,
393+ emitWasm : Boolean = false ,
394+ denoPathOpt : Option [String ] = None
395+ ): Either [BuildException , Process ] = either {
396+ val denoPath : String = denoPathOpt.getOrElse {
397+ value(findInPath(" deno" )
398+ .map(_.toString)
399+ .toRight(DenoNotFoundError ()))
400+ }
401+ val denoFlags = Seq (" run" , " --allow-read" )
402+ val extraEnv =
403+ if (emitWasm && denoNeedsWasmFlag) Map (" DENO_V8_FLAGS" -> " --experimental-wasm-exnref" )
404+ else Map .empty
405+
406+ if (allowExecve && Execve .available()) {
407+ val command = Seq (denoPath) ++ denoFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
408+
409+ logger.log(
410+ s " Running ${command.mkString(" " )}" ,
411+ " Running" + System .lineSeparator() +
412+ command.iterator.map(_ + System .lineSeparator()).mkString
413+ )
414+
415+ logger.debug(" execve available" )
416+ Execve .execve(
417+ command.head,
418+ " deno" +: command.tail.toArray,
419+ (sys.env ++ extraEnv).toArray.sorted.map { case (k, v) => s " $k= $v" }
420+ )
421+ sys.error(" should not happen" )
422+ }
423+ else {
424+ val command = Seq (denoPath) ++ denoFlags ++ Seq (entrypoint.getAbsolutePath) ++ args
425+
426+ logger.log(
427+ s " Running ${command.mkString(" " )}" ,
428+ " Running" + System .lineSeparator() +
429+ command.iterator.map(_ + System .lineSeparator()).mkString
430+ )
431+
432+ val builder = new ProcessBuilder (command* )
433+ .inheritIO()
434+ val env = builder.environment()
435+ for ((k, v) <- extraEnv)
436+ env.put(k, v)
437+ builder.start()
438+ }
439+ }
440+
307441 def runNative (
308442 launcher : File ,
309443 args : Seq [String ],
0 commit comments