fix(esm): >= Node.js 21 globalPreload -> initialize (#28526)
https://github.com/microsoft/playwright/issues/28524 https://github.com/microsoft/playwright/issues/28732 https://nodejs.org/en/blog/announcements/v21-release-announce#module-customization-hook-globalpreload-removed-use-register-and-initialize-instead
This commit is contained in:
parent
37634df0c4
commit
7670fd21e2
|
|
@ -21,7 +21,7 @@ import fs from 'fs';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { Runner } from './runner/runner';
|
import { Runner } from './runner/runner';
|
||||||
import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
||||||
import { execArgvWithoutExperimentalLoaderOptions, execArgvWithExperimentalLoaderOptions, fileIsModule, serializeError } from './util';
|
import { fileIsModule, serializeError } from './util';
|
||||||
import { showHTMLReport } from './reporters/html';
|
import { showHTMLReport } from './reporters/html';
|
||||||
import { createMergedReport } from './reporters/merge';
|
import { createMergedReport } from './reporters/merge';
|
||||||
import { ConfigLoader, resolveConfigFile } from './common/configLoader';
|
import { ConfigLoader, resolveConfigFile } from './common/configLoader';
|
||||||
|
|
@ -33,6 +33,8 @@ import type { FullConfigInternal } from './common/config';
|
||||||
import program from 'playwright-core/lib/cli/program';
|
import program from 'playwright-core/lib/cli/program';
|
||||||
import type { ReporterDescription } from '../types/test';
|
import type { ReporterDescription } from '../types/test';
|
||||||
import { prepareErrorStack } from './reporters/base';
|
import { prepareErrorStack } from './reporters/base';
|
||||||
|
import { registerESMLoader } from './common/esmLoaderHost';
|
||||||
|
import { execArgvWithExperimentalLoaderOptions, execArgvWithoutExperimentalLoaderOptions } from './transform/esmUtils';
|
||||||
|
|
||||||
function addTestCommand(program: Command) {
|
function addTestCommand(program: Command) {
|
||||||
const command = program.command('test [test-filter...]');
|
const command = program.command('test [test-filter...]');
|
||||||
|
|
@ -277,26 +279,33 @@ function restartWithExperimentalTsEsm(configFile: string | null): boolean {
|
||||||
return false;
|
return false;
|
||||||
if (process.env.PW_DISABLE_TS_ESM)
|
if (process.env.PW_DISABLE_TS_ESM)
|
||||||
return false;
|
return false;
|
||||||
if (process.env.PW_TS_ESM_ON) {
|
// Node.js < 20
|
||||||
|
if ((globalThis as any).__esmLoaderPortPreV20) {
|
||||||
// clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader.
|
// clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader.
|
||||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!fileIsModule(configFile))
|
if (!fileIsModule(configFile))
|
||||||
return false;
|
return false;
|
||||||
const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('./cli'), process.argv.slice(2), {
|
// Node.js < 20
|
||||||
env: {
|
if (!require('node:module').register) {
|
||||||
...process.env,
|
const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('./cli'), process.argv.slice(2), {
|
||||||
PW_TS_ESM_ON: '1',
|
env: {
|
||||||
},
|
...process.env,
|
||||||
execArgv: execArgvWithExperimentalLoaderOptions(),
|
PW_TS_ESM_LEGACY_LOADER_ON: '1',
|
||||||
});
|
},
|
||||||
|
execArgv: execArgvWithExperimentalLoaderOptions(),
|
||||||
|
});
|
||||||
|
|
||||||
innerProcess.on('close', (code: number | null) => {
|
innerProcess.on('close', (code: number | null) => {
|
||||||
if (code !== 0 && code !== null)
|
if (code !== 0 && code !== null)
|
||||||
gracefullyProcessExitDoNotHang(code);
|
gracefullyProcessExitDoNotHang(code);
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
|
// Nodejs >= 21
|
||||||
|
registerESMLoader();
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure'];
|
const kTraceModes: TraceMode[] = ['on', 'off', 'on-first-retry', 'on-all-retries', 'retain-on-failure'];
|
||||||
|
|
|
||||||
|
|
@ -14,16 +14,36 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import url from 'url';
|
||||||
import { addToCompilationCache, serializeCompilationCache } from '../transform/compilationCache';
|
import { addToCompilationCache, serializeCompilationCache } from '../transform/compilationCache';
|
||||||
import { transformConfig } from '../transform/transform';
|
import { transformConfig } from '../transform/transform';
|
||||||
import { PortTransport } from '../transform/portTransport';
|
import { PortTransport } from '../transform/portTransport';
|
||||||
|
|
||||||
const port = (globalThis as any).__esmLoaderPort;
|
let loaderChannel: PortTransport | undefined;
|
||||||
|
// Node.js < 20
|
||||||
|
if ((globalThis as any).__esmLoaderPortPreV20)
|
||||||
|
loaderChannel = createPortTransport((globalThis as any).__esmLoaderPortPreV20);
|
||||||
|
|
||||||
const loaderChannel = port ? new PortTransport(port, async (method, params) => {
|
// Node.js >= 20
|
||||||
if (method === 'pushToCompilationCache')
|
export let esmLoaderRegistered = false;
|
||||||
addToCompilationCache(params.cache);
|
export function registerESMLoader() {
|
||||||
}) : undefined;
|
const { port1, port2 } = new MessageChannel();
|
||||||
|
// register will wait until the loader is initialized.
|
||||||
|
require('node:module').register(url.pathToFileURL(require.resolve('../transform/esmLoader')), {
|
||||||
|
parentURL: url.pathToFileURL(__filename),
|
||||||
|
data: { port: port2 },
|
||||||
|
transferList: [port2],
|
||||||
|
});
|
||||||
|
loaderChannel = createPortTransport(port1);
|
||||||
|
esmLoaderRegistered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPortTransport(port: MessagePort) {
|
||||||
|
return new PortTransport(port, async (method, params) => {
|
||||||
|
if (method === 'pushToCompilationCache')
|
||||||
|
addToCompilationCache(params.cache);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function startCollectingFileDeps() {
|
export async function startCollectingFileDeps() {
|
||||||
if (!loaderChannel)
|
if (!loaderChannel)
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,9 @@ import type { WriteStream } from 'tty';
|
||||||
import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc';
|
import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc';
|
||||||
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
import { startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||||
import type { TestInfoError } from '../../types/test';
|
import type { TestInfoError } from '../../types/test';
|
||||||
import { execArgvWithoutExperimentalLoaderOptions, serializeError } from '../util';
|
import { serializeError } from '../util';
|
||||||
|
import { registerESMLoader } from './esmLoaderHost';
|
||||||
|
import { execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||||
|
|
||||||
export type ProtocolRequest = {
|
export type ProtocolRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
@ -54,6 +56,10 @@ process.on('SIGTERM', () => {});
|
||||||
// Clear execArgv immediately, so that the user-code does not inherit our loader.
|
// Clear execArgv immediately, so that the user-code does not inherit our loader.
|
||||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||||
|
|
||||||
|
// Node.js >= 20
|
||||||
|
if (process.env.PW_TS_ESM_LOADER_ON)
|
||||||
|
registerESMLoader();
|
||||||
|
|
||||||
let processRunner: ProcessRunner | undefined;
|
let processRunner: ProcessRunner | undefined;
|
||||||
let processName: string | undefined;
|
let processName: string | undefined;
|
||||||
const startingEnv = { ...process.env };
|
const startingEnv = { ...process.env };
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,9 @@ import { EventEmitter } from 'events';
|
||||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||||
import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc';
|
import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc';
|
||||||
import type { ProtocolResponse } from '../common/process';
|
import type { ProtocolResponse } from '../common/process';
|
||||||
import { execArgvWithExperimentalLoaderOptions } from '../util';
|
import { execArgvWithExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||||
import { assert } from 'playwright-core/lib/utils';
|
import { assert } from 'playwright-core/lib/utils';
|
||||||
|
import { esmLoaderRegistered } from '../common/esmLoaderHost';
|
||||||
|
|
||||||
export type ProcessExitData = {
|
export type ProcessExitData = {
|
||||||
unexpectedly: boolean;
|
unexpectedly: boolean;
|
||||||
|
|
@ -51,14 +52,18 @@ export class ProcessHost extends EventEmitter {
|
||||||
assert(!this.process, 'Internal error: starting the same process twice');
|
assert(!this.process, 'Internal error: starting the same process twice');
|
||||||
this.process = child_process.fork(require.resolve('../common/process'), {
|
this.process = child_process.fork(require.resolve('../common/process'), {
|
||||||
detached: false,
|
detached: false,
|
||||||
env: { ...process.env, ...this._extraEnv },
|
env: {
|
||||||
|
...process.env,
|
||||||
|
...this._extraEnv,
|
||||||
|
...(esmLoaderRegistered ? { PW_TS_ESM_LOADER_ON: '1' } : {}),
|
||||||
|
},
|
||||||
stdio: [
|
stdio: [
|
||||||
'ignore',
|
'ignore',
|
||||||
options.onStdOut ? 'pipe' : 'inherit',
|
options.onStdOut ? 'pipe' : 'inherit',
|
||||||
(options.onStdErr && !process.env.PW_RUNNER_DEBUG) ? 'pipe' : 'inherit',
|
(options.onStdErr && !process.env.PW_RUNNER_DEBUG) ? 'pipe' : 'inherit',
|
||||||
'ipc',
|
'ipc',
|
||||||
],
|
],
|
||||||
...(process.env.PW_TS_ESM_ON ? { execArgv: execArgvWithExperimentalLoaderOptions() } : {}),
|
...(process.env.PW_TS_ESM_LEGACY_LOADER_ON ? { execArgv: execArgvWithExperimentalLoaderOptions() } : {}),
|
||||||
});
|
});
|
||||||
this.process.on('exit', async (code, signal) => {
|
this.process.on('exit', async (code, signal) => {
|
||||||
this._processDidExit = true;
|
this._processDidExit = true;
|
||||||
|
|
|
||||||
|
|
@ -66,8 +66,21 @@ async function load(moduleUrl: string, context: { format?: string }, defaultLoad
|
||||||
|
|
||||||
let transport: PortTransport | undefined;
|
let transport: PortTransport | undefined;
|
||||||
|
|
||||||
|
// Node.js < 20
|
||||||
function globalPreload(context: { port: MessagePort }) {
|
function globalPreload(context: { port: MessagePort }) {
|
||||||
transport = new PortTransport(context.port, async (method, params) => {
|
transport = createTransport(context.port);
|
||||||
|
return `
|
||||||
|
globalThis.__esmLoaderPortPreV20 = port;
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js >= 20
|
||||||
|
function initialize(data: { port: MessagePort }) {
|
||||||
|
transport = createTransport(data?.port);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createTransport(port: MessagePort) {
|
||||||
|
return new PortTransport(port, async (method, params) => {
|
||||||
if (method === 'setTransformConfig') {
|
if (method === 'setTransformConfig') {
|
||||||
setTransformConfig(params.config);
|
setTransformConfig(params.config);
|
||||||
return;
|
return;
|
||||||
|
|
@ -91,10 +104,7 @@ function globalPreload(context: { port: MessagePort }) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return `
|
|
||||||
globalThis.__esmLoaderPort = port;
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = { resolve, load, globalPreload };
|
|
||||||
|
module.exports = { resolve, load, globalPreload, initialize };
|
||||||
|
|
|
||||||
33
packages/playwright/src/transform/esmUtils.ts
Normal file
33
packages/playwright/src/transform/esmUtils.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
/**
|
||||||
|
* Copyright (c) Microsoft Corporation.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import url from 'url';
|
||||||
|
|
||||||
|
const kExperimentalLoaderOptions = [
|
||||||
|
'--no-warnings',
|
||||||
|
`--experimental-loader=${url.pathToFileURL(require.resolve('playwright/lib/transform/esmLoader')).toString()}`,
|
||||||
|
];
|
||||||
|
|
||||||
|
export function execArgvWithExperimentalLoaderOptions() {
|
||||||
|
return [
|
||||||
|
...process.execArgv,
|
||||||
|
...kExperimentalLoaderOptions,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function execArgvWithoutExperimentalLoaderOptions() {
|
||||||
|
return process.execArgv.filter(arg => !kExperimentalLoaderOptions.includes(arg));
|
||||||
|
}
|
||||||
|
|
@ -286,22 +286,6 @@ function folderIsModule(folder: string): boolean {
|
||||||
return require(packageJsonPath).type === 'module';
|
return require(packageJsonPath).type === 'module';
|
||||||
}
|
}
|
||||||
|
|
||||||
const kExperimentalLoaderOptions = [
|
|
||||||
'--no-warnings',
|
|
||||||
`--experimental-loader=${url.pathToFileURL(require.resolve('playwright/lib/transform/esmLoader')).toString()}`,
|
|
||||||
];
|
|
||||||
|
|
||||||
export function execArgvWithExperimentalLoaderOptions() {
|
|
||||||
return [
|
|
||||||
...process.execArgv,
|
|
||||||
...kExperimentalLoaderOptions,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function execArgvWithoutExperimentalLoaderOptions() {
|
|
||||||
return process.execArgv.filter(arg => !kExperimentalLoaderOptions.includes(arg));
|
|
||||||
}
|
|
||||||
|
|
||||||
// This follows the --moduleResolution=bundler strategy from tsc.
|
// This follows the --moduleResolution=bundler strategy from tsc.
|
||||||
// https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#moduleresolution-bundler
|
// https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-beta/#moduleresolution-bundler
|
||||||
const kExtLookups = new Map([
|
const kExtLookups = new Map([
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue