diff --git a/packages/playwright-ct-core/src/tsxTransform.ts b/packages/playwright-ct-core/src/tsxTransform.ts index 6ef051a6b4..0a61d4e3d9 100644 --- a/packages/playwright-ct-core/src/tsxTransform.ts +++ b/packages/playwright-ct-core/src/tsxTransform.ts @@ -17,7 +17,6 @@ import path from 'path'; import type { T, BabelAPI, PluginObj } from 'playwright/src/transform/babelBundle'; import { types, declare, traverse } from 'playwright/lib/transform/babelBundle'; -import { resolveImportSpecifierExtension } from 'playwright/lib/util'; import { setTransformData } from 'playwright/lib/transform/transform'; const t: typeof T = types; @@ -144,25 +143,19 @@ function collectJsxComponentUsages(node: T.Node): Set { export type ImportInfo = { id: string; - isModuleOrAlias: boolean; - importPath: string; + filename: string; + importSource: string; remoteName: string | undefined; }; export function importInfo(importNode: T.ImportDeclaration, specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, filename: string): { localName: string, info: ImportInfo } { const importSource = importNode.source.value; - const isModuleOrAlias = !importSource.startsWith('.'); - 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 idPrefix = importSource.replace(/[^\w_\d]/g, '_'); const result: ImportInfo = { id: idPrefix, - importPath, - isModuleOrAlias, + filename, + importSource, remoteName: undefined, }; diff --git a/packages/playwright-ct-core/src/vitePlugin.ts b/packages/playwright-ct-core/src/vitePlugin.ts index afe3fff9e9..9e2f96bfa7 100644 --- a/packages/playwright-ct-core/src/vitePlugin.ts +++ b/packages/playwright-ct-core/src/vitePlugin.ts @@ -31,6 +31,7 @@ import { source as injectedSource } from './generated/indexSource'; import type { ImportInfo } from './tsxTransform'; import type { ComponentRegistry } from './viteUtils'; import { createConfig, hasJSComponents, populateComponentsFromTests, resolveDirs, resolveEndpoint, transformIndexFile } from './viteUtils'; +import { resolveHook } from 'playwright/lib/transform/transform'; const log = debug('pw:vite'); @@ -239,12 +240,15 @@ function vitePlugin(registerSource: string, templateDir: string, buildInfo: Buil async writeBundle(this: PluginContext) { for (const importInfo of importInfos.values()) { + const importPath = resolveHook(importInfo.filename, importInfo.importSource); + if (!importPath) + continue; const deps = new Set(); - const id = await moduleResolver(importInfo.importPath); + const id = await moduleResolver(importPath); if (!id) continue; collectViteModuleDependencies(this, id, deps); - depsCollector.set(importInfo.importPath, [...deps]); + depsCollector.set(importPath, [...deps]); } }, }; diff --git a/packages/playwright-ct-core/src/viteUtils.ts b/packages/playwright-ct-core/src/viteUtils.ts index 249537b4a7..25e6060e65 100644 --- a/packages/playwright-ct-core/src/viteUtils.ts +++ b/packages/playwright-ct-core/src/viteUtils.ts @@ -21,6 +21,7 @@ import { getUserData } from 'playwright/lib/transform/compilationCache'; import type { PlaywrightTestConfig as BasePlaywrightTestConfig, FullConfig } from 'playwright/test'; import type { InlineConfig, Plugin, TransformResult, UserConfig } from 'vite'; import type { ImportInfo } from './tsxTransform'; +import { resolveHook } from 'playwright/lib/transform/transform'; const log = debug('pw:vite'); @@ -143,14 +144,15 @@ export async function populateComponentsFromTests(componentRegistry: ComponentRe for (const importInfo of importList) componentRegistry.set(importInfo.id, importInfo); 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 { for (const component of components) { - const extname = path.extname(component.importPath); - if (extname === '.js' || !extname && fs.existsSync(component.importPath + '.js')) + const importPath = resolveHook(component.filename, component.importSource); + const extname = importPath ? path.extname(importPath) : ''; + if (extname === '.js' || (importPath && !extname && fs.existsSync(importPath + '.js'))) return true; } 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)) return null; - const folder = path.dirname(id); const lines = [content, '']; lines.push(registerSource); for (const value of importInfos.values()) { - const importPath = value.isModuleOrAlias ? value.importPath : './' + path.relative(folder, value.importPath).replace(/\\/g, '/'); - lines.push(`const ${value.id} = () => import('${importPath}').then((mod) => mod.${value.remoteName || 'default'});`); + const importPath = resolveHook(value.filename, value.importSource); + lines.push(`const ${value.id} = () => import('${importPath?.replaceAll(path.sep, '/')}').then((mod) => mod.${value.remoteName || 'default'});`); } lines.push(`__pwRegistry.initialize({ ${[...importInfos.keys()].join(',\n ')} });`); diff --git a/packages/playwright/src/third_party/tsconfig-loader.ts b/packages/playwright/src/third_party/tsconfig-loader.ts index 668f3650f6..ffe8af1a00 100644 --- a/packages/playwright/src/third_party/tsconfig-loader.ts +++ b/packages/playwright/src/third_party/tsconfig-loader.ts @@ -137,7 +137,7 @@ function loadTsConfig( const extendsDir = path.dirname(extendedConfig); base.baseUrl = path.join(extendsDir, base.baseUrl); } - result = { ...result, ...base }; + result = { ...result, ...base, tsConfigPath: configFilePath }; } const loadedConfig = Object.fromEntries(Object.entries({ diff --git a/packages/playwright/src/transform/transform.ts b/packages/playwright/src/transform/transform.ts index 801bf0c0f9..e53adf4494 100644 --- a/packages/playwright/src/transform/transform.ts +++ b/packages/playwright/src/transform/transform.ts @@ -132,7 +132,7 @@ export function resolveHook(filename: string, specifier: string): string | undef let candidate = value; if (value.includes('*')) candidate = candidate.replace('*', matchedPartOfSpecifier); - candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate.replace(/\//g, path.sep)); + candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate); const existing = resolveImportSpecifierExtension(candidate); if (existing) { longestPrefixLength = keyPrefix.length; diff --git a/tests/playwright-test/playwright.ct-build.spec.ts b/tests/playwright-test/playwright.ct-build.spec.ts index d518bcccac..38bae33604 100644 --- a/tests/playwright-test/playwright.ct-build.spec.ts +++ b/tests/playwright-test/playwright.ct-build.spec.ts @@ -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')); 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([{ - id: expect.stringContaining('playwright_test_src_button_tsx_Button'), + id: expect.stringContaining('button_Button'), remoteName: 'Button', - importPath: expect.stringContaining('button.tsx'), - isModuleOrAlias: false, + importSource: expect.stringContaining('./button'), + filename: expect.stringContaining('one-import.spec.tsx'), }, { - id: expect.stringContaining('playwright_test_src_clashingNames1_tsx_ClashingName'), + id: expect.stringContaining('clashingNames1_ClashingName'), remoteName: 'ClashingName', - importPath: expect.stringContaining('clashingNames1.tsx'), - isModuleOrAlias: false, + importSource: expect.stringContaining('./clashingNames1'), + filename: expect.stringContaining('clashing-imports.spec.tsx'), }, { - id: expect.stringContaining('playwright_test_src_clashingNames2_tsx_ClashingName'), + id: expect.stringContaining('clashingNames2_ClashingName'), remoteName: 'ClashingName', - importPath: expect.stringContaining('clashingNames2.tsx'), - isModuleOrAlias: false, + importSource: expect.stringContaining('./clashingNames2'), + filename: expect.stringContaining('clashing-imports.spec.tsx'), }, { - id: expect.stringContaining('playwright_test_src_components_tsx_Component1'), + id: expect.stringContaining('components_Component1'), remoteName: 'Component1', - importPath: expect.stringContaining('components.tsx'), - isModuleOrAlias: false, + importSource: expect.stringContaining('./components'), + filename: expect.stringContaining('named-imports.spec.tsx'), }, { - id: expect.stringContaining('playwright_test_src_components_tsx_Component2'), + id: expect.stringContaining('components_Component2'), remoteName: 'Component2', - importPath: expect.stringContaining('components.tsx'), - isModuleOrAlias: false, + importSource: expect.stringContaining('./components'), + filename: expect.stringContaining('named-imports.spec.tsx'), }, { - id: expect.stringContaining('playwright_test_src_defaultExport_tsx'), - importPath: expect.stringContaining('defaultExport.tsx'), - isModuleOrAlias: false, + id: expect.stringContaining('defaultExport'), + importSource: expect.stringContaining('./defaultExport'), + filename: expect.stringContaining('default-import.spec.tsx'), }]); 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')); expect(metainfo.components).toEqual([{ - id: expect.stringContaining('playwright_test_src_button_tsx_Button'), + id: expect.stringContaining('button_tsx_Button'), remoteName: 'Button', - importPath: expect.stringContaining('button.tsx'), - isModuleOrAlias: false, + importSource: expect.stringContaining('button.tsx'), + filename: expect.stringContaining('button.test.tsx'), }]); for (const [, value] of Object.entries(metainfo.deps))