mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
Cleans up some code that was left in place to support old versions of TypeScript. PR Close #52099
324 lines
11 KiB
TypeScript
324 lines
11 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright Google LLC All Rights Reserved.
|
|
*
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
* found in the LICENSE file at https://angular.io/license
|
|
*/
|
|
|
|
import ts from 'typescript';
|
|
|
|
import {absoluteFrom, AbsoluteFsPath, FileSystem, getFileSystem, ReadonlyFileSystem} from '../src/ngtsc/file_system';
|
|
|
|
import {NgCompilerOptions} from './ngtsc/core/api';
|
|
import {replaceTsWithNgInErrors} from './ngtsc/diagnostics';
|
|
import * as api from './transformers/api';
|
|
import * as ng from './transformers/entry_points';
|
|
import {createMessageDiagnostic} from './transformers/util';
|
|
|
|
const defaultFormatHost: ts.FormatDiagnosticsHost = {
|
|
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
|
|
getCanonicalFileName: fileName => fileName,
|
|
getNewLine: () => ts.sys.newLine
|
|
};
|
|
|
|
export function formatDiagnostics(
|
|
diags: ReadonlyArray<ts.Diagnostic>,
|
|
host: ts.FormatDiagnosticsHost = defaultFormatHost): string {
|
|
if (diags && diags.length) {
|
|
return diags
|
|
.map(
|
|
diagnostic => replaceTsWithNgInErrors(
|
|
ts.formatDiagnosticsWithColorAndContext([diagnostic], host)))
|
|
.join('');
|
|
} else {
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/** Used to read configuration files. */
|
|
export type ConfigurationHost = Pick<
|
|
ReadonlyFileSystem, 'readFile'|'exists'|'lstat'|'resolve'|'join'|'dirname'|'extname'|'pwd'>;
|
|
|
|
export interface ParsedConfiguration {
|
|
project: string;
|
|
options: api.CompilerOptions;
|
|
rootNames: string[];
|
|
projectReferences?: readonly ts.ProjectReference[]|undefined;
|
|
emitFlags: api.EmitFlags;
|
|
errors: ts.Diagnostic[];
|
|
}
|
|
|
|
export function calcProjectFileAndBasePath(
|
|
project: string, host: ConfigurationHost = getFileSystem()):
|
|
{projectFile: AbsoluteFsPath, basePath: AbsoluteFsPath} {
|
|
const absProject = host.resolve(project);
|
|
const projectIsDir = host.lstat(absProject).isDirectory();
|
|
const projectFile = projectIsDir ? host.join(absProject, 'tsconfig.json') : absProject;
|
|
const projectDir = projectIsDir ? absProject : host.dirname(absProject);
|
|
const basePath = host.resolve(projectDir);
|
|
|
|
return {projectFile, basePath};
|
|
}
|
|
|
|
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 =
|
|
(configFile: string, parentOptions: NgCompilerOptions = {}): NgCompilerOptions => {
|
|
const {config, error} = readConfigFile(configFile);
|
|
|
|
if (error) {
|
|
// Errors are handled later on by 'parseJsonConfigFileContent'
|
|
return parentOptions;
|
|
}
|
|
|
|
// we are only interested into merging 'angularCompilerOptions' as
|
|
// other options like 'compilerOptions' are merged by TS
|
|
let existingNgCompilerOptions = {...config.angularCompilerOptions, ...parentOptions};
|
|
if (!config.extends) {
|
|
return existingNgCompilerOptions;
|
|
}
|
|
|
|
const extendsPaths: string[] =
|
|
typeof config.extends === 'string' ? [config.extends] : config.extends;
|
|
|
|
// Call readAngularCompilerOptions recursively to merge NG Compiler options
|
|
// Reverse the array so the overrides happen from right to left.
|
|
return [...extendsPaths].reverse().reduce((prevOptions, extendsPath) => {
|
|
const extendedConfigPath = getExtendedConfigPath(
|
|
configFile,
|
|
extendsPath,
|
|
host,
|
|
fs,
|
|
);
|
|
|
|
return extendedConfigPath === null ?
|
|
prevOptions :
|
|
readAngularCompilerOptions(extendedConfigPath, prevOptions);
|
|
}, existingNgCompilerOptions);
|
|
};
|
|
|
|
const {projectFile, basePath} = calcProjectFileAndBasePath(project, host);
|
|
const configFileName = host.resolve(host.pwd(), projectFile);
|
|
const {config, error} = readConfigFile(projectFile);
|
|
|
|
if (error) {
|
|
return {
|
|
project,
|
|
errors: [error],
|
|
rootNames: [],
|
|
options: {},
|
|
emitFlags: api.EmitFlags.Default
|
|
};
|
|
}
|
|
|
|
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);
|
|
|
|
let emitFlags = api.EmitFlags.Default;
|
|
if (!(options['skipMetadataEmit'] || options['flatModuleOutFile'])) {
|
|
emitFlags |= api.EmitFlags.Metadata;
|
|
}
|
|
if (options['skipTemplateCodegen']) {
|
|
emitFlags = emitFlags & ~api.EmitFlags.Codegen;
|
|
}
|
|
return {project: projectFile, rootNames, projectReferences, options, errors, emitFlags};
|
|
} catch (e) {
|
|
const errors: ts.Diagnostic[] = [{
|
|
category: ts.DiagnosticCategory.Error,
|
|
messageText: (e as Error).stack ?? (e as Error).message,
|
|
file: undefined,
|
|
start: undefined,
|
|
length: undefined,
|
|
source: 'angular',
|
|
code: api.UNKNOWN_ERROR_CODE,
|
|
}];
|
|
return {project: '', errors, rootNames: [], options: {}, emitFlags: api.EmitFlags.Default};
|
|
}
|
|
}
|
|
|
|
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 {
|
|
const result = getExtendedConfigPathWorker(configFile, extendsValue, host, fs);
|
|
if (result !== null) {
|
|
return result;
|
|
}
|
|
|
|
// Try to resolve the paths with a json extension append a json extension to the file in case if
|
|
// it is missing and the resolution failed. This is to replicate TypeScript behaviour, see:
|
|
// https://github.com/microsoft/TypeScript/blob/294a5a7d784a5a95a8048ee990400979a6bc3a1c/src/compiler/commandLineParser.ts#L2806
|
|
return getExtendedConfigPathWorker(configFile, `${extendsValue}.json`, host, fs);
|
|
}
|
|
|
|
function getExtendedConfigPathWorker(
|
|
configFile: string, extendsValue: string, host: ConfigurationHost,
|
|
fs: FileSystem): AbsoluteFsPath|null {
|
|
if (extendsValue.startsWith('.') || fs.isRooted(extendsValue)) {
|
|
const extendedConfigPath = host.resolve(host.dirname(configFile), extendsValue);
|
|
if (host.exists(extendedConfigPath)) {
|
|
return extendedConfigPath;
|
|
}
|
|
} 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.Node10, resolveJsonModule: true},
|
|
parseConfigHost);
|
|
if (resolvedModule) {
|
|
return absoluteFrom(resolvedModule.resolvedFileName);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
export interface PerformCompilationResult {
|
|
diagnostics: ReadonlyArray<ts.Diagnostic>;
|
|
program?: api.Program;
|
|
emitResult?: ts.EmitResult;
|
|
}
|
|
|
|
export function exitCodeFromResult(diags: ReadonlyArray<ts.Diagnostic>|undefined): number {
|
|
if (!diags) return 0;
|
|
if (diags.every((diag) => diag.category !== ts.DiagnosticCategory.Error)) {
|
|
// If we have a result and didn't get any errors, we succeeded.
|
|
return 0;
|
|
}
|
|
|
|
// Return 2 if any of the errors were unknown.
|
|
return diags.some(d => d.source === 'angular' && d.code === api.UNKNOWN_ERROR_CODE) ? 2 : 1;
|
|
}
|
|
|
|
export function performCompilation<CbEmitRes extends ts.EmitResult = ts.EmitResult>({
|
|
rootNames,
|
|
options,
|
|
host,
|
|
oldProgram,
|
|
emitCallback,
|
|
mergeEmitResultsCallback,
|
|
gatherDiagnostics = defaultGatherDiagnostics,
|
|
customTransformers,
|
|
emitFlags = api.EmitFlags.Default,
|
|
forceEmit = false,
|
|
modifiedResourceFiles = null
|
|
}: {
|
|
rootNames: string[],
|
|
options: api.CompilerOptions,
|
|
host?: api.CompilerHost,
|
|
oldProgram?: api.Program,
|
|
emitCallback?: api.TsEmitCallback<CbEmitRes>,
|
|
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback<CbEmitRes>,
|
|
gatherDiagnostics?: (program: api.Program) => ReadonlyArray<ts.Diagnostic>,
|
|
customTransformers?: api.CustomTransformers,
|
|
emitFlags?: api.EmitFlags,
|
|
forceEmit?: boolean,
|
|
modifiedResourceFiles?: Set<string>| null,
|
|
}): PerformCompilationResult {
|
|
let program: api.Program|undefined;
|
|
let emitResult: ts.EmitResult|undefined;
|
|
let allDiagnostics: Array<ts.Diagnostic> = [];
|
|
try {
|
|
if (!host) {
|
|
host = ng.createCompilerHost({options});
|
|
}
|
|
if (modifiedResourceFiles) {
|
|
host.getModifiedResourceFiles = () => modifiedResourceFiles;
|
|
}
|
|
|
|
program = ng.createProgram({rootNames, host, options, oldProgram});
|
|
|
|
const beforeDiags = Date.now();
|
|
allDiagnostics.push(...gatherDiagnostics(program!));
|
|
if (options.diagnostics) {
|
|
const afterDiags = Date.now();
|
|
allDiagnostics.push(
|
|
createMessageDiagnostic(`Time for diagnostics: ${afterDiags - beforeDiags}ms.`));
|
|
}
|
|
|
|
if (!hasErrors(allDiagnostics)) {
|
|
emitResult = program!.emit(
|
|
{emitCallback, mergeEmitResultsCallback, customTransformers, emitFlags, forceEmit});
|
|
allDiagnostics.push(...emitResult.diagnostics);
|
|
return {diagnostics: allDiagnostics, program, emitResult};
|
|
}
|
|
return {diagnostics: allDiagnostics, program};
|
|
} catch (e) {
|
|
// We might have a program with unknown state, discard it.
|
|
program = undefined;
|
|
allDiagnostics.push({
|
|
category: ts.DiagnosticCategory.Error,
|
|
messageText: (e as Error).stack ?? (e as Error).message,
|
|
code: api.UNKNOWN_ERROR_CODE,
|
|
file: undefined,
|
|
start: undefined,
|
|
length: undefined,
|
|
});
|
|
return {diagnostics: allDiagnostics, program};
|
|
}
|
|
}
|
|
export function defaultGatherDiagnostics(program: api.Program): ReadonlyArray<ts.Diagnostic> {
|
|
const allDiagnostics: Array<ts.Diagnostic> = [];
|
|
|
|
function checkDiagnostics(diags: ReadonlyArray<ts.Diagnostic>|undefined) {
|
|
if (diags) {
|
|
allDiagnostics.push(...diags);
|
|
return !hasErrors(diags);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
let checkOtherDiagnostics = true;
|
|
// Check parameter diagnostics
|
|
checkOtherDiagnostics = checkOtherDiagnostics &&
|
|
checkDiagnostics([...program.getTsOptionDiagnostics(), ...program.getNgOptionDiagnostics()]);
|
|
|
|
// Check syntactic diagnostics
|
|
checkOtherDiagnostics =
|
|
checkOtherDiagnostics && checkDiagnostics(program.getTsSyntacticDiagnostics());
|
|
|
|
// Check TypeScript semantic and Angular structure diagnostics
|
|
checkOtherDiagnostics =
|
|
checkOtherDiagnostics &&
|
|
checkDiagnostics(
|
|
[...program.getTsSemanticDiagnostics(), ...program.getNgStructuralDiagnostics()]);
|
|
|
|
// Check Angular semantic diagnostics
|
|
checkOtherDiagnostics =
|
|
checkOtherDiagnostics && checkDiagnostics(program.getNgSemanticDiagnostics());
|
|
|
|
return allDiagnostics;
|
|
}
|
|
|
|
function hasErrors(diags: ReadonlyArray<ts.Diagnostic>) {
|
|
return diags.some(d => d.category === ts.DiagnosticCategory.Error);
|
|
}
|