mirror of
https://github.com/microsoft/playwright.git
synced 2025-01-05 19:04:43 +03:00
feat(tsconfig): respect tsconfig references (#29330)
Fixes https://github.com/microsoft/playwright/issues/29256
This commit is contained in:
parent
b9565ea26e
commit
dd0ef72cd8
@ -31,7 +31,7 @@ import { json5 } from '../utilsBundle';
|
||||
/**
|
||||
* Typing for the parts of tsconfig that we care about
|
||||
*/
|
||||
interface Tsconfig {
|
||||
interface TsConfig {
|
||||
extends?: string;
|
||||
compilerOptions?: {
|
||||
baseUrl?: string;
|
||||
@ -39,55 +39,29 @@ interface Tsconfig {
|
||||
strict?: boolean;
|
||||
allowJs?: boolean;
|
||||
};
|
||||
references?: any[];
|
||||
references?: { path: string }[];
|
||||
}
|
||||
|
||||
export interface TsConfigLoaderResult {
|
||||
tsConfigPath: string | undefined;
|
||||
baseUrl: string | undefined;
|
||||
paths: { [key: string]: Array<string> } | undefined;
|
||||
serialized: string | undefined;
|
||||
allowJs: boolean;
|
||||
export interface LoadedTsConfig {
|
||||
tsConfigPath: string;
|
||||
baseUrl?: string;
|
||||
paths?: { [key: string]: Array<string> };
|
||||
allowJs?: boolean;
|
||||
}
|
||||
|
||||
export interface TsConfigLoaderParams {
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export function tsConfigLoader({
|
||||
cwd,
|
||||
}: TsConfigLoaderParams): TsConfigLoaderResult {
|
||||
const loadResult = loadSyncDefault(cwd);
|
||||
loadResult.serialized = JSON.stringify(loadResult);
|
||||
return loadResult;
|
||||
}
|
||||
|
||||
function loadSyncDefault(
|
||||
cwd: string,
|
||||
): TsConfigLoaderResult {
|
||||
// Tsconfig.loadSync uses path.resolve. This is why we can use an absolute path as filename
|
||||
|
||||
export function tsConfigLoader({ cwd, }: TsConfigLoaderParams): LoadedTsConfig[] {
|
||||
const configPath = resolveConfigPath(cwd);
|
||||
|
||||
if (!configPath) {
|
||||
return {
|
||||
tsConfigPath: undefined,
|
||||
baseUrl: undefined,
|
||||
paths: undefined,
|
||||
serialized: undefined,
|
||||
allowJs: false,
|
||||
};
|
||||
}
|
||||
const config = loadTsconfig(configPath);
|
||||
if (!configPath)
|
||||
return [];
|
||||
|
||||
return {
|
||||
tsConfigPath: configPath,
|
||||
baseUrl:
|
||||
(config && config.compilerOptions && config.compilerOptions.baseUrl),
|
||||
paths: config && config.compilerOptions && config.compilerOptions.paths,
|
||||
serialized: undefined,
|
||||
allowJs: !!config?.compilerOptions?.allowJs,
|
||||
};
|
||||
const references: LoadedTsConfig[] = [];
|
||||
const config = loadTsConfig(configPath, references);
|
||||
return [config, ...references];
|
||||
}
|
||||
|
||||
function resolveConfigPath(cwd: string): string | undefined {
|
||||
@ -122,79 +96,64 @@ export function walkForTsConfig(
|
||||
return walkForTsConfig(parentDirectory, existsSync);
|
||||
}
|
||||
|
||||
function loadTsconfig(
|
||||
function resolveConfigFile(baseConfigFile: string, referencedConfigFile: string) {
|
||||
if (!referencedConfigFile.endsWith('.json'))
|
||||
referencedConfigFile += '.json';
|
||||
const currentDir = path.dirname(baseConfigFile);
|
||||
let resolvedConfigFile = path.resolve(currentDir, referencedConfigFile);
|
||||
if (referencedConfigFile.indexOf('/') !== -1 && referencedConfigFile.indexOf('.') !== -1 && !fs.existsSync(referencedConfigFile))
|
||||
resolvedConfigFile = path.join(currentDir, 'node_modules', referencedConfigFile);
|
||||
return resolvedConfigFile;
|
||||
}
|
||||
|
||||
function loadTsConfig(
|
||||
configFilePath: string,
|
||||
): Tsconfig | undefined {
|
||||
if (!fs.existsSync(configFilePath)) {
|
||||
return undefined;
|
||||
}
|
||||
references: LoadedTsConfig[],
|
||||
visited = new Map<string, LoadedTsConfig>(),
|
||||
): LoadedTsConfig {
|
||||
if (visited.has(configFilePath))
|
||||
return visited.get(configFilePath)!;
|
||||
|
||||
let result: LoadedTsConfig = {
|
||||
tsConfigPath: configFilePath,
|
||||
};
|
||||
visited.set(configFilePath, result);
|
||||
|
||||
if (!fs.existsSync(configFilePath))
|
||||
return result;
|
||||
|
||||
const configString = fs.readFileSync(configFilePath, 'utf-8');
|
||||
const cleanedJson = StripBom(configString);
|
||||
const parsedConfig: Tsconfig = json5.parse(cleanedJson);
|
||||
|
||||
let config: Tsconfig = {};
|
||||
const parsedConfig: TsConfig = json5.parse(cleanedJson);
|
||||
|
||||
const extendsArray = Array.isArray(parsedConfig.extends) ? parsedConfig.extends : (parsedConfig.extends ? [parsedConfig.extends] : []);
|
||||
for (let extendedConfig of extendsArray) {
|
||||
if (
|
||||
typeof extendedConfig === "string" &&
|
||||
extendedConfig.indexOf(".json") === -1
|
||||
) {
|
||||
extendedConfig += ".json";
|
||||
}
|
||||
const currentDir = path.dirname(configFilePath);
|
||||
let extendedConfigPath = path.join(currentDir, extendedConfig);
|
||||
if (
|
||||
extendedConfig.indexOf("/") !== -1 &&
|
||||
extendedConfig.indexOf(".") !== -1 &&
|
||||
!fs.existsSync(extendedConfigPath)
|
||||
) {
|
||||
extendedConfigPath = path.join(
|
||||
currentDir,
|
||||
"node_modules",
|
||||
extendedConfig
|
||||
);
|
||||
}
|
||||
|
||||
const base =
|
||||
loadTsconfig(extendedConfigPath) || {};
|
||||
for (const extendedConfig of extendsArray) {
|
||||
const extendedConfigPath = resolveConfigFile(configFilePath, extendedConfig);
|
||||
const base = loadTsConfig(extendedConfigPath, references, visited);
|
||||
|
||||
// baseUrl should be interpreted as relative to the base tsconfig,
|
||||
// but we need to update it so it is relative to the original tsconfig being loaded
|
||||
if (base.compilerOptions && base.compilerOptions.baseUrl) {
|
||||
if (base.baseUrl && base.baseUrl) {
|
||||
const extendsDir = path.dirname(extendedConfig);
|
||||
base.compilerOptions.baseUrl = path.join(
|
||||
extendsDir,
|
||||
base.compilerOptions.baseUrl
|
||||
);
|
||||
base.baseUrl = path.join(extendsDir, base.baseUrl);
|
||||
}
|
||||
|
||||
config = mergeConfigs(config, base);
|
||||
result = { ...result, ...base };
|
||||
}
|
||||
|
||||
config = mergeConfigs(config, parsedConfig);
|
||||
// The only top-level property that is excluded from inheritance is "references".
|
||||
// https://www.typescriptlang.org/tsconfig#extends
|
||||
config.references = parsedConfig.references;
|
||||
const loadedConfig = Object.fromEntries(Object.entries({
|
||||
baseUrl: parsedConfig.compilerOptions?.baseUrl,
|
||||
paths: parsedConfig.compilerOptions?.paths,
|
||||
allowJs: parsedConfig?.compilerOptions?.allowJs,
|
||||
}).filter(([, value]) => value !== undefined));
|
||||
|
||||
if (path.basename(configFilePath) === 'jsconfig.json' && config.compilerOptions?.allowJs === undefined) {
|
||||
config.compilerOptions = config.compilerOptions || {};
|
||||
config.compilerOptions.allowJs = true;
|
||||
}
|
||||
result = { ...result, ...loadedConfig };
|
||||
|
||||
return config;
|
||||
}
|
||||
for (const ref of parsedConfig.references || [])
|
||||
references.push(loadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
|
||||
|
||||
function mergeConfigs(base: Tsconfig, override: Tsconfig): Tsconfig {
|
||||
return {
|
||||
...base,
|
||||
...override,
|
||||
compilerOptions: {
|
||||
...base.compilerOptions,
|
||||
...override.compilerOptions,
|
||||
},
|
||||
};
|
||||
if (path.basename(configFilePath) === 'jsconfig.json' && result.allowJs === undefined)
|
||||
result.allowJs = true;
|
||||
return result;
|
||||
}
|
||||
|
||||
function StripBom(string: string) {
|
||||
|
@ -19,7 +19,7 @@ import path from 'path';
|
||||
import url from 'url';
|
||||
import { sourceMapSupport, pirates } from '../utilsBundle';
|
||||
import type { Location } from '../../types/testReporter';
|
||||
import type { TsConfigLoaderResult } from '../third_party/tsconfig-loader';
|
||||
import type { LoadedTsConfig } from '../third_party/tsconfig-loader';
|
||||
import { tsConfigLoader } from '../third_party/tsconfig-loader';
|
||||
import Module from 'module';
|
||||
import type { BabelPlugin, BabelTransformFunction } from './babelBundle';
|
||||
@ -34,7 +34,7 @@ type ParsedTsConfigData = {
|
||||
paths: { key: string, values: string[] }[];
|
||||
allowJs: boolean;
|
||||
};
|
||||
const cachedTSConfigs = new Map<string, ParsedTsConfigData | undefined>();
|
||||
const cachedTSConfigs = new Map<string, ParsedTsConfigData[]>();
|
||||
|
||||
export type TransformConfig = {
|
||||
babelPlugins: [string, any?][];
|
||||
@ -57,9 +57,7 @@ export function transformConfig(): TransformConfig {
|
||||
return _transformConfig;
|
||||
}
|
||||
|
||||
function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData | undefined {
|
||||
if (!tsconfig.tsConfigPath)
|
||||
return;
|
||||
function validateTsConfig(tsconfig: LoadedTsConfig): ParsedTsConfigData {
|
||||
// Make 'baseUrl' absolute, because it is relative to the tsconfig.json, not to cwd.
|
||||
// When no explicit baseUrl is set, resolve paths relative to the tsconfig file.
|
||||
// See https://www.typescriptlang.org/tsconfig#paths
|
||||
@ -67,21 +65,19 @@ function validateTsConfig(tsconfig: TsConfigLoaderResult): ParsedTsConfigData |
|
||||
// Only add the catch-all mapping when baseUrl is specified
|
||||
const pathsFallback = tsconfig.baseUrl ? [{ key: '*', values: ['*'] }] : [];
|
||||
return {
|
||||
allowJs: tsconfig.allowJs,
|
||||
allowJs: !!tsconfig.allowJs,
|
||||
absoluteBaseUrl,
|
||||
paths: Object.entries(tsconfig.paths || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
|
||||
};
|
||||
}
|
||||
|
||||
function loadAndValidateTsconfigForFile(file: string): ParsedTsConfigData | undefined {
|
||||
function loadAndValidateTsconfigsForFile(file: string): ParsedTsConfigData[] {
|
||||
const cwd = path.dirname(file);
|
||||
if (!cachedTSConfigs.has(cwd)) {
|
||||
const loaded = tsConfigLoader({
|
||||
cwd
|
||||
});
|
||||
cachedTSConfigs.set(cwd, validateTsConfig(loaded));
|
||||
const loaded = tsConfigLoader({ cwd });
|
||||
cachedTSConfigs.set(cwd, loaded.map(validateTsConfig));
|
||||
}
|
||||
return cachedTSConfigs.get(cwd);
|
||||
return cachedTSConfigs.get(cwd)!;
|
||||
}
|
||||
|
||||
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||
@ -97,8 +93,10 @@ export function resolveHook(filename: string, specifier: string): string | undef
|
||||
return resolveImportSpecifierExtension(path.resolve(path.dirname(filename), specifier));
|
||||
|
||||
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
|
||||
const tsconfig = loadAndValidateTsconfigForFile(filename);
|
||||
if (tsconfig && (isTypeScript || tsconfig.allowJs)) {
|
||||
const tsconfigs = loadAndValidateTsconfigsForFile(filename);
|
||||
for (const tsconfig of tsconfigs) {
|
||||
if (!isTypeScript && !tsconfig.allowJs)
|
||||
continue;
|
||||
let longestPrefixLength = -1;
|
||||
let pathMatchedByLongestPrefix: string | undefined;
|
||||
|
||||
|
@ -570,3 +570,39 @@ test('should import packages with non-index main script through path resolver',
|
||||
expect(result.output).not.toContain(`find module`);
|
||||
expect(result.output).toContain(`foo=42`);
|
||||
});
|
||||
|
||||
test('should respect tsconfig project references', async ({ runInlineTest }) => {
|
||||
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/29256' });
|
||||
|
||||
const result = await runInlineTest({
|
||||
'playwright.config.ts': `export default { projects: [{name: 'foo'}], };`,
|
||||
'tsconfig.json': `{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.test.json" }
|
||||
]
|
||||
}`,
|
||||
'tsconfig.test.json': `{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"util/*": ["./foo/bar/util/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'foo/bar/util/b.ts': `
|
||||
export const foo: string = 'foo';
|
||||
`,
|
||||
'a.test.ts': `
|
||||
import { foo } from 'util/b';
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe('foo');
|
||||
});
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.exitCode).toBe(0);
|
||||
expect(result.passed).toBe(1);
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user