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.
*/
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 { TemplateBuilder } from '@babel/template';
import * as babel from '@babel/core';
@ -26,14 +26,7 @@ export { parse } from '@babel/parser';
import traverseFunction from '@babel/traverse';
export const traverse = traverseFunction;
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 {
function babelTransformOptions(isTypeScript: boolean, isModule: boolean, pluginsPrologue: [string, any?][], pluginsEpilogue: [string, any?][]): TransformOptions {
const plugins = [];
if (isTypeScript) {
@ -82,12 +75,7 @@ export function babelTransform(filename: string, isTypeScript: boolean, isModule
plugins.push([require('@babel/plugin-syntax-import-assertions')]);
}
plugins.unshift(...additionalPlugins.map(([name, options]) => [require(name), options]));
if (scriptPreprocessor)
plugins.push([scriptPreprocessor]);
return babel.transformFileSync(filename, {
return {
babelrc: false,
configFile: false,
assumptions: {
@ -98,8 +86,28 @@ export function babelTransform(filename: string, isTypeScript: boolean, isModule
presets: [
[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,
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 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 type BabelTransformFunction = (filename: string, isTypeScript: boolean, isModule: boolean, scriptPreprocessor: string | undefined) => BabelFileResult;
export const setBabelPlugins: (plugins: [string, any?][]) => void = require('./babelBundleImpl').setBabelPlugins;
export type BabelPlugin = [string, any?];
export type BabelTransformFunction = (filename: string, isTypeScript: boolean, isModule: boolean, pluginsPrefix: BabelPlugin[], pluginsSuffix: BabelPlugin[]) => BabelFileResult;
export const babelTransform: BabelTransformFunction = require('./babelBundleImpl').babelTransform;
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';

View File

@ -14,7 +14,6 @@
* limitations under the License.
*/
import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';
@ -26,8 +25,6 @@ export type MemoryCache = {
moduleUrl?: string;
};
const version = 13;
const cacheDir = process.env.PWTEST_CACHE_DIR || (() => {
if (process.platform === 'win32')
return path.join(os.tmpdir(), `playwright-transform-cache`);
@ -68,7 +65,7 @@ function _innerAddToCompilationCache(filename: string, options: { codePath: stri
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,
// because we just compiled this file in the loader.
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') };
// Then do the disk cache, this cache works between the Playwright Test runs.
const isModule = !!moduleUrl;
const cachePath = calculateCachePath(code, filename, isModule);
const cachePath = calculateCachePath(filename, hash);
const codePath = cachePath + '.js';
const sourceMapPath = cachePath + '.map';
if (fs.existsSync(codePath)) {
@ -121,14 +117,7 @@ export function addToCompilationCache(payload: any) {
externalDependencies.set(entry[0], new Set(entry[1]));
}
function calculateCachePath(content: string, filePath: string, isModule: boolean): 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');
function calculateCachePath(filePath: string, hash: string): string {
const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash;
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 { isRegExp } from 'playwright-core/lib/utils';
import type { ConfigCLIOverrides, SerializedConfig } from './ipc';
import { requireOrImport } from './transform';
import { requireOrImport, setBabelPlugins } from './transform';
import type { Config, Project } from '../../types/test';
import { errorWithFile } from '../util';
import { setCurrentConfig } from './globals';
import { FullConfigInternal } from './config';
import { setBabelPlugins } from './babelBundle';
const kDefineConfigWasUsed = Symbol('defineConfigWasUsed');
export const defineConfig = (config: any) => {

View File

@ -14,6 +14,7 @@
* limitations under the License.
*/
import crypto from 'crypto';
import path from 'path';
import { sourceMapSupport, pirates } from '../utilsBundle';
import url from 'url';
@ -21,10 +22,12 @@ import type { Location } from '../../types/testReporter';
import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
import { tsConfigLoader } from '../third_party/tsconfig-loader';
import Module from 'module';
import type { BabelTransformFunction } from './babelBundle';
import type { BabelPlugin, BabelTransformFunction } from './babelBundle';
import { fileIsModule, resolveImportSpecifierExtension } from '../util';
import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules } from './compilationCache';
const version = require('../../package.json').version;
type ParsedTsConfigData = {
absoluteBaseUrl: string;
paths: { key: string, values: string[] }[];
@ -32,6 +35,12 @@ type ParsedTsConfigData = {
};
const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>();
let babelPlugins: BabelPlugin[] = [];
export function setBabelPlugins(plugins: BabelPlugin[]) {
babelPlugins = plugins;
}
function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined {
if (!tsconfig.tsConfigPath || !tsconfig.baseUrl)
return;
@ -57,8 +66,6 @@ function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | unde
}
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);
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 {
const { cachedCode, addToCache } = getFromCompilationCache(filename, code, moduleUrl);
if (cachedCode)
return cachedCode;
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
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 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.
// Silence the annoying warning.
@ -144,7 +153,7 @@ export function transformHook(code: string, filename: string, moduleUrl?: string
try {
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)
addToCache!(code, map);
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) {
const revertBabelRequire = installTransform();
const isModule = fileIsModule(file);

View File

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

View File

@ -411,3 +411,28 @@ test('should work with https enabled', async ({ runInlineTest }) => {
expect(result.exitCode).toBe(0);
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);
});