Skip to content

Add WASM support: --wasm flag with Node.js and Deno runtimes#4176

Open
lostflydev wants to merge 5 commits into
VirtusLab:mainfrom
lostflydev:scala-wasm-support
Open

Add WASM support: --wasm flag with Node.js and Deno runtimes#4176
lostflydev wants to merge 5 commits into
VirtusLab:mainfrom
lostflydev:scala-wasm-support

Conversation

@lostflydev
Copy link
Copy Markdown

Implements scala-cli issue #3316: integrate WebAssembly with Scala CLI.

  • --wasm CLI flag and //> using wasm directive to enable WASM output

  • --wasm-runtime <runtime> option and //> using wasmRuntime directive Supported values: node (default), deno, wasmtime, wasmedge, wasmer

  • --deno-version, --wasmtime-version, --wasmer-version options and corresponding directives for pinning runtime versions

  • Node.js (default): runs Scala.js WASM output with --experimental-wasm-exnref flag, requires Node.js >= 22

  • Deno: runs Scala.js WASM output; if not found on PATH, downloads from GitHub releases via Coursier cache

  • Wasmtime / WasmEdge / Wasmer: return UnsupportedWasmRuntimeError pending upstream Scala.js standalone WASM support (Make Scala.js Wasm backend suitable for standalone Wasm VMs (a.k.a. support "server-side Wasm") scala-js/scala-js#4991)

@Gedochao Gedochao linked an issue Mar 11, 2026 that may be closed by this pull request
7 tasks
@Gedochao
Copy link
Copy Markdown
Contributor

@Gedochao
Copy link
Copy Markdown
Contributor

@lostflydev I will go over the review in the coming days (this is a hefty one, might take a bit), but in the meantime - it seems the reference doc hasn't been generated.
Refer to https://github.com/VirtusLab/scala-cli/blob/main/CONTRIBUTING.md#rules-for-a-well-formed-pr

* Deno is first looked up on the system PATH. If not found, it is downloaded from GitHub releases
* and cached via Coursier's ArchiveCache.
*/
object WasmRuntimeDownloader {
Copy link
Copy Markdown
Contributor

@bishabosha bishabosha Mar 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the current behavior for scala-cli run --js foo.scala? i could be wrong but i would think it does not download a runtime

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also wondering the current behavior for that command.
I'm not sure about the policy on downloading the runtime through scala-cli (I personally don't like it though), but I believe downloading runtime stuff should be in different PR, because it's not very relevant to "supporing wasm".

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd treat WASM runtimes similar to how we treat node with Scala.js - the user needs to download it themselves, rather than Scala CLI doing it for them.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scala-cli has --jvm flag and downloads jvm by itself. This behaviour with wasm runtime downloading looks similar.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dos65 JVM is key to Scala CLI's core functionalities, and thus it's treated as a major dependency.
Node is considered necessary setup for Scala.js, similar to how clang is for Scala Native.
I understand this is blurry and vague, but I think we will expect the user to install WASM runtimes on their own, rather than do it out of the box here (certainly out of scope for this PR, but feel free to start a dedicated discussion or issue on this as a follow-up).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Gedochao
Actually it was impelemented because it was written in #3316 - Additionally, I'd like to be able to choose a runtime and let Scala CLI download it for me.

I don't have any preference of having this functionality - I only want to clarify if this thing really isn't required.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You got me there. 😅
Not sure why did I define the requirements this way when creating the issue.
Will amend it there.
My bad.

* Deno is first looked up on the system PATH. If not found, it is downloaded from GitHub releases
* and cached via Coursier's ArchiveCache.
*/
object WasmRuntimeDownloader {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I also wondering the current behavior for that command.
I'm not sure about the policy on downloading the runtime through scala-cli (I personally don't like it though), but I believe downloading runtime stuff should be in different PR, because it's not very relevant to "supporing wasm".

Comment on lines +28 to +31
// Standalone runtimes (future - requires upstream Scala.js standalone WASM support)
case object Wasmtime extends WasmRuntime("wasmtime")
case object WasmEdge extends WasmRuntime("wasmedge")
case object Wasmer extends WasmRuntime("wasmer")
Copy link
Copy Markdown

@tanishiking tanishiking Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If they don't work, I feel we don't need to add those options yet

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

@Gedochao Gedochao marked this pull request as draft March 12, 2026 09:46
@Gedochao
Copy link
Copy Markdown
Contributor

(converted to a draft, as this clearly needs more work; feel free to change it back when it's ready to review)

@Gedochao
Copy link
Copy Markdown
Contributor

(I can see the post-review changes, but a rebase will be necessary to re-run the CI)

@lostflydev
Copy link
Copy Markdown
Author

(I can see the post-review changes, but a rebase will be necessary to re-run the CI)

Hi @Gedochao ! I`ve rebased onto actual main, could you pls re-run CI/CD

@lostflydev lostflydev force-pushed the scala-wasm-support branch 3 times, most recently from 3358e24 to 7f43349 Compare April 2, 2026 15:27
@lostflydev lostflydev marked this pull request as ready for review April 2, 2026 15:28
@He-Pin
Copy link
Copy Markdown

He-Pin commented Apr 6, 2026

why not use bun as a runtime

@lostflydev
Copy link
Copy Markdown
Author

why not use bun as a runtime

@He-Pin Thanks for comment, added bun as a runtime

@lostflydev lostflydev marked this pull request as draft April 28, 2026 06:35
@lostflydev lostflydev force-pushed the scala-wasm-support branch from 721bbcf to 714e11a Compare May 6, 2026 09:01
@lostflydev lostflydev marked this pull request as ready for review May 6, 2026 09:02
Implements scala-cli issue VirtusLab#3316: integrate WebAssembly with Scala CLI.

- `--wasm` CLI flag and `//> using wasm` directive to enable WASM output
- `--wasm-runtime <runtime>` option and `//> using wasmRuntime` directive
  Supported values: node (default), deno
- `--deno-version`, `--wasmtime-version`, `--wasmer-version` options
  and corresponding directives for pinning runtime versions

- **Node.js** (default): runs Scala.js WASM output with
  `--experimental-wasm-exnref` flag, requires Node.js >= 22
- **Deno**: runs Scala.js WASM output
…imes

- Move --wasm flag to dedicated Wasm help group with --help-wasm option
- Simplify wasmOptions parsing with fold/toRight pattern
- Add runtime validation with UnrecognizedWasmRuntimeError in directives
- Auto-enable WASM when wasmRuntime directive is set
- Update reference documentation

Code style: simplify denoNeedsWasmFlag, explicit runtime match cases, clean type annotation, scalfmt
…smRuntime bun)

  - Add BunNotFoundError with install hint
  - Add integration test for Bun (conditional on bun being on PATH)
  - Add actions/setup-node@v6 node-version:24 to all Linux integration test
    jobs: the default Node.js on ubuntu-24.04 runners is too old for Scala.js
    WASM GC (which requires Node.js >= 22). Matches docs-tests job which
    already pins node-version: 24
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
@lostflydev lostflydev force-pushed the scala-wasm-support branch from 714e11a to 077bf00 Compare May 11, 2026 08:16
@lostflydev
Copy link
Copy Markdown
Author

Hi @Gedochao, could you please re-run the failed native-windows-tests-default (https://github.com/VirtusLab/scala-cli/actions/runs/25658534589/job/75315398257?pr=4176#logs) job? I think it might be flaky
and not caused by the changes in this PR

@Gedochao
Copy link
Copy Markdown
Contributor

Yep, almost certainly flaky. I restarted it.
There's a bunch of flaky tests and GitHub is very unstable this week, ping me if anything else needs restarting.

@lostflydev
Copy link
Copy Markdown
Author

Hi @tanishiking @sjrd @Florian3k @lbialy @dos65 👋

Gentle ping — the PR is ready for another look. Addressed previous feedback: dropped runtime auto-download and unsupported standalone runtimes, added Bun support, rebased, fixed style and docs.
All required CI checks are green.

Whenever you have a spare moment, I'd really appreciate your feedback

final case class WasmOptions(
@Group(HelpGroup.Wasm.toString)
@Tag(tags.experimental)
@HelpMessage("Enable WebAssembly output (Scala.js WASM backend). Uses Node.js by default. To show more options for WASM pass `--help-wasm`")
Copy link
Copy Markdown

@tanishiking tanishiking May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use "Wasm" instead of "WASM" ;)

A contraction of “WebAssembly”, not an acronym, hence not using all-caps.
https://webassembly.github.io/spec/core/intro/introduction.html#wasm

}
else if (emitWasm) {
// For WASM mode with ES modules, run node directly instead of NodeJSEnv.
// NodeJSEnv's stdin piping with "-" doesn't work with Input.ESModule.
Copy link
Copy Markdown

@tanishiking tanishiking May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it works with ESModule, and we don't need this else if (emitWasm) branch. What didn't work here?


// Detects the major version of Node.js on PATH; cached for the JVM lifetime (lazy val).
// Returns None if node is not found or version cannot be parsed.
private lazy val nodeMajorVersion: Option[Int] =
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about scala-cli policy, but I feel like detecting node version if it supports wasm or not is too much.

my 2 cents: scala-cli should loosely couple with the runtime environment, just try to run and let them fail if it's too old.

nodeMajorVersion.foreach { v =>
if (v < 22) value(Left(new NodeVersionTooOldForWasmError(v)))
}
val nodeFlags = if (emitWasm && nodeNeedsWasmFlag) List("--experimental-wasm-exnref") else Nil
Copy link
Copy Markdown

@tanishiking tanishiking May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think tools like scala-cli to hardcode Node options, and instead, let users explicitly specify Node options by themselves something like like: --node-args=--experimental-wasm-exnref,--experimental-wasm-imported-strings ?

in Node 26, options like --experimental-wasm-imported-strings is removed, and --experimental-wasm-exnref is now enabled by default (and may eventually be removed as well). If scala-cli hardcode options, we'll be in trouble when underlying runtime (node) removes options.

* @param runtime
* The WASM runtime to use for execution (node, deno)
*/
final case class WasmOptions(
Copy link
Copy Markdown

@tanishiking tanishiking May 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making wasm a standalone option sounds like it's an another platform like JS/JVM/Native/Wasm, but in reality everything is passed as scala-js options.

It makes more sense to register it as one of the scala-js options, like --js-wasm under ScalaJSOptions. WDYT?

@tanishiking
Copy link
Copy Markdown

tanishiking commented May 12, 2026

Just left some drive by comments. (Also, I think adding deno and bun should be in a separate issue/PR).
Anyway, good work, thank you :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Integrate WASM with Scala CLI

6 participants