chore: allow marking scripts as external for transform (#23449)

Fixes https://github.com/microsoft/playwright/issues/22874
This commit is contained in:
Pavel Feldman 2023-06-01 20:28:49 -07:00 committed by GitHub
parent 14a1eaa474
commit 96b2247e28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 214 additions and 58 deletions

View File

@ -17,6 +17,25 @@ export default defineConfig({
});
```
## property: TestConfig.build
* since: v1.35
- type: ?<[Object]>
- `external` ?<[Array]<[string]>> Paths to exclude from the transpilation expressed as glob patterns. Typically heavy JS bundles your tests reference.
Transpiler configuration.
**Usage**
```js title="playwright.config.ts"
import { defineConfig } from '@playwright/test';
export default defineConfig({
build: {
external: '**/*bundle.js',
},
});
```
## property: TestConfig.expect
* since: v1.10
- type: ?<[Object]>

66
packages/playwright-ct-core/index.d.ts vendored Normal file
View File

@ -0,0 +1,66 @@
/**
* 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 type {
TestType,
PlaywrightTestArgs,
PlaywrightTestConfig as BasePlaywrightTestConfig,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
Locator,
} from '@playwright/test';
import type { JsonObject } from '@playwright/experimental-ct-core/types/component';
import type { InlineConfig } from 'vite';
export type PlaywrightTestConfig<T = {}, W = {}> = Omit<BasePlaywrightTestConfig<T, W>, 'use'> & {
use?: BasePlaywrightTestConfig<T, W>['use'] & {
ctPort?: number;
ctTemplateDir?: string;
ctCacheDir?: string;
ctViteConfig?: InlineConfig | (() => Promise<InlineConfig>);
};
};
export interface MountOptions<HooksConfig extends JsonObject> {
hooksConfig?: HooksConfig;
}
interface MountResult extends Locator {
unmount(): Promise<void>;
update(component: JSX.Element): Promise<void>;
}
export interface ComponentFixtures {
mount<HooksConfig extends JsonObject>(
component: JSX.Element,
options?: MountOptions<HooksConfig>
): Promise<MountResult>;
}
export const test: TestType<
PlaywrightTestArgs & PlaywrightTestOptions & ComponentFixtures,
PlaywrightWorkerArgs & PlaywrightWorkerOptions
>;
/**
* Defines Playwright config
*/
export function defineConfig(config: PlaywrightTestConfig): PlaywrightTestConfig;
export function defineConfig<T>(config: PlaywrightTestConfig<T>): PlaywrightTestConfig<T>;
export function defineConfig<T, W>(config: PlaywrightTestConfig<T, W>): PlaywrightTestConfig<T, W>;
export { expect, devices } from '@playwright/test';

View File

@ -0,0 +1,32 @@
/**
* 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.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('./lib/mount');
const defineConfig = config => originalDefineConfig({
...config,
build: {
...config.build,
babelPlugins: [
[require.resolve('./lib/tsxTransform')]
],
}
});
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -12,6 +12,10 @@
},
"license": "Apache-2.0",
"exports": {
".": {
"types": "./index.d.ts",
"default": "./index.js"
},
"./cli": "./cli.js",
"./lib/mount": "./lib/mount.js",
"./lib/vitePlugin": "./lib/vitePlugin.js"

View File

@ -58,10 +58,6 @@ export function createPlugin(
configDir = configDirectory;
},
babelPlugins: async () => [
[require.resolve('./tsxTransform')]
],
begin: async (suite: Suite) => {
const use = config.projects[0].use as CtConfig;
const port = use.ctPort || 3100;

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/experimental-ct-core/lib/mount');
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
const path = require('path');
const plugin = () => {
@ -26,6 +25,5 @@ const plugin = () => {
() => import('@vitejs/plugin-react').then(plugin => plugin.default()));
};
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/experimental-ct-core/lib/mount');
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
const path = require('path');
const plugin = () => {
@ -26,6 +25,5 @@ const plugin = () => {
() => import('@vitejs/plugin-react').then(plugin => plugin.default()));
};
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/experimental-ct-core/lib/mount');
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
const path = require('path');
const plugin = () => {
@ -26,6 +25,5 @@ const plugin = () => {
() => import('vite-plugin-solid').then(plugin => plugin.default()));
};
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/experimental-ct-core/lib/mount');
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
const path = require('path');
const plugin = () => {
@ -26,6 +25,5 @@ const plugin = () => {
() => import('@sveltejs/vite-plugin-svelte').then(plugin => plugin.svelte()));
};
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/experimental-ct-core/lib/mount');
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
const path = require('path');
const plugin = () => {
@ -26,6 +25,5 @@ const plugin = () => {
() => import('@vitejs/plugin-vue').then(plugin => plugin.default()));
}
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -14,8 +14,7 @@
* limitations under the License.
*/
const { test: baseTest, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/test');
const { fixtures } = require('@playwright/experimental-ct-core/lib/mount');
const { test, expect, devices, defineConfig: originalDefineConfig } = require('@playwright/experimental-ct-core');
const path = require('path');
const plugin = () => {
@ -26,6 +25,5 @@ const plugin = () => {
() => import('@vitejs/plugin-vue2').then(plugin => plugin.default()));
};
const defineConfig = config => originalDefineConfig({ ...config, _plugins: [plugin] });
const test = baseTest.extend(fixtures);
module.exports = { test, expect, devices, defineConfig };

View File

@ -24,6 +24,7 @@ import { getPackageJsonPath, mergeObjects } from '../util';
import type { Matcher } from '../util';
import type { ConfigCLIOverrides } from './ipc';
import type { FullConfig, FullProject } from '../../types/test';
import { setTransformConfig } from '../transform/transform';
export type FixturesWithLocation = {
fixtures: Fixtures;
@ -124,6 +125,10 @@ export class FullConfigInternal {
this.projects = projectConfigs.map(p => new FullProjectInternal(configDir, config, this, p, this.configCLIOverrides, throwawayArtifactsPath));
resolveProjectDependencies(this.projects);
this._assignUniqueProjectIds(this.projects);
setTransformConfig({
babelPlugins: (config as any).build?.babelPlugins || [],
external: config.build?.external || [],
});
this.config.projects = this.projects.map(p => p.project);
}

View File

@ -18,7 +18,7 @@ import * as fs from 'fs';
import * as path from 'path';
import { isRegExp } from 'playwright-core/lib/utils';
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
import { requireOrImport, setBabelPlugins } from '../transform/transform';
import { requireOrImport } from '../transform/transform';
import type { Config, Project } from '../../types/test';
import { errorWithFile } from '../util';
import { setCurrentConfig } from './globals';
@ -41,15 +41,12 @@ export class ConfigLoader {
}
static async deserialize(data: SerializedConfig): Promise<FullConfigInternal> {
setBabelPlugins(data.babelTransformPlugins);
addToCompilationCache(data.compilationCache);
await initializeEsmLoader();
const loader = new ConfigLoader(data.configCLIOverrides);
if (data.configFile)
return await loader.loadConfigFile(data.configFile);
return await loader.loadEmptyConfig(data.configDir);
const config = data.configFile ? await loader.loadConfigFile(data.configFile) : await loader.loadEmptyConfig(data.configDir);
await initializeEsmLoader();
return config;
}
async loadConfigFile(file: string, ignoreProjectDependencies = false): Promise<FullConfigInternal> {

View File

@ -15,7 +15,7 @@
*/
import { addToCompilationCache, serializeCompilationCache } from '../transform/compilationCache';
import { getBabelPlugins } from '../transform/transform';
import { transformConfig } from '../transform/transform';
import { PortTransport } from '../transform/portTransport';
const port = (globalThis as any).__esmLoaderPort;
@ -47,6 +47,6 @@ export async function incorporateCompilationCache() {
export async function initializeEsmLoader() {
if (!loaderChannel)
return;
await loaderChannel.send('setBabelPlugins', { plugins: getBabelPlugins() });
await loaderChannel.send('setTransformConfig', { config: transformConfig() });
await loaderChannel.send('addToCompilationCache', { cache: serializeCompilationCache() });
}

View File

@ -42,7 +42,6 @@ export type SerializedConfig = {
configDir: string;
configCLIOverrides: ConfigCLIOverrides;
compilationCache: any;
babelTransformPlugins: [string, any?][];
};
export type TtyParams = {
@ -126,15 +125,11 @@ export type TeardownErrorsPayload = {
export type EnvProducedPayload = [string, string | null][];
export function serializeConfig(config: FullConfigInternal): SerializedConfig {
const babelTransformPlugins: [string, any?][] = [];
for (const plugin of config.plugins)
babelTransformPlugins.push(...plugin.babelPlugins || []);
const result: SerializedConfig = {
configFile: config.config.configFile,
configDir: config.configDir,
configCLIOverrides: config.configCLIOverrides,
compilationCache: serializeCompilationCache(),
babelTransformPlugins,
};
return result;
}

View File

@ -20,7 +20,6 @@ import type { InternalReporter } from '../reporters/internalReporter';
export interface TestRunnerPlugin {
name: string;
setup?(config: FullConfig, configDir: string, reporter: InternalReporter): Promise<void>;
babelPlugins?(): Promise<[string, any?][]>;
begin?(suite: Suite): Promise<void>;
end?(): Promise<void>;
teardown?(): Promise<void>;
@ -29,7 +28,6 @@ export interface TestRunnerPlugin {
export type TestRunnerPluginRegistration = {
factory: TestRunnerPlugin | (() => TestRunnerPlugin | Promise<TestRunnerPlugin>);
instance?: TestRunnerPlugin;
babelPlugins?: [string, any?][];
};
export { webServer } from './webServerPlugin';

View File

@ -22,7 +22,6 @@ import { loadTestFile } from '../common/testLoader';
import type { FullConfigInternal } from '../common/config';
import { PoolBuilder } from '../common/poolBuilder';
import { addToCompilationCache } from '../transform/compilationCache';
import { setBabelPlugins } from '../transform/transform';
import { incorporateCompilationCache, initializeEsmLoader } from '../common/esmLoaderHost';
export class InProcessLoaderHost {
@ -35,10 +34,6 @@ export class InProcessLoaderHost {
}
async start() {
const babelTransformPlugins: [string, any?][] = [];
for (const plugin of this._config.plugins)
babelTransformPlugins.push(...plugin.babelPlugins || []);
setBabelPlugins(babelTransformPlugins);
await initializeEsmLoader();
}

View File

@ -119,7 +119,6 @@ function createPluginSetupTask(plugin: TestRunnerPluginRegistration): Task<TestR
else
plugin.instance = plugin.factory;
await plugin.instance?.setup?.(config.config, config.configDir, reporter);
plugin.babelPlugins = await plugin.instance?.babelPlugins?.() || [];
return () => plugin.instance?.teardown?.();
};
}

View File

@ -16,8 +16,8 @@
import fs from 'fs';
import url from 'url';
import { addToCompilationCache, belongsToNodeModules, currentFileDepsCollector, serializeCompilationCache, startCollectingFileDeps, stopCollectingFileDeps } from './compilationCache';
import { transformHook, resolveHook, setBabelPlugins } from './transform';
import { addToCompilationCache, currentFileDepsCollector, serializeCompilationCache, startCollectingFileDeps, stopCollectingFileDeps } from './compilationCache';
import { transformHook, resolveHook, setTransformConfig, shouldTransform } from './transform';
import { PortTransport } from './portTransport';
// Node < 18.6: defaultResolve takes 3 arguments.
@ -50,7 +50,7 @@ async function load(moduleUrl: string, context: { format?: string }, defaultLoad
const filename = url.fileURLToPath(moduleUrl);
// Bail for node_modules.
if (belongsToNodeModules(filename))
if (!shouldTransform(filename))
return defaultLoad(moduleUrl, context, defaultLoad);
const code = fs.readFileSync(filename, 'utf-8');
@ -68,8 +68,8 @@ let transport: PortTransport | undefined;
function globalPreload(context: { port: MessagePort }) {
transport = new PortTransport(context.port, async (method, params) => {
if (method === 'setBabelPlugins') {
setBabelPlugins(params.plugins);
if (method === 'setTransformConfig') {
setTransformConfig(params.config);
return;
}

View File

@ -23,7 +23,8 @@ import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
import { tsConfigLoader } from '../third_party/tsconfig-loader';
import Module from 'module';
import type { BabelPlugin, BabelTransformFunction } from './babelBundle';
import { fileIsModule, resolveImportSpecifierExtension } from '../util';
import { createFileMatcher, fileIsModule, resolveImportSpecifierExtension } from '../util';
import type { Matcher } from '../util';
import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules } from './compilationCache';
const version = require('../../package.json').version;
@ -35,14 +36,25 @@ type ParsedTsConfigData = {
};
const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>();
let babelPlugins: BabelPlugin[] = [];
export type TransformConfig = {
babelPlugins: [string, any?][];
external: string[];
};
export function setBabelPlugins(plugins: BabelPlugin[]) {
babelPlugins = plugins;
let _transformConfig: TransformConfig = {
babelPlugins: [],
external: [],
};
let _externalMatcher: Matcher = () => false;
export function setTransformConfig(config: TransformConfig) {
_transformConfig = config;
_externalMatcher = createFileMatcher(_transformConfig.external);
}
export function getBabelPlugins(): BabelPlugin[] {
return babelPlugins;
export function transformConfig(): TransformConfig {
return _transformConfig;
}
function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined {
@ -75,7 +87,7 @@ const builtins = new Set(Module.builtinModules);
export function resolveHook(filename: string, specifier: string): string | undefined {
if (specifier.startsWith('node:') || builtins.has(specifier))
return;
if (belongsToNodeModules(filename))
if (!shouldTransform(filename))
return;
if (isRelativeSpecifier(specifier))
@ -138,13 +150,19 @@ export function resolveHook(filename: string, specifier: string): string | undef
}
}
export function shouldTransform(filename: string): boolean {
if (_externalMatcher(filename))
return false;
return !belongsToNodeModules(filename);
}
export function transformHook(originalCode: string, filename: string, moduleUrl?: string): string {
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx') || filename.endsWith('.mts') || filename.endsWith('.cts');
const hasPreprocessor =
process.env.PW_TEST_SOURCE_TRANSFORM &&
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE &&
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f));
const pluginsPrologue = babelPlugins;
const pluginsPrologue = _transformConfig.babelPlugins;
const pluginsEpilogue = hasPreprocessor ? [[process.env.PW_TEST_SOURCE_TRANSFORM!]] as BabelPlugin[] : [];
const hash = calculateHash(originalCode, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
const { cachedCode, addToCache } = getFromCompilationCache(filename, hash, moduleUrl);
@ -209,7 +227,7 @@ function installTransform(): () => void {
(Module as any)._resolveFilename = resolveFilename;
const revertPirates = pirates.addHook((code: string, filename: string) => {
if (belongsToNodeModules(filename))
if (!shouldTransform(filename))
return code;
return transformHook(code, filename);
}, { exts: ['.ts', '.tsx', '.js', '.jsx', '.mjs'] });

View File

@ -569,6 +569,31 @@ interface TestConfig {
*
*/
webServer?: TestConfigWebServer | TestConfigWebServer[];
/**
* Transpiler configuration.
*
* **Usage**
*
* ```js
* // playwright.config.ts
* import { defineConfig } from '@playwright/test';
*
* export default defineConfig({
* build: {
* external: '**\/*bundle.js',
* },
* });
* ```
*
*/
build?: {
/**
* Paths to exclude from the transpilation expressed as glob patterns. Typically heavy JS bundles your tests
* reference.
*/
external?: Array<string>;
};
/**
* Configuration for the `expect` assertion library. Learn more about [various timeouts](https://playwright.dev/docs/test-timeouts).
*

View File

@ -131,3 +131,22 @@ test('should not read browserslist file', async ({ runInlineTest }) => {
expect(result.passed).toBe(1);
expect(result.failed).toBe(0);
});
test('should not transform external', async ({ runInlineTest }) => {
const result = await runInlineTest({
'playwright.config.ts': `
import { defineConfig } from '@playwright/test';
export default defineConfig({
build: {
external: ['**/a.spec.ts']
}
});
`,
'a.spec.ts': `
import { test, expect } from '@playwright/test';
test('succeeds', () => {});
`
});
expect(result.exitCode).toBe(1);
expect(result.output).toContain('Cannot use import statement outside a module');
});