Skip to content

Commit 5415703

Browse files
aslushnikovdgozman
andauthored
cherry-pick(#11662) fix(test runner): resolve tsconfig for each file (#11695)
This allows us to properly handle path mappings that are not too ambiguous. Co-authored-by: Dmitry Gozman <[email protected]>
1 parent 9422974 commit 5415703

File tree

15 files changed

+166
-95
lines changed

15 files changed

+166
-95
lines changed

packages/playwright-test/src/experimentalLoader.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,8 @@
1515
*/
1616

1717
import fs from 'fs';
18-
import path from 'path';
19-
import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader';
2018
import { transformHook } from './transform';
2119

22-
const tsConfigCache = new Map<string, TsConfigLoaderResult>();
23-
2420
async function resolve(specifier: string, context: { parentURL: string }, defaultResolve: any) {
2521
if (specifier.endsWith('.js') || specifier.endsWith('.ts') || specifier.endsWith('.mjs'))
2622
return defaultResolve(specifier, context, defaultResolve);
@@ -36,17 +32,8 @@ async function resolve(specifier: string, context: { parentURL: string }, defaul
3632
async function load(url: string, context: any, defaultLoad: any) {
3733
if (url.endsWith('.ts') || url.endsWith('.tsx')) {
3834
const filename = url.substring('file://'.length);
39-
const cwd = path.dirname(filename);
40-
let tsconfig = tsConfigCache.get(cwd);
41-
if (!tsconfig) {
42-
tsconfig = tsConfigLoader({
43-
getEnv: (name: string) => process.env[name],
44-
cwd
45-
});
46-
tsConfigCache.set(cwd, tsconfig);
47-
}
4835
const code = fs.readFileSync(filename, 'utf-8');
49-
const source = transformHook(code, filename, tsconfig, true);
36+
const source = transformHook(code, filename, true);
5037
return { format: 'module', source };
5138
}
5239
return defaultLoad(url, context, defaultLoad);

packages/playwright-test/src/loader.ts

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,10 @@ import { ProjectImpl } from './project';
2727
import { Reporter } from '../types/testReporter';
2828
import { BuiltInReporter, builtInReporters } from './runner';
2929
import { isRegExp } from 'playwright-core/lib/utils/utils';
30-
import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader';
3130

3231
// To allow multiple loaders in the same process without clearing require cache,
3332
// we make these maps global.
3433
const cachedFileSuites = new Map<string, Suite>();
35-
const cachedTSConfigs = new Map<string, TsConfigLoaderResult>();
3634

3735
export class Loader {
3836
private _defaultConfig: Config;
@@ -194,18 +192,7 @@ export class Loader {
194192

195193

196194
private async _requireOrImport(file: string) {
197-
// Respect tsconfig paths.
198-
const cwd = path.dirname(file);
199-
let tsconfig = cachedTSConfigs.get(cwd);
200-
if (!tsconfig) {
201-
tsconfig = tsConfigLoader({
202-
getEnv: (name: string) => process.env[name],
203-
cwd
204-
});
205-
cachedTSConfigs.set(cwd, tsconfig);
206-
}
207-
208-
const revertBabelRequire = installTransform(tsconfig);
195+
const revertBabelRequire = installTransform();
209196

210197
// Figure out if we are importing or requiring.
211198
let isModule: boolean;

packages/playwright-test/src/transform.ts

Lines changed: 75 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@ import * as pirates from 'pirates';
2222
import * as sourceMapSupport from 'source-map-support';
2323
import * as url from 'url';
2424
import type { Location } from './types';
25-
import { TsConfigLoaderResult } from './third_party/tsconfig-loader';
25+
import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader';
2626

2727
const version = 6;
2828
const cacheDir = process.env.PWTEST_CACHE_DIR || path.join(os.tmpdir(), 'playwright-transform-cache');
2929
const sourceMaps: Map<string, string> = new Map();
3030

31+
type ParsedTsConfigData = {
32+
absoluteBaseUrl: string,
33+
singlePath: { [key: string]: string },
34+
hash: string,
35+
alias: { [key: string]: string | ((s: string[]) => string) },
36+
};
37+
const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>();
38+
3139
const kStackTraceLimit = 15;
3240
Error.stackTraceLimit = kStackTraceLimit;
3341

@@ -47,9 +55,9 @@ sourceMapSupport.install({
4755
}
4856
});
4957

50-
function calculateCachePath(tsconfig: TsConfigLoaderResult, content: string, filePath: string): string {
58+
function calculateCachePath(tsconfigData: ParsedTsConfigData | undefined, content: string, filePath: string): string {
5159
const hash = crypto.createHash('sha1')
52-
.update(tsconfig.serialized || '')
60+
.update(tsconfigData?.hash || '')
5361
.update(process.env.PW_EXPERIMENTAL_TS_ESM ? 'esm' : 'no_esm')
5462
.update(content)
5563
.update(filePath)
@@ -59,10 +67,64 @@ function calculateCachePath(tsconfig: TsConfigLoaderResult, content: string, fil
5967
return path.join(cacheDir, hash[0] + hash[1], fileName);
6068
}
6169

62-
export function transformHook(code: string, filename: string, tsconfig: TsConfigLoaderResult, isModule = false): string {
70+
function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined {
71+
if (!tsconfig.tsConfigPath || !tsconfig.paths || !tsconfig.baseUrl)
72+
return;
73+
74+
const paths = tsconfig.paths;
75+
// Path that only contains "*", ".", "/" and "\" is too ambiguous.
76+
const ambiguousPath = Object.keys(paths).find(key => key.match(/^[*./\\]+$/));
77+
if (ambiguousPath)
78+
return;
79+
const multiplePath = Object.keys(paths).find(key => paths[key].length > 1);
80+
if (multiplePath)
81+
return;
82+
// Only leave a single path mapping.
83+
const singlePath = Object.fromEntries(Object.entries(paths).map(([key, values]) => ([key, values[0]])));
84+
// Make 'baseUrl' absolute, because it is relative to the tsconfig.json, not to cwd.
85+
const absoluteBaseUrl = path.resolve(path.dirname(tsconfig.tsConfigPath), tsconfig.baseUrl);
86+
const hash = JSON.stringify({ absoluteBaseUrl, singlePath });
87+
88+
const alias: ParsedTsConfigData['alias'] = {};
89+
for (const [key, value] of Object.entries(singlePath)) {
90+
const regexKey = '^' + key.replace('*', '.*');
91+
alias[regexKey] = ([name]) => {
92+
let relative: string;
93+
if (key.endsWith('/*'))
94+
relative = value.substring(0, value.length - 1) + name.substring(key.length - 1);
95+
else
96+
relative = value;
97+
relative = relative.replace(/\//g, path.sep);
98+
return path.resolve(absoluteBaseUrl, relative);
99+
};
100+
}
101+
102+
return {
103+
absoluteBaseUrl,
104+
singlePath,
105+
hash,
106+
alias,
107+
};
108+
}
109+
110+
function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | undefined {
111+
const cwd = path.dirname(file);
112+
if (!cachedTSConfigs.has(cwd)) {
113+
const loaded = tsConfigLoader({
114+
getEnv: (name: string) => process.env[name],
115+
cwd
116+
});
117+
cachedTSConfigs.set(cwd, validateTsConfig(loaded));
118+
}
119+
return cachedTSConfigs.get(cwd);
120+
}
121+
122+
export function transformHook(code: string, filename: string, isModule = false): string {
63123
if (isComponentImport(filename))
64124
return componentStub();
65-
const cachePath = calculateCachePath(tsconfig, code, filename);
125+
126+
const tsconfigData = loadAndValidateTsconfigForFile(filename);
127+
const cachePath = calculateCachePath(tsconfigData, code, filename);
66128
const codePath = cachePath + '.js';
67129
const sourceMapPath = cachePath + '.map';
68130
sourceMaps.set(filename, sourceMapPath);
@@ -73,30 +135,6 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
73135
process.env.BROWSERSLIST_IGNORE_OLD_DATA = 'true';
74136
const babel: typeof import('@babel/core') = require('@babel/core');
75137

76-
const hasBaseUrl = !!tsconfig.baseUrl;
77-
const extensions = ['', '.js', '.ts', '.mjs', ...(process.env.PW_COMPONENT_TESTING ? ['.tsx', '.jsx'] : [])]; const alias: { [key: string]: string | ((s: string[]) => string) } = {};
78-
for (const [key, values] of Object.entries(tsconfig.paths || { '*': '*' })) {
79-
const regexKey = '^' + key.replace('*', '.*');
80-
alias[regexKey] = ([name]) => {
81-
for (const value of values) {
82-
let relative: string;
83-
if (key === '*' && value === '*')
84-
relative = name;
85-
else if (key.endsWith('/*'))
86-
relative = value.substring(0, value.length - 1) + name.substring(key.length - 1);
87-
else
88-
relative = value;
89-
relative = relative.replace(/\//g, path.sep);
90-
const result = path.resolve(tsconfig.baseUrl || '', relative);
91-
for (const extension of extensions) {
92-
if (fs.existsSync(result + extension))
93-
return result + extension;
94-
}
95-
}
96-
return name;
97-
};
98-
}
99-
100138
const plugins = [
101139
[require.resolve('@babel/plugin-proposal-class-properties')],
102140
[require.resolve('@babel/plugin-proposal-numeric-separator')],
@@ -110,10 +148,13 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
110148
[require.resolve('@babel/plugin-proposal-export-namespace-from')],
111149
] as any;
112150

113-
if (hasBaseUrl) {
151+
if (tsconfigData) {
114152
plugins.push([require.resolve('babel-plugin-module-resolver'), {
115153
root: ['./'],
116-
alias
154+
alias: tsconfigData.alias,
155+
// Silences warning 'Could not resovle ...' that we trigger because we resolve
156+
// into 'foo/bar', and not 'foo/bar.ts'.
157+
loglevel: 'silent',
117158
}]);
118159
}
119160

@@ -143,16 +184,13 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
143184
fs.mkdirSync(path.dirname(cachePath), { recursive: true });
144185
if (result.map)
145186
fs.writeFileSync(sourceMapPath, JSON.stringify(result.map), 'utf8');
146-
// Compiled files with base URL depend on the FS state during compilation,
147-
// never cache them.
148-
if (!hasBaseUrl)
149-
fs.writeFileSync(codePath, result.code, 'utf8');
187+
fs.writeFileSync(codePath, result.code, 'utf8');
150188
}
151189
return result.code || '';
152190
}
153191

154-
export function installTransform(tsconfig: TsConfigLoaderResult): () => void {
155-
return pirates.addHook((code: string, filename: string) => transformHook(code, filename, tsconfig), { exts: ['.ts', '.tsx'] });
192+
export function installTransform(): () => void {
193+
return pirates.addHook((code: string, filename: string) => transformHook(code, filename), { exts: ['.ts', '.tsx'] });
156194
}
157195

158196
export function wrapFunctionWithLocation<A extends any[], R>(func: (location: Location, ...args: A) => R): (...args: A) => R {

tests/browsertype-connect.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import fs from 'fs';
1919
import * as path from 'path';
20-
import { getUserAgent } from 'playwright-core/lib/utils/utils';
20+
import { getUserAgent } from '../packages/playwright-core/lib/utils/utils';
2121
import WebSocket from 'ws';
2222
import { expect, playwrightTest as test } from './config/browserTest';
2323
import { parseTrace, suppressCertificateWarning } from './config/utils';

tests/chromium/chromium.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { contextTest as test, expect } from '../config/browserTest';
1919
import { playwrightTest } from '../config/browserTest';
2020
import http from 'http';
2121
import fs from 'fs';
22-
import { getUserAgent } from 'playwright-core/lib/utils/utils';
22+
import { getUserAgent } from '../../packages/playwright-core/lib/utils/utils';
2323
import { suppressCertificateWarning } from '../config/utils';
2424

2525
test('should create a worker from a service worker', async ({ page, server }) => {

tests/config/browserTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import * as os from 'os';
1919
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
2020
import * as path from 'path';
2121
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
22-
import { removeFolders } from 'playwright-core/lib/utils/utils';
22+
import { removeFolders } from '../../packages/playwright-core/lib/utils/utils';
2323
import { baseTest } from './baseTest';
2424
import { RemoteServer, RemoteServerOptions } from './remoteServer';
2525

tests/global-fetch.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import http from 'http';
1818
import os from 'os';
1919
import * as util from 'util';
20-
import { getPlaywrightVersion } from 'playwright-core/lib/utils/utils';
20+
import { getPlaywrightVersion } from '../packages/playwright-core/lib/utils/utils';
2121
import { expect, playwrightTest as it } from './config/browserTest';
2222

2323
it.skip(({ mode }) => mode !== 'default');

tests/har.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import fs from 'fs';
2121
import http2 from 'http2';
2222
import type { BrowserContext, BrowserContextOptions } from 'playwright-core';
2323
import type { AddressInfo } from 'net';
24-
import type { Log } from 'playwright-core/lib/server/supplements/har/har';
24+
import type { Log } from '../packages/playwright-core/src/server/supplements/har/har';
2525

2626
async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, outputPath: string = 'test.har') {
2727
const harPath = testInfo.outputPath(outputPath);

tests/inspector/inspectorTest.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
import { contextTest } from '../config/browserTest';
1818
import type { Page } from 'playwright-core';
1919
import * as path from 'path';
20-
import type { Source } from 'playwright-core/lib/server/supplements/recorder/recorderTypes';
20+
import type { Source } from '../../packages/playwright-core/src/server/supplements/recorder/recorderTypes';
2121
import { CommonFixtures, TestChildProcess } from '../config/commonFixtures';
2222
export { expect } from '@playwright/test';
2323

tests/playwright-test/playwright.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { test, expect, stripAscii } from './playwright-test-fixtures';
1818
import fs from 'fs';
1919
import path from 'path';
2020
import { spawnSync } from 'child_process';
21-
import { registry } from 'playwright-core/lib/utils/registry';
21+
import { registry } from '../../packages/playwright-core/lib/utils/registry';
2222

2323
const ffmpeg = registry.findExecutable('ffmpeg')!.executablePath();
2424

0 commit comments

Comments
 (0)