Visitar URL original
cherry-pick(#42005): fix(tsconfig): do not throw when "extends"/"refe… · microsoft/playwright@9632f8e · GitHub
Skip to content

Commit 9632f8e

Browse files
committed
cherry-pick(#42005): fix(tsconfig): do not throw when "extends"/"references" cannot be resolved
1 parent e3950d9 commit 9632f8e

2 files changed

Lines changed: 85 additions & 24 deletions

File tree

‎packages/playwright/src/transform/tsconfig-loader.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,16 +61,18 @@ export function loadTsConfig(configPath: string): LoadedTsConfig[] {
6161
}
6262
}
6363

64-
function resolveConfigFile(baseConfigFile: string, referencedConfigFile: string, kind: 'extends' | 'references') {
65-
const originalReferencedConfigFile = referencedConfigFile;
64+
function resolveConfigFile(baseConfigFile: string, referencedConfigFile: string) {
6665
if (!referencedConfigFile.endsWith('.json'))
6766
referencedConfigFile += '.json';
6867
const currentDir = path.dirname(baseConfigFile);
6968
let resolvedConfigFile = path.resolve(currentDir, referencedConfigFile);
7069
if (referencedConfigFile.includes('/') && referencedConfigFile.includes('.') && !fs.existsSync(resolvedConfigFile))
7170
resolvedConfigFile = path.join(currentDir, 'node_modules', referencedConfigFile);
72-
if (!fs.existsSync(resolvedConfigFile))
73-
throw new Error(`Failed to resolve "${kind}" path "${originalReferencedConfigFile}" referenced from ${baseConfigFile}`);
71+
// Note: this function may return a non-existing file, and the caller silently ignores it.
72+
// We deliberately do not throw in this case, because we do not want to repeat the whole
73+
// resolution process that tsc has, e.g. node_modules walk-up and package.json "exports".
74+
// See https://github.com/microsoft/playwright/issues/41989.
75+
// TODO: implement tsc-compatible resolution and start throwing on invalid "extends"/"references".
7476
return resolvedConfigFile;
7577
}
7678

@@ -97,7 +99,7 @@ function innerLoadTsConfig(
9799

98100
const extendsArray = Array.isArray(parsedConfig.extends) ? parsedConfig.extends : (parsedConfig.extends ? [parsedConfig.extends] : []);
99101
for (const extendedConfig of extendsArray) {
100-
const extendedConfigPath = resolveConfigFile(configFilePath, extendedConfig, 'extends');
102+
const extendedConfigPath = resolveConfigFile(configFilePath, extendedConfig);
101103
const base = innerLoadTsConfig(extendedConfigPath, references, visited);
102104
// Retain result instance, so that caching works.
103105
Object.assign(result, base, { tsConfigPath: configFilePath });
@@ -122,7 +124,7 @@ function innerLoadTsConfig(
122124
}
123125

124126
for (const ref of parsedConfig.references || [])
125-
references.push(innerLoadTsConfig(resolveConfigFile(configFilePath, ref.path, 'references'), references, visited));
127+
references.push(innerLoadTsConfig(resolveConfigFile(configFilePath, ref.path), references, visited));
126128

127129
if (path.basename(configFilePath) === 'jsconfig.json' && result.allowJs === undefined)
128130
result.allowJs = true;

‎tests/playwright-test/resolver.spec.ts‎

Lines changed: 77 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414
* limitations under the License.
1515
*/
1616

17+
import fs from 'fs';
18+
import path from 'path';
19+
1720
import { test, expect } from './playwright-test-fixtures';
1821

1922
test('should print tsconfig parsing error', async ({ runInlineTest }) => {
@@ -622,41 +625,100 @@ test('should resolve extends from an explicit node_modules subpath', async ({ ru
622625
expect(result.exitCode).toBe(0);
623626
});
624627

625-
test('should fail loudly when extends path cannot be resolved', async ({ runInlineTest }) => {
626-
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41543' });
628+
test('should ignore extends bare specifier resolvable only via node_modules walk-up', async ({ runInlineTest }, testInfo) => {
629+
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41989' });
630+
631+
const baseDir = testInfo.outputPath();
632+
const symlinkType = process.platform === 'win32' ? 'junction' : 'dir';
633+
// npm symlinks "file:" dependencies into node_modules. The realpath escapes node_modules,
634+
// so the dependency goes through per-file tsconfig discovery.
635+
await fs.promises.mkdir(path.join(baseDir, 'node_modules'), { recursive: true });
636+
await fs.promises.symlink(path.join(baseDir, 'dep-src'), path.join(baseDir, 'node_modules', 'dep'), symlinkType);
627637

628638
const result = await runInlineTest({
629-
'tsconfig.json': `{
630-
"extends": "./tsconfig.bas.json",
631-
}`,
639+
'package.json': JSON.stringify({ name: 'repro', private: true, dependencies: { 'dep': 'file:./dep-src', 'foo': '1.0.0' } }),
640+
// "foo" is hoisted to the root node_modules and is absent from dep's own node_modules.
641+
// tsc resolves the "extends" below by walking up node_modules from dep-src.
642+
'node_modules/foo/package.json': JSON.stringify({ name: 'foo', version: '1.0.0', main: 'index.js' }),
643+
'node_modules/foo/index.js': `module.exports = 'foo';`,
644+
'node_modules/foo/tsconfig.json': `{ "compilerOptions": {} }`,
645+
'dep-src/package.json': JSON.stringify({ name: 'dep', version: '1.0.0', main: 'index.js' }),
646+
'dep-src/tsconfig.json': `{ "extends": "foo/tsconfig.json" }`,
647+
'dep-src/index.js': `
648+
require('foo');
649+
module.exports = 'dep';
650+
`,
651+
'a.test.ts': `
652+
import { test, expect } from '@playwright/test';
653+
const dep = require('dep');
654+
test('test', () => {
655+
expect(dep).toBe('dep');
656+
});
657+
`,
658+
});
659+
660+
expect(result.passed).toBe(1);
661+
expect(result.exitCode).toBe(0);
662+
});
663+
664+
test('should ignore extends subpath resolvable only via package.json exports', async ({ runInlineTest }) => {
665+
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41989' });
666+
667+
const result = await runInlineTest({
668+
'package.json': JSON.stringify({ name: 'test-project' }),
669+
// The "./next" subpath only exists in the exports map, there is no "next.json" file.
670+
'node_modules/@repo/tsconfig/package.json': JSON.stringify({ name: '@repo/tsconfig', version: '1.0.0', exports: { './next': './tsconfig.next.json' } }),
671+
'node_modules/@repo/tsconfig/tsconfig.next.json': `{ "compilerOptions": {} }`,
672+
'tsconfig.json': `{ "extends": "@repo/tsconfig/next" }`,
632673
'a.test.ts': `
633674
import { test, expect } from '@playwright/test';
634675
test('test', () => {});
635676
`,
636677
});
637678

638-
expect(result.exitCode).toBe(1);
639-
expect(result.output).toContain('Failed to resolve "extends" path "./tsconfig.bas.json"');
679+
expect(result.passed).toBe(1);
680+
expect(result.exitCode).toBe(0);
640681
});
641682

642-
test('should fail loudly when references path cannot be resolved', async ({ runInlineTest }) => {
643-
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41543' });
683+
test('should ignore extends bare package name resolvable only via package.json exports', async ({ runInlineTest }) => {
684+
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41989' });
644685

645686
const result = await runInlineTest({
687+
'package.json': JSON.stringify({ name: 'test-project' }),
688+
'node_modules/typescript-config-silverwind/package.json': JSON.stringify({ name: 'typescript-config-silverwind', version: '1.0.0', exports: './tsconfig.json' }),
689+
'node_modules/typescript-config-silverwind/tsconfig.json': `{ "compilerOptions": {} }`,
690+
'tsconfig.json': `{ "extends": "typescript-config-silverwind" }`,
691+
'a.test.ts': `
692+
import { test, expect } from '@playwright/test';
693+
test('test', () => {});
694+
`,
695+
});
696+
697+
expect(result.passed).toBe(1);
698+
expect(result.exitCode).toBe(0);
699+
});
700+
701+
test('should ignore tsconfig references pointing to a directory', async ({ runInlineTest }) => {
702+
test.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/41998' });
703+
704+
const result = await runInlineTest({
705+
// tsc resolves a directory-form reference to "./sub/tsconfig.json",
706+
// as written by "tsc --build" and "nx sync".
646707
'tsconfig.json': `{
647-
"files": [],
648-
"references": [
649-
{ "path": "./tsconfig.doesnotexist.json" }
650-
]
708+
"compilerOptions": { "composite": true },
709+
"references": [{ "path": "./sub" }],
710+
}`,
711+
'sub/tsconfig.json': `{
712+
"compilerOptions": { "composite": true },
651713
}`,
652714
'a.test.ts': `
653715
import { test, expect } from '@playwright/test';
654716
test('test', () => {});
655717
`,
656718
});
657719

658-
expect(result.exitCode).toBe(1);
659-
expect(result.output).toContain('Failed to resolve "references" path "./tsconfig.doesnotexist.json"');
720+
expect(result.passed).toBe(1);
721+
expect(result.exitCode).toBe(0);
660722
});
661723

662724
test('should respect tsconfig project references', async ({ runInlineTest }) => {
@@ -671,9 +733,6 @@ test('should respect tsconfig project references', async ({ runInlineTest }) =>
671733
{ "path": "./tsconfig.test.json" }
672734
]
673735
}`,
674-
'tsconfig.app.json': `{
675-
"compilerOptions": {},
676-
}`,
677736
'tsconfig.test.json': `{
678737
"compilerOptions": {
679738
"baseUrl": ".",

0 commit comments

Comments
 (0)