Skip to content

CLI: Auto-enable JSPI flags on Node.js 23+#3407

Closed
obenland wants to merge 2 commits intoWordPress:trunkfrom
obenland:cli/auto-enable-jspi
Closed

CLI: Auto-enable JSPI flags on Node.js 23+#3407
obenland wants to merge 2 commits intoWordPress:trunkfrom
obenland:cli/auto-enable-jspi

Conversation

@obenland
Copy link
Copy Markdown
Member

@obenland obenland commented Mar 17, 2026

I tried to see if the plugin-check plugin could be run against new plugin directory submissions from CLI via Playground, but the wp-cli blueprint step crashes on Asyncify builds because PHPCS uses proc_open() internally.

Summary

When running on Node.js 23+, the CLI now automatically re-executes itself with --experimental-wasm-jspi and --experimental-wasm-stack-switching flags if they are not already present.

Without JSPI, the CLI falls back to the Asyncify WASM build which crashes with RuntimeError: unreachable when PHP code triggers proc_open() — for example when running wp plugin check via the wp-cli blueprint step, which internally invokes PHPCS. JSPI handles these async WASM operations correctly.

On Node.js < 23, the behavior is unchanged (Asyncify remains the only option).

How it works

The entry point (cli.ts) checks:

  1. Is Node.js version >= 23?
  2. Are the JSPI flags already in process.execArgv?

If flags are missing, it re-executes the same command with the flags added to exec args using execFileSync with stdio: 'inherit', then exits with the child's exit code (or re-raises the signal if the child was terminated by one).

How I discovered this

While exploring using Playground CLI to run WordPress Plugin Check (PCP) before plugin submission, the wp-cli blueprint step consistently crashed:

Error: Error when executing the blueprint step #3: unreachable
    at php.wasm.wasm_sapi_handle_request (asyncify/php_8_5.js)
    at Object.doRewind (asyncify/php_8_5.js)

Running the same command with node --experimental-wasm-jspi --experimental-wasm-stack-switching cli.js ... worked perfectly — all 22 plugin_repo checks ran including plugin_review_phpcs (which uses proc_open internally for PHPCS).

The root cause: npx @wp-playground/cli spawns Node without JSPI flags, so wasm-feature-detect falls back to Asyncify, and Asyncify cannot handle the proc_open call chain.

Test plan

  • On Node.js 23+: npx @wp-playground/cli@latest run-blueprint --blueprint=<blueprint-with-wp-cli-step> should use JSPI (check for jspi/php_*.js in stack traces, not asyncify/php_*.js)
  • On Node.js 22: behavior unchanged, still uses Asyncify
  • wp plugin check via wp-cli blueprint step should complete without crashing
  • Verify the re-exec doesn't cause issues with --port, --mount, or other flags

Ref: #1872

When running on Node.js 23+, the CLI now automatically re-executes
itself with --experimental-wasm-jspi and
--experimental-wasm-stack-switching flags if they are not already
present.

Without JSPI, the CLI falls back to the Asyncify WASM build which
crashes with "RuntimeError: unreachable" when PHP code triggers
proc_open() — for example when running `wp plugin check` via
WP-CLI, which internally invokes PHPCS. JSPI handles these async
WASM operations correctly.

On Node.js < 23, the behavior is unchanged (Asyncify remains the
only option).

Ref: WordPress#1872
@obenland obenland requested review from a team, Copilot and zaerl March 17, 2026 16:10
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Automatically enables Node.js 23+ experimental JSPI/stack-switching flags by re-executing the CLI when the flags are missing, preventing Asyncify-related crashes for PHP proc_open() chains.

Changes:

  • Added a startup check for Node.js major version >= 23 and presence of JSPI flags.
  • Re-executes the current CLI command with missing JSPI flags using execFileSync and inherits stdio.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

- Use `error: unknown` with proper narrowing instead of `error: any`
- Handle signal-terminated children by re-raising the signal
- Add explicit `process.exit(0)` after successful re-exec
@adamziel
Copy link
Copy Markdown
Collaborator

Thank you so much @obenland! This is a great addition. Let's do it! I'll just move forward with #3281, not this PR, since that covers some more bases (such as running this code in bun).

@adamziel adamziel closed this Mar 17, 2026
@adamziel
Copy link
Copy Markdown
Collaborator

@obenland that other PR is now merge and will get released no later than Monday when the automated job runs.

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.

3 participants