From 736cf5c585caa56700a55ade66caf315d873b6fc Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Fri, 13 Jan 2023 10:49:10 -0800 Subject: [PATCH] fix(tsx): resolve .js imports to .tsx as well (#20092) Fixes: https://github.com/microsoft/playwright/issues/20039 --- packages/playwright-test/src/transform.ts | 13 ++-- packages/playwright-test/src/tsxTransform.ts | 7 +- tests/playwright-test/esm.spec.ts | 69 ++++++++++++++++++++ tests/playwright-test/loader.spec.ts | 41 ++++++++++++ 4 files changed, 124 insertions(+), 6 deletions(-) diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index aff93471fb..6c1505653c 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -169,11 +169,14 @@ export function resolveHook(filename: string, specifier: string): string | undef export function js2ts(resolved: string): string | undefined { const match = resolved.match(/(.*)(\.js|\.jsx|\.mjs)$/); - if (match) { - const tsResolved = match[1] + match[2].replace('j', 't'); - if (!fs.existsSync(resolved) && fs.existsSync(tsResolved)) - return tsResolved; - } + if (!match || fs.existsSync(resolved)) + return; + const tsResolved = match[1] + match[2].replace('js', 'ts'); + if (fs.existsSync(tsResolved)) + return tsResolved; + const tsxResolved = match[1] + match[2].replace('js', 'tsx'); + if (fs.existsSync(tsxResolved)) + return tsxResolved; } export function transformHook(code: string, filename: string, moduleUrl?: string): string { diff --git a/packages/playwright-test/src/tsxTransform.ts b/packages/playwright-test/src/tsxTransform.ts index f635ab6df0..ac34320455 100644 --- a/packages/playwright-test/src/tsxTransform.ts +++ b/packages/playwright-test/src/tsxTransform.ts @@ -17,6 +17,7 @@ import path from 'path'; import type { T, BabelAPI } from './babelBundle'; import { types, declare, traverse } from './babelBundle'; +import { js2ts } from './transform'; const t: typeof T = types; const fullNames = new Map(); @@ -173,7 +174,11 @@ export type ComponentInfo = { export function componentInfo(specifier: T.ImportSpecifier | T.ImportDefaultSpecifier, importSource: string, filename: string): ComponentInfo { const isModuleOrAlias = !importSource.startsWith('.'); - const importPath = isModuleOrAlias ? importSource : require.resolve(path.resolve(path.dirname(filename), importSource)); + const unresolvedImportPath = path.resolve(path.dirname(filename), importSource); + // Support following notations for Button.tsx: + // - import { Button } from './Button.js' - via js2ts, it handles tsx too + // - import { Button } from './Button' - via require.resolve + const importPath = isModuleOrAlias ? importSource : js2ts(unresolvedImportPath) || require.resolve(unresolvedImportPath); const prefix = importPath.replace(/[^\w_\d]/g, '_'); const pathInfo = { importPath, isModuleOrAlias }; diff --git a/tests/playwright-test/esm.spec.ts b/tests/playwright-test/esm.spec.ts index cecdc81eb5..ef199297f9 100644 --- a/tests/playwright-test/esm.spec.ts +++ b/tests/playwright-test/esm.spec.ts @@ -209,3 +209,72 @@ test('should filter by line', async ({ runInlineTest, nodeVersion }) => { expect(result.failed).toBe(1); expect(result.output).toMatch(/x\.spec\.ts.*two/); }); + +test('should resolve .js import to .ts file in ESM mode', async ({ runInlineTest, nodeVersion }) => { + test.skip(nodeVersion.major < 16); + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + const { test } = pwt; + import { gimmeAOne } from './playwright-utils.js'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils.ts': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve .js import to .tsx file in ESM mode', async ({ runInlineTest, nodeVersion }) => { + test.skip(nodeVersion.major < 16); + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + const { test } = pwt; + import { gimmeAOne } from './playwright-utils.js'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils.tsx': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve .js import to .tsx file in ESM mode for components', async ({ runInlineTest, nodeVersion }) => { + test.skip(nodeVersion.major < 16); + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'playwright/index.html': ``, + 'playwright/index.ts': ``, + + 'src/button.tsx': ` + export const Button = () => ; + `, + + 'src/test.spec.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button.js'; + test('pass', async ({ mount }) => { + await mount(); + }); + `, + }, { workers: 1 }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); diff --git a/tests/playwright-test/loader.spec.ts b/tests/playwright-test/loader.spec.ts index 235ff3c1b0..d25cd5b669 100644 --- a/tests/playwright-test/loader.spec.ts +++ b/tests/playwright-test/loader.spec.ts @@ -542,6 +542,47 @@ test('should resolve .js import to .ts file in non-ESM mode', async ({ runInline expect(result.exitCode).toBe(0); }); +test('should resolve .js import to .tsx file in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + const { test } = pwt; + import { gimmeAOne } from './playwright-utils.js'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils.tsx': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve .js import to .tsx file in non-ESM mode for components', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright/index.html': ``, + 'playwright/index.ts': ``, + + 'src/button.tsx': ` + export const Button = () => ; + `, + + 'src/test.spec.tsx': ` + //@no-header + import { test, expect } from '@playwright/experimental-ct-react'; + import { Button } from './button.js'; + test('pass', async ({ mount }) => { + await mount(); + }); + `, + }, { workers: 1 }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + test('should import export assignment from ts', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': `