Skip to content

fix: hand Node.js' null source back as null; cover the load hook's dependency and addon edge cases - #767

Merged
Brooooooklyn merged 2 commits into
mainfrom
fix/register-hooks-sync-followup
Sep 18, 2026
Merged

Brooooooklyn merged 2 commits into
mainfrom
fix/register-hooks-sync-followup

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Sep 18, 2026

Copy link
Copy Markdown
Member

Follow-up to #764 (now on main), for the notes left in its review. Two commits.

Summary

transform_output                                   register.mjs
  no-source arm ─► source: None ─► undefined        startsWith("commonjs") also matched
  addon translator asserts === null ─► throws         commonjs-typescript ─► null ─► throws
  fix: Some(Either4::D(Null))                        fix: === "commonjs" (JS + Rust guard)

LoadContext.format: Either<String, Null>
  Node passes format: undefined for an unknown extension (.node under
  node_modules, flag off) ─► napi "Missing field `format`"
  fix: Option<…> so Node's own ERR_UNKNOWN_FILE_EXTENSION shows

The first and third are pre-existing on both hook paths (shared code); only the startsWith came in with #764.

Testsregister-hooks.spec.ts, 20 cases: local .node import (with and without --experimental-addon-modules), require() of a .node, latin-1; dependencies × OXC_TRANSFORM_ALL false/true set in-spec: addon with the flag, addon following Node's default (loads on ≥ 24.19 / ≥ 26.5, ERR_UNKNOWN_FILE_EXTENSION otherwise), latin-1, .cts and .mts → Node's own ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING; --conditions=require|import in both directions. Failing cases also assert neither ERR_INVALID_RETURN_PROPERTY_VALUE nor Missing field appears.

CItest-linux-binding gains Node 26 (armv7 excluded: node:26-slim ships no arm/v7 image, same as 24), so the synchronous path runs on every OS in the matrix.

Test plan

  • Full suite 95/95 on v22.23.2, v24.11.1, v26.9.0 (direct vitest; local pnpm test resolves Vite+'s bundled 24.x and only exercises the fallback)
  • Spec alone on v24.21.0 (default-on addon branch on the 24 line) and with an outer OXC_TRANSFORM_ALL=true
  • Each new test red before its fix, green after
  • Rebased onto main after fix: register module hooks synchronously on Node.js >= 26.2.0 #764 landed; cargo clippy -D warnings, cargo fmt --check, cargo test, vp check
  • CI green across the 22/24/26 × transform-all matrix on all hosts

Generated with Devin

@cursor

cursor Bot commented Sep 18, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_000d841b-c0b8-4588-a77a-7b7b66b05f32)

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 18, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-18T07:04:26.791095Z ba923c2 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

Brooooooklyn and others added 2 commits September 18, 2026 15:01
…ipt off the deferral

Node.js' `addon` translator asserts `source === null` for a `.node` file
under node_modules, but `transform_output` dropped the `Option` and the
result reached JavaScript as `undefined`, on both the asynchronous and
the synchronous hooks. The `startsWith("commonjs")` deferral also caught
`commonjs-typescript`, whose translator needs the source, turning Node's
own type-stripping error into one blaming the hook.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…very matrix runtime

Node.js hands the load hook `format: undefined` when the resolve chain
reported none — a `.node` under node_modules without its flag — and the
required `LoadContext.format` rejected the whole context with "Missing
field `format`" where Node.js would have raised ERR_UNKNOWN_FILE_EXTENSION.
Make it optional so Node.js' own error is what shows.

The dependency cases (addon with and without the flag, latin-1, `.cts`,
`.mts`) now run under both OXC_TRANSFORM_ALL values in-spec, with the
addon default keyed on the runtime (on from v24.19.0 / v26.5.0), and the
Linux matrix gains Node.js 26 so the synchronous hooks are exercised there
as well as on macOS and Windows.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@Brooooooklyn
Brooooooklyn force-pushed the fix/register-hooks-sync-followup branch from ba923c2 to f8c9b60 Compare September 18, 2026 07:01
@cursor

cursor Bot commented Sep 18, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_ffd3dffa-6c7e-4f58-8f61-5fa5a48a8fc1)

@Brooooooklyn Brooooooklyn changed the title fix: register module hooks synchronously on Node.js >= 26.2.0 fix: hand Node.js' null source back as null; cover the load hook's dependency and addon edge cases Sep 18, 2026
@chatgpt-codex-connector

Copy link
Copy Markdown

💡 Codex Review

registerHooks({ load, resolve });

P2 Badge Preserve later asynchronous resolve hooks

When a process imports this registration module and subsequently installs another loader with module.register(), the synchronous hook always runs before that asynchronous chain. Because createResolve() short-circuits every specifier it successfully resolves, the later loader's resolve hook is never called for ordinary local/package imports, so loaders that redirect or annotate those imports silently stop working on Node.js ≥26.2.0; with the previous asynchronous registration, the later hook wrapped this one and could perform that resolution first. The synchronous path needs to preserve composition with subsequently registered asynchronous loaders rather than short-circuiting them.

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Brooooooklyn

Copy link
Copy Markdown
Member Author

Re the Codex P2 on registerHooks({ load, resolve }) — checked it against real binaries rather than taking it as read, with --import @oxc-node/core/register --import ./later-loader-register.mjs where the later loader is a plain module.register() loader that logs what it sees:

Node 26.9 (oxc = sync, later loader = async)      Node 24.11 (both async, later wraps oxc)
  [later resolve] file:///…/dyn.mts                 [later resolve] ./dyn
  [later load]    source len 28  (raw .mts)         [later load]    source len 989 (oxc output)
  extensionless: 5                                  extensionless: 5

So the claim as stated is not right: the later loader's resolve is called for every ordinary import — createResolve short-circuits but still hands the resolved URL down the chain. What changed is the position: it now receives the resolved file: URL instead of the raw specifier, and its load runs inside oxc-node's rather than around it. That is how Node.js orders things — ModuleLoader.resolveresolveWithHooks, whose terminal nextResolve is the async hook chain — and it applies to every loader that moves to registerHooks(), which is what DEP0205 asks for. The only way for a synchronous hook to "preserve composition" would be to call nextResolve(specifier) first and use oxc-node's resolver only on failure, which reverses this loader's resolution precedence (tsconfig paths, the .js.ts alias when both exist) — a product decision about #764's design, which is already on main, and not a defect in this PR; the flagged line is unchanged here.

Not acting on it in #767. If chaining with other module.register() loaders is something we want to guarantee, that's a README note ("register oxc-node's hooks last" no longer changes the order on ≥ 26.2) or a separate discussion, not a fix here.

@Brooooooklyn
Brooooooklyn merged commit 09a6bfb into main Sep 18, 2026
68 checks passed
@Brooooooklyn
Brooooooklyn deleted the fix/register-hooks-sync-followup branch September 18, 2026 07:54
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.

1 participant