mirror of
https://github.com/microsoft/playwright.git
synced 2025-06-26 21:40:17 +00:00
fix(tsconfig): when extending, retain pathsBasePath from the original config (#29822)
This fixes a case where we incorrectly used the final config's base path when resolving relative path mappings in the absence of the baseUrl. Fixes #29816.
This commit is contained in:
parent
8bf8091cb1
commit
a3ed799cd5
@ -44,8 +44,11 @@ interface TsConfig {
|
||||
|
||||
export interface LoadedTsConfig {
|
||||
tsConfigPath: string;
|
||||
baseUrl?: string;
|
||||
paths?: { [key: string]: Array<string> };
|
||||
paths?: {
|
||||
mapping: { [key: string]: Array<string> };
|
||||
pathsBasePath: string; // absolute path
|
||||
};
|
||||
absoluteBaseUrl?: string;
|
||||
allowJs?: boolean;
|
||||
}
|
||||
|
||||
@ -132,25 +135,27 @@ function loadTsConfig(
|
||||
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.baseUrl && base.baseUrl) {
|
||||
const extendsDir = path.dirname(extendedConfig);
|
||||
base.baseUrl = path.join(extendsDir, base.baseUrl);
|
||||
}
|
||||
// Retain result instance, so that caching works.
|
||||
Object.assign(result, base, { tsConfigPath: configFilePath });
|
||||
}
|
||||
|
||||
const loadedConfig = Object.fromEntries(Object.entries({
|
||||
baseUrl: parsedConfig.compilerOptions?.baseUrl,
|
||||
paths: parsedConfig.compilerOptions?.paths,
|
||||
allowJs: parsedConfig?.compilerOptions?.allowJs,
|
||||
}).filter(([, value]) => value !== undefined));
|
||||
|
||||
// Retain result instance, so that caching works.
|
||||
Object.assign(result, loadedConfig);
|
||||
if (parsedConfig.compilerOptions?.allowJs !== undefined)
|
||||
result.allowJs = parsedConfig.compilerOptions.allowJs;
|
||||
if (parsedConfig.compilerOptions?.paths !== undefined) {
|
||||
// We must store pathsBasePath from the config that defines "paths" and later resolve
|
||||
// based on this absolute path, when no "baseUrl" is specified. See tsc for reference:
|
||||
// https://github.com/microsoft/TypeScript/blob/353ccb7688351ae33ccf6e0acb913aa30621eaf4/src/compiler/commandLineParser.ts#L3129
|
||||
// https://github.com/microsoft/TypeScript/blob/353ccb7688351ae33ccf6e0acb913aa30621eaf4/src/compiler/moduleSpecifiers.ts#L510
|
||||
result.paths = {
|
||||
mapping: parsedConfig.compilerOptions.paths,
|
||||
pathsBasePath: path.dirname(configFilePath),
|
||||
};
|
||||
}
|
||||
if (parsedConfig.compilerOptions?.baseUrl !== undefined) {
|
||||
// Follow tsc and resolve all relative file paths in the config right away.
|
||||
// This way it is safe to inherit paths between the configs.
|
||||
result.absoluteBaseUrl = path.resolve(path.dirname(configFilePath), parsedConfig.compilerOptions.baseUrl);
|
||||
}
|
||||
|
||||
for (const ref of parsedConfig.references || [])
|
||||
references.push(loadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
|
||||
|
@ -30,7 +30,7 @@ import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules
|
||||
const version = require('../../package.json').version;
|
||||
|
||||
type ParsedTsConfigData = {
|
||||
absoluteBaseUrl: string;
|
||||
pathsBase?: string;
|
||||
paths: { key: string, values: string[] }[];
|
||||
allowJs: boolean;
|
||||
};
|
||||
@ -58,16 +58,15 @@ export function transformConfig(): TransformConfig {
|
||||
}
|
||||
|
||||
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
|
||||
const absoluteBaseUrl = path.resolve(path.dirname(tsconfig.tsConfigPath), tsconfig.baseUrl ?? '.');
|
||||
const pathsBase = tsconfig.absoluteBaseUrl ?? tsconfig.paths?.pathsBasePath;
|
||||
// Only add the catch-all mapping when baseUrl is specified
|
||||
const pathsFallback = tsconfig.baseUrl ? [{ key: '*', values: ['*'] }] : [];
|
||||
const pathsFallback = tsconfig.absoluteBaseUrl ? [{ key: '*', values: ['*'] }] : [];
|
||||
return {
|
||||
allowJs: !!tsconfig.allowJs,
|
||||
absoluteBaseUrl,
|
||||
paths: Object.entries(tsconfig.paths || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
|
||||
pathsBase,
|
||||
paths: Object.entries(tsconfig.paths?.mapping || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
|
||||
};
|
||||
}
|
||||
|
||||
@ -132,7 +131,7 @@ export function resolveHook(filename: string, specifier: string): string | undef
|
||||
let candidate = value;
|
||||
if (value.includes('*'))
|
||||
candidate = candidate.replace('*', matchedPartOfSpecifier);
|
||||
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate);
|
||||
candidate = path.resolve(tsconfig.pathsBase!, candidate);
|
||||
const existing = resolveImportSpecifierExtension(candidate);
|
||||
if (existing) {
|
||||
longestPrefixLength = keyPrefix.length;
|
||||
|
@ -505,6 +505,9 @@ test('should support extends in tsconfig.json', async ({ runInlineTest }) => {
|
||||
}`,
|
||||
'tsconfig.base1.json': `{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
},
|
||||
}`,
|
||||
'tsconfig.base2.json': `{
|
||||
"compilerOptions": {
|
||||
@ -518,7 +521,9 @@ test('should support extends in tsconfig.json', async ({ runInlineTest }) => {
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'a.test.ts': `
|
||||
'a.test.js': `
|
||||
// This js file is affected by tsconfig because allowJs is inherited.
|
||||
// Next line resolve to the final baseUrl ("dir") + relative path mapping ("./foo/bar/util/*").
|
||||
const { foo } = require('util/file');
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
@ -534,6 +539,36 @@ test('should support extends in tsconfig.json', async ({ runInlineTest }) => {
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should resolve paths relative to the originating config when extending and no baseUrl', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'tsconfig.json': `{
|
||||
"extends": ["./dir/tsconfig.base.json"],
|
||||
}`,
|
||||
'dir/tsconfig.base.json': `{
|
||||
"compilerOptions": {
|
||||
"paths": {
|
||||
"~/*": ["../mapped/*"],
|
||||
},
|
||||
},
|
||||
}`,
|
||||
'a.test.ts': `
|
||||
// This resolves relative to the base tsconfig that defined path mapping,
|
||||
// because there is no baseUrl in the final tsconfig.
|
||||
const { foo } = require('~/file');
|
||||
import { test, expect } from '@playwright/test';
|
||||
test('test', ({}, testInfo) => {
|
||||
expect(foo).toBe('foo');
|
||||
});
|
||||
`,
|
||||
'mapped/file.ts': `
|
||||
module.exports = { foo: 'foo' };
|
||||
`,
|
||||
});
|
||||
|
||||
expect(result.passed).toBe(1);
|
||||
expect(result.exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test('should import packages with non-index main script through path resolver', async ({ runInlineTest }) => {
|
||||
const result = await runInlineTest({
|
||||
'app/pkg/main.ts': `
|
||||
|
Loading…
x
Reference in New Issue
Block a user