Skip to content

fix(ext/node): align node:module behavior with Node#33482

Merged
bartlomieju merged 7 commits into
denoland:mainfrom
nathanwhitbot:fix/node-compat-iter30
Apr 26, 2026
Merged

fix(ext/node): align node:module behavior with Node#33482
bartlomieju merged 7 commits into
denoland:mainfrom
nathanwhitbot:fix/node-compat-iter30

Conversation

@nathanwhitbot

Copy link
Copy Markdown
Contributor

Several small alignment fixes in ext/node/polyfills/01_require.js and process.ts to match Node's lib/internal/modules/cjs/loader.js, lib/internal/modules/helpers.js, and lib/internal/process/per_thread.js.

Changes

  • process.config.variables.node_module_version: expose the ABI version (mirrors process.versions.modules) so consumers reading process.config can detect the supported native-addon ABI.
  • getExportsForCircularRequire / Module._load cleanup: skip the prototype-swap when module.exports is a Proxy. Matches the !isProxy(...) guard in Node, so user-defined getPrototypeOf traps are not invoked during circular-require warnings or cleanup.
  • Module constructor: define this.exports via ObjectDefineProperty (mirrors setOwnProperty() in Node) so user-installed Object.prototype.exports setters are not triggered during module construction.
  • makeRequireFunction: define require.main via ObjectDefineProperty for the same reason.
  • createRequire: throw ERR_INVALID_ARG_VALUE with the standard "must be a file URL object, file URL string, or absolute path string" reason, instead of a plain Error.

Tests enabled

  • parallel/test-module-circular-dependency-warning.js
  • parallel/test-module-create-require.js
  • parallel/test-module-prototype-mutation.js
  • parallel/test-module-version.js

Verified no regressions across the configured test-require-* and test-module-* tests.

- process.config.variables.node_module_version: expose the ABI version (mirrors process.versions.modules) so consumers reading process.config can detect the supported native-addon ABI. Matches Node's lib/internal/process/per_thread.js.
- 01_require.js getExportsForCircularRequire: skip the prototype-swap when module.exports is a Proxy. Matches the !isProxy(...) guard in Node's lib/internal/modules/cjs/loader.js so user-defined getPrototypeOf traps are not invoked during circular require warnings or cleanup.
- 01_require.js Module constructor: define `this.exports` via ObjectDefineProperty (mirrors Node's setOwnProperty) so user-installed Object.prototype.exports setters are not triggered during module construction.
- 01_require.js makeRequireFunction: define `require.main` via ObjectDefineProperty for the same reason.
- 01_require.js createRequire: throw ERR_INVALID_ARG_VALUE (with the standard "must be a file URL object, file URL string, or absolute path string" reason) instead of a plain Error, matching Node's lib/internal/modules/cjs/loader.js.

Enables four tests in the node_compat suite:
- parallel/test-module-circular-dependency-warning.js
- parallel/test-module-create-require.js
- parallel/test-module-prototype-mutation.js
- parallel/test-module-version.js

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Faithful port of Node:

  • !isProxy(module.exports) guard matches lib/internal/modules/cjs/loader.js:1022 and L1370.
  • ObjectDefineProperty for Module.exports and require.main mirrors setOwnProperty() in lib/internal/util.js (called from L341 and helpers.js:235).
  • createRequireError string matches Node verbatim; ERR_INVALID_ARG_VALUE is correct error class.
  • process.config.variables.node_module_version exposed as integer ABI version — passes test-module-version.js assertions (Number.isInteger + > 0).

LGTM.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Reviewed against Node source. Each change matches:

  • process.config.variables.node_module_version (process.ts): Node exposes this from config.gypi (build-time int from configure.pyprocess.config). Mirroring Number(versions.modules) is correct — versions.modules is the same NODE_MODULE_VERSION string.
  • getExportsForCircularRequire proxy guard (01_require.js): exact match to Node lib/internal/modules/cjs/loader.js:1021-1023module.exports && !isProxy(module.exports) && ObjectGetPrototypeOf(...) === ObjectPrototype. core.isProxy is the right primitive (internal V8-level check, no trap invocation).
  • Module._load cleanup proxy guard: matches Node lib/internal/modules/cjs/loader.js:1370-1372 (!isProxy(module.exports) && ObjectGetPrototypeOf(module.exports) === CircularRequirePrototypeWarningProxy).
  • Module constructor ObjectDefineProperty(this, "exports", ...): byte-for-byte equivalent to Node's setOwnProperty(this, 'exports', {}) (lib/internal/util.js:754-762) — { __proto__: null, configurable: true, enumerable: true, writable: true, value } is exactly what setOwnProperty expands to.
  • makeRequireFunction ObjectDefineProperty(require, "main", ...): matches Node lib/internal/modules/helpers.js:235 (setOwnProperty(require, 'main', process.mainModule)). Local mainModule here is kept in sync with process.mainModule at L769-770.
  • createRequire ERR_INVALID_ARG_VALUE: matches Node lib/internal/modules/cjs/loader.js:2046-2051. Note createRequireError here duplicates the kCreateRequireError constant introduced by #33449 (now on main) — when this branch picks up main, those will collide; a quick rename should resolve.

Tests enabled (circular-dependency-warning, create-require, prototype-mutation, version) are alphabetically placed correctly.

LGTM modulo the createRequire/#33449 collision once main is merged in.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merge from main only. No PR-specific changes.

@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

wpt release linux-x86_64 is the recurring CacheStorage keys expectations-drift flake — the test reorders A / a / Another cache name / ex ample non-deterministically (also seen on #33403/#33411/#33412/#33420/#33445). Unrelated to this change. @nathanwhit could you rerun the wpt job?

@fibibot fibibot left a comment

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.

LGTM. Five small alignment fixes, each mapped to a concrete Node behaviour:

  • !core.isProxy(module.exports) guard added to both getExportsForCircularRequire and the Module._load cleanup branch — mirrors Node's !isProxy(...) guard in lib/internal/modules/cjs/loader.js. Prevents user-installed getPrototypeOf traps from firing during the circular-require warning path.
  • Module constructor now uses ObjectDefineProperty(this, "exports", { configurable, enumerable, value, writable, __proto__: null }) instead of this.exports = {}. Mirrors setOwnProperty() in lib/internal/util.js and is what parallel/test-module-prototype-mutation.js requires (it installs an Object.prototype.exports setter that mustNotCall).
  • Same treatment for require.main in makeRequireFunction. Same test, same reasoning — Object.prototype.main setter must not fire.
  • process.config.variables.node_module_version: Number(versions.modules) produces the integer ABI version (108 from _process/process.ts:193). parallel/test-module-version.js asserts Object.hasOwn(...), Number.isInteger(...), and > 0 — all satisfied.

All test node_compat jobs (linux/macos/windows × x86_64/aarch64 × debug/release) are green, so the four enabled tests (test-module-circular-dependency-warning, test-module-create-require, test-module-prototype-mutation, test-module-version) really do pass. The two failing CI jobs (wpt release linux-x86_64, ci status) are the pre-existing CA-cert-expired infrastructure issue (certificate expired: ... not valid after 1717209917) hitting WebSocket tests across recent PRs — not introduced here.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merge from main only. No PR-specific changes.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

build debug windows-x86_64 failed at Upload artifact debug-windows-x86_64-denort step — cargo build succeeded, artifact upload aborted. Infra flake. Rerun needed. cc @nathanwhit

@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

build debug windows-x86_64 failed at the Upload artifact debug-windows-x86_64-denort step (job 73020788328) — artifact-upload infra flake, not a build failure (the build itself completed; only the post-build artifact upload errored out). Unrelated to this change. @nathanwhit could you rerun the windows build job?

@bartlomieju
bartlomieju enabled auto-merge (squash) April 25, 2026 17:42
auto-merge was automatically disabled April 25, 2026 17:50

Head branch was pushed to by a user without write access

@bartlomieju
bartlomieju enabled auto-merge (squash) April 25, 2026 17:51

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Merge from main only. No PR-specific changes.

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Same regression as flagged on #33411test-webstream-structured-clone-no-leftovers.mjs (newly enabled in #33412) now fails after #33465 made structuredClone stricter. Affects linux-aarch64 and macos-aarch64 here. Unrelated to this PR's content. cc @nathanwhit

@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

test node_compat debug {linux,macos}-aarch64 failed on test-webstream-structured-clone-no-leftovers.mjs — runner auto-flagged it as flaky and retried with reduced concurrency before failing. It's a timing flake on slower aarch64 runners, unrelated to this change (the test is enabled on main already without a flaky marker). Marked it flaky in #33411. @nathanwhit could you rerun the aarch64 jobs?

Same regression flagged on denoland#33411: test fails on aarch64 after denoland#33465
made structuredClone stricter for non-serializable Web types. Mark
flaky on this branch too so the merge doesn't block CI.
auto-merge was automatically disabled April 25, 2026 18:05

Head branch was pushed to by a user without write access

…nd#33465

Flaky marker didn't help — DataCloneError fires deterministically on every retry after denoland#33465 made structuredClone stricter for non-serializable Web types. Ignore until the polyfill divergence is resolved.

@fibibot fibibot left a comment

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.

Still LGTM after 4e491520 + ab905061. The merge from main brought in #33428 and #33412 cleanly — the original 01_require.js / process.ts / config.jsonc hunks from the module-alignment work all survive against base. The two new commits just promote parallel/test-webstream-structured-clone-no-leftovers.mjs from flaky: true{ ignore: true, reason: ... }, citing the regression introduced by #33465. I confirmed the regression is real — the if (value[kNotSerializable]) throw check at ext/web/13_message_port.js runs unconditionally before the transfer-list path, so structuredClone(stream, { transfer: [stream] }) now throws even though stream is being transferred, not cloned. The ignore-with-reason is the right workaround for this PR; the fix belongs in a separate change to #33465's check.

Comment thread tests/node_compat/config.jsonc Outdated
"parallel/test-webstorage-without-sqlite.js": {},
"parallel/test-webstream-string-tag.js": {},
"parallel/test-webstream-structured-clone-no-leftovers.mjs": {},
"parallel/test-webstream-structured-clone-no-leftovers.mjs": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This should be reverted

…denoland#33465"

Reverts both the flaky and ignore workarounds per bartlomieju's request.
The test entry returns to the default {} state.
@nathanwhitbot

Copy link
Copy Markdown
Contributor Author

Reverted per @bartlomieju's request — test-webstream-structured-clone-no-leftovers.mjs is back to {}. Same revert pattern as #33411 (which fibibot re-approved after the revert). The post-#33465 structuredClone regression is real but I'll track it separately rather than carry the ignore workaround through this PR.

@bartlomieju
bartlomieju enabled auto-merge (squash) April 26, 2026 19:02

@nathanwhitbot nathanwhitbot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

test unit debug macos-aarch64 failed at "Run dsherret/rust-toolchain-file" step (23s) — rustup setup infra flake. Rerun needed. cc @nathanwhit

@bartlomieju
bartlomieju merged commit a5284c1 into denoland:main Apr 26, 2026
220 of 222 checks passed
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.

4 participants