From fed77067fe3a30913c8c00f15d1e0576cc1292ec Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 10 Apr 2026 11:16:18 -0700 Subject: [PATCH 1/3] chore(build): fix esbuild bundle warnings and externals - Suppress direct-eval warnings for all bundled steps (Playwright intentionally uses eval in evaluate() callbacks sent to browser) - Add 'playwright' to babelBundle externals (require.resolve at runtime) - Add '../transform/esmLoader.js' to common/runner/worker bundle externals (require.resolve for ESM loader registration) - Fix babelBundle.ts importSource to use bare 'playwright' specifier - Remove unused WebSocketEventEmitter type alias in browserServerImpl --- .../playwright-core/src/browserServerImpl.ts | 3 +-- .../playwright/src/transform/babelBundle.ts | 2 +- utils/build/build.js | 20 +++++++++++++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/playwright-core/src/browserServerImpl.ts b/packages/playwright-core/src/browserServerImpl.ts index be65d80bab37b..d0776eae0a204 100644 --- a/packages/playwright-core/src/browserServerImpl.ts +++ b/packages/playwright-core/src/browserServerImpl.ts @@ -29,7 +29,6 @@ import { ProgressController } from './server/progress'; import type { BrowserServer, BrowserServerLauncher } from './client/browserType'; import type { LaunchServerOptions, Logger } from './client/types'; import type { ProtocolLogger } from './server/types'; -import type { EventEmitter as WebSocketEventEmitter } from 'events'; import type { Browser } from './server/browser'; export class BrowserServerLauncherImpl implements BrowserServerLauncher { @@ -86,7 +85,7 @@ export class BrowserServerLauncherImpl implements BrowserServerLauncher { const wsEndpoint = await server.listen(options.port, options.host); // 3. Return the BrowserServer interface - const browserServer = new EventEmitter() as (BrowserServer & WebSocketEventEmitter); + const browserServer = new EventEmitter() as (BrowserServer & EventEmitter); browserServer.process = () => browser.options.browserProcess.process!; browserServer.wsEndpoint = () => wsEndpoint; browserServer.close = () => browser.options.browserProcess.close(); diff --git a/packages/playwright/src/transform/babelBundle.ts b/packages/playwright/src/transform/babelBundle.ts index a9ea0db621867..fd32f0ee9d54a 100644 --- a/packages/playwright/src/transform/babelBundle.ts +++ b/packages/playwright/src/transform/babelBundle.ts @@ -77,7 +77,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins plugins.push([require('@babel/plugin-transform-react-jsx'), { throwIfNamespace: false, runtime: 'automatic', - importSource: path.dirname(require.resolve('playwright')), + importSource: 'playwright', }]); if (!isModule) { diff --git a/utils/build/build.js b/utils/build/build.js index 34c88e803ac90..2c61407538ec8 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -280,10 +280,18 @@ class EsbuildStep extends Step { super({ concurrent: false }); this._options = options; this._watchPaths = watchPaths; - // For bundled outputs we always want a metafile so we can emit a - // sidecar .bundle.txt report next to each output. - if (options.bundle && !options.metafile) - options.metafile = true; + if (options.bundle) { + // For bundled outputs we always want a metafile so we can emit a + // sidecar report next to each output. + if (!options.metafile) + options.metafile = true; + // Suppress direct-eval warnings — Playwright intentionally uses eval + // in evaluate() callbacks that get stringified and sent to the browser. + if (!options.logOverride) + options.logOverride = {}; + if (!options.logOverride['direct-eval']) + options.logOverride['direct-eval'] = 'silent'; + } } /** @override */ @@ -743,6 +751,7 @@ steps.push(new EsbuildStep({ '../package', '../utils', '../matchers/expect', + '../transform/esmLoader.js', ], plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); @@ -766,6 +775,7 @@ steps.push(new EsbuildStep({ '../loader/loaderProcessEntry.js', '../worker/workerProcessEntry.js', '../transform/babelBundle', + '../transform/esmLoader', ], plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); @@ -787,6 +797,7 @@ steps.push(new EsbuildStep({ '../globals', '../package', '../util', + '../transform/esmLoader', ], plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); @@ -809,6 +820,7 @@ steps.push(new EsbuildStep({ '../package', '../utils', '../matchers/expect', + '../transform/esmLoader', ], plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); From 8ffe466d90043937df79fc8aac1841bd00aa2360 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 10 Apr 2026 11:59:26 -0700 Subject: [PATCH 2/3] fix: revert babelBundle importSource to absolute path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bare 'playwright' specifier breaks under pnpm's strict node_modules — transitive deps aren't hoisted, so user test files can't resolve 'playwright/jsx-runtime'. Revert to path.dirname(require.resolve('playwright')) which gives an absolute path that always resolves. Suppress the resulting require-resolve-not-external warning in the babelBundle step since 'playwright' is already external. --- packages/playwright/src/transform/babelBundle.ts | 4 +--- utils/build/build.js | 2 ++ 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/playwright/src/transform/babelBundle.ts b/packages/playwright/src/transform/babelBundle.ts index fd32f0ee9d54a..31feeabcf58e1 100644 --- a/packages/playwright/src/transform/babelBundle.ts +++ b/packages/playwright/src/transform/babelBundle.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import path from 'path'; - import * as babel from '@babel/core'; import traverseFunction from '@babel/traverse'; @@ -77,7 +75,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins plugins.push([require('@babel/plugin-transform-react-jsx'), { throwIfNamespace: false, runtime: 'automatic', - importSource: 'playwright', + importSource: path.dirname(require.resolve('playwright')), }]); if (!isModule) { diff --git a/utils/build/build.js b/utils/build/build.js index 2c61407538ec8..2109305d05dee 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -714,7 +714,9 @@ steps.push(new EsbuildStep({ format: 'cjs', external: [ '../package', + 'playwright', ], + logOverride: { 'require-resolve-not-external': 'silent' }, plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); From 92a000f4b9c179eea62e56603ef0cbb0e595d2e5 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 10 Apr 2026 12:12:17 -0700 Subject: [PATCH 3/3] fix: plumb jsxImportSource through TransformConfig Instead of babelBundle.ts doing require.resolve('playwright') (which creates a bundling dependency on the 'playwright' package), pass the resolved path through TransformConfig.jsxImportSource. configLoader.ts resolves it once at config load time and passes it through the existing transform config pipeline. --- packages/playwright/src/common/configLoader.ts | 3 ++- packages/playwright/src/transform/babelBundle.ts | 10 +++++----- packages/playwright/src/transform/transform.ts | 3 ++- utils/build/build.js | 3 +-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/playwright/src/common/configLoader.ts b/packages/playwright/src/common/configLoader.ts index c511f294e6793..166313e06342b 100644 --- a/packages/playwright/src/common/configLoader.ts +++ b/packages/playwright/src/common/configLoader.ts @@ -128,7 +128,8 @@ export async function loadConfig(location: ConfigLocation, overrides?: ConfigCLI // 3. Load transform options from the playwright config. const babelPlugins = (userConfig as any)['@playwright/test']?.babelPlugins || []; const external = userConfig.build?.external || []; - setTransformConfig({ babelPlugins, external }); + const jsxImportSource = path.dirname(require.resolve('playwright')); + setTransformConfig({ babelPlugins, external, jsxImportSource }); if (!overrides?.tsconfig) setSingleTSConfig(fullConfig?.singleTSConfigPath); diff --git a/packages/playwright/src/transform/babelBundle.ts b/packages/playwright/src/transform/babelBundle.ts index 31feeabcf58e1..0d4a9b3c561f2 100644 --- a/packages/playwright/src/transform/babelBundle.ts +++ b/packages/playwright/src/transform/babelBundle.ts @@ -30,9 +30,9 @@ export type { NodePath, PluginObj, types as T } from '@babel/core'; export type { BabelAPI } from '@babel/helper-plugin-utils'; export type BabelPlugin = [string, any?]; -export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult | null; +export type BabelTransformFunction = (code: string, filename: string, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[], jsxImportSource?: string) => BabelFileResult | null; -function babelTransformOptions(isTypeScript: boolean, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): TransformOptions { +function babelTransformOptions(isTypeScript: boolean, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][], jsxImportSource?: string): TransformOptions { const plugins = [ [require('@babel/plugin-syntax-import-attributes'), { deprecatedAssertSyntax: true }], ]; @@ -75,7 +75,7 @@ function babelTransformOptions(isTypeScript: boolean, isModule: boolean, plugins plugins.push([require('@babel/plugin-transform-react-jsx'), { throwIfNamespace: false, runtime: 'automatic', - importSource: path.dirname(require.resolve('playwright')), + ...(jsxImportSource ? { importSource: jsxImportSource } : {}), }]); if (!isModule) { @@ -124,14 +124,14 @@ function isTypeScript(filename: string) { return filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts'); } -export function babelTransform(code: string, filename: string, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): BabelFileResult | null { +export function babelTransform(code: string, filename: string, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][], jsxImportSource?: string): BabelFileResult | null { if (isTransforming) return null; // Prevent reentry while requiring plugins lazily. isTransforming = true; try { - const options = babelTransformOptions(isTypeScript(filename), isModule, pluginsPrologue, pluginsEpilogue); + const options = babelTransformOptions(isTypeScript(filename), isModule, pluginsPrologue, pluginsEpilogue, jsxImportSource); return babel.transform(code, { filename, ...options }); } finally { isTransforming = false; diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts index 2da2b9819beb5..9811d97eabedf 100644 --- a/packages/playwright/src/transform/transform.ts +++ b/packages/playwright/src/transform/transform.ts @@ -46,6 +46,7 @@ const cachedTSConfigs = new Map(); export type TransformConfig = { babelPlugins: [string, any?][]; external: string[]; + jsxImportSource?: string; }; let _transformConfig: TransformConfig = { @@ -248,7 +249,7 @@ export function transformHook(originalCode: string, filename: string, moduleUrl? name, { ...(opts || {}), setTransformData: setTransformDataForPlugin }, ]); - const babelResult = babelTransform(originalCode, filename, !!moduleUrl, wrappedPrologue, pluginsEpilogue); + const babelResult = babelTransform(originalCode, filename, !!moduleUrl, wrappedPrologue, pluginsEpilogue, _transformConfig.jsxImportSource); if (!babelResult?.code) return { code: originalCode, serializedCache }; const { code, map } = babelResult; diff --git a/utils/build/build.js b/utils/build/build.js index 2109305d05dee..227063722dfe9 100644 --- a/utils/build/build.js +++ b/utils/build/build.js @@ -714,9 +714,7 @@ steps.push(new EsbuildStep({ format: 'cjs', external: [ '../package', - 'playwright', ], - logOverride: { 'require-resolve-not-external': 'silent' }, plugins: [dynamicImportToRequirePlugin], }, [filePath('packages/playwright/src')])); @@ -749,6 +747,7 @@ steps.push(new EsbuildStep({ external: [ 'playwright-core', 'playwright-core/*', + 'playwright', '../globals', '../package', '../utils',