From d5d155df1faccd6ee3265632c5db3f741ce66991 Mon Sep 17 00:00:00 2001 From: Kristo Jorgenson Date: Mon, 5 Jun 2023 17:58:25 +0200 Subject: [PATCH] fix(typescript): allow directory imports (#23254) This updates previous work in #22887 to align more fully with `--moduleResolution=bundler`, allowing index files to be imported with the /index extension --------- Signed-off-by: Kristo Jorgenson Co-authored-by: Dmitry Gozman --- packages/playwright-test/src/util.ts | 18 +++- tests/playwright-test/esm.spec.ts | 132 +++++++++++++++++++++++++++ tests/playwright-test/loader.spec.ts | 120 ++++++++++++++++++++++++ 3 files changed, 268 insertions(+), 2 deletions(-) diff --git a/packages/playwright-test/src/util.ts b/packages/playwright-test/src/util.ts index 3cd88fbf81..e67ca4719d 100644 --- a/packages/playwright-test/src/util.ts +++ b/packages/playwright-test/src/util.ts @@ -321,16 +321,30 @@ const kExtLookups = new Map([ ['', ['.js', '.ts', '.jsx', '.tsx', '.cjs', '.mjs', '.cts', '.mts']], ]); export function resolveImportSpecifierExtension(resolved: string): string | undefined { - if (fs.existsSync(resolved)) + if (fileExists(resolved)) return resolved; + for (const [ext, others] of kExtLookups) { if (!resolved.endsWith(ext)) continue; for (const other of others) { const modified = resolved.substring(0, resolved.length - ext.length) + other; - if (fs.existsSync(modified)) + if (fileExists(modified)) return modified; } break; // Do not try '' when a more specific extesion like '.jsx' matched. } + // try directory imports last + if (dirExists(resolved)) { + const dirImport = path.join(resolved, 'index'); + return resolveImportSpecifierExtension(dirImport); + } +} + +function fileExists(resolved: string) { + return fs.statSync(resolved, { throwIfNoEntry: false })?.isFile(); +} + +function dirExists(resolved: string) { + return fs.statSync(resolved, { throwIfNoEntry: false })?.isDirectory(); } diff --git a/tests/playwright-test/esm.spec.ts b/tests/playwright-test/esm.spec.ts index 48f1d0bfa6..2577629216 100644 --- a/tests/playwright-test/esm.spec.ts +++ b/tests/playwright-test/esm.spec.ts @@ -220,6 +220,138 @@ test('should filter by line', async ({ runInlineTest }) => { expect(result.output).toMatch(/x\.spec\.ts.*two/); }); +test('should resolve directory import to index.js file in ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.js': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.ts file in ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.ts': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.tsx file in ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.tsx': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.mjs file in ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.mjs': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.jsx file in ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.jsx': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve file import before directory import in ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'package.json': `{ "type": "module" }`, + 'playwright.config.ts': `export default { projects: [{name: 'foo'}] };`, + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils.js': ` + export function gimmeAOne() { + return 1; + } + `, + 'playwright-utils/index.js': ` + export function gimmeAOne() { + // intentionally return the wrong thing because this file shouldn't be resolved. + return 2; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + test('should resolve .js import to .ts file in ESM mode', async ({ runInlineTest }) => { const result = await runInlineTest({ 'package.json': `{ "type": "module" }`, diff --git a/tests/playwright-test/loader.spec.ts b/tests/playwright-test/loader.spec.ts index cd480a2094..ffd467c6ce 100644 --- a/tests/playwright-test/loader.spec.ts +++ b/tests/playwright-test/loader.spec.ts @@ -594,6 +594,126 @@ test('should remove type imports from ts', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); +test('should resolve directory import to index.js file in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.js': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.ts file in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.ts': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.tsx file in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.tsx': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.mjs file in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.mjs': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve directory import to index.jsx file in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils/index.jsx': ` + export function gimmeAOne() { + return 1; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + +test('should resolve file import before directory import in non-ESM mode', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'a.test.ts': ` + import { test, expect } from '@playwright/test'; + import { gimmeAOne } from './playwright-utils'; + test('pass', ({}) => { + expect(gimmeAOne()).toBe(1); + }); + `, + 'playwright-utils.jsx': ` + export function gimmeAOne() { + return 1; + } + `, + 'playwright-utils/index.jsx': ` + export function gimmeAOne() { + // intentionally return the wrong thing because this file shouldn't be resolved. + return 2; + } + `, + }); + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + test('should resolve .js import to .ts file in non-ESM mode', async ({ runInlineTest }) => { const result = await runInlineTest({ 'a.test.ts': `