From 9ffe33fae84e505e119357a8fb3620cea265ef97 Mon Sep 17 00:00:00 2001 From: Dmitry Gozman Date: Thu, 11 May 2023 19:18:13 -0700 Subject: [PATCH] feat(test runner): support tsconfig.extends array (#22975) Fixes #22151. --- .../src/third_party/tsconfig-loader.ts | 45 +++++++++++-------- tests/playwright-test/resolver.spec.ts | 36 +++++++++++++++ 2 files changed, 63 insertions(+), 18 deletions(-) diff --git a/packages/playwright-test/src/third_party/tsconfig-loader.ts b/packages/playwright-test/src/third_party/tsconfig-loader.ts index dead55937a..d925d588e9 100644 --- a/packages/playwright-test/src/third_party/tsconfig-loader.ts +++ b/packages/playwright-test/src/third_party/tsconfig-loader.ts @@ -39,6 +39,7 @@ interface Tsconfig { strict?: boolean; allowJs?: boolean; }; + references?: any[]; } export interface TsConfigLoaderResult { @@ -123,20 +124,19 @@ export function walkForTsConfig( function loadTsconfig( configFilePath: string, - existsSync: (path: string) => boolean = fs.existsSync, - readFileSync: (filename: string) => string = (filename: string) => - fs.readFileSync(filename, "utf8") ): Tsconfig | undefined { - if (!existsSync(configFilePath)) { + if (!fs.existsSync(configFilePath)) { return undefined; } - const configString = readFileSync(configFilePath); + const configString = fs.readFileSync(configFilePath, 'utf-8'); const cleanedJson = StripBom(configString); - let config: Tsconfig = json5.parse(cleanedJson); - let extendedConfig = config.extends; + const parsedConfig: Tsconfig = json5.parse(cleanedJson); - if (extendedConfig) { + let config: Tsconfig = {}; + + 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 @@ -148,7 +148,7 @@ function loadTsconfig( if ( extendedConfig.indexOf("/") !== -1 && extendedConfig.indexOf(".") !== -1 && - !existsSync(extendedConfigPath) + !fs.existsSync(extendedConfigPath) ) { extendedConfigPath = path.join( currentDir, @@ -158,7 +158,7 @@ function loadTsconfig( } const base = - loadTsconfig(extendedConfigPath, existsSync, readFileSync) || {}; + loadTsconfig(extendedConfigPath) || {}; // 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 @@ -170,16 +170,14 @@ function loadTsconfig( ); } - config = { - ...base, - ...config, - compilerOptions: { - ...base.compilerOptions, - ...config.compilerOptions, - }, - }; + config = mergeConfigs(config, 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; + if (path.basename(configFilePath) === 'jsconfig.json' && config.compilerOptions?.allowJs === undefined) { config.compilerOptions = config.compilerOptions || {}; config.compilerOptions.allowJs = true; @@ -188,6 +186,17 @@ function loadTsconfig( return config; } +function mergeConfigs(base: Tsconfig, override: Tsconfig): Tsconfig { + return { + ...base, + ...override, + compilerOptions: { + ...base.compilerOptions, + ...override.compilerOptions, + }, + }; +} + function StripBom(string: string) { if (typeof string !== 'string') { throw new TypeError(`Expected a string, got ${typeof string}`); diff --git a/tests/playwright-test/resolver.spec.ts b/tests/playwright-test/resolver.spec.ts index afeb9bfc2f..3101bcd7a9 100644 --- a/tests/playwright-test/resolver.spec.ts +++ b/tests/playwright-test/resolver.spec.ts @@ -469,3 +469,39 @@ test('should respect path resolver for JS and TS files from jsconfig.json', asyn expect(result.passed).toBe(2); expect(result.exitCode).toBe(0); }); + +test('should support extends in tsconfig.json', async ({ runInlineTest }) => { + const result = await runInlineTest({ + 'tsconfig.json': `{ + "extends": ["./tsconfig.base1.json", "./tsconfig.base2.json"], + }`, + 'tsconfig.base1.json': `{ + "extends": "./tsconfig.base.json", + }`, + 'tsconfig.base2.json': `{ + "compilerOptions": { + "baseUrl": "dir", + }, + }`, + 'tsconfig.base.json': `{ + "compilerOptions": { + "paths": { + "util/*": ["./foo/bar/util/*"], + }, + }, + }`, + 'a.test.ts': ` + const { foo } = require('util/file'); + import { test, expect } from '@playwright/test'; + test('test', ({}, testInfo) => { + expect(foo).toBe('foo'); + }); + `, + 'dir/foo/bar/util/file.ts': ` + module.exports = { foo: 'foo' }; + `, + }); + + expect(result.passed).toBe(1); + expect(result.exitCode).toBe(0); +});