fix(compiler-cli): readConfiguration existing options should override options in tsconfig (#40694)

At the moment, when passing an Angular Compiler option
in the `existingOptions` it doesn't override the defined in the TSConfig.

PR Close #40694
This commit is contained in:
Alan Agius 2021-02-04 12:46:51 +01:00 committed by Joey Perrott
parent dfc9f36dd8
commit b7c4d07e81
3 changed files with 65 additions and 47 deletions

View File

@ -10,6 +10,7 @@ import {isSyntaxError, Position} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {absoluteFrom, AbsoluteFsPath, getFileSystem, ReadonlyFileSystem, relative, resolve} from '../src/ngtsc/file_system'; import {absoluteFrom, AbsoluteFsPath, getFileSystem, ReadonlyFileSystem, relative, resolve} from '../src/ngtsc/file_system';
import {NgCompilerOptions} from './ngtsc/core/api';
import {replaceTsWithNgInErrors} from './ngtsc/diagnostics'; import {replaceTsWithNgInErrors} from './ngtsc/diagnostics';
import * as api from './transformers/api'; import * as api from './transformers/api';
@ -132,39 +133,28 @@ export function calcProjectFileAndBasePath(
return {projectFile, basePath}; return {projectFile, basePath};
} }
export function createNgCompilerOptions(
basePath: string, config: any, tsOptions: ts.CompilerOptions): api.CompilerOptions {
// enableIvy `ngtsc` is an alias for `true`.
const {angularCompilerOptions = {}} = config;
const {enableIvy} = angularCompilerOptions;
angularCompilerOptions.enableIvy = enableIvy !== false && enableIvy !== 'tsc';
return {...tsOptions, ...angularCompilerOptions, genDir: basePath, basePath};
}
export function readConfiguration( export function readConfiguration(
project: string, existingOptions?: ts.CompilerOptions, project: string, existingOptions?: api.CompilerOptions,
host: ConfigurationHost = getFileSystem()): ParsedConfiguration { host: ConfigurationHost = getFileSystem()): ParsedConfiguration {
try { try {
const {projectFile, basePath} = calcProjectFileAndBasePath(project, host); const readConfigFile = (configFile: string) =>
ts.readConfigFile(configFile, file => host.readFile(host.resolve(file)));
const readExtendedConfigFile = const readAngularCompilerOptions =
(configFile: string, existingConfig?: any): {config?: any, error?: ts.Diagnostic} => { (configFile: string, parentOptions: NgCompilerOptions = {}): NgCompilerOptions => {
const {config, error} = const {config, error} = readConfigFile(configFile);
ts.readConfigFile(configFile, file => host.readFile(host.resolve(file)));
if (error) { if (error) {
return {error}; // Errors are handled later on by 'parseJsonConfigFileContent'
return parentOptions;
} }
// we are only interested into merging 'angularCompilerOptions' as // we are only interested into merging 'angularCompilerOptions' as
// other options like 'compilerOptions' are merged by TS // other options like 'compilerOptions' are merged by TS
const baseConfig = existingConfig || config; let existingNgCompilerOptions: NgCompilerOptions;
if (existingConfig) { if (parentOptions && config.angularCompilerOptions) {
baseConfig.angularCompilerOptions = { existingNgCompilerOptions = {...config.angularCompilerOptions, ...parentOptions};
...config.angularCompilerOptions, } else {
...baseConfig.angularCompilerOptions existingNgCompilerOptions = parentOptions || config.angularCompilerOptions;
};
} }
if (config.extends) { if (config.extends) {
@ -174,16 +164,24 @@ export function readConfiguration(
absoluteFrom(`${extendedConfigPath}.json`); absoluteFrom(`${extendedConfigPath}.json`);
if (host.exists(extendedConfigPath)) { if (host.exists(extendedConfigPath)) {
// Call read config recursively as TypeScript only merges CompilerOptions // Call readAngularCompilerOptions recursively to merge NG Compiler options
return readExtendedConfigFile(extendedConfigPath, baseConfig); return readAngularCompilerOptions(extendedConfigPath, existingNgCompilerOptions);
} }
} }
return {config: baseConfig}; return existingNgCompilerOptions;
}; };
const {config, error} = readExtendedConfigFile(projectFile); 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);
if (error) { if (error) {
return { return {
project, project,
@ -193,19 +191,20 @@ export function readConfiguration(
emitFlags: api.EmitFlags.Default emitFlags: api.EmitFlags.Default
}; };
} }
const parseConfigHost = { const existingCompilerOptions = {
useCaseSensitiveFileNames: true, genDir: basePath,
fileExists: host.exists.bind(host), basePath,
readDirectory: ts.sys.readDirectory, ...readAngularCompilerOptions(configFileName),
readFile: ts.sys.readFile ...existingOptions,
}; };
const configFileName = host.resolve(host.pwd(), projectFile);
const parsed = ts.parseJsonConfigFileContent(
config, parseConfigHost, basePath, existingOptions, configFileName);
const rootNames = parsed.fileNames;
const projectReferences = parsed.projectReferences;
const options = createNgCompilerOptions(basePath, config, parsed.options); const {options, errors, fileNames: rootNames, projectReferences} =
ts.parseJsonConfigFileContent(
config, parseConfigHost, basePath, existingCompilerOptions, configFileName);
// Coerce to boolean as `enableIvy` can be `ngtsc|true|false|undefined` here.
options.enableIvy = !!(options.enableIvy ?? true);
let emitFlags = api.EmitFlags.Default; let emitFlags = api.EmitFlags.Default;
if (!(options.skipMetadataEmit || options.flatModuleOutFile)) { if (!(options.skipMetadataEmit || options.flatModuleOutFile)) {
emitFlags |= api.EmitFlags.Metadata; emitFlags |= api.EmitFlags.Metadata;
@ -213,14 +212,7 @@ export function readConfiguration(
if (options.skipTemplateCodegen) { if (options.skipTemplateCodegen) {
emitFlags = emitFlags & ~api.EmitFlags.Codegen; emitFlags = emitFlags & ~api.EmitFlags.Codegen;
} }
return { return {project: projectFile, rootNames, projectReferences, options, errors, emitFlags};
project: projectFile,
rootNames,
projectReferences,
options,
errors: parsed.errors,
emitFlags
};
} catch (e) { } catch (e) {
const errors: ts.Diagnostic[] = [{ const errors: ts.Diagnostic[] = [{
category: ts.DiagnosticCategory.Error, category: ts.DiagnosticCategory.Error,

View File

@ -135,6 +135,7 @@ ts_library(
":test_utils", ":test_utils",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli", "//packages/compiler-cli",
"@npm//typescript",
], ],
) )

View File

@ -7,6 +7,7 @@
*/ */
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript';
import {readConfiguration} from '../src/perform_compile'; import {readConfiguration} from '../src/perform_compile';
@ -77,4 +78,28 @@ describe('perform_compile', () => {
const {options} = readConfiguration(path.resolve(basePath, 'tsconfig-level-1.json')); const {options} = readConfiguration(path.resolve(basePath, 'tsconfig-level-1.json'));
expect(options.enableIvy).toBe(false); expect(options.enableIvy).toBe(false);
}); });
it('should override options defined in tsconfig with those defined in `existingOptions`', () => {
support.writeFiles({
'tsconfig-level-1.json': `{
"compilerOptions": {
"target": "es2020"
},
"angularCompilerOptions": {
"annotateForClosureCompiler": true
}
}
`
});
const {options} = readConfiguration(
path.resolve(basePath, 'tsconfig-level-1.json'),
{annotateForClosureCompiler: false, target: ts.ScriptTarget.ES2015, enableIvy: false});
expect(options).toEqual(jasmine.objectContaining({
enableIvy: false,
target: ts.ScriptTarget.ES2015,
annotateForClosureCompiler: false,
}));
});
}); });