From 6b3e596fd8a408dece0fac46aff5e7ff6a2e5350 Mon Sep 17 00:00:00 2001 From: Pavel Feldman Date: Thu, 20 Jan 2022 18:11:56 -0800 Subject: [PATCH] fix(baseurl): support path-less baseurl (#11527) --- .../src/third_party/tsconfig-loader.ts | 4 ++ packages/playwright-test/src/transform.ts | 42 ++++++++++++++----- tests/playwright-test/resolver.spec.ts | 31 ++++++++++++++ 3 files changed, 66 insertions(+), 11 deletions(-) diff --git a/packages/playwright-test/src/third_party/tsconfig-loader.ts b/packages/playwright-test/src/third_party/tsconfig-loader.ts index 0d40cddbad..e7d9d99e10 100644 --- a/packages/playwright-test/src/third_party/tsconfig-loader.ts +++ b/packages/playwright-test/src/third_party/tsconfig-loader.ts @@ -44,6 +44,7 @@ export interface TsConfigLoaderResult { tsConfigPath: string | undefined; baseUrl: string | undefined; paths: { [key: string]: Array } | undefined; + serialized: string | undefined; } export interface TsConfigLoaderParams { @@ -67,6 +68,7 @@ export function tsConfigLoader({ // tsconfig.loadSync handles if TS_NODE_PROJECT is a file or directory // and also overrides baseURL if TS_NODE_BASEURL is available. const loadResult = loadSync(cwd, TS_NODE_PROJECT, TS_NODE_BASEURL); + loadResult.serialized = JSON.stringify(loadResult); return loadResult; } @@ -84,6 +86,7 @@ function loadSyncDefault( tsConfigPath: undefined, baseUrl: undefined, paths: undefined, + serialized: undefined, }; } const config = loadTsconfig(configPath); @@ -94,6 +97,7 @@ function loadSyncDefault( baseUrl || (config && config.compilerOptions && config.compilerOptions.baseUrl), paths: config && config.compilerOptions && config.compilerOptions.paths, + serialized: undefined, }; } diff --git a/packages/playwright-test/src/transform.ts b/packages/playwright-test/src/transform.ts index a3e3d7f7b1..f9d48186e7 100644 --- a/packages/playwright-test/src/transform.ts +++ b/packages/playwright-test/src/transform.ts @@ -24,7 +24,7 @@ import * as url from 'url'; import type { Location } from './types'; import { TsConfigLoaderResult } from './third_party/tsconfig-loader'; -const version = 5; +const version = 6; const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache'); const sourceMaps: Map = new Map(); @@ -47,8 +47,14 @@ sourceMapSupport.install({ } }); -function calculateCachePath(content: string, filePath: string): string { - const hash = crypto.createHash('sha1').update(process.env.PW_EXPERIMENTAL_TS_ESM ? 'esm' : 'no_esm').update(content).update(filePath).update(String(version)).digest('hex'); +function calculateCachePath(tsconfig: TsConfigLoaderResult, content: string, filePath: string): string { + const hash = crypto.createHash('sha1') + .update(tsconfig.serialized || '') + .update(process.env.PW_EXPERIMENTAL_TS_ESM ? 'esm' : 'no_esm') + .update(content) + .update(filePath) + .update(String(version)) + .digest('hex'); const fileName = path.basename(filePath, path.extname(filePath)).replace(/\W/g, '') + '_' + hash; return path.join(cacheDir, hash[0] + hash[1], fileName); } @@ -56,7 +62,7 @@ function calculateCachePath(content: string, filePath: string): string { export function transformHook(code: string, filename: string, tsconfig: TsConfigLoaderResult, isModule = false): string { if (isComponentImport(filename)) return componentStub(); - const cachePath = calculateCachePath(code, filename); + const cachePath = calculateCachePath(tsconfig, code, filename); const codePath = cachePath + '.js'; const sourceMapPath = cachePath + '.map'; sourceMaps.set(filename, sourceMapPath); @@ -67,15 +73,23 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true'; const babel: typeof import('@babel/core') = require('@babel/core'); + const hasBaseUrl = !!tsconfig.baseUrl; const extensions = ['', '.js', '.ts', '.mjs', ...(process.env.PW_COMPONENT_TESTING ? ['.tsx', '.jsx'] : [])]; const alias: { [key: string]: string | ((s: string[]) => string) } = {}; - for (const [key, values] of Object.entries(tsconfig.paths || {})) { + for (const [key, values] of Object.entries(tsconfig.paths || { '*': '*' })) { const regexKey = '^' + key.replace('*', '.*'); alias[regexKey] = ([name]) => { for (const value of values) { - const relative = (key.endsWith('/*') ? value.substring(0, value.length - 1) + name.substring(key.length - 1) : value) - .replace(/\//g, path.sep); + let relative: string; + if (key === '*' && value === '*') + relative = name; + else if (key.endsWith('/*')) + relative = value.substring(0, value.length - 1) + name.substring(key.length - 1); + else + relative = value; + relative = relative.replace(/\//g, path.sep); const result = path.resolve(tsconfig.baseUrl || '', relative); for (const extension of extensions) { + // TODO: We can't cover this one with the hash! if (fs.existsSync(result + extension)) return result; } @@ -95,11 +109,14 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig [require.resolve('@babel/plugin-syntax-async-generators')], [require.resolve('@babel/plugin-syntax-object-rest-spread')], [require.resolve('@babel/plugin-proposal-export-namespace-from')], - [require.resolve('babel-plugin-module-resolver'), { + ] as any; + + if (hasBaseUrl) { + plugins.push([require.resolve('babel-plugin-module-resolver'), { root: ['./'], alias - }], - ]; + }]); + } if (process.env.PW_COMPONENT_TESTING) plugins.unshift([require.resolve('@babel/plugin-transform-react-jsx')]); @@ -127,7 +144,10 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig fs.mkdirSync(path.dirname(cachePath), { recursive: true }); if (result.map) fs.writeFileSync(sourceMapPath, JSON.stringify(result.map), 'utf8'); - fs.writeFileSync(codePath, result.code, 'utf8'); + // Compiled files with base URL depend on the FS state during compilation, + // never cache them. + if (!hasBaseUrl) + fs.writeFileSync(codePath, result.code, 'utf8'); } return result.code || ''; } diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts index d3162ed5ba..fc09a7ed06 100644 --- a/tests/playwright-test/resolver.spec.ts +++ b/tests/playwright-test/resolver.spec.ts @@ -108,6 +108,37 @@ test('should respect baseurl', async ({ runInlineTest }) => { expect(result.exitCode).toBe(0); }); +test('should respect baseurl w/o paths', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'playwright.config.ts': ` + export default { + projects: [{name: 'foo'}], + }; + `, + 'tsconfig.json': `{ + "compilerOptions": { + "target": "ES2019", + "module": "commonjs", + "lib": ["esnext", "dom", "DOM.Iterable"], + "baseUrl": "./" + }, + }`, + 'a.test.ts': ` + import { foo } from 'foo/b'; + const { test } = pwt; + test('test', ({}, testInfo) => { + expect(testInfo.project.name).toBe(foo); + }); + `, + 'foo/b.ts': ` + export const foo: string = 'foo'; + `, + }); + + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +}); + test('should respect path resolver in experimental mode', async ({ runInlineTest }) => { // We only support experimental esm mode on Node 16+ test.skip(parseInt(process.version.slice(1), 10) < 16);