diff --git a/packages/playwright-test/src/cli.ts b/packages/playwright-test/src/cli.ts index 62fb72acfa..c88c336644 100644 --- a/packages/playwright-test/src/cli.ts +++ b/packages/playwright-test/src/cli.ts @@ -21,7 +21,7 @@ import fs from 'fs'; import path from 'path'; import { Runner } from './runner/runner'; import { stopProfiling, startProfiling, gracefullyProcessExitDoNotHang } from 'playwright-core/lib/utils'; -import { execArgvWithoutExperimentalLoaderOptions, execArgvWithExperimentalLoaderOptions, fileIsModule, serializeError } from './util'; +import { experimentalLoaderOption, fileIsModule, serializeError } from './util'; import { showHTMLReport } from './reporters/html'; import { createMergedReport } from './reporters/merge'; import { ConfigLoader, resolveConfigFile } from './common/configLoader'; @@ -275,19 +275,17 @@ function restartWithExperimentalTsEsm(configFile: string | null): boolean { return false; if (process.env.PW_DISABLE_TS_ESM) return false; - if (process.env.PW_TS_ESM_ON) { - // clear execArgv after restart, so that childProcess.fork in user code does not inherit our loader. - process.execArgv = execArgvWithoutExperimentalLoaderOptions(); + if (process.env.PW_TS_ESM_ON) return false; - } if (!fileIsModule(configFile)) return false; - const innerProcess = (require('child_process') as typeof import('child_process')).fork(require.resolve('./cli'), process.argv.slice(2), { + const NODE_OPTIONS = (process.env.NODE_OPTIONS || '') + experimentalLoaderOption(); + const innerProcess = require('child_process').fork(require.resolve('./cli'), process.argv.slice(2), { env: { ...process.env, + NODE_OPTIONS, PW_TS_ESM_ON: '1', - }, - execArgv: execArgvWithExperimentalLoaderOptions(), + } }); innerProcess.on('close', (code: number | null) => { diff --git a/packages/playwright-test/src/common/process.ts b/packages/playwright-test/src/common/process.ts index e03fcbf9a1..8847c7f1c1 100644 --- a/packages/playwright-test/src/common/process.ts +++ b/packages/playwright-test/src/common/process.ts @@ -18,7 +18,7 @@ import type { WriteStream } from 'tty'; import type { EnvProducedPayload, ProcessInitParams, TtyParams } from './ipc'; import { startProfiling, stopProfiling } from 'playwright-core/lib/utils'; import type { TestInfoError } from '../../types/test'; -import { execArgvWithoutExperimentalLoaderOptions, serializeError } from '../util'; +import { serializeError } from '../util'; export type ProtocolRequest = { id: number; @@ -51,9 +51,6 @@ process.on('disconnect', gracefullyCloseAndExit); process.on('SIGINT', () => {}); process.on('SIGTERM', () => {}); -// Clear execArgv immediately, so that the user-code does not inherit our loader. -process.execArgv = execArgvWithoutExperimentalLoaderOptions(); - let processRunner: ProcessRunner; let processName: string; const startingEnv = { ...process.env }; diff --git a/packages/playwright-test/src/plugins/webServerPlugin.ts b/packages/playwright-test/src/plugins/webServerPlugin.ts index a77e162afb..5443ff9736 100644 --- a/packages/playwright-test/src/plugins/webServerPlugin.ts +++ b/packages/playwright-test/src/plugins/webServerPlugin.ts @@ -22,6 +22,7 @@ import { raceAgainstDeadline, launchProcess, httpRequest, monotonicTime } from ' import type { FullConfig } from '../../types/testReporter'; import type { TestRunnerPlugin } from '.'; import type { FullConfigInternal } from '../common/config'; +import { envWithoutExperimentalLoaderOptions } from '../util'; import type { ReporterV2 } from '../reporters/reporterV2'; @@ -92,6 +93,7 @@ export class WebServerPlugin implements TestRunnerPlugin { command: this._options.command, env: { ...DEFAULT_ENVIRONMENT_VARIABLES, + ...envWithoutExperimentalLoaderOptions(), ...this._options.env, }, cwd: this._options.cwd, diff --git a/packages/playwright-test/src/runner/processHost.ts b/packages/playwright-test/src/runner/processHost.ts index c40694f829..72af3f64a7 100644 --- a/packages/playwright-test/src/runner/processHost.ts +++ b/packages/playwright-test/src/runner/processHost.ts @@ -19,7 +19,6 @@ import { EventEmitter } from 'events'; import { debug } from 'playwright-core/lib/utilsBundle'; import type { EnvProducedPayload, ProcessInitParams } from '../common/ipc'; import type { ProtocolResponse } from '../common/process'; -import { execArgvWithExperimentalLoaderOptions } from '../util'; export type ProcessExitData = { unexpectedly: boolean; @@ -51,7 +50,6 @@ export class ProcessHost extends EventEmitter { detached: false, env: { ...process.env, ...this._extraEnv }, stdio: inheritStdio ? ['ignore', 'inherit', 'inherit', 'ipc'] : ['ignore', 'ignore', process.env.PW_RUNNER_DEBUG ? 'inherit' : 'ignore', 'ipc'], - ...(process.env.PW_TS_ESM_ON ? { execArgv: execArgvWithExperimentalLoaderOptions() } : {}), }); this.process.on('exit', (code, signal) => { this.didExit = true; diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 1ef5c61452..fab927c2da 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -303,20 +303,16 @@ function folderIsModule(folder: string): boolean { return require(packageJsonPath).type === 'module'; } -const kExperimentalLoaderOptions = [ - '--no-warnings', - `--experimental-loader=${url.pathToFileURL(require.resolve('@playwright/test/lib/transform/esmLoader')).toString()}`, -]; - -export function execArgvWithExperimentalLoaderOptions() { - return [ - ...process.execArgv, - ...kExperimentalLoaderOptions, - ]; +export function experimentalLoaderOption() { + return ` --no-warnings --experimental-loader=${url.pathToFileURL(require.resolve('@playwright/test/lib/transform/esmLoader')).toString()}`; } -export function execArgvWithoutExperimentalLoaderOptions() { - return process.execArgv.filter(arg => !kExperimentalLoaderOptions.includes(arg)); +export function envWithoutExperimentalLoaderOptions(): NodeJS.ProcessEnv { + const substring = experimentalLoaderOption(); + const result = { ...process.env }; + if (result.NODE_OPTIONS) + result.NODE_OPTIONS = result.NODE_OPTIONS.replace(substring, '').trim() || undefined; + return result; } // This follows the --moduleResolution=bundler strategy from tsc. diff --git a/tests/playwright-test/esm.spec.ts b/tests/playwright-test/esm.spec.ts index d3ad3c7360..2577629216 100644 --- a/tests/playwright-test/esm.spec.ts +++ b/tests/playwright-test/esm.spec.ts @@ -547,88 +547,3 @@ test('should disallow ESM when config is cjs', async ({ runInlineTest }) => { expect(result.exitCode).toBe(1); expect(result.output).toContain('Unknown file extension ".ts"'); }); - -test('should be able to use use execSync with a Node.js file inside a spec', async ({ runInlineTest }) => { - test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/24516' }); - const result = await runInlineTest({ - 'global-setup.ts': ` - import { execSync, spawnSync, fork } from 'child_process'; - console.log('%%global-setup import level'); - console.log('%%execSync: ' + execSync('node hello.js').toString()); - console.log('%%spawnSync: ' + spawnSync('node', ['hello.js']).stdout.toString()); - export default async () => { - console.log('%%global-setup export level'); - console.log('%%execSync: ' + execSync('node hello.js').toString()); - console.log('%%spawnSync: ' + spawnSync('node', ['hello.js']).stdout.toString()); - const child = fork('hellofork.js'); - child.on('message', (m) => console.log('%%fork: ' + m)); - await new Promise((resolve) => child.on('exit', (code) => resolve(code))); - } - `, - 'global-teardown.ts': ` - import { execSync, spawnSync, fork } from 'child_process'; - console.log('%%global-teardown import level'); - console.log('%%execSync: ' + execSync('node hello.js').toString()); - console.log('%%spawnSync: ' + spawnSync('node', ['hello.js']).stdout.toString()); - export default async () => { - console.log('%%global-teardown export level'); - console.log('%%execSync: ' + execSync('node hello.js').toString()); - console.log('%%spawnSync: ' + spawnSync('node', ['hello.js']).stdout.toString()); - const child = fork('hellofork.js'); - child.on('message', (m) => console.log('%%fork: ' + m)); - await new Promise((resolve) => child.on('exit', (code) => resolve(code))); - } - `, - 'package.json': `{ "type": "module" }`, - 'playwright.config.ts': `export default { - projects: [{name: 'foo'}], - globalSetup: './global-setup.ts', - globalTeardown: './global-teardown.ts', - };`, - 'hello.js': `console.log('hello from hello.js');`, - 'hellofork.js': `process.send('hello from hellofork.js');`, - 'a.test.ts': ` - import { test, expect } from '@playwright/test'; - import { execSync, spawnSync, fork } from 'child_process'; - console.log('%%inside test file'); - console.log('%%execSync: ' + execSync('node hello.js').toString()); - console.log('%%spawnSync: ' + spawnSync('node', ['hello.js']).stdout.toString()); - test('check project name', async ({}) => { - console.log('%%inside test'); - console.log('%%execSync: ' + execSync('node hello.js').toString()); - console.log('%%spawnSync: ' + spawnSync('node', ['hello.js']).stdout.toString()); - const child = fork('hellofork.js'); - child.on('message', (m) => console.log('%%fork: ' + m)); - await new Promise((resolve) => child.on('exit', (code) => resolve(code))); - }); - `, - }); - expect(result.exitCode).toBe(0); - expect(result.passed).toBe(1); - expect(result.outputLines).toEqual([ - 'global-setup import level', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'global-teardown import level', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'global-setup export level', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'fork: hello from hellofork.js', - 'inside test file', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'inside test file', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'inside test', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'fork: hello from hellofork.js', - 'global-teardown export level', - 'execSync: hello from hello.js', - 'spawnSync: hello from hello.js', - 'fork: hello from hellofork.js', - ]); -});