feat(ct): only rebuild when necessary (#14026)

This commit is contained in:
Pavel Feldman 2022-05-09 08:10:47 -08:00 committed by GitHub
parent 5aa82dc5e4
commit 46e82e8fea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 176 additions and 62 deletions

View File

@ -15,4 +15,5 @@ output/
test-results/ test-results/
tests/components/ tests/components/
examples/ examples/
DEPS DEPS
.cache/

2
.gitignore vendored
View File

@ -28,4 +28,4 @@ test-results
.env .env
/tests/installation/output/ /tests/installation/output/
/tests/installation/.registry.json /tests/installation/.registry.json
/playwright/out/ .cache/

View File

@ -23,8 +23,10 @@ import { parse, traverse, types as t } from '../babelBundle';
import type { ComponentInfo } from '../tsxTransform'; import type { ComponentInfo } from '../tsxTransform';
import { collectComponentUsages, componentInfo } from '../tsxTransform'; import { collectComponentUsages, componentInfo } from '../tsxTransform';
import type { FullConfig } from '../types'; import type { FullConfig } from '../types';
import { assert } from 'playwright-core/lib/utils';
let previewServer: PreviewServer; let previewServer: PreviewServer;
const VERSION = 1;
export function createPlugin( export function createPlugin(
registerSourceFile: string, registerSourceFile: string,
@ -37,42 +39,68 @@ export function createPlugin(
const use = config.projects[0].use as any; const use = config.projects[0].use as any;
const viteConfig: InlineConfig = use.viteConfig || {}; const viteConfig: InlineConfig = use.viteConfig || {};
const port = use.vitePort || 3100; const port = use.vitePort || 3100;
configDir = configDirectory; configDir = configDirectory;
process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL = `http://localhost:${port}/playwright/index.html`; process.env.PLAYWRIGHT_VITE_COMPONENTS_BASE_URL = `http://localhost:${port}/playwright/index.html`;
viteConfig.root = viteConfig.root || configDir; const rootDir = viteConfig.root || configDir;
viteConfig.plugins = viteConfig.plugins || [ const outDir = viteConfig?.build?.outDir || path.join(rootDir, 'playwright', '.cache');
frameworkPluginFactory() const templateDir = path.join(rootDir, 'playwright');
];
const files = new Set<string>(); const buildInfoFile = path.join(outDir, 'metainfo.json');
for (const project of suite.suites) { let buildInfo: BuildInfo;
for (const file of project.suites) try {
files.add(file.location!.file); buildInfo = JSON.parse(await fs.promises.readFile(buildInfoFile, 'utf-8')) as BuildInfo;
assert(buildInfo.version === VERSION);
} catch (e) {
buildInfo = {
version: VERSION,
components: [],
tests: {},
sources: {},
};
} }
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
viteConfig.plugins.push(vitePlugin(registerSource, [...files])); const componentRegistry: ComponentRegistry = new Map();
viteConfig.configFile = viteConfig.configFile || false; // 1. Re-parse changed tests and collect required components.
viteConfig.define = viteConfig.define || {}; const hasNewTests = await checkNewTests(suite, buildInfo, componentRegistry);
viteConfig.define.__VUE_PROD_DEVTOOLS__ = true; // 2. Check if the set of required components has changed.
viteConfig.css = viteConfig.css || {}; const hasNewComponents = await checkNewComponents(buildInfo, componentRegistry);
viteConfig.css.devSourcemap = true; // 3. Check component sources.
const sourcesDirty = hasNewComponents || await checkSources(buildInfo);
viteConfig.root = rootDir;
viteConfig.preview = { port }; viteConfig.preview = { port };
viteConfig.build = { viteConfig.build = {
target: 'esnext', outDir
minify: false,
rollupOptions: {
treeshake: false,
input: {
index: path.join(viteConfig.root, 'playwright', 'index.html')
},
},
sourcemap: true,
outDir: viteConfig?.build?.outDir || path.join(viteConfig.root, 'playwright', 'out')
}; };
const { build, preview } = require('vite'); const { build, preview } = require('vite');
await build(viteConfig); if (sourcesDirty) {
viteConfig.plugins = viteConfig.plugins || [
frameworkPluginFactory()
];
const registerSource = await fs.promises.readFile(registerSourceFile, 'utf-8');
viteConfig.plugins.push(vitePlugin(registerSource, buildInfo, componentRegistry));
viteConfig.configFile = viteConfig.configFile || false;
viteConfig.define = viteConfig.define || {};
viteConfig.define.__VUE_PROD_DEVTOOLS__ = true;
viteConfig.css = viteConfig.css || {};
viteConfig.css.devSourcemap = true;
viteConfig.build = {
...viteConfig.build,
target: 'esnext',
minify: false,
rollupOptions: {
treeshake: false,
input: {
index: path.join(templateDir, 'index.html')
},
},
sourcemap: true,
};
await build(viteConfig);
}
if (hasNewTests || hasNewComponents || sourcesDirty)
await fs.promises.writeFile(buildInfoFile, JSON.stringify(buildInfo, undefined, 2));
previewServer = await preview(viteConfig); previewServer = await preview(viteConfig);
}, },
@ -87,41 +115,126 @@ export function createPlugin(
}; };
} }
const imports: Map<string, ComponentInfo> = new Map(); type BuildInfo = {
version: number,
sources: {
[key: string]: {
timestamp: number;
}
};
components: ComponentInfo[];
tests: {
[key: string]: {
timestamp: number;
components: string[];
}
};
};
function vitePlugin(registerSource: string, files: string[]): Plugin { type ComponentRegistry = Map<string, ComponentInfo>;
async function checkSources(buildInfo: BuildInfo): Promise<boolean> {
for (const [source, sourceInfo] of Object.entries(buildInfo.sources)) {
try {
const timestamp = (await fs.promises.stat(source)).mtimeMs;
if (sourceInfo.timestamp !== timestamp)
return true;
} catch (e) {
return true;
}
}
return false;
}
async function checkNewTests(suite: Suite, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
const testFiles = new Set<string>();
for (const project of suite.suites) {
for (const file of project.suites)
testFiles.add(file.location!.file);
}
let hasNewTests = false;
for (const testFile of testFiles) {
const timestamp = (await fs.promises.stat(testFile)).mtimeMs;
if (buildInfo.tests[testFile]?.timestamp !== timestamp) {
const components = await parseTestFile(testFile);
for (const component of components)
componentRegistry.set(component.fullName, component);
buildInfo.tests[testFile] = { timestamp, components: components.map(c => c.fullName) };
hasNewTests = true;
} else {
// The test has not changed, populate component registry from the buildInfo.
for (const componentName of buildInfo.tests[testFile].components) {
const component = buildInfo.components.find(c => c.fullName === componentName)!;
componentRegistry.set(component.fullName, component);
}
}
}
return hasNewTests;
}
async function checkNewComponents(buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Promise<boolean> {
const newComponents = [...componentRegistry.keys()];
const oldComponents = new Set(buildInfo.components.map(c => c.fullName));
let hasNewComponents = false;
for (const c of newComponents) {
if (!oldComponents.has(c)) {
hasNewComponents = true;
break;
}
}
if (!hasNewComponents)
return false;
buildInfo.components = newComponents.map(n => componentRegistry.get(n)!);
return true;
}
async function parseTestFile(testFile: string): Promise<ComponentInfo[]> {
const text = await fs.promises.readFile(testFile, 'utf-8');
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
const componentUsages = collectComponentUsages(ast);
const result: ComponentInfo[] = [];
traverse(ast, {
enter: p => {
if (t.isImportDeclaration(p.node)) {
const importNode = p.node;
if (!t.isStringLiteral(importNode.source))
return;
for (const specifier of importNode.specifiers) {
if (!componentUsages.names.has(specifier.local.name))
continue;
if (t.isImportNamespaceSpecifier(specifier))
continue;
result.push(componentInfo(specifier, importNode.source.value, testFile));
}
}
}
});
return result;
}
function vitePlugin(registerSource: string, buildInfo: BuildInfo, componentRegistry: ComponentRegistry): Plugin {
buildInfo.sources = {};
return { return {
name: 'playwright:component-index', name: 'playwright:component-index',
configResolved: async config => {
for (const file of files) {
const text = await fs.promises.readFile(file, 'utf-8');
const ast = parse(text, { errorRecovery: true, plugins: ['typescript', 'jsx'], sourceType: 'module' });
const components = collectComponentUsages(ast);
traverse(ast, {
enter: p => {
if (t.isImportDeclaration(p.node)) {
const importNode = p.node;
if (!t.isStringLiteral(importNode.source))
return;
for (const specifier of importNode.specifiers) {
if (!components.names.has(specifier.local.name))
continue;
if (t.isImportNamespaceSpecifier(specifier))
continue;
const info = componentInfo(specifier, importNode.source.value, file);
imports.set(info.fullName, info);
}
}
}
});
}
},
transform: async (content, id) => { transform: async (content, id) => {
const queryIndex = id.indexOf('?');
const file = queryIndex !== -1 ? id.substring(0, queryIndex) : id;
if (!buildInfo.sources[file]) {
try {
const timestamp = (await fs.promises.stat(file)).mtimeMs;
buildInfo.sources[file] = { timestamp };
} catch {
// Silent if can't read the file.
}
}
if (!id.endsWith('playwright/index.ts') && !id.endsWith('playwright/index.tsx') && !id.endsWith('playwright/index.js')) if (!id.endsWith('playwright/index.ts') && !id.endsWith('playwright/index.tsx') && !id.endsWith('playwright/index.js'))
return; return;
@ -129,7 +242,7 @@ function vitePlugin(registerSource: string, files: string[]): Plugin {
const lines = [content, '']; const lines = [content, ''];
lines.push(registerSource); lines.push(registerSource);
for (const [alias, value] of imports) { for (const [alias, value] of componentRegistry) {
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/');
if (value.importedName) if (value.importedName)
lines.push(`import { ${value.importedName} as ${alias} } from '${importPath}';`); lines.push(`import { ${value.importedName} as ${alias} } from '${importPath}';`);
@ -137,7 +250,7 @@ function vitePlugin(registerSource: string, files: string[]): Plugin {
lines.push(`import ${alias} from '${importPath}';`); lines.push(`import ${alias} from '${importPath}';`);
} }
lines.push(`register({ ${[...imports.keys()].join(',\n ')} });`); lines.push(`register({ ${[...componentRegistry.keys()].join(',\n ')} });`);
return { return {
code: lines.join('\n'), code: lines.join('\n'),
map: { mappings: '' } map: { mappings: '' }