Skip to content

Commit

Permalink
cherry-pick(#11662) fix(test runner): resolve tsconfig for each file (#…
Browse files Browse the repository at this point in the history
…11695)

This allows us to properly handle path mappings
that are not too ambiguous.

Co-authored-by: Dmitry Gozman <dgozman@gmail.com>
  • Loading branch information
aslushnikov and dgozman committed Jan 27, 2022
1 parent 9422974 commit 5415703
Show file tree
Hide file tree
Showing 15 changed files with 166 additions and 95 deletions.
15 changes: 1 addition & 14 deletions packages/playwright-test/src/experimentalLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,8 @@
*/

import fs from 'fs';
import path from 'path';
import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader';
import { transformHook } from './transform';

const tsConfigCache = new Map<string, TsConfigLoaderResult>();

async function resolve(specifier: string, context: { parentURL: string }, defaultResolve: any) {
if (specifier.endsWith('.js') || specifier.endsWith('.ts') || specifier.endsWith('.mjs'))
return defaultResolve(specifier, context, defaultResolve);
Expand All @@ -36,17 +32,8 @@ async function resolve(specifier: string, context: { parentURL: string }, defaul
async function load(url: string, context: any, defaultLoad: any) {
if (url.endsWith('.ts') || url.endsWith('.tsx')) {
const filename = url.substring('file://'.length);
const cwd = path.dirname(filename);
let tsconfig = tsConfigCache.get(cwd);
if (!tsconfig) {
tsconfig = tsConfigLoader({
getEnv: (name: string) => process.env[name],
cwd
});
tsConfigCache.set(cwd, tsconfig);
}
const code = fs.readFileSync(filename, 'utf-8');
const source = transformHook(code, filename, tsconfig, true);
const source = transformHook(code, filename, true);
return { format: 'module', source };
}
return defaultLoad(url, context, defaultLoad);
Expand Down
15 changes: 1 addition & 14 deletions packages/playwright-test/src/loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,10 @@ import { ProjectImpl } from './project';
import { Reporter } from '../types/testReporter';
import { BuiltInReporter, builtInReporters } from './runner';
import { isRegExp } from 'playwright-core/lib/utils/utils';
import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader';

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

export class Loader {
private _defaultConfig: Config;
Expand Down Expand Up @@ -194,18 +192,7 @@ export class Loader {


private async _requireOrImport(file: string) {
// Respect tsconfig paths.
const cwd = path.dirname(file);
let tsconfig = cachedTSConfigs.get(cwd);
if (!tsconfig) {
tsconfig = tsConfigLoader({
getEnv: (name: string) => process.env[name],
cwd
});
cachedTSConfigs.set(cwd, tsconfig);
}

const revertBabelRequire = installTransform(tsconfig);
const revertBabelRequire = installTransform();

// Figure out if we are importing or requiring.
let isModule: boolean;
Expand Down
112 changes: 75 additions & 37 deletions packages/playwright-test/src/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,20 @@ import * as pirates from 'pirates';
import * as sourceMapSupport from 'source-map-support';
import * as url from 'url';
import type { Location } from './types';
import { TsConfigLoaderResult } from './third_party/tsconfig-loader';
import { tsConfigLoader, TsConfigLoaderResult } from './third_party/tsconfig-loader';

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

type ParsedTsConfigData = {
absoluteBaseUrl: string,
singlePath: { [key: string]: string },
hash: string,
alias: { [key: string]: string | ((s: string[]) => string) },
};
const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>();

const kStackTraceLimit = 15;
Error.stackTraceLimit = kStackTraceLimit;

Expand All @@ -47,9 +55,9 @@ sourceMapSupport.install({
}
});

function calculateCachePath(tsconfig: TsConfigLoaderResult, content: string, filePath: string): string {
function calculateCachePath(tsconfigData: ParsedTsConfigData | undefined, content: string, filePath: string): string {
const hash = crypto.createHash('sha1')
.update(tsconfig.serialized || '')
.update(tsconfigData?.hash || '')
.update(process.env.PW_EXPERIMENTAL_TS_ESM ? 'esm' : 'no_esm')
.update(content)
.update(filePath)
Expand All @@ -59,10 +67,64 @@ function calculateCachePath(tsconfig: TsConfigLoaderResult, content: string, fil
return path.join(cacheDir, hash[0] + hash[1], fileName);
}

export function transformHook(code: string, filename: string, tsconfig: TsConfigLoaderResult, isModule = false): string {
function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined {
if (!tsconfig.tsConfigPath || !tsconfig.paths || !tsconfig.baseUrl)
return;

const paths = tsconfig.paths;
// Path that only contains "*", ".", "/" and "\" is too ambiguous.
const ambiguousPath = Object.keys(paths).find(key => key.match(/^[*./\\]+$/));
if (ambiguousPath)
return;
const multiplePath = Object.keys(paths).find(key => paths[key].length > 1);
if (multiplePath)
return;
// Only leave a single path mapping.
const singlePath = Object.fromEntries(Object.entries(paths).map(([key, values]) => ([key, values[0]])));
// Make 'baseUrl' absolute, because it is relative to the tsconfig.json, not to cwd.
const absoluteBaseUrl = path.resolve(path.dirname(tsconfig.tsConfigPath), tsconfig.baseUrl);
const hash = JSON.stringify({ absoluteBaseUrl, singlePath });

const alias: ParsedTsConfigData['alias'] = {};
for (const [key, value] of Object.entries(singlePath)) {
const regexKey = '^' + key.replace('*', '.*');
alias[regexKey] = ([name]) => {
let relative: string;
if (key.endsWith('/*'))
relative = value.substring(0, value.length - 1) + name.substring(key.length - 1);
else
relative = value;
relative = relative.replace(/\//g, path.sep);
return path.resolve(absoluteBaseUrl, relative);
};
}

return {
absoluteBaseUrl,
singlePath,
hash,
alias,
};
}

function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | undefined {
const cwd = path.dirname(file);
if (!cachedTSConfigs.has(cwd)) {
const loaded = tsConfigLoader({
getEnv: (name: string) => process.env[name],
cwd
});
cachedTSConfigs.set(cwd, validateTsConfig(loaded));
}
return cachedTSConfigs.get(cwd);
}

export function transformHook(code: string, filename: string, isModule = false): string {
if (isComponentImport(filename))
return componentStub();
const cachePath = calculateCachePath(tsconfig, code, filename);

const tsconfigData = loadAndValidateTsconfigForFile(filename);
const cachePath = calculateCachePath(tsconfigData, code, filename);
const codePath = cachePath + '.js';
const sourceMapPath = cachePath + '.map';
sourceMaps.set(filename, sourceMapPath);
Expand All @@ -73,30 +135,6 @@ 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 || { '*': '*' })) {
const regexKey = '^' + key.replace('*', '.*');
alias[regexKey] = ([name]) => {
for (const value of values) {
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) {
if (fs.existsSync(result + extension))
return result + extension;
}
}
return name;
};
}

const plugins = [
[require.resolve('@babel/plugin-proposal-class-properties')],
[require.resolve('@babel/plugin-proposal-numeric-separator')],
Expand All @@ -110,10 +148,13 @@ export function transformHook(code: string, filename: string, tsconfig: TsConfig
[require.resolve('@babel/plugin-proposal-export-namespace-from')],
] as any;

if (hasBaseUrl) {
if (tsconfigData) {
plugins.push([require.resolve('babel-plugin-module-resolver'), {
root: ['./'],
alias
alias: tsconfigData.alias,
// Silences warning 'Could not resovle ...' that we trigger because we resolve
// into 'foo/bar', and not 'foo/bar.ts'.
loglevel: 'silent',
}]);
}

Expand Down Expand Up @@ -143,16 +184,13 @@ 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');
// Compiled files with base URL depend on the FS state during compilation,
// never cache them.
if (!hasBaseUrl)
fs.writeFileSync(codePath, result.code, 'utf8');
fs.writeFileSync(codePath, result.code, 'utf8');
}
return result.code || '';
}

export function installTransform(tsconfig: TsConfigLoaderResult): () => void {
return pirates.addHook((code: string, filename: string) => transformHook(code, filename, tsconfig), { exts: ['.ts', '.tsx'] });
export function installTransform(): () => void {
return pirates.addHook((code: string, filename: string) => transformHook(code, filename), { exts: ['.ts', '.tsx'] });
}

export function wrapFunctionWithLocation<A extends any[], R>(func: (location: Location, ...args: A) => R): (...args: A) => R {
Expand Down
2 changes: 1 addition & 1 deletion tests/browsertype-connect.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import fs from 'fs';
import * as path from 'path';
import { getUserAgent } from 'playwright-core/lib/utils/utils';
import { getUserAgent } from '../packages/playwright-core/lib/utils/utils';
import WebSocket from 'ws';
import { expect, playwrightTest as test } from './config/browserTest';
import { parseTrace, suppressCertificateWarning } from './config/utils';
Expand Down
2 changes: 1 addition & 1 deletion tests/chromium/chromium.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { contextTest as test, expect } from '../config/browserTest';
import { playwrightTest } from '../config/browserTest';
import http from 'http';
import fs from 'fs';
import { getUserAgent } from 'playwright-core/lib/utils/utils';
import { getUserAgent } from '../../packages/playwright-core/lib/utils/utils';
import { suppressCertificateWarning } from '../config/utils';

test('should create a worker from a service worker', async ({ page, server }) => {
Expand Down
2 changes: 1 addition & 1 deletion tests/config/browserTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import * as os from 'os';
import { PageTestFixtures, PageWorkerFixtures } from '../page/pageTestApi';
import * as path from 'path';
import type { BrowserContext, BrowserContextOptions, BrowserType, Page } from 'playwright-core';
import { removeFolders } from 'playwright-core/lib/utils/utils';
import { removeFolders } from '../../packages/playwright-core/lib/utils/utils';
import { baseTest } from './baseTest';
import { RemoteServer, RemoteServerOptions } from './remoteServer';

Expand Down
2 changes: 1 addition & 1 deletion tests/global-fetch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import http from 'http';
import os from 'os';
import * as util from 'util';
import { getPlaywrightVersion } from 'playwright-core/lib/utils/utils';
import { getPlaywrightVersion } from '../packages/playwright-core/lib/utils/utils';
import { expect, playwrightTest as it } from './config/browserTest';

it.skip(({ mode }) => mode !== 'default');
Expand Down
2 changes: 1 addition & 1 deletion tests/har.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import fs from 'fs';
import http2 from 'http2';
import type { BrowserContext, BrowserContextOptions } from 'playwright-core';
import type { AddressInfo } from 'net';
import type { Log } from 'playwright-core/lib/server/supplements/har/har';
import type { Log } from '../packages/playwright-core/src/server/supplements/har/har';

async function pageWithHar(contextFactory: (options?: BrowserContextOptions) => Promise<BrowserContext>, testInfo: any, outputPath: string = 'test.har') {
const harPath = testInfo.outputPath(outputPath);
Expand Down
2 changes: 1 addition & 1 deletion tests/inspector/inspectorTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import { contextTest } from '../config/browserTest';
import type { Page } from 'playwright-core';
import * as path from 'path';
import type { Source } from 'playwright-core/lib/server/supplements/recorder/recorderTypes';
import type { Source } from '../../packages/playwright-core/src/server/supplements/recorder/recorderTypes';
import { CommonFixtures, TestChildProcess } from '../config/commonFixtures';
export { expect } from '@playwright/test';

Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/playwright.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { test, expect, stripAscii } from './playwright-test-fixtures';
import fs from 'fs';
import path from 'path';
import { spawnSync } from 'child_process';
import { registry } from 'playwright-core/lib/utils/registry';
import { registry } from '../../packages/playwright-core/lib/utils/registry';

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

Expand Down
2 changes: 1 addition & 1 deletion tests/playwright-test/reporter-html.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*/

import { test as baseTest, expect } from './playwright-test-fixtures';
import { HttpServer } from 'playwright-core/lib/utils/httpServer';
import { HttpServer } from '../../packages/playwright-core/lib/utils/httpServer';
import { startHtmlReportServer } from '../../packages/playwright-test/lib/reporters/html';

const test = baseTest.extend<{ showReport: () => Promise<void> }>({
Expand Down
Loading

0 comments on commit 5415703

Please sign in to comment.