mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
chore: remove support for esm loader with process restart (#35732)
This commit is contained in:
parent
8abe6ac2b4
commit
b536b38a78
@ -17,7 +17,6 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
import { gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils';
|
||||
import { isRegExp } from 'playwright-core/lib/utils';
|
||||
|
||||
import { requireOrImport, setSingleTSConfig, setTransformConfig } from '../transform/transform';
|
||||
@ -25,7 +24,6 @@ import { errorWithFile, fileIsModule } from '../util';
|
||||
import { FullConfigInternal } from './config';
|
||||
import { configureESMLoader, configureESMLoaderTransformConfig, registerESMLoader } from './esmLoaderHost';
|
||||
import { addToCompilationCache } from '../transform/compilationCache';
|
||||
import { execArgvWithExperimentalLoaderOptions, execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||
|
||||
import type { ConfigLocation } from './config';
|
||||
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
|
||||
@ -102,6 +100,14 @@ async function loadUserConfig(location: ConfigLocation): Promise<Config> {
|
||||
}
|
||||
|
||||
export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLIOverrides, ignoreProjectDependencies = false, metadata?: Config['metadata']): Promise<FullConfigInternal> {
|
||||
// 0. Setup ESM loader if needed.
|
||||
if (!registerESMLoader()) {
|
||||
// In Node.js < 18, complain if the config file is ESM. Historically, we would restart
|
||||
// the process with --loader, but now we require newer Node.js.
|
||||
if (location.resolvedConfigFile && fileIsModule(location.resolvedConfigFile))
|
||||
throw errorWithFile(location.resolvedConfigFile, `Playwright requires Node.js 18 or higher to load esm modules. Please update your version of Node.js.`);
|
||||
}
|
||||
|
||||
// 1. Setup tsconfig; configure ESM loader with tsconfig and compilation cache.
|
||||
setSingleTSConfig(overrides?.tsconfig);
|
||||
await configureESMLoader();
|
||||
@ -359,57 +365,11 @@ function resolveConfigFile(configFileOrDirectory: string): string | undefined {
|
||||
return configFileOrDirectory!;
|
||||
}
|
||||
|
||||
export async function loadConfigFromFileRestartIfNeeded(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal | null> {
|
||||
const location = resolveConfigLocation(configFile);
|
||||
if (restartWithExperimentalTsEsm(location.resolvedConfigFile))
|
||||
return null;
|
||||
return await loadConfig(location, overrides, ignoreDeps);
|
||||
export async function loadConfigFromFile(configFile: string | undefined, overrides?: ConfigCLIOverrides, ignoreDeps?: boolean): Promise<FullConfigInternal> {
|
||||
return await loadConfig(resolveConfigLocation(configFile), overrides, ignoreDeps);
|
||||
}
|
||||
|
||||
export async function loadEmptyConfigForMergeReports() {
|
||||
// Merge reports is "different" for no good reason. It should not pick up local config from the cwd.
|
||||
return await loadConfig({ configDir: process.cwd() });
|
||||
}
|
||||
|
||||
export function restartWithExperimentalTsEsm(configFile: string | undefined, force: boolean = false): boolean {
|
||||
// Opt-out switch.
|
||||
if (process.env.PW_DISABLE_TS_ESM)
|
||||
return false;
|
||||
|
||||
// There are two esm loader APIs:
|
||||
// - Older API that needs a process restart. Available in Node 16, 17, and non-latest 18, 19 and 20.
|
||||
// - Newer API that works in-process. Available in Node 21+ and latest 18, 19 and 20.
|
||||
|
||||
// First check whether we have already restarted with the ESM loader from the older API.
|
||||
if ((globalThis as any).__esmLoaderPortPreV20) {
|
||||
// clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader.
|
||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Now check for the newer API presence.
|
||||
if (!require('node:module').register) {
|
||||
// With older API requiring a process restart, do so conditionally on the config.
|
||||
const configIsModule = !!configFile && fileIsModule(configFile);
|
||||
if (!force && !configIsModule)
|
||||
return false;
|
||||
|
||||
const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('../../cli'), process.argv.slice(2), {
|
||||
env: {
|
||||
...process.env,
|
||||
PW_TS_ESM_LEGACY_LOADER_ON: '1',
|
||||
},
|
||||
execArgv: execArgvWithExperimentalLoaderOptions(),
|
||||
});
|
||||
|
||||
innerProcess.on('close', (code: number | null) => {
|
||||
if (code !== 0 && code !== null)
|
||||
gracefullyProcessExitDoNotHang(code);
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// With the newer API, always enable the ESM loader, because it does not need a restart.
|
||||
registerESMLoader();
|
||||
return false;
|
||||
}
|
||||
|
@ -21,21 +21,27 @@ import { PortTransport } from '../transform/portTransport';
|
||||
import { singleTSConfig, transformConfig } from '../transform/transform';
|
||||
|
||||
let loaderChannel: PortTransport | undefined;
|
||||
// Node.js < 20
|
||||
if ((globalThis as any).__esmLoaderPortPreV20)
|
||||
loaderChannel = createPortTransport((globalThis as any).__esmLoaderPortPreV20);
|
||||
|
||||
// Node.js >= 20
|
||||
export let esmLoaderRegistered = false;
|
||||
export function registerESMLoader() {
|
||||
// Opt-out switch.
|
||||
if (process.env.PW_DISABLE_TS_ESM)
|
||||
return true;
|
||||
|
||||
if (loaderChannel)
|
||||
return true;
|
||||
|
||||
const register = require('node:module').register;
|
||||
if (!register)
|
||||
return false;
|
||||
|
||||
const { port1, port2 } = new MessageChannel();
|
||||
// register will wait until the loader is initialized.
|
||||
require('node:module').register(url.pathToFileURL(require.resolve('../transform/esmLoader')), {
|
||||
register(url.pathToFileURL(require.resolve('../transform/esmLoader')), {
|
||||
data: { port: port2 },
|
||||
transferList: [port2],
|
||||
});
|
||||
loaderChannel = createPortTransport(port1);
|
||||
esmLoaderRegistered = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
function createPortTransport(port: MessagePort) {
|
||||
|
@ -17,8 +17,6 @@
|
||||
import { setTimeOrigin, startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
|
||||
import { serializeError } from '../util';
|
||||
import { registerESMLoader } from './esmLoaderHost';
|
||||
import { execArgvWithoutExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||
|
||||
import type { EnvProducedPayload, ProcessInitParams, TestInfoErrorImpl } from './ipc';
|
||||
|
||||
@ -54,13 +52,6 @@ process.on('disconnect', () => gracefullyCloseAndExit(true));
|
||||
process.on('SIGINT', () => {});
|
||||
process.on('SIGTERM', () => {});
|
||||
|
||||
// Clear execArgv immediately, so that the user-code does not inherit our loader.
|
||||
process.execArgv = execArgvWithoutExperimentalLoaderOptions();
|
||||
|
||||
// Node.js >= 20
|
||||
if (process.env.PW_TS_ESM_LOADER_ON)
|
||||
registerESMLoader();
|
||||
|
||||
let processRunner: ProcessRunner | undefined;
|
||||
let processName: string | undefined;
|
||||
const startingEnv = { ...process.env };
|
||||
|
@ -23,7 +23,7 @@ import { program } from 'playwright-core/lib/cli/program';
|
||||
import { gracefullyProcessExitDoNotHang, startProfiling, stopProfiling } from 'playwright-core/lib/utils';
|
||||
|
||||
import { builtInReporters, defaultReporter, defaultTimeout } from './common/config';
|
||||
import { loadConfigFromFileRestartIfNeeded, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader';
|
||||
import { loadConfigFromFile, loadEmptyConfigForMergeReports, resolveConfigLocation } from './common/configLoader';
|
||||
export { program } from 'playwright-core/lib/cli/program';
|
||||
import { prepareErrorStack } from './reporters/base';
|
||||
import { showHTMLReport } from './reporters/html';
|
||||
@ -77,9 +77,7 @@ function addClearCacheCommand(program: Command) {
|
||||
command.description('clears build and test caches');
|
||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||
command.action(async opts => {
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config);
|
||||
if (!config)
|
||||
return;
|
||||
const config = await loadConfigFromFile(opts.config);
|
||||
const runner = new Runner(config);
|
||||
const { status } = await runner.clearCache();
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
@ -102,9 +100,7 @@ function addDevServerCommand(program: Command) {
|
||||
command.description('start dev server');
|
||||
command.option('-c, --config <file>', `Configuration file, or a test directory with optional "playwright.config.{m,c}?{js,ts}"`);
|
||||
command.action(async options => {
|
||||
const config = await loadConfigFromFileRestartIfNeeded(options.config);
|
||||
if (!config)
|
||||
return;
|
||||
const config = await loadConfigFromFile(options.config);
|
||||
const runner = new Runner(config);
|
||||
const { status } = await runner.runDevServer();
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
@ -161,10 +157,7 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
await startProfiling();
|
||||
const cliOverrides = overridesFromOptions(opts);
|
||||
|
||||
const config = await loadConfigFromFileRestartIfNeeded(opts.config, cliOverrides, opts.deps === false);
|
||||
if (!config)
|
||||
return;
|
||||
|
||||
const config = await loadConfigFromFile(opts.config, cliOverrides, opts.deps === false);
|
||||
config.cliArgs = args;
|
||||
config.cliGrep = opts.grep as string | undefined;
|
||||
config.cliOnlyChanged = opts.onlyChanged === true ? 'HEAD' : opts.onlyChanged;
|
||||
@ -191,8 +184,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
reporter: Array.isArray(opts.reporter) ? opts.reporter : opts.reporter ? [opts.reporter] : undefined,
|
||||
});
|
||||
await stopProfiling('runner');
|
||||
if (status === 'restarted')
|
||||
return;
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
return;
|
||||
@ -211,8 +202,6 @@ async function runTests(args: string[], opts: { [key: string]: any }) {
|
||||
}
|
||||
);
|
||||
await stopProfiling('runner');
|
||||
if (status === 'restarted')
|
||||
return;
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
return;
|
||||
@ -229,8 +218,6 @@ async function runTestServer(opts: { [key: string]: any }) {
|
||||
const host = opts.host || 'localhost';
|
||||
const port = opts.port ? +opts.port : 0;
|
||||
const status = await testServer.runTestServer(opts.config, { }, { host, port });
|
||||
if (status === 'restarted')
|
||||
return;
|
||||
const exitCode = status === 'interrupted' ? 130 : (status === 'passed' ? 0 : 1);
|
||||
gracefullyProcessExitDoNotHang(exitCode);
|
||||
}
|
||||
@ -240,9 +227,7 @@ export async function withRunnerAndMutedWrite(configFile: string | undefined, ca
|
||||
const stdoutWrite = process.stdout.write.bind(process.stdout);
|
||||
process.stdout.write = ((a: any, b: any, c: any) => process.stderr.write(a, b, c)) as any;
|
||||
try {
|
||||
const config = await loadConfigFromFileRestartIfNeeded(configFile);
|
||||
if (!config)
|
||||
return;
|
||||
const config = await loadConfigFromFile(configFile);
|
||||
const runner = new Runner(config);
|
||||
const result = await callback(runner);
|
||||
stdoutWrite(JSON.stringify(result, undefined, 2), () => {
|
||||
@ -265,9 +250,7 @@ async function listTestFiles(opts: { [key: string]: any }) {
|
||||
|
||||
async function mergeReports(reportDir: string | undefined, opts: { [key: string]: any }) {
|
||||
const configFile = opts.config;
|
||||
const config = configFile ? await loadConfigFromFileRestartIfNeeded(configFile) : await loadEmptyConfigForMergeReports();
|
||||
if (!config)
|
||||
return;
|
||||
const config = configFile ? await loadConfigFromFile(configFile) : await loadEmptyConfigForMergeReports();
|
||||
|
||||
const dir = path.resolve(process.cwd(), reportDir || '');
|
||||
const dirStat = await fs.promises.stat(dir).catch(e => null);
|
||||
|
@ -20,9 +20,6 @@ import { EventEmitter } from 'events';
|
||||
import { assert, timeOrigin } from 'playwright-core/lib/utils';
|
||||
import { debug } from 'playwright-core/lib/utilsBundle';
|
||||
|
||||
import { esmLoaderRegistered } from '../common/esmLoaderHost';
|
||||
import { execArgvWithExperimentalLoaderOptions } from '../transform/esmUtils';
|
||||
|
||||
import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc';
|
||||
import type { ProtocolResponse } from '../common/process';
|
||||
|
||||
@ -58,7 +55,6 @@ export class ProcessHost extends EventEmitter {
|
||||
env: {
|
||||
...process.env,
|
||||
...this._extraEnv,
|
||||
...(esmLoaderRegistered ? { PW_TS_ESM_LOADER_ON: '1' } : {}),
|
||||
},
|
||||
stdio: [
|
||||
'ignore',
|
||||
@ -66,7 +62,6 @@ export class ProcessHost extends EventEmitter {
|
||||
(options.onStdErr && !process.env.PW_RUNNER_DEBUG) ? 'pipe' : 'inherit',
|
||||
'ipc',
|
||||
],
|
||||
...(process.env.PW_TS_ESM_LEGACY_LOADER_ON ? { execArgv: execArgvWithExperimentalLoaderOptions() } : {}),
|
||||
});
|
||||
this.process.on('exit', async (code, signal) => {
|
||||
this._processDidExit = true;
|
||||
|
@ -24,7 +24,7 @@ import { open } from 'playwright-core/lib/utilsBundle';
|
||||
import { createErrorCollectingReporter, createReporterForTestServer, createReporters } from './reporters';
|
||||
import { SigIntWatcher } from './sigIntWatcher';
|
||||
import { TestRun, createApplyRebaselinesTask, createClearCacheTask, createGlobalSetupTasks, createListFilesTask, createLoadTask, createReportBeginTask, createRunTestsTasks, createStartDevServerTask, runTasks, runTasksDeferCleanup } from './tasks';
|
||||
import { loadConfig, resolveConfigLocation, restartWithExperimentalTsEsm } from '../common/configLoader';
|
||||
import { loadConfig, resolveConfigLocation } from '../common/configLoader';
|
||||
import { Watcher } from '../fsWatcher';
|
||||
import { baseFullConfig } from '../isomorphic/teleReceiver';
|
||||
import { addGitCommitInfoPlugin } from '../plugins/gitCommitInfoPlugin';
|
||||
@ -435,7 +435,7 @@ export class TestServerDispatcher implements TestServerInterface {
|
||||
}
|
||||
}
|
||||
|
||||
export async function runUIMode(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
|
||||
export async function runUIMode(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: TraceViewerServerOptions & TraceViewerRedirectOptions): Promise<reporterTypes.FullResult['status']> {
|
||||
const configLocation = resolveConfigLocation(configFile);
|
||||
return await innerRunTestServer(configLocation, configCLIOverrides, options, async (server: HttpServer, cancelPromise: ManualPromise<void>) => {
|
||||
await installRootRedirect(server, [], { ...options, webApp: 'uiMode.html' });
|
||||
@ -469,7 +469,7 @@ async function installedChromiumChannelForUI(configLocation: ConfigLocation, con
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export async function runTestServer(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
|
||||
export async function runTestServer(configFile: string | undefined, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }): Promise<reporterTypes.FullResult['status']> {
|
||||
const configLocation = resolveConfigLocation(configFile);
|
||||
return await innerRunTestServer(configLocation, configCLIOverrides, options, async server => {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -477,9 +477,7 @@ export async function runTestServer(configFile: string | undefined, configCLIOve
|
||||
});
|
||||
}
|
||||
|
||||
async function innerRunTestServer(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status'] | 'restarted'> {
|
||||
if (restartWithExperimentalTsEsm(undefined, true))
|
||||
return 'restarted';
|
||||
async function innerRunTestServer(configLocation: ConfigLocation, configCLIOverrides: ConfigCLIOverrides, options: { host?: string, port?: number }, openUI: (server: HttpServer, cancelPromise: ManualPromise<void>) => Promise<void>): Promise<reporterTypes.FullResult['status']> {
|
||||
const testServer = new TestServer(configLocation, configCLIOverrides);
|
||||
const cancelPromise = new ManualPromise<void>();
|
||||
const sigintWatcher = new SigIntWatcher();
|
||||
|
@ -25,7 +25,6 @@ import { colors } from 'playwright-core/lib/utils';
|
||||
import { separator, terminalScreen } from '../reporters/base';
|
||||
import { enquirer } from '../utilsBundle';
|
||||
import { TestServerDispatcher } from './testServer';
|
||||
import { restartWithExperimentalTsEsm } from '../common/configLoader';
|
||||
import { TeleSuiteUpdater } from '../isomorphic/teleSuiteUpdater';
|
||||
import { TestServerConnection } from '../isomorphic/testServerConnection';
|
||||
|
||||
@ -73,10 +72,7 @@ interface WatchModeOptions {
|
||||
grep?: string;
|
||||
}
|
||||
|
||||
export async function runWatchModeLoop(configLocation: ConfigLocation, initialOptions: WatchModeOptions): Promise<FullResult['status'] | 'restarted'> {
|
||||
if (restartWithExperimentalTsEsm(undefined, true))
|
||||
return 'restarted';
|
||||
|
||||
export async function runWatchModeLoop(configLocation: ConfigLocation, initialOptions: WatchModeOptions): Promise<FullResult['status']> {
|
||||
const options: WatchModeOptions = { ...initialOptions };
|
||||
let bufferMode = false;
|
||||
|
||||
|
@ -86,15 +86,6 @@ async function load(moduleUrl: string, context: { format?: string }, defaultLoad
|
||||
|
||||
let transport: PortTransport | undefined;
|
||||
|
||||
// Node.js < 20
|
||||
function globalPreload(context: { port: MessagePort }) {
|
||||
transport = createTransport(context.port);
|
||||
return `
|
||||
globalThis.__esmLoaderPortPreV20 = port;
|
||||
`;
|
||||
}
|
||||
|
||||
// Node.js >= 20
|
||||
function initialize(data: { port: MessagePort }) {
|
||||
transport = createTransport(data?.port);
|
||||
}
|
||||
@ -132,4 +123,4 @@ function createTransport(port: MessagePort) {
|
||||
}
|
||||
|
||||
|
||||
module.exports = { globalPreload, initialize, load, resolve };
|
||||
module.exports = { initialize, load, resolve };
|
||||
|
@ -1,33 +0,0 @@
|
||||
/**
|
||||
* 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));
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user