fix(ct): resolve components used in tests during the vite build (#29407)

This commit is contained in:
Pavel Feldman 2024-02-07 20:39:45 -08:00 committed by GitHub
parent 3abd7c808e
commit 84dea09cb9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 42 additions and 44 deletions

View file

@ -17,7 +17,6 @@
import path from 'path'; import path from 'path';
import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle'; import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle';
import { types, declare, traverse } from 'playwright/lib/transform/babelBundle'; import { types, declare, traverse } from 'playwright/lib/transform/babelBundle';
import { resolveImportSpecifierExtension } from 'playwright/lib/util';
import { setTransformData } from 'playwright/lib/transform/transform'; import { setTransformData } from 'playwright/lib/transform/transform';
const t: typeof T = types; const t: typeof T = types;
@ -144,25 +143,19 @@ function collectJsxComponentUsages(node: T.Node): Set<string> {
export type ImportInfo = { export type ImportInfo = {
id: string; id: string;
isModuleOrAlias: boolean; filename: string;
importPath: string; importSource: string;
remoteName: string | undefined; remoteName: string | undefined;
}; };
export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } { export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } {
const importSource = importNode.source.value; const importSource = importNode.source.value;
const isModuleOrAlias = !importSource.startsWith('.'); const idPrefix = importSource.replace(/[^\w_\d]/g, '_');
const unresolvedImportPath = path.resolve(path.dirname(filename), importSource);
// Support following notations for Button.tsx:
// - import { Button } from './Button.js' - via resolveImportSpecifierExtension
// - import { Button } from './Button' - via require.resolve
const importPath = isModuleOrAlias ? importSource : resolveImportSpecifierExtension(unresolvedImportPath) || require.resolve(unresolvedImportPath);
const idPrefix = importPath.replace(/[^\w_\d]/g, '_');
const result: ImportInfo = { const result: ImportInfo = {
id: idPrefix, id: idPrefix,
importPath, filename,
isModuleOrAlias, importSource,
remoteName: undefined, remoteName: undefined,
}; };

View file

@ -31,6 +31,7 @@ import { source as injectedSource } from './generated/indexSource';
import type { ImportInfo } from './tsxTransform'; import type { ImportInfo } from './tsxTransform';
import type { ComponentRegistry } from './viteUtils'; import type { ComponentRegistry } from './viteUtils';
import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, resolveEndpoint, transformIndexFile } from './viteUtils'; import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, resolveEndpoint, transformIndexFile } from './viteUtils';
import { resolveHook } from 'playwright/lib/transform/transform';
const log = debug('pw:vite'); const log = debug('pw:vite');
@ -239,12 +240,15 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil
async writeBundle(this: PluginContext) { async writeBundle(this: PluginContext) {
for (const importInfo of importInfos.values()) { for (const importInfo of importInfos.values()) {
const importPath = resolveHook(importInfo.filename, importInfo.importSource);
if (!importPath)
continue;
const deps = new Set<string>(); const deps = new Set<string>();
const id = await moduleResolver(importInfo.importPath); const id = await moduleResolver(importPath);
if (!id) if (!id)
continue; continue;
collectViteModuleDependencies(this, id, deps); collectViteModuleDependencies(this, id, deps);
depsCollector.set(importInfo.importPath, [...deps]); depsCollector.set(importPath, [...deps]);
} }
}, },
}; };

View file

@ -21,6 +21,7 @@ import { getUserData } from 'playwright/lib/transform/compilationCache';
import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from 'playwright/test'; import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from 'playwright/test';
import type { InlineConfig, Plugin, TransformResult, UserConfig } from 'vite'; import type { InlineConfig, Plugin, TransformResult, UserConfig } from 'vite';
import type { ImportInfo } from './tsxTransform'; import type { ImportInfo } from './tsxTransform';
import { resolveHook } from 'playwright/lib/transform/transform';
const log = debug('pw:vite'); const log = debug('pw:vite');
@ -143,14 +144,15 @@ export async function populateComponentsFromTests(componentRegistry: ComponentRe
for (const importInfo of importList) for (const importInfo of importList)
componentRegistry.set(importInfo.id, importInfo); componentRegistry.set(importInfo.id, importInfo);
if (componentsByImportingFile) if (componentsByImportingFile)
componentsByImportingFile.set(file, importList.filter(i => !i.isModuleOrAlias).map(i => i.importPath)); componentsByImportingFile.set(file, importList.map(i => resolveHook(i.filename, i.importSource)).filter(Boolean) as string[]);
} }
} }
export function hasJSComponents(components: ImportInfo[]): boolean { export function hasJSComponents(components: ImportInfo[]): boolean {
for (const component of components) { for (const component of components) {
const extname = path.extname(component.importPath); const importPath = resolveHook(component.filename, component.importSource);
if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js')) const extname = importPath ? path.extname(importPath) : '';
if (extname === '.js' || (importPath && !extname && fs.existsSync(importPath + '.js')))
return true; return true;
} }
return false; return false;
@ -174,13 +176,12 @@ export function transformIndexFile(id: string, content: string, templateDir: str
if (!idResolved.endsWith(indexTs) && !idResolved.endsWith(indexTsx) && !idResolved.endsWith(indexJs) && !idResolved.endsWith(indexJsx)) if (!idResolved.endsWith(indexTs) && !idResolved.endsWith(indexTsx) && !idResolved.endsWith(indexJs) && !idResolved.endsWith(indexJsx))
return null; return null;
const folder = path.dirname(id);
const lines = [content, '']; const lines = [content, ''];
lines.push(registerSource); lines.push(registerSource);
for (const value of importInfos.values()) { for (const value of importInfos.values()) {
const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); const importPath = resolveHook(value.filename, value.importSource);
lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`); lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`);
} }
lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n ')} });`); lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n ')} });`);

View file

@ -137,7 +137,7 @@ function loadTsConfig(
const extendsDir = path.dirname(extendedConfig); const extendsDir = path.dirname(extendedConfig);
base.baseUrl = path.join(extendsDir, base.baseUrl); base.baseUrl = path.join(extendsDir, base.baseUrl);
} }
result = { ...result, ...base }; result = { ...result, ...base, tsConfigPath: configFilePath };
} }
const loadedConfig = Object.fromEntries(Object.entries({ const loadedConfig = Object.fromEntries(Object.entries({

View file

@ -132,7 +132,7 @@ export function resolveHook(filename: string, specifier: string): string | undef
let candidate = value; let candidate = value;
if (value.includes('*')) if (value.includes('*'))
candidate = candidate.replace('*', matchedPartOfSpecifier); candidate = candidate.replace('*', matchedPartOfSpecifier);
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep)); candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate);
const existing = resolveImportSpecifierExtension(candidate); const existing = resolveImportSpecifierExtension(candidate);
if (existing) { if (existing) {
longestPrefixLength = keyPrefix.length; longestPrefixLength = keyPrefix.length;

View file

@ -126,38 +126,38 @@ test('should extract component list', async ({ runInlineTest }, testInfo) => {
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
metainfo.components.sort((a, b) => { metainfo.components.sort((a, b) => {
return (a.importPath + '/' + a.importedName).localeCompare(b.importPath + '/' + b.importedName); return (a.importSource + '/' + a.importedName).localeCompare(b.importSource + '/' + b.importedName);
}); });
expect(metainfo.components).toEqual([{ expect(metainfo.components).toEqual([{
id: expect.stringContaining('playwright_test_src_button_tsx_Button'), id: expect.stringContaining('button_Button'),
remoteName: 'Button', remoteName: 'Button',
importPath: expect.stringContaining('button.tsx'), importSource: expect.stringContaining('./button'),
isModuleOrAlias: false, filename: expect.stringContaining('one-import.spec.tsx'),
}, { }, {
id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'), id: expect.stringContaining('clashingNames1_ClashingName'),
remoteName: 'ClashingName', remoteName: 'ClashingName',
importPath: expect.stringContaining('clashingNames1.tsx'), importSource: expect.stringContaining('./clashingNames1'),
isModuleOrAlias: false, filename: expect.stringContaining('clashing-imports.spec.tsx'),
}, { }, {
id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'), id: expect.stringContaining('clashingNames2_ClashingName'),
remoteName: 'ClashingName', remoteName: 'ClashingName',
importPath: expect.stringContaining('clashingNames2.tsx'), importSource: expect.stringContaining('./clashingNames2'),
isModuleOrAlias: false, filename: expect.stringContaining('clashing-imports.spec.tsx'),
}, { }, {
id: expect.stringContaining('playwright_test_src_components_tsx_Component1'), id: expect.stringContaining('components_Component1'),
remoteName: 'Component1', remoteName: 'Component1',
importPath: expect.stringContaining('components.tsx'), importSource: expect.stringContaining('./components'),
isModuleOrAlias: false, filename: expect.stringContaining('named-imports.spec.tsx'),
}, { }, {
id: expect.stringContaining('playwright_test_src_components_tsx_Component2'), id: expect.stringContaining('components_Component2'),
remoteName: 'Component2', remoteName: 'Component2',
importPath: expect.stringContaining('components.tsx'), importSource: expect.stringContaining('./components'),
isModuleOrAlias: false, filename: expect.stringContaining('named-imports.spec.tsx'),
}, { }, {
id: expect.stringContaining('playwright_test_src_defaultExport_tsx'), id: expect.stringContaining('defaultExport'),
importPath: expect.stringContaining('defaultExport.tsx'), importSource: expect.stringContaining('./defaultExport'),
isModuleOrAlias: false, filename: expect.stringContaining('default-import.spec.tsx'),
}]); }]);
for (const [, value] of Object.entries(metainfo.deps)) for (const [, value] of Object.entries(metainfo.deps))
@ -451,10 +451,10 @@ test('should retain deps when test changes', async ({ runInlineTest }, testInfo)
const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8')); const metainfo = JSON.parse(fs.readFileSync(testInfo.outputPath('playwright/.cache/metainfo.json'), 'utf-8'));
expect(metainfo.components).toEqual([{ expect(metainfo.components).toEqual([{
id: expect.stringContaining('playwright_test_src_button_tsx_Button'), id: expect.stringContaining('button_tsx_Button'),
remoteName: 'Button', remoteName: 'Button',
importPath: expect.stringContaining('button.tsx'), importSource: expect.stringContaining('button.tsx'),
isModuleOrAlias: false, filename: expect.stringContaining('button.test.tsx'),
}]); }]);
for (const [, value] of Object.entries(metainfo.deps)) for (const [, value] of Object.entries(metainfo.deps))