chore: include plugin list into the cache digest (#22946)

Fixes https://github.com/microsoft/playwright/issues/22931
This commit is contained in:
Pavel Feldman 2023-05-11 21:09:15 -07:00 committed by GitHub
parent 9ffe33fae8
commit e6d8cf9693
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 86 additions and 44 deletions

View file

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import type { BabelFileResult, NodePath, PluginObj } from '@babel/core'; import type { BabelFileResult, NodePath, PluginObj, TransformOptions } from '@babel/core';
import type { TSExportAssignment } from '@babel/types'; import type { TSExportAssignment } from '@babel/types';
import type { TemplateBuilder } from '@babel/template'; import type { TemplateBuilder } from '@babel/template';
import * as babel from '@babel/core'; import * as babel from '@babel/core';
@ -26,14 +26,7 @@ export { parse } from '@babel/parser';
import traverseFunction from '@babel/traverse'; import traverseFunction from '@babel/traverse';
export const traverse = traverseFunction; export const traverse = traverseFunction;
function babelTransformOptions(isTypeScript: boolean, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): TransformOptions {
let additionalPlugins: [string, any?][] = [];
export function setBabelPlugins(plugins: [string, any?][]) {
additionalPlugins = plugins;
}
export function babelTransform(filename: string, isTypeScript: boolean, isModule: boolean, scriptPreprocessor: string | undefined): BabelFileResult {
const plugins = []; const plugins = [];
if (isTypeScript) { if (isTypeScript) {
@ -82,12 +75,7 @@ export function babelTransform(filename: string, isTypeScript: boolean, isModule
plugins.push([require('@babel/plugin-syntax-import-assertions')]); plugins.push([require('@babel/plugin-syntax-import-assertions')]);
} }
plugins.unshift(...additionalPlugins.map(([name, options]) => [require(name), options])); return {
if (scriptPreprocessor)
plugins.push([scriptPreprocessor]);
return babel.transformFileSync(filename, {
babelrc: false, babelrc: false,
configFile: false, configFile: false,
assumptions: { assumptions: {
@ -98,8 +86,28 @@ export function babelTransform(filename: string, isTypeScript: boolean, isModule
presets: [ presets: [
[require('@babel/preset-typescript'), { onlyRemoveTypeImports: false }], [require('@babel/preset-typescript'), { onlyRemoveTypeImports: false }],
], ],
plugins, plugins: [
...pluginsPrologue.map(([name, options]) => [require(name), options]),
...plugins,
...pluginsEpilogue.map(([name, options]) => [require(name), options]),
],
compact: false, compact: false,
sourceMaps: 'both', sourceMaps: 'both',
} as babel.TransformOptions)!; };
}
let isTransforming = false;
export function babelTransform(filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): BabelFileResult {
if (isTransforming)
return {};
// Prevent reentry while requiring plugins lazily.
isTransforming = true;
try {
const options = babelTransformOptions(isTypeScript, isModule, pluginsPrologue, pluginsEpilogue);
return babel.transformFileSync(filename, options)!;
} finally {
isTransforming = false;
}
} }

View file

@ -20,8 +20,8 @@ export const declare: typeof import('../../bundles/babel/node_modules/@types/bab
export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types; export const types: typeof import('../../bundles/babel/node_modules/@types/babel__core').types = require('./babelBundleImpl').types;
export const parse: typeof import('../../bundles/babel/node_modules/@babel/parser/typings/babel-parser').parse = require('./babelBundleImpl').parse; export const parse: typeof import('../../bundles/babel/node_modules/@babel/parser/typings/babel-parser').parse = require('./babelBundleImpl').parse;
export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse; export const traverse: typeof import('../../bundles/babel/node_modules/@types/babel__traverse').default = require('./babelBundleImpl').traverse;
export type BabelTransformFunction = (filename: string, isTypeScript: boolean, isModule: boolean, scriptPreprocessor: string | undefined) => BabelFileResult; export type BabelPlugin = [string, any?];
export const setBabelPlugins: (plugins: [string, any?][]) => void = require('./babelBundleImpl').setBabelPlugins; export type BabelTransformFunction = (filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult;
export const babelTransform: BabelTransformFunction = require('./babelBundleImpl').babelTransform; export const babelTransform: BabelTransformFunction = require('./babelBundleImpl').babelTransform;
export type { NodePath, types as T } from '../../bundles/babel/node_modules/@types/babel__core'; export type { NodePath, types as T } from '../../bundles/babel/node_modules/@types/babel__core';
export type { BabelAPI } from '../../bundles/babel/node_modules/@types/babel__helper-plugin-utils'; export type { BabelAPI } from '../../bundles/babel/node_modules/@types/babel__helper-plugin-utils';

View file

@ -14,7 +14,6 @@
* limitations under the License. * limitations under the License.
*/ */
import crypto from 'crypto';
import fs from 'fs'; import fs from 'fs';
import os from 'os'; import os from 'os';
import path from 'path'; import path from 'path';
@ -26,8 +25,6 @@ export type MemoryCache = {
moduleUrl?: string; moduleUrl?: string;
}; };
const version = 13;
const cacheDir = process.env.PWTEST_CACHE_DIR || (() => { const cacheDir = process.env.PWTEST_CACHE_DIR || (() => {
if (process.platform === 'win32') if (process.platform === 'win32')
return path.join(os.tmpdir(), `playwright-transform-cache`); return path.join(os.tmpdir(), `playwright-transform-cache`);
@ -68,7 +65,7 @@ function _innerAddToCompilationCache(filename: string, options: { codePath: stri
memoryCache.set(filename, options); memoryCache.set(filename, options);
} }
export function getFromCompilationCache(filename: string, code: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map?: any) => void } { export function getFromCompilationCache(filename: string, hash: string, moduleUrl?: string): { cachedCode?: string, addToCache?: (code: string, map?: any) => void } {
// First check the memory cache by filename, this cache will always work in the worker, // First check the memory cache by filename, this cache will always work in the worker,
// because we just compiled this file in the loader. // because we just compiled this file in the loader.
const cache = memoryCache.get(filename); const cache = memoryCache.get(filename);
@ -76,8 +73,7 @@ export function getFromCompilationCache(filename: string, code: string, moduleUr
return { cachedCode: fs.readFileSync(cache.codePath, 'utf-8') }; return { cachedCode: fs.readFileSync(cache.codePath, 'utf-8') };
// Then do the disk cache, this cache works between the Playwright Test runs. // Then do the disk cache, this cache works between the Playwright Test runs.
const isModule = !!moduleUrl; const cachePath = calculateCachePath(filename, hash);
const cachePath = calculateCachePath(code, filename, isModule);
const codePath = cachePath + '.js'; const codePath = cachePath + '.js';
const sourceMapPath = cachePath + '.map'; const sourceMapPath = cachePath + '.map';
if (fs.existsSync(codePath)) { if (fs.existsSync(codePath)) {
@ -121,14 +117,7 @@ export function addToCompilationCache(payload: any) {
externalDependencies.set(entry[0], new Set(entry[1])); externalDependencies.set(entry[0], new Set(entry[1]));
} }
function calculateCachePath(content: string, filePath: string, isModule: boolean): string { function calculateCachePath(filePath: string, hash: string): string {
const hash = crypto.createHash('sha1')
.update(process.env.PW_TEST_SOURCE_TRANSFORM || '')
.update(isModule ? 'esm' : 'no_esm')
.update(content)
.update(filePath)
.update(String(version))
.digest('hex');
const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash; const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash;
return path.join(cacheDir, hash[0] + hash[1], fileName); return path.join(cacheDir, hash[0] + hash[1], fileName);
} }

View file

@ -18,12 +18,11 @@ import * as fs from 'fs';
import * as path from 'path'; import * as path from 'path';
import { isRegExp } from 'playwright-core/lib/utils'; import { isRegExp } from 'playwright-core/lib/utils';
import type { ConfigCLIOverrides, SerializedConfig } from './ipc'; import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
import { requireOrImport } from './transform'; import { requireOrImport, setBabelPlugins } from './transform';
import type { Config, Project } from '../../types/test'; import type { Config, Project } from '../../types/test';
import { errorWithFile } from '../util'; import { errorWithFile } from '../util';
import { setCurrentConfig } from './globals'; import { setCurrentConfig } from './globals';
import { FullConfigInternal } from './config'; import { FullConfigInternal } from './config';
import { setBabelPlugins } from './babelBundle';
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed'); const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
export const defineConfig = (config: any) => { export const defineConfig = (config: any) => {

View file

@ -14,6 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import crypto from 'crypto';
import path from 'path'; import path from 'path';
import { sourceMapSupport, pirates } from '../utilsBundle'; import { sourceMapSupport, pirates } from '../utilsBundle';
import url from 'url'; import url from 'url';
@ -21,10 +22,12 @@ import type { Location } from '../../types/testReporter';
import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader'; import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
import { tsConfigLoader } from '../third_party/tsconfig-loader'; import { tsConfigLoader } from '../third_party/tsconfig-loader';
import Module from 'module'; import Module from 'module';
import type { BabelTransformFunction } from './babelBundle'; import type { BabelPlugin, BabelTransformFunction } from './babelBundle';
import { fileIsModule, resolveImportSpecifierExtension } from '../util'; import { fileIsModule, resolveImportSpecifierExtension } from '../util';
import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules } from './compilationCache'; import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules } from './compilationCache';
const version = require('../../package.json').version;
type ParsedTsConfigData = { type ParsedTsConfigData = {
absoluteBaseUrl: string; absoluteBaseUrl: string;
paths: { key: string, values: string[] }[]; paths: { key: string, values: string[] }[];
@ -32,6 +35,12 @@ type ParsedTsConfigData = {
}; };
const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>(); const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>();
let babelPlugins: BabelPlugin[] = [];
export function setBabelPlugins(plugins: BabelPlugin[]) {
babelPlugins = plugins;
}
function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined { function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined {
if (!tsconfig.tsConfigPath || !tsconfig.baseUrl) if (!tsconfig.tsConfigPath || !tsconfig.baseUrl)
return; return;
@ -57,8 +66,6 @@ function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | unde
} }
const pathSeparator = process.platform === 'win32' ? ';' : ':'; const pathSeparator = process.platform === 'win32' ? ';' : ':';
const scriptPreprocessor = process.env.PW_TEST_SOURCE_TRANSFORM ?
require(process.env.PW_TEST_SOURCE_TRANSFORM) : undefined;
const builtins = new Set(Module.builtinModules); const builtins = new Set(Module.builtinModules);
export function resolveHook(filename: string, specifier: string): string | undefined { export function resolveHook(filename: string, specifier: string): string | undefined {
@ -128,15 +135,17 @@ export function resolveHook(filename: string, specifier: string): string | undef
} }
export function transformHook(code: string, filename: string, moduleUrl?: string): string { export function transformHook(code: string, filename: string, moduleUrl?: string): string {
const { cachedCode, addToCache } = getFromCompilationCache(filename, code, moduleUrl);
if (cachedCode)
return cachedCode;
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx'); const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
const hasPreprocessor = const hasPreprocessor =
process.env.PW_TEST_SOURCE_TRANSFORM && process.env.PW_TEST_SOURCE_TRANSFORM &&
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE && process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE &&
process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f)); process.env.PW_TEST_SOURCE_TRANSFORM_SCOPE.split(pathSeparator).some(f => filename.startsWith(f));
const pluginsPrologue = babelPlugins;
const pluginsEpilogue = hasPreprocessor ? [[process.env.PW_TEST_SOURCE_TRANSFORM!]] as BabelPlugin[] : [];
const hash = calculateHash(code, filename, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
const { cachedCode, addToCache } = getFromCompilationCache(filename, hash, moduleUrl);
if (cachedCode)
return cachedCode;
// We don't use any browserslist data, but babel checks it anyway. // We don't use any browserslist data, but babel checks it anyway.
// Silence the annoying warning. // Silence the annoying warning.
@ -144,7 +153,7 @@ export function transformHook(code: string, filename: string, moduleUrl?: string
try { try {
const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle'); const { babelTransform }: { babelTransform: BabelTransformFunction } = require('./babelBundle');
const { code, map } = babelTransform(filename, isTypeScript, !!moduleUrl, hasPreprocessor ? scriptPreprocessor : undefined); const { code, map } = babelTransform(filename, isTypeScript, !!moduleUrl, pluginsPrologue, pluginsEpilogue);
if (code) if (code)
addToCache!(code, map); addToCache!(code, map);
return code || ''; return code || '';
@ -155,6 +164,18 @@ export function transformHook(code: string, filename: string, moduleUrl?: string
} }
} }
function calculateHash(content: string, filePath: string, isModule: boolean, pluginsPrologue: BabelPlugin[], pluginsEpilogue: BabelPlugin[]): string {
const hash = crypto.createHash('sha1')
.update(isModule ? 'esm' : 'no_esm')
.update(content)
.update(filePath)
.update(version)
.update(pluginsPrologue.map(p => p[0]).join(','))
.update(pluginsEpilogue.map(p => p[0]).join(','))
.digest('hex');
return hash;
}
export async function requireOrImport(file: string) { export async function requireOrImport(file: string) {
const revertBabelRequire = installTransform(); const revertBabelRequire = installTransform();
const isModule = fileIsModule(file); const isModule = fileIsModule(file);

View file

@ -22,7 +22,7 @@ import { loadTestFile } from '../common/testLoader';
import type { FullConfigInternal } from '../common/config'; import type { FullConfigInternal } from '../common/config';
import { PoolBuilder } from '../common/poolBuilder'; import { PoolBuilder } from '../common/poolBuilder';
import { addToCompilationCache } from '../common/compilationCache'; import { addToCompilationCache } from '../common/compilationCache';
import { setBabelPlugins } from '../common/babelBundle'; import { setBabelPlugins } from '../common/transform';
export class InProcessLoaderHost { export class InProcessLoaderHost {
private _config: FullConfigInternal; private _config: FullConfigInternal;

View file

@ -411,3 +411,28 @@ test('should work with https enabled', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0); expect(result.exitCode).toBe(0);
expect(result.passed).toBe(1); expect(result.passed).toBe(1);
}); });
test('list compilation cache should not clash with the run one', async ({ runInlineTest }) => {
const listResult = await runInlineTest({
'playwright.config.ts': playwrightConfig,
'playwright/index.html': `<script type="module" src="./index.ts"></script>`,
'playwright/index.ts': ``,
'src/button.tsx': `
export const Button = () => <button>Button</button>;
`,
'src/button.spec.tsx': `
import { test, expect } from '@playwright/experimental-ct-react';
import { Button } from './button';
test('pass', async ({ mount }) => {
const component = await mount(<Button></Button>);
await expect(component).toHaveText('Button');
});
`,
}, { workers: 1 }, {}, { additionalArgs: ['--list'] });
expect(listResult.exitCode).toBe(0);
expect(listResult.passed).toBe(0);
const runResult = await runInlineTest({}, { workers: 1 });
expect(runResult.exitCode).toBe(0);
expect(runResult.passed).toBe(1);
});