[CLI] Auto restart with --experimental-wasm-jspi support#3281
[CLI] Auto restart with --experimental-wasm-jspi support#3281
Conversation
Until now, using JSPI with the CLI required manually passing --experimental-wasm-jspi or using the unbuilt-jspi NX target. This is easy to forget and means most users fall back to Asyncify without realizing it. The CLI now detects at startup whether JSPI could be enabled and respawns itself with the flag when needed. On Node 24+ where JSPI is unflagged, no respawn happens. On Node 22 where the flag exists but JSPI is non-functional, the child process sees the flag in execArgv and stops — no infinite loop. The heavy run-cli module is loaded via dynamic import only in the process that will actually run the server, keeping the parent process lightweight.
There was a problem hiding this comment.
Pull request overview
This PR adds automatic JSPI (JavaScript Promise Integration) enablement for the CLI by detecting at startup whether JSPI support can be enabled and respawning the process with the --experimental-wasm-jspi flag when appropriate. This eliminates the need for users to manually pass the flag or use special NX targets, preventing silent fallback to Asyncify.
Changes:
- Added detection logic to determine when process respawn is needed based on JSPI availability, existing flags, and Node.js version
- Modified CLI entry point to conditionally respawn with JSPI flag and handle signal forwarding for clean shutdown
- Changed to dynamic import of run-cli module to avoid loading heavy modules in parent process before respawn
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| packages/playground/cli/src/ensure-jspi.ts | New module implementing JSPI availability detection and respawn decision logic |
| packages/playground/cli/src/cli.ts | Updated entry point with respawn logic, signal handling, and dynamic import |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
The Vite externals list includes 'child_process' but not 'node:child_process'. The node: prefix caused Rollup to treat the module as a browser external, breaking the production build.
Node 22 ships V8 12.4 which only has the old JSPI API (WebAssembly.Suspender, removed in V8 12.8). The new API we need (WebAssembly.Suspending) arrived in V8 12.6 = Node 23. Respawning on Node 22 was harmless but wasteful — it would spawn a child, discover the flag didn't help, and fall back to Asyncify anyway. Bun reports process.versions.node as v22.x for compatibility but uses JavaScriptCore, which doesn't support JSPI or the --experimental-wasm-jspi flag. Deno uses V8 and has JSPI since 2.4, but handles it through its own mechanisms — the Node flag doesn't apply. Guard against both so we only respawn on actual Node.js.
If the child process exits within the first second with a non-zero code, the --experimental-wasm-jspi flag was likely rejected by the runtime. Instead of propagating the failure, the parent process continues and runs the CLI without JSPI. This makes the respawn attempt safe even on runtimes we haven't explicitly guarded against — the worst case is a brief failed spawn followed by normal Asyncify execution. Also handles spawn() errors (e.g. ENOENT) the same way.
|
We need to make sure the asyncify tests still run under asyncify |
The unbuilt-asyncify target runs cli.ts directly to test the Asyncify code path. On Node 24 (which the CI test script upgrades to), shouldRespawnWithJSPI() would return true and silently switch to JSPI, defeating the purpose of the test. PLAYGROUND_FORCE_ASYNCIFY=1 now opts out of the auto-respawn, and the unbuilt-asyncify target sets it.
daa4f32 to
87d905b
Compare
|
This is such a good idea. It's weird... I feel like I dreamed this this week. 😆 Then I saw the PR. |
|
This just came up in #3407. I'll go ahead and merge this one since it covers some more bases (such as non-Node runtimes). I'll just wait for the rebased branch to finish testing in CI. @wojtekn any concerns on Studio end? I don't expect so since Studio uses JSPI already, but I'm happy to rollback and revisit if needed. |
|
@adamziel Studio always passes |
Summary
Using JSPI with the CLI used to require manually passing
--experimental-wasm-jspior using theunbuilt-jspiNX target. Most users never do this, especially when running Playground CLI vianpx @wp-playground/cli, and silently fall back to Asyncify.The CLI now detects at startup whether JSPI could be enabled and respawns itself with the flag. On Node 24+ (JSPI unflagged), no respawn happens. On Node 22 (flag exists but JSPI non-functional), the child sees the flag in
execArgvand stops — no infinite loop. The heavyrun-climodule is only loaded via dynamic import in the process that actually runs the server.Test plan
npx nx dev playground-cli serveron Node 23 — should auto-respawn and use JSPInpx nx unbuilt-asyncify playground-cli -- serveron Node 22 — should try flag, fall back to Asyncify (no infinite loop)node --experimental-wasm-jspi packages/playground/cli/src/cli.ts server— should NOT respawn (flag already present)npx nx lint playground-cliandnpx nx typecheck playground-clipasscc @JanJakes @wojtekn