Skip to content

fix(test): use synchronous module customization hooks#40891

Open
dgozman wants to merge 6 commits into
microsoft:mainfrom
dgozman:fix-40868
Open

fix(test): use synchronous module customization hooks#40891
dgozman wants to merge 6 commits into
microsoft:mainfrom
dgozman:fix-40868

Conversation

@dgozman
Copy link
Copy Markdown
Collaborator

@dgozman dgozman commented May 18, 2026

Summary

  • Prefer Node's synchronous module.registerHooks() over the deprecated module.register() (deprecated as of Node 26). The synchronous loader runs in-process: no .esm.preflight round-trip and no port transport, and it intercepts require() so the pirates-based CommonJS hook stands down.
  • Fall back to the asynchronous loader on older Node (Node 20.x has neither registerHooks nor reliable require(esm)); PLAYWRIGHT_FORCE_ASYNC_LOADER=1 opts back in to async on newer Node.
  • Several follow-up fixes for edge cases the new loader exposed: TypeScript reporter resolution, recording package.json as a test dep, CJS deps collection, and lazy pirates install when the async path is taken after another module triggered transform setup first.
  • Refactor: fold esmLoaderHost into transform.ts so the transform layer owns the full ESM-loader lifecycle (register hooks, push state to the worker thread, pull cache back). setSingleTSConfig / setTransformConfig are now async and propagate to the loader thread reactively, so configureESMLoader* calls are no longer needed in loadConfig. loaderChannel is the single source of truth for "we are on the async path".

Test matrix

Targeted suites (loader.spec.ts + global-setup.spec.ts, 70 tests) on every supported Node, exercising both loader paths where applicable:

Node Path Result
20.19.5 async (no registerHooks) 70 ✓
22.22.0 sync (default) 70 ✓
22.22.0 async (forced) 70 ✓
24.13.1 sync (default) 70 ✓
24.13.1 async (forced) 70 ✓
26.2.0 sync (default) 70 ✓
26.2.0 async (forced) 70 ✓

The original VSCode-extension codegen failure (globalSetup.js with mixed import/module.exports syntax) reproduced under Node 20 / PLAYWRIGHT_FORCE_ASYNC_LOADER=1 against playwright-vscode locally and now passes.

Fixes #40868

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

dgozman added 4 commits May 20, 2026 14:20
Node 26 deprecates `module.register()` in favour of the synchronous
`module.registerHooks()` API. Prefer the synchronous hooks when
available: they run in-process, need no preflight round-trip and no
port transport. Fall back to the asynchronous loader on older Node, and
expose `PLAYWRIGHT_FORCE_ASYNC_LOADER` as an opt-out.

Fixes: microsoft#40868
The synchronous module customization hooks do not intercept the
`require.resolve(id, { paths })` form, so `require.resolve()` of a
TypeScript reporter (or any extensionless TypeScript specifier) failed.
Register dummy loaders for our extensions so the default CommonJS
resolver considers them; the `load` hook still does the transformation.

Fixes: microsoft#40868
The synchronous module customization hooks intercept require(), so the
require() of package.json in folderIsModule() was recorded as a dependency
of the test file being loaded, confusing --only-changed. Read and parse
package.json directly instead.
The synchronous module customization hooks' resolve hook pre-populates
the global deps collector, so collectCJSDependencies, which uses the
collector itself as its visited set, would skip transitive deps under
any child the hook had already added. To make matters worse, Node short-
circuits the resolve hook for already-resolved (parent_dir, request)
pairs via its relativeResolveCache, so transitive deps shared between
sibling test files are silently missed.

Walk the CJS module tree into a fresh set first, then merge into the
global collector.
@github-actions

This comment has been minimized.

@github-actions

This comment has been minimized.

dgozman added 2 commits May 21, 2026 09:19
When loadReporter(null, …) runs before loadConfig — as the test server
does on runGlobalSetup — installTransformIfNeeded() was called before
registerESMLoader() got a chance to set _needsPreflightAndPirates.
That took the sync-only branch and skipped pirates for good, since
transformInstalled became true. On Node where the async loader is
picked (Node 20, or PLAYWRIGHT_FORCE_ASYNC_LOADER=1), pirates never
intercepted require() afterwards, so a CJS globalSetup.js with mixed
`import`/`module.exports` syntax got routed through Node 23+'s
require(esm) auto-detection and threw "module is not defined".

Split the install into a light step (extension shortcuts + source map
support) and a pirates step. Run the light step unconditionally — when
pirates installs later it overrides the same extensions. Have
setNeedsPreflightAndPirates() retroactively install pirates if the
light step already ran.

Fixes: microsoft#40868
Merge esmLoaderHost.ts into transform.ts so the transform layer owns the
full ESM loader lifecycle: register Node hooks, push state to the worker
thread, pair local deps collection with the worker notification, and pull
the cache back. transform.installTransformIfNeeded() now calls
registerESMLoader() itself, so any requireOrImport entry settles the
sync-vs-async decision before deciding whether to install pirates —
removing the ordering bug at the source.

setSingleTSConfig and setTransformConfig are async and push changes to
the loader thread inline. registerESMLoader seeds the worker with the
current snapshot when it falls back to the async loader. loadConfig no
longer needs configureESMLoader/configureESMLoaderTransformConfig — the
setters do it themselves. loaderChannel is the single source of truth
for "we have a worker": installTransformIfNeeded checks it to pick the
CJS-hooks path, requireOrImport checks it for the preflight.
@github-actions
Copy link
Copy Markdown
Contributor

Test results for "MCP"

7181 passed, 1113 skipped


Merge workflow run.

@github-actions
Copy link
Copy Markdown
Contributor

Test results for "tests 1"

3 flaky ⚠️ [chromium-library] › library/video.spec.ts:647 › screencast › should capture full viewport `@chromium-ubuntu-22.04-arm-node20`
⚠️ [chromium-library] › library/video.spec.ts:337 › screencast › should work for popups `@chromium-ubuntu-22.04-node24`
⚠️ [chromium-library] › library/beforeunload.spec.ts:130 › should support dismissing the dialog multiple times `@chromium-ubuntu-22.04-node22`

42061 passed, 850 skipped


Merge workflow run.

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.

[Bug]: module.register() deprecation warning (DEP0205) on Node 26

2 participants