From 9e4882b0ee970024742b3a23516ba70f543d0e7a Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 18:16:49 -0400 Subject: [PATCH 1/2] test: cover the pre-22.15 register() fallback in helpers-jsx.mjs tests/helpers-jsx.mjs picks registerHooks() on Node >= 22.15 and register() on anything older. package.json pins engines.node to 22.x, so the second arm is live code for a supported interpreter (22.0-22.14) rather than dead compatibility ballast -- but it is unreachable from inside a test process, which always runs a Node where registerHooks exists, and CI pins the latest 22.x. The arm the oldest supported Node would take was the one arm nothing checked, and a regression in it breaks npm run test:unit outright there. Drives the fallback by redirecting the helper's own node:module import at a stub through a registerHooks resolve hook keyed on the importing module. No test-runner flags and no change to helpers-jsx.mjs itself. Signed-off-by: quality --- tests/helpers-jsx-legacy-register.test.mjs | 85 ++++++++++++++++++++++ tests/tools/legacy-module-stub.mjs | 16 ++++ 2 files changed, 101 insertions(+) create mode 100644 tests/helpers-jsx-legacy-register.test.mjs create mode 100644 tests/tools/legacy-module-stub.mjs diff --git a/tests/helpers-jsx-legacy-register.test.mjs b/tests/helpers-jsx-legacy-register.test.mjs new file mode 100644 index 00000000..f3b89766 --- /dev/null +++ b/tests/helpers-jsx-legacy-register.test.mjs @@ -0,0 +1,85 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { registerHooks } from 'node:module'; + +// `tests/helpers-jsx.mjs` picks one of two module-customization APIs: +// +// registerHooks(await import(HOOKS_URL.href)) // Node >= 22.15 +// register(HOOKS_URL) // everything older +// +// package.json pins `engines.node` to `22.x`, and `registerHooks` only landed +// in 22.15, so the second arm is live code for a supported interpreter rather +// than dead compatibility ballast. It is also unreachable from inside a test +// process, which always runs a Node where `registerHooks` exists — so the arm +// that the oldest supported Node would take was the one nothing checked. A +// regression there (a typo in the `register` call, the wrong argument shape) +// would be invisible in CI and would break `npm run test:unit` outright on +// 22.0-22.14, because every test that imports a `src/**` React file depends on +// those hooks being installed. +// +// The branch is driven by redirecting the helper's own `node:module` import at +// a stub, using a `registerHooks` resolve hook — public API, no flags. The +// redirect is keyed on the importing module, so only the cache-busted copy of +// the helper loaded below sees the stub; `node:module` resolves normally +// everywhere else in this process, including for the hook machinery itself. +const STUB_URL = new URL('./tools/legacy-module-stub.mjs', import.meta.url) + .href; + +// A fresh query string defeats the module cache, which matters twice over: the +// helper memoizes its registration in a module-scoped `registration`, and the +// instance under test has to be the one resolved through the redirect. +const HELPER_URL = new URL( + './helpers-jsx.mjs?legacy-register-fallback', + import.meta.url, +).href; + +function withModuleStubbedFor(importerUrl, run) { + const hooks = registerHooks({ + resolve(specifier, context, nextResolve) { + if (specifier === 'node:module' && context.parentURL === importerUrl) { + return { url: STUB_URL, shortCircuit: true }; + } + return nextResolve(specifier, context); + }, + }); + try { + return run(); + } finally { + hooks.deregister(); + } +} + +test('importSource falls back to register() when registerHooks is absent', async () => { + const { calls, importSource } = await withModuleStubbedFor( + HELPER_URL, + async () => { + const helper = await import(HELPER_URL); + const stub = await import(STUB_URL); + return { calls: stub.registerCalls, importSource: helper.importSource }; + }, + ); + + assert.deepEqual(calls, [], 'the helper must not register at import time'); + + // `src/lib/profile-links.mjs` is plain ESM, so it imports successfully even + // though the stubbed `register` installed nothing. That keeps the assertion + // on the helper's own contract -- the fallback still resolves the requested + // path against the repository root and returns its namespace -- rather than + // on what the hooks would have done. + const mod = await importSource('src/lib/profile-links.mjs'); + assert.equal(typeof mod.websiteUrl, 'function'); + + assert.equal(calls.length, 1, 'the legacy arm registers exactly once'); + // Asserted as the exact `file:` URL, not a suffix: `register()` resolves a + // bare string against the caller, so handing it a plain filesystem path + // instead of a URL would not register anything. + assert.equal( + calls[0], + new URL('./tools/jsx-hooks.mjs', import.meta.url).href, + ); + + // The memoized `registration` is shared by the fallback arm too: a second + // call must reuse it rather than register the hooks again. + await importSource('src/lib/profile-links.mjs'); + assert.equal(calls.length, 1, 'a second import reuses the registration'); +}); diff --git a/tests/tools/legacy-module-stub.mjs b/tests/tools/legacy-module-stub.mjs new file mode 100644 index 00000000..dd828422 --- /dev/null +++ b/tests/tools/legacy-module-stub.mjs @@ -0,0 +1,16 @@ +// A stand-in for `node:module` as it looked before Node 22.15, used by +// tests/helpers-jsx-legacy-register.test.mjs to reach the `register()` arm of +// tests/helpers-jsx.mjs on an interpreter where `registerHooks` exists. +// +// `registerHooks` is exported as `undefined` rather than omitted so the import +// in helpers-jsx.mjs still binds: a missing named export is a link-time error, +// which would fail the import instead of exercising the fallback. + +export const registerHooks = undefined; + +/** URLs passed to `register`, in call order. */ +export const registerCalls = []; + +export function register(url) { + registerCalls.push(String(url)); +} From a884e93a06d3d13fe00ba253621b5f9191694c30 Mon Sep 17 00:00:00 2001 From: quality Date: Thu, 8 Oct 2026 20:12:59 -0400 Subject: [PATCH 2/2] test: await the stubbed import before deregistering the module hook The resolve hook that redirects helpers-jsx.mjs's `node:module` import at the pre-22.15 stub was torn down on the synchronous return of `run()`, while the dynamic `import()` inside it still had the specifier to resolve in a later microtask. On Node 22 the hook was gone by then: the real builtin loaded, `registerHooks` was a function, and the fallback arm the test exists to cover was never taken ("the legacy arm registers exactly once", 0 !== 1). Awaiting `run()` keeps the hook installed for the whole load. Verified on node v22.23.3 and v26.10.0: 1/1 pass on both, and `npm run test:unit:coverage:check` exits 0 with helpers-jsx.mjs, helpers-jsx-legacy-register.test.mjs and tools/legacy-module-stub.mjs all at 100.00 lines / 100.00 regions. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: quality --- tests/helpers-jsx-legacy-register.test.mjs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/helpers-jsx-legacy-register.test.mjs b/tests/helpers-jsx-legacy-register.test.mjs index f3b89766..18d32dca 100644 --- a/tests/helpers-jsx-legacy-register.test.mjs +++ b/tests/helpers-jsx-legacy-register.test.mjs @@ -33,7 +33,12 @@ const HELPER_URL = new URL( import.meta.url, ).href; -function withModuleStubbedFor(importerUrl, run) { +// `run` is awaited before the hook is removed. Module resolution inside a +// dynamic `import()` happens in a later microtask, so deregistering on the +// synchronous return of `run()` would tear the hook down before the helper's +// `node:module` specifier is ever resolved -- the redirect would silently not +// apply and the real builtin (with a working `registerHooks`) would load. +async function withModuleStubbedFor(importerUrl, run) { const hooks = registerHooks({ resolve(specifier, context, nextResolve) { if (specifier === 'node:module' && context.parentURL === importerUrl) { @@ -43,7 +48,7 @@ function withModuleStubbedFor(importerUrl, run) { }, }); try { - return run(); + return await run(); } finally { hooks.deregister(); }