From 5eb195416bf73f2fa59de52531724d8d19392975 Mon Sep 17 00:00:00 2001 From: Alan Agius Date: Mon, 8 Feb 2021 16:35:08 +0100 Subject: [PATCH] fix(compiler-cli): extend `angularCompilerOptions` in tsconfig from node (#40694) TypeScript supports non rooted extends, we should do the same https://github.com/microsoft/TypeScript/blob/b346f5764e4d500ebdeff7086e43690ea533a305/src/compiler/commandLineParser.ts#L2603-L2628 Closes: #36715 PR Close #40694 --- packages/compiler-cli/src/perform_compile.ts | 73 +++++++++++++------ .../compiler-cli/test/perform_compile_spec.ts | 34 +++++++++ 2 files changed, 86 insertions(+), 21 deletions(-) diff --git a/packages/compiler-cli/src/perform_compile.ts b/packages/compiler-cli/src/perform_compile.ts index 522b5af41f..5220547af9 100644 --- a/packages/compiler-cli/src/perform_compile.ts +++ b/packages/compiler-cli/src/perform_compile.ts @@ -9,7 +9,7 @@ import {isSyntaxError, Position} from '@angular/compiler'; import * as ts from 'typescript'; -import {absoluteFrom, AbsoluteFsPath, getFileSystem, ReadonlyFileSystem, relative, resolve} from '../src/ngtsc/file_system'; +import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, ReadonlyFileSystem, relative, resolve} from '../src/ngtsc/file_system'; import {NgCompilerOptions} from './ngtsc/core/api'; import {replaceTsWithNgInErrors} from './ngtsc/diagnostics'; @@ -137,6 +137,8 @@ export function readConfiguration( project: string, existingOptions?: api.CompilerOptions, host: ConfigurationHost = getFileSystem()): ParsedConfiguration { try { + const fs = getFileSystem(); + const readConfigFile = (configFile: string) => ts.readConfigFile(configFile, file => host.readFile(host.resolve(file))); const readAngularCompilerOptions = @@ -150,20 +152,14 @@ export function readConfiguration( // we are only interested into merging 'angularCompilerOptions' as // other options like 'compilerOptions' are merged by TS - let existingNgCompilerOptions: NgCompilerOptions; - if (parentOptions && config.angularCompilerOptions) { - existingNgCompilerOptions = {...config.angularCompilerOptions, ...parentOptions}; - } else { - existingNgCompilerOptions = parentOptions || config.angularCompilerOptions; - } + const existingNgCompilerOptions = {...config.angularCompilerOptions, ...parentOptions}; - if (config.extends) { - let extendedConfigPath = host.resolve(host.dirname(configFile), config.extends); - extendedConfigPath = host.extname(extendedConfigPath) ? - extendedConfigPath : - absoluteFrom(`${extendedConfigPath}.json`); + if (config.extends && typeof config.extends === 'string') { + const extendedConfigPath = getExtendedConfigPath( + configFile, config.extends, host, fs, + ); - if (host.exists(extendedConfigPath)) { + if (extendedConfigPath !== null) { // Call readAngularCompilerOptions recursively to merge NG Compiler options return readAngularCompilerOptions(extendedConfigPath, existingNgCompilerOptions); } @@ -172,13 +168,6 @@ export function readConfiguration( return existingNgCompilerOptions; }; - const parseConfigHost = { - useCaseSensitiveFileNames: true, - fileExists: host.exists.bind(host), - readDirectory: ts.sys.readDirectory, - readFile: ts.sys.readFile - }; - const {projectFile, basePath} = calcProjectFileAndBasePath(project, host); const configFileName = host.resolve(host.pwd(), projectFile); const {config, error} = readConfigFile(projectFile); @@ -191,13 +180,14 @@ export function readConfiguration( emitFlags: api.EmitFlags.Default }; } - const existingCompilerOptions = { + const existingCompilerOptions: api.CompilerOptions = { genDir: basePath, basePath, ...readAngularCompilerOptions(configFileName), ...existingOptions, }; + const parseConfigHost = createParseConfigHost(host, fs); const {options, errors, fileNames: rootNames, projectReferences} = ts.parseJsonConfigFileContent( config, parseConfigHost, basePath, existingCompilerOptions, configFileName); @@ -227,6 +217,47 @@ export function readConfiguration( } } +function createParseConfigHost(host: ConfigurationHost, fs = getFileSystem()): ts.ParseConfigHost { + return { + fileExists: host.exists.bind(host), + readDirectory: ts.sys.readDirectory, + readFile: host.readFile.bind(host), + useCaseSensitiveFileNames: fs.isCaseSensitive(), + }; +} + +function getExtendedConfigPath( + configFile: string, extendsValue: string, host: ConfigurationHost, + fs: FileSystem): AbsoluteFsPath|null { + let extendedConfigPath: AbsoluteFsPath|null = null; + + if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) { + extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue); + extendedConfigPath = host.extname(extendedConfigPath) ? + extendedConfigPath : + absoluteFrom(`${extendedConfigPath}.json`); + } else { + const parseConfigHost = createParseConfigHost(host, fs); + + // Path isn't a rooted or relative path, resolve like a module. + const { + resolvedModule, + } = + ts.nodeModuleNameResolver( + extendsValue, configFile, + {moduleResolution: ts.ModuleResolutionKind.NodeJs, resolveJsonModule: true}, + parseConfigHost); + if (resolvedModule) { + extendedConfigPath = absoluteFrom(resolvedModule.resolvedFileName); + } + } + + if (extendedConfigPath !== null && host.exists(extendedConfigPath)) { + return extendedConfigPath; + } + + return null; +} export interface PerformCompilationResult { diagnostics: Diagnostics; program?: api.Program; diff --git a/packages/compiler-cli/test/perform_compile_spec.ts b/packages/compiler-cli/test/perform_compile_spec.ts index 3281f2dc28..c140f487e9 100644 --- a/packages/compiler-cli/test/perform_compile_spec.ts +++ b/packages/compiler-cli/test/perform_compile_spec.ts @@ -102,4 +102,38 @@ describe('perform_compile', () => { annotateForClosureCompiler: false, })); }); + + it('should merge tsconfig "angularCompilerOptions" when extends point to node package', () => { + support.writeFiles({ + 'tsconfig-level-1.json': `{ + "extends": "@angular-ru/tsconfig", + "angularCompilerOptions": { + "enableIvy": false + } + } + `, + 'node_modules/@angular-ru/tsconfig/tsconfig.json': `{ + "compilerOptions": { + "strict": true + }, + "angularCompilerOptions": { + "skipMetadataEmit": true + } + } + `, + 'node_modules/@angular-ru/tsconfig/package.json': `{ + "name": "@angular-ru/tsconfig", + "version": "0.0.0", + "main": "./tsconfig.json" + } + `, + }); + + const {options} = readConfiguration(path.resolve(basePath, 'tsconfig-level-1.json')); + expect(options).toEqual(jasmine.objectContaining({ + strict: true, + skipMetadataEmit: true, + enableIvy: false, + })); + }); });