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 {
|
export interface LoadedTsConfig {
|
||||||
tsConfigPath: string;
|
tsConfigPath: string;
|
||||||
baseUrl?: string;
|
paths?: {
|
||||||
paths?: { [key: string]: Array<string> };
|
mapping: { [key: string]: Array<string> };
|
||||||
|
pathsBasePath: string; // absolute path
|
||||||
|
};
|
||||||
|
absoluteBaseUrl?: string;
|
||||||
allowJs?: boolean;
|
allowJs?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,25 +135,27 @@ function loadTsConfig(
|
|||||||
for (const extendedConfig of extendsArray) {
|
for (const extendedConfig of extendsArray) {
|
||||||
const extendedConfigPath = resolveConfigFile(configFilePath, extendedConfig);
|
const extendedConfigPath = resolveConfigFile(configFilePath, extendedConfig);
|
||||||
const base = loadTsConfig(extendedConfigPath, references, visited);
|
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.
|
// Retain result instance, so that caching works.
|
||||||
Object.assign(result, base, { tsConfigPath: configFilePath });
|
Object.assign(result, base, { tsConfigPath: configFilePath });
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadedConfig = Object.fromEntries(Object.entries({
|
if (parsedConfig.compilerOptions?.allowJs !== undefined)
|
||||||
baseUrl: parsedConfig.compilerOptions?.baseUrl,
|
result.allowJs = parsedConfig.compilerOptions.allowJs;
|
||||||
paths: parsedConfig.compilerOptions?.paths,
|
if (parsedConfig.compilerOptions?.paths !== undefined) {
|
||||||
allowJs: parsedConfig?.compilerOptions?.allowJs,
|
// We must store pathsBasePath from the config that defines "paths" and later resolve
|
||||||
}).filter(([, value]) => value !== undefined));
|
// 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
|
||||||
// Retain result instance, so that caching works.
|
// https://github.com/microsoft/TypeScript/blob/353ccb7688351ae33ccf6e0acb913aa30621eaf4/src/compiler/moduleSpecifiers.ts#L510
|
||||||
Object.assign(result, loadedConfig);
|
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 || [])
|
for (const ref of parsedConfig.references || [])
|
||||||
references.push(loadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
|
references.push(loadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
|
||||||
|
@ -30,7 +30,7 @@ import { getFromCompilationCache, currentFileDepsCollector, belongsToNodeModules
|
|||||||
const version = require('../../package.json').version;
|
const version = require('../../package.json').version;
|
||||||
|
|
||||||
type ParsedTsConfigData = {
|
type ParsedTsConfigData = {
|
||||||
absoluteBaseUrl: string;
|
pathsBase?: string;
|
||||||
paths: { key: string, values: string[] }[];
|
paths: { key: string, values: string[] }[];
|
||||||
allowJs: boolean;
|
allowJs: boolean;
|
||||||
};
|
};
|
||||||
@ -58,16 +58,15 @@ export function transformConfig(): TransformConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateTsConfig(tsconfig: LoadedTsConfig): ParsedTsConfigData {
|
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.
|
// When no explicit baseUrl is set, resolve paths relative to the tsconfig file.
|
||||||
// See https://www.typescriptlang.org/tsconfig#paths
|
// 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
|
// Only add the catch-all mapping when baseUrl is specified
|
||||||
const pathsFallback = tsconfig.baseUrl ? [{ key: '*', values: ['*'] }] : [];
|
const pathsFallback = tsconfig.absoluteBaseUrl ? [{ key: '*', values: ['*'] }] : [];
|
||||||
return {
|
return {
|
||||||
allowJs: !!tsconfig.allowJs,
|
allowJs: !!tsconfig.allowJs,
|
||||||
absoluteBaseUrl,
|
pathsBase,
|
||||||
paths: Object.entries(tsconfig.paths || {}).map(([key, values]) => ({ key, values })).concat(pathsFallback)
|
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;
|
let candidate = value;
|
||||||
if (value.includes('*'))
|
if (value.includes('*'))
|
||||||
candidate = candidate.replace('*', matchedPartOfSpecifier);
|
candidate = candidate.replace('*', matchedPartOfSpecifier);
|
||||||
candidate = path.resolve(tsconfig.absoluteBaseUrl, candidate);
|
candidate = path.resolve(tsconfig.pathsBase!, candidate);
|
||||||
const existing = resolveImportSpecifierExtension(candidate);
|
const existing = resolveImportSpecifierExtension(candidate);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
longestPrefixLength = keyPrefix.length;
|
longestPrefixLength = keyPrefix.length;
|
||||||
|
@ -505,6 +505,9 @@ test('should support extends in tsconfig.json', async ({ runInlineTest }) => {
|
|||||||
}`,
|
}`,
|
||||||
'tsconfig.base1.json': `{
|
'tsconfig.base1.json': `{
|
||||||
"extends": "./tsconfig.base.json",
|
"extends": "./tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"allowJs": true,
|
||||||
|
},
|
||||||
}`,
|
}`,
|
||||||
'tsconfig.base2.json': `{
|
'tsconfig.base2.json': `{
|
||||||
"compilerOptions": {
|
"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');
|
const { foo } = require('util/file');
|
||||||
import { test, expect } from '@playwright/test';
|
import { test, expect } from '@playwright/test';
|
||||||
test('test', ({}, testInfo) => {
|
test('test', ({}, testInfo) => {
|
||||||
@ -534,6 +539,36 @@ test('should support extends in tsconfig.json', async ({ runInlineTest }) => {
|
|||||||
expect(result.exitCode).toBe(0);
|
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 }) => {
|
test('should import packages with non-index main script through path resolver', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'app/pkg/main.ts': `
|
'app/pkg/main.ts': `
|
||||||
|
Loading…
x
Reference in New Issue
Block a user