Skip to content

[Fix] Pass NODE_OPTIONS to Electron as arguments in the macOS shims - #528

Merged
juanmaguitar merged 3 commits into
trunkfrom
fix/forward-node-options-macos
Sep 24, 2026
Merged

juanmaguitar merged 3 commits into
trunkfrom
fix/forward-node-options-macos

Conversation

@juanmaguitar

@juanmaguitar juanmaguitar commented Sep 24, 2026 •

Copy link
Copy Markdown
Collaborator

Why

On macOS, "Apply and rebuild" fails on Gutenberg pull requests based on trunk from before 17 April 2026, such as WordPress/gutenberg#76292. The screen says only Build failed: Command failed: npm run --if-present --workspaces --silent build, exit 1. Without --silent, @wordpress/theme's build:tokens fails:

[ERROR:electron/shell/app/node_main.cc:153] Node.js environment variables are disabled because this process is invoked by other apps.
✗  [config] Cannot find module '.../packages/theme/bin/terrazzo-plugin-mode-overrides/index'

Reported in #525.

What changes

Root cause: the Gutenberg script runs cross-env NODE_OPTIONS=--import=esbuild-esm-loader/register tz build. tz starts through our bash node shim, and on macOS a signed Electron removes NODE_OPTIONS from any process whose parent is not part of the app, so the --import never arrives. It is not a regression from #519: the main binary fails the same way as the Helper, and the same build passes on the system node.

The macOS node/npm/npx shims now split NODE_OPTIONS the way Node does (quotes, backslash escapes, no globbing) and pass the options as arguments, after our --require and before the CLI and the caller's own arguments. Then they unset the variable, so an unsigned Electron (npm start, CI) does not apply every option twice and dev behaves like the release. Unset, not emptied: a signed Electron prints a node_main.cc:153 warning whenever the variable exists, even empty. Linux and Windows shims are byte-identical to before.

Deliberately not in it: only the shim's own child gets the options. What that child starts inherits no NODE_OPTIONS, as before this PR (follow-up below).

How to test this

Platforms: macOS only; the change is macOS only. Build: the Buildkite artifact for the current head.

Starting state: a Gutenberg site on trunk, no work item linked, no pull request checked out. A copy you can throw away is best: the build rewrites packages/theme/tokens/color.json.

  1. In Check out a pull request, enter 76292, check it out and let the install finish.
  2. Click Apply and rebuild. Expected: the build finishes. On beta.2 it fails with the message above.
  3. Unlink to go back to trunk and rebuild. Expected: the trunk build finishes as before.

What must not have happened: no node_main.cc:153 Node.js environment variables are disabled lines in the terminal; no Dock tiles while the build runs (#518).

If the root build then fails on routes/dashboard/package.json, that is a different bug (a trunk-only route left behind with only node_modules, same with the system node); delete routes/dashboard/ on the test site and rebuild.

I reproduced the failure and checked the fix on a copy of a real site on the PR 76292 commit, running the app's exact command, npm run --if-present --workspaces --silent build:

  • the installed beta.2 shims: exit 1;
  • shims generated from this branch, with the same Helper: exit 0;
  • the system node: exit 0.

The new tests in tests/unit/node-shims.test.cjs run the real shim under /bin/bash (3.2, as macOS ships it) and fail on the old code.

Risks and limitations

  • An option that Node accepts only in NODE_OPTIONS but not as a flag: none found. Command-line-only flags are now accepted, which is more lenient, not broken.
  • Malformed NODE_OPTIONS (unterminated quote) is passed through as text where Node would refuse it.
  • Self-review (fresh context): no bugs; 2 fix-here findings applied (the variable cleared for the call, /bin/bash in the tests) and the wording corrected. 1 follow-up, the grandchild gap above.
  • The first version of that fix emptied the variable instead of unsetting it, and the signed Buildkite build then printed the warning for every process. The unit tests use a stand-in binary, and CI never runs a signed Electron, so only the test's expectation guards this now (it asserts the variable arrives unset).
  • Checked on the copy with the installed signed Helper: the workspace build and the full npm run build exit 0 with no warnings.

Related

Fixes #525. Found while testing #524.

🤖 Generated with Claude Code

juanmaguitar and others added 2 commits September 24, 2026 12:29
On macOS, Electron ignores NODE_OPTIONS in any process the app did not
start itself, and every child launched through the node shim is started
by bash. A build that sets NODE_OPTIONS for a child lost it: older
Gutenberg trunk runs `cross-env NODE_OPTIONS=--import=... tz build` in
@wordpress/theme, so "Apply and rebuild" failed on pull requests based
on it (reproduced with gutenberg#76292).

The macOS shims now split NODE_OPTIONS the way Node does and pass the
options as arguments ahead of the CLI and the caller's own. Linux and
Windows shims are unchanged.

Fixes #525

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ch signed ones

An unsigned Electron (npm start, CI) still reads NODE_OPTIONS from a
bash parent, so it applied every forwarded option twice, and a --loader
registered twice. The shims now empty the variable for that call. The
tests run the shim under /bin/bash, the 3.2 users get, and the comments
say what a signed Electron actually does: it removes the variable, so
only the shim's own child gets the options.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juanmaguitar juanmaguitar added this to the v1.2.0 milestone Sep 24, 2026
@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Navigate logical layers of code changes, visualize relationships, and explore their blast radius.

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository: WordPress/contributor-toolkit/.coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: a36bfd95-e05a-40f3-a9c6-3a9366b7fbc5

📥 Commits

Reviewing files that changed from the base of the PR and between 277202a and 9766088.

📒 Files selected for processing (3)
  • AGENTS.md
  • src/node-shims.cjs
  • tests/unit/node-shims.test.cjs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

On macOS, the POSIX node, npm, and npx shims tokenize NODE_OPTIONS, pass the resulting options as command-line arguments, and unset the variable. Linux and Windows shim behavior remains unchanged. Tests cover option parsing, argument ordering, environment handling, and platform-specific output.

Assessment against linked issues

Objective Addressed Explanation
Forward macOS NODE_OPTIONS to the Node process as command-line flags, including values that require quoting or escaping [#525] ✅
Add tests for the macOS shim forwarding behavior [#525] ✅

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 97660

The macOS shims forward options to Electron as intended; no identified issue blocks merging after normal checks.

🚥 Pre-merge checks | ✅ 1
✅ Passed checks (1 passed)
Check name Status Explanation
Description check ✅ Passed The description includes the required Why, What changes, testing, risks, and Related sections. It documents the root cause, scope, platform coverage, regression checks, limitations, and test evidence.…

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

A signed Electron prints "Node.js environment variables are disabled
because this process is invoked by other apps" whenever NODE_OPTIONS is
present, even empty, so emptying it for the Electron call printed that
line for every process a Gutenberg build starts. Unset, Electron never
sees it: no warning, and the options still arrive once, as arguments.

The test expected an empty variable, which is what let this through; it
now expects the variable unset.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@juanmaguitar

Copy link
Copy Markdown
Collaborator Author

Manual pass on macOS, Buildkite artifact for 9766088, on a new Gutenberg site with PR 76292 checked out:

  • The workspace build, including @wordpress/theme's build:tokens that failed on beta.2, now passes.
  • No node_main.cc:153 Node.js environment variables are disabled lines anywhere in the terminal.
  • The root build then failed on routes/dashboard/package.json, which is Switching to an older Gutenberg pull request leaves trunk-only directories that break wp-build #529 (a trunk-only directory left behind with its node_modules, same with the system node). After rm -rf routes/dashboard, npm run build finished: Build completed successfully, exit 0.

@juanmaguitar

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Sep 24, 2026 •

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@juanmaguitar
juanmaguitar merged commit 375f962 into trunk Sep 24, 2026
8 checks passed
@juanmaguitar
juanmaguitar deleted the fix/forward-node-options-macos branch September 24, 2026 11:44
juanmaguitar added a commit that referenced this pull request Sep 25, 2026
## Why

`v1.2.0-beta.2` shipped on 23 September. Five commits reach trunk since
then: #524, #528, #530 and #532 fix what the beta.2 testing turned up
(files another branch ignored counted as changes after a switch,
`NODE_OPTIONS` lost in the macOS shims, and a trunk-only Gutenberg route
left behind by a switch to an older pull request, which broke
`wp-build`), and #531 makes the manual **Real WordPress setup** workflow
pass again, green on both platforms and both targets on trunk's app
code. Milestone `v1.2.0` holds only the umbrella #251.

`electron-builder` derives artifact names from `package.json`, so the
version has to move before the tag and the artifacts exist. This is a
stable release: it takes `releases/latest` from `v1.1.0`.

## What changes

Only the version. All three package-version fields in `package.json` and
`package-lock.json` move together from `1.2.0-beta.2` to `1.2.0`. No
dependency versions change, and there is no application behaviour change
in this PR.

## How to test this

Platforms: any for the suite; macOS and Windows for the artifact checks
that follow the merge.

1. `npm run lint` is clean and `npm test` passes, from the repository
root.
2. `node -e "console.log(require('./package.json').version)"` prints
`1.2.0`.
3. `python3 -c "import
json;d=json.load(open('package-lock.json'));print(d['version'],
d['packages']['']['version'])"` prints `1.2.0 1.2.0`.

**What must not have happened:** no dependency version may change. `git
diff` must show exactly three changed lines, all of them the root
package version.

After merge, the release build must produce artifacts named
`wordpress-contributor-toolkit-1.2.0-*`; the asar root must be exactly
`node_modules`, `package.json`, `src`, with npm 11 under
`node_modules/npm` and no `.codesigning` directory; on macOS
`CFBundleShortVersionString` reads `1.2.0`.

## Risks and limitations

No UI change. The risk is a mis-scoped edit in `package-lock.json`,
where dependencies also carry version fields; the replacement was
confined to the first 2000 bytes of the file, which hold the two root
entries and nothing else, and both files were re-parsed as JSON
afterwards.

Manual passes on what reached trunk after beta.2: #528 and #530 on macOS
in their PRs; #530 plus #532 on Windows on #532's artifact (c7ec033),
including the case that failed on e0a3faa.

## Related

Part of #251. Release notes follow on the release; docs publish for the
tag per #487.

---

<details>
<summary>Review outcome (required, see AGENTS.md)</summary>

0 [fix here] · 0 [follow-up]. Deterministic layer only: lint clean, 1667
tests pass, both files valid JSON, diff confined to the three root
version fields. No judgement pass was run: this change has no logic to
review.

- **Review:** completed, deterministic layer, in session; head is the
single bump commit / base `4c3b05a` (trunk); evidence above; outcome:
nothing to fix.
- **Since review:** none.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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.

Builds that set NODE_OPTIONS fail on macOS because Electron ignores it in the app's Node children

1 participant