refactor(ivy): introduce the 'core' package and split apart NgtscProgram (#34887)

Previously, NgtscProgram lived in the main @angular/compiler-cli package
alongside the legacy View Engine compiler. As a result, the main package
depended on all of the ngtsc internal packages, and a significant portion of
ngtsc logic lived in NgtscProgram.

This commit refactors NgtscProgram and moves the main logic of compilation
into a new 'core' package. The new package defines a new API which enables
implementers of TypeScript compilers (compilers built using the TS API) to
support Angular transpilation as well. It involves a new NgCompiler type
which takes a ts.Program and performs Angular analysis and transformations,
as well as an NgCompilerHost which wraps an input ts.CompilerHost and adds
any extra Angular files.

Together, these two classes are used to implement a new NgtscProgram which
adapts the legacy api.Program interface used by the View Engine compiler
onto operations on the new types. The new NgtscProgram implementation is
significantly smaller and easier to reason about.

The new NgCompilerHost replaces the previous GeneratedShimsHostWrapper which
lived in the 'shims' package.

A new 'resource' package is added to support the HostResourceLoader which
previously lived in the outer compiler package.

As a result of the refactoring, the dependencies of the outer
@angular/compiler-cli package on ngtsc internal packages are significantly
trimmed.

This refactoring was driven by the desire to build a plugin interface to the
compiler so that tsc_wrapped (another consumer of the TS compiler APIs) can
perform Angular transpilation on user request.

PR Close #34887
This commit is contained in:
Alex Rickabaugh
2020-01-17 16:00:07 -08:00
committed by Andrew Kushnir
parent 31e9dda2c8
commit 24b2f1da2b
38 changed files with 1915 additions and 1300 deletions

View File

@ -6,371 +6,147 @@
* found in the LICENSE file at https://angular.io/license
*/
import {GeneratedFile, Type} from '@angular/compiler';
import {GeneratedFile} from '@angular/compiler';
import * as ts from 'typescript';
import * as api from '../transformers/api';
import {nocollapseHack} from '../transformers/nocollapse_hack';
import {verifySupportedTypeScriptVersion} from '../typescript_support';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from './annotations';
import {CycleAnalyzer, ImportGraph} from './cycles';
import {ErrorCode, ngErrorCode} from './diagnostics';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point';
import {AbsoluteFsPath, LogicalFileSystem, absoluteFrom} from './file_system';
import {AbsoluteModuleStrategy, AliasStrategy, AliasingHost, DefaultImportTracker, FileToModuleAliasingHost, FileToModuleHost, FileToModuleStrategy, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy} from './imports';
import {IncrementalDriver} from './incremental';
import {IndexedComponent, IndexingContext} from './indexer';
import {generateAnalysis} from './indexer/src/transform';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, LocalMetadataRegistry, MetadataReader} from './metadata';
import {InjectableClassRegistry} from './metadata/src/registry';
import {ModuleWithProvidersScanner} from './modulewithproviders';
import {PartialEvaluator} from './partial_evaluator';
import {NgCompilerHost} from './core';
import {NgCompilerOptions} from './core/api';
import {NgCompiler} from './core/src/compiler';
import {IndexedComponent} from './indexer';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
import {TypeScriptReflectionHost} from './reflection';
import {HostResourceLoader} from './resource_loader';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from './routing';
import {ComponentScopeReader, CompoundComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from './scope';
import {FactoryGenerator, FactoryTracker, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims';
import {ivySwitchTransform} from './switch';
import {DecoratorHandler, DtsTransformRegistry, TraitCompiler, declarationTransformFactory, ivyTransformFactory} from './transform';
import {aliasTransformFactory} from './transform/src/alias';
import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck';
import {normalizeSeparators} from './util/src/path';
import {getRootDirs, getSourceFileOrNull, isDtsPath, resolveModuleName} from './util/src/typescript';
/**
* Entrypoint to the Angular Compiler (Ivy+) which sits behind the `api.Program` interface, allowing
* it to be a drop-in replacement for the legacy View Engine compiler to tooling such as the
* command-line main() function or the Angular CLI.
*/
export class NgtscProgram implements api.Program {
private compiler: NgCompiler;
/**
* The primary TypeScript program, which is used for analysis and emit.
*/
private tsProgram: ts.Program;
/**
* The TypeScript program to use for the next incremental compilation.
*
* Once a TS program is used to create another (an incremental compilation operation), it can no
* longer be used to do so again.
*
* Since template type-checking uses the primary program to create a type-checking program, after
* this happens the primary program is no longer suitable for starting a subsequent compilation,
* and the template type-checking program should be used instead.
*
* Thus, the program which should be used for the next incremental compilation is tracked in
* `reuseTsProgram`, separately from the "primary" program which is always used for emit.
*/
private reuseTsProgram: ts.Program;
private resourceManager: HostResourceLoader;
private compilation: TraitCompiler|undefined = undefined;
private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
private _importRewriter: ImportRewriter|undefined = undefined;
private _reflector: TypeScriptReflectionHost|undefined = undefined;
private _isCore: boolean|undefined = undefined;
private rootDirs: AbsoluteFsPath[];
private closureCompilerEnabled: boolean;
private entryPoint: ts.SourceFile|null;
private exportReferenceGraph: ReferenceGraph|null = null;
private flatIndexGenerator: FlatIndexGenerator|null = null;
private routeAnalyzer: NgModuleRouteAnalyzer|null = null;
private scopeRegistry: LocalModuleScopeRegistry|null = null;
private constructionDiagnostics: ts.Diagnostic[] = [];
private moduleResolver: ModuleResolver;
private cycleAnalyzer: CycleAnalyzer;
private metaReader: MetadataReader|null = null;
private aliasingHost: AliasingHost|null = null;
private refEmitter: ReferenceEmitter|null = null;
private fileToModuleHost: FileToModuleHost|null = null;
private defaultImportTracker: DefaultImportTracker;
private host: NgCompilerHost;
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
private perfTracker: PerfTracker|null = null;
private incrementalDriver: IncrementalDriver;
private typeCheckFilePath: AbsoluteFsPath;
private factoryTracker: FactoryTracker|null = null;
private modifiedResourceFiles: Set<string>|null;
private dtsTransforms: DtsTransformRegistry|null = null;
private mwpScanner: ModuleWithProvidersScanner|null = null;
constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
private host: api.CompilerHost, oldProgram?: NgtscProgram) {
rootNames: ReadonlyArray<string>, private options: NgCompilerOptions,
delegateHost: api.CompilerHost, oldProgram?: NgtscProgram) {
// First, check whether the current TS version is supported.
if (!options.disableTypeScriptVersionCheck) {
verifySupportedTypeScriptVersion();
}
const incompatibleTypeCheckOptionsDiagnostic = verifyCompatibleTypeCheckOptions(options);
if (incompatibleTypeCheckOptionsDiagnostic !== null) {
this.constructionDiagnostics.push(incompatibleTypeCheckOptionsDiagnostic);
}
if (shouldEnablePerfTracing(options)) {
if (options.tracePerformance !== undefined) {
this.perfTracker = PerfTracker.zeroedToNow();
this.perfRecorder = this.perfTracker;
}
this.modifiedResourceFiles =
this.host.getModifiedResourceFiles && this.host.getModifiedResourceFiles() || null;
this.rootDirs = getRootDirs(host, options);
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
this.resourceManager = new HostResourceLoader(host, options);
// TODO(alxhub): remove the fallback to allowEmptyCodegenFiles after verifying that the rest of
// our build tooling is no longer relying on it.
const allowEmptyCodegenFiles = options.allowEmptyCodegenFiles || false;
const shouldGenerateFactoryShims = options.generateNgFactoryShims !== undefined ?
options.generateNgFactoryShims :
allowEmptyCodegenFiles;
const shouldGenerateSummaryShims = options.generateNgSummaryShims !== undefined ?
options.generateNgSummaryShims :
allowEmptyCodegenFiles;
const normalizedRootNames = rootNames.map(n => absoluteFrom(n));
if (host.fileNameToModuleName !== undefined) {
this.fileToModuleHost = host as FileToModuleHost;
}
let rootFiles = [...rootNames];
const generators: ShimGenerator[] = [];
let summaryGenerator: SummaryGenerator|null = null;
if (shouldGenerateSummaryShims) {
// Summary generation.
summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames);
generators.push(summaryGenerator);
}
this.host = NgCompilerHost.wrap(delegateHost, rootNames, options);
if (shouldGenerateFactoryShims) {
// Factory generation.
const factoryGenerator = FactoryGenerator.forRootFiles(normalizedRootNames);
const factoryFileMap = factoryGenerator.factoryFileMap;
const factoryFileNames = Array.from(factoryFileMap.keys());
rootFiles.push(...factoryFileNames);
generators.push(factoryGenerator);
this.factoryTracker = new FactoryTracker(factoryGenerator);
}
// Done separately to preserve the order of factory files before summary files in rootFiles.
// TODO(alxhub): validate that this is necessary.
if (shouldGenerateSummaryShims) {
rootFiles.push(...summaryGenerator !.getSummaryFileNames());
}
this.typeCheckFilePath = typeCheckFilePath(this.rootDirs);
generators.push(new TypeCheckShimGenerator(this.typeCheckFilePath));
rootFiles.push(this.typeCheckFilePath);
let entryPoint: AbsoluteFsPath|null = null;
if (options.flatModuleOutFile != null && options.flatModuleOutFile !== '') {
entryPoint = findFlatIndexEntryPoint(normalizedRootNames);
if (entryPoint === null) {
// This error message talks specifically about having a single .ts file in "files". However
// the actual logic is a bit more permissive. If a single file exists, that will be taken,
// otherwise the highest level (shortest path) "index.ts" file will be used as the flat
// module entry point instead. If neither of these conditions apply, the error below is
// given.
//
// The user is not informed about the "index.ts" option as this behavior is deprecated -
// an explicit entrypoint should always be specified.
this.constructionDiagnostics.push({
category: ts.DiagnosticCategory.Error,
code: ngErrorCode(ErrorCode.CONFIG_FLAT_MODULE_NO_INDEX),
file: undefined,
start: undefined,
length: undefined,
messageText:
'Angular compiler option "flatModuleOutFile" requires one and only one .ts file in the "files" field.',
});
} else {
const flatModuleId = options.flatModuleId || null;
const flatModuleOutFile = normalizeSeparators(options.flatModuleOutFile);
this.flatIndexGenerator =
new FlatIndexGenerator(entryPoint, flatModuleOutFile, flatModuleId);
generators.push(this.flatIndexGenerator);
rootFiles.push(this.flatIndexGenerator.flatIndexPath);
}
}
if (generators.length > 0) {
// FIXME: Remove the any cast once google3 is fully on TS3.6.
this.host = (new GeneratedShimsHostWrapper(host, generators) as any);
}
this.tsProgram =
ts.createProgram(rootFiles, options, this.host, oldProgram && oldProgram.reuseTsProgram);
const reuseProgram = oldProgram && oldProgram.reuseTsProgram;
this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram);
this.reuseTsProgram = this.tsProgram;
this.entryPoint = entryPoint !== null ? getSourceFileOrNull(this.tsProgram, entryPoint) : null;
const moduleResolutionCache = ts.createModuleResolutionCache(
this.host.getCurrentDirectory(), fileName => this.host.getCanonicalFileName(fileName));
this.moduleResolver =
new ModuleResolver(this.tsProgram, options, this.host, moduleResolutionCache);
this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver));
this.defaultImportTracker = new DefaultImportTracker();
if (oldProgram === undefined) {
this.incrementalDriver = IncrementalDriver.fresh(this.tsProgram);
} else {
this.incrementalDriver = IncrementalDriver.reconcile(
oldProgram.reuseTsProgram, oldProgram.incrementalDriver, this.tsProgram,
this.modifiedResourceFiles);
}
// Create the NgCompiler which will drive the rest of the compilation.
this.compiler =
new NgCompiler(this.host, options, this.tsProgram, reuseProgram, this.perfRecorder);
}
getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic> {
undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
}
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic> {
return this.constructionDiagnostics;
}
getTsSyntacticDiagnostics(
sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
}
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<api.Diagnostic> {
return [];
}
getTsSemanticDiagnostics(
sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken);
}
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly(ts.Diagnostic|api.Diagnostic)[] {
return this.compiler.getOptionDiagnostics();
}
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly api.Diagnostic[] {
return [];
}
getNgSemanticDiagnostics(
fileName?: string|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> {
const compilation = this.ensureAnalyzed();
const diagnostics = [...compilation.diagnostics, ...this.getTemplateDiagnostics()];
if (this.entryPoint !== null && this.exportReferenceGraph !== null) {
diagnostics.push(...checkForPrivateExports(
this.entryPoint, this.tsProgram.getTypeChecker(), this.exportReferenceGraph));
fileName?: string|undefined, cancellationToken?: ts.CancellationToken|
undefined): readonly(ts.Diagnostic|api.Diagnostic)[] {
let sf: ts.SourceFile|undefined = undefined;
if (fileName !== undefined) {
sf = this.tsProgram.getSourceFile(fileName);
if (sf === undefined) {
// There are no diagnostics for files which don't exist in the program - maybe the caller
// has stale data?
return [];
}
}
const diagnostics = this.compiler.getDiagnostics(sf);
this.reuseTsProgram = this.compiler.getNextProgram();
return diagnostics;
}
async loadNgStructureAsync(): Promise<void> {
if (this.compilation === undefined) {
this.compilation = this.makeCompilation();
}
const analyzeSpan = this.perfRecorder.start('analyze');
const promises: Promise<void>[] = [];
for (const sf of this.tsProgram.getSourceFiles()) {
if (sf.isDeclarationFile) {
continue;
}
const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf);
let analysisPromise = this.compilation !.analyzeAsync(sf);
this.scanForMwp(sf);
if (analysisPromise === undefined) {
this.perfRecorder.stop(analyzeFileSpan);
} else if (this.perfRecorder.enabled) {
analysisPromise = analysisPromise.then(() => this.perfRecorder.stop(analyzeFileSpan));
}
if (analysisPromise !== undefined) {
promises.push(analysisPromise);
}
}
await Promise.all(promises);
this.perfRecorder.stop(analyzeSpan);
this.resolveCompilation(this.compilation);
}
/**
* Ensure that the `NgCompiler` has properly analyzed the program, and allow for the asynchronous
* loading of any resources during the process.
*
* This is used by the Angular CLI to allow for spawning (async) child compilations for things
* like SASS files used in `styleUrls`.
*/
loadNgStructureAsync(): Promise<void> { return this.compiler.analyzeAsync(); }
listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] {
if (entryRoute) {
// Note:
// This resolution step is here to match the implementation of the old `AotCompilerHost` (see
// https://github.com/angular/angular/blob/50732e156/packages/compiler-cli/src/transformers/compiler_host.ts#L175-L188).
//
// `@angular/cli` will always call this API with an absolute path, so the resolution step is
// not necessary, but keeping it backwards compatible in case someone else is using the API.
// Relative entry paths are disallowed.
if (entryRoute.startsWith('.')) {
throw new Error(
`Failed to list lazy routes: Resolution of relative paths (${entryRoute}) is not supported.`);
}
// Non-relative entry paths fall into one of the following categories:
// - Absolute system paths (e.g. `/foo/bar/my-project/my-module`), which are unaffected by the
// logic below.
// - Paths to enternal modules (e.g. `some-lib`).
// - Paths mapped to directories in `tsconfig.json` (e.g. `shared/my-module`).
// (See https://www.typescriptlang.org/docs/handbook/module-resolution.html#path-mapping.)
//
// In all cases above, the `containingFile` argument is ignored, so we can just take the first
// of the root files.
const containingFile = this.tsProgram.getRootFileNames()[0];
const [entryPath, moduleName] = entryRoute.split('#');
const resolvedModule =
resolveModuleName(entryPath, containingFile, this.options, this.host, null);
if (resolvedModule) {
entryRoute = entryPointKeyFor(resolvedModule.resolvedFileName, moduleName);
}
}
this.ensureAnalyzed();
return this.routeAnalyzer !.listLazyRoutes(entryRoute);
}
getLibrarySummaries(): Map<string, api.LibrarySummary> {
throw new Error('Method not implemented.');
}
getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
throw new Error('Method not implemented.');
}
getEmittedSourceFiles(): Map<string, ts.SourceFile> {
throw new Error('Method not implemented.');
}
private scanForMwp(sf: ts.SourceFile): void {
this.mwpScanner !.scan(sf, {
addTypeReplacement: (node: ts.Declaration, type: Type): void => {
// Only obtain the return type transform for the source file once there's a type to replace,
// so that no transform is allocated when there's nothing to do.
this.dtsTransforms !.getReturnTypeTransform(sf).addTypeReplacement(node, type);
}
});
}
private ensureAnalyzed(): TraitCompiler {
if (this.compilation === undefined) {
const analyzeSpan = this.perfRecorder.start('analyze');
this.compilation = this.makeCompilation();
for (const sf of this.tsProgram.getSourceFiles()) {
if (sf.isDeclarationFile) {
continue;
}
const analyzeFileSpan = this.perfRecorder.start('analyzeFile', sf);
this.compilation !.analyzeSync(sf);
this.scanForMwp(sf);
this.perfRecorder.stop(analyzeFileSpan);
}
this.perfRecorder.stop(analyzeSpan);
this.resolveCompilation(this.compilation);
}
return this.compilation;
}
private resolveCompilation(compilation: TraitCompiler): void {
compilation.resolve();
this.recordNgModuleScopeDependencies();
// At this point, analysis is complete and the compiler can now calculate which files need to
// be emitted, so do that.
this.incrementalDriver.recordSuccessfulAnalysis(compilation);
return this.compiler.listLazyRoutes(entryRoute);
}
emit(opts?: {
emitFlags?: api.EmitFlags,
cancellationToken?: ts.CancellationToken,
customTransformers?: api.CustomTransformers,
emitCallback?: api.TsEmitCallback,
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback
}): ts.EmitResult {
emitFlags?: api.EmitFlags | undefined; cancellationToken?: ts.CancellationToken | undefined;
customTransformers?: api.CustomTransformers | undefined;
emitCallback?: api.TsEmitCallback | undefined;
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback | undefined;
}|undefined): ts.EmitResult {
const {transformers, ignoreFiles} = this.compiler.prepareEmit();
const emitCallback = opts && opts.emitCallback || defaultEmitCallback;
const compilation = this.ensureAnalyzed();
const writeFile: ts.WriteFileCallback =
(fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void) | undefined,
@ -383,9 +159,16 @@ export class NgtscProgram implements api.Program {
continue;
}
this.incrementalDriver.recordSuccessfulEmit(writtenSf);
this.compiler.incrementalDriver.recordSuccessfulEmit(writtenSf);
}
}
// If Closure annotations are being produced, tsickle should be adding `@nocollapse` to
// any static fields present. However, tsickle doesn't yet handle synthetic fields added
// during other transformations, so this hack is in place to ensure Ivy definitions get
// properly annotated, pending an upstream fix in tsickle.
//
// TODO(alxhub): remove when tsickle properly annotates synthetic fields.
if (this.closureCompilerEnabled && fileName.endsWith('.js')) {
data = nocollapseHack(data);
}
@ -393,46 +176,22 @@ export class NgtscProgram implements api.Program {
};
const customTransforms = opts && opts.customTransformers;
const beforeTransforms = transformers.before || [];
const afterDeclarationsTransforms = transformers.afterDeclarations;
const beforeTransforms = [
ivyTransformFactory(
compilation, this.reflector, this.importRewriter, this.defaultImportTracker, this.isCore,
this.closureCompilerEnabled),
aliasTransformFactory(compilation.exportStatements) as ts.TransformerFactory<ts.SourceFile>,
this.defaultImportTracker.importPreservingTransformer(),
];
const afterDeclarationsTransforms: ts.TransformerFactory<ts.Bundle|ts.SourceFile>[] = [];
if (this.dtsTransforms !== null) {
afterDeclarationsTransforms.push(
declarationTransformFactory(this.dtsTransforms, this.importRewriter));
}
// Only add aliasing re-exports to the .d.ts output if the `AliasingHost` requests it.
if (this.aliasingHost !== null && this.aliasingHost.aliasExportsInDts) {
afterDeclarationsTransforms.push(aliasTransformFactory(compilation.exportStatements));
}
if (this.factoryTracker !== null) {
beforeTransforms.push(
generatedFactoryTransform(this.factoryTracker.sourceInfo, this.importRewriter));
}
beforeTransforms.push(ivySwitchTransform);
if (customTransforms && customTransforms.beforeTs) {
if (customTransforms !== undefined && customTransforms.beforeTs !== undefined) {
beforeTransforms.push(...customTransforms.beforeTs);
}
const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = [];
const typeCheckFile = getSourceFileOrNull(this.tsProgram, this.typeCheckFilePath);
for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) {
if (targetSourceFile.isDeclarationFile || ignoreFiles.has(targetSourceFile)) {
continue;
}
if (this.incrementalDriver.safeToSkipEmit(targetSourceFile)) {
if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) {
continue;
}
@ -447,7 +206,7 @@ export class NgtscProgram implements api.Program {
before: beforeTransforms,
after: customTransforms && customTransforms.afterTs,
afterDeclarations: afterDeclarationsTransforms,
},
} as any,
}));
this.perfRecorder.stop(fileEmitSpan);
}
@ -461,323 +220,20 @@ export class NgtscProgram implements api.Program {
return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults);
}
private getTemplateDiagnostics(): ReadonlyArray<ts.Diagnostic> {
// Determine the strictness level of type checking based on compiler options. As
// `strictTemplates` is a superset of `fullTemplateTypeCheck`, the former implies the latter.
// Also see `verifyCompatibleTypeCheckOptions` where it is verified that `fullTemplateTypeCheck`
// is not disabled when `strictTemplates` is enabled.
const strictTemplates = !!this.options.strictTemplates;
const fullTemplateTypeCheck = strictTemplates || !!this.options.fullTemplateTypeCheck;
// Skip template type-checking if it's disabled.
if (this.options.ivyTemplateTypeCheck === false && !fullTemplateTypeCheck) {
return [];
}
const compilation = this.ensureAnalyzed();
// Run template type-checking.
// First select a type-checking configuration, based on whether full template type-checking is
// requested.
let typeCheckingConfig: TypeCheckingConfig;
if (fullTemplateTypeCheck) {
typeCheckingConfig = {
applyTemplateContextGuards: strictTemplates,
checkQueries: false,
checkTemplateBodies: true,
checkTypeOfInputBindings: strictTemplates,
strictNullInputBindings: strictTemplates,
checkTypeOfAttributes: strictTemplates,
// Even in full template type-checking mode, DOM binding checks are not quite ready yet.
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: strictTemplates,
checkTypeOfAnimationEvents: strictTemplates,
// Checking of DOM events currently has an adverse effect on developer experience,
// e.g. for `<input (blur)="update($event.target.value)">` enabling this check results in:
// - error TS2531: Object is possibly 'null'.
// - error TS2339: Property 'value' does not exist on type 'EventTarget'.
checkTypeOfDomEvents: strictTemplates,
checkTypeOfDomReferences: strictTemplates,
// Non-DOM references have the correct type in View Engine so there is no strictness flag.
checkTypeOfNonDomReferences: true,
// Pipes are checked in View Engine so there is no strictness flag.
checkTypeOfPipes: true,
strictSafeNavigationTypes: strictTemplates,
useContextGenericType: strictTemplates,
};
} else {
typeCheckingConfig = {
applyTemplateContextGuards: false,
checkQueries: false,
checkTemplateBodies: false,
checkTypeOfInputBindings: false,
strictNullInputBindings: false,
checkTypeOfAttributes: false,
checkTypeOfDomBindings: false,
checkTypeOfOutputEvents: false,
checkTypeOfAnimationEvents: false,
checkTypeOfDomEvents: false,
checkTypeOfDomReferences: false,
checkTypeOfNonDomReferences: false,
checkTypeOfPipes: false,
strictSafeNavigationTypes: false,
useContextGenericType: false,
};
}
// Apply explicitly configured strictness flags on top of the default configuration
// based on "fullTemplateTypeCheck".
if (this.options.strictInputTypes !== undefined) {
typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes;
typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes;
}
if (this.options.strictNullInputTypes !== undefined) {
typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes;
}
if (this.options.strictOutputEventTypes !== undefined) {
typeCheckingConfig.checkTypeOfOutputEvents = this.options.strictOutputEventTypes;
typeCheckingConfig.checkTypeOfAnimationEvents = this.options.strictOutputEventTypes;
}
if (this.options.strictDomEventTypes !== undefined) {
typeCheckingConfig.checkTypeOfDomEvents = this.options.strictDomEventTypes;
}
if (this.options.strictSafeNavigationTypes !== undefined) {
typeCheckingConfig.strictSafeNavigationTypes = this.options.strictSafeNavigationTypes;
}
if (this.options.strictDomLocalRefTypes !== undefined) {
typeCheckingConfig.checkTypeOfDomReferences = this.options.strictDomLocalRefTypes;
}
if (this.options.strictAttributeTypes !== undefined) {
typeCheckingConfig.checkTypeOfAttributes = this.options.strictAttributeTypes;
}
if (this.options.strictContextGenerics !== undefined) {
typeCheckingConfig.useContextGenericType = this.options.strictContextGenerics;
}
// Execute the typeCheck phase of each decorator in the program.
const prepSpan = this.perfRecorder.start('typeCheckPrep');
const ctx = new TypeCheckContext(
typeCheckingConfig, this.refEmitter !, this.reflector, this.typeCheckFilePath);
compilation.typeCheck(ctx);
this.perfRecorder.stop(prepSpan);
// Get the diagnostics.
const typeCheckSpan = this.perfRecorder.start('typeCheckDiagnostics');
const {diagnostics, program} =
ctx.calculateTemplateDiagnostics(this.tsProgram, this.host, this.options);
this.perfRecorder.stop(typeCheckSpan);
this.reuseTsProgram = program;
return diagnostics;
}
getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
const compilation = this.ensureAnalyzed();
const context = new IndexingContext();
compilation.index(context);
return generateAnalysis(context);
return this.compiler.getIndexedComponents();
}
private makeCompilation(): TraitCompiler {
const checker = this.tsProgram.getTypeChecker();
// Construct the ReferenceEmitter.
if (this.fileToModuleHost === null || !this.options._useHostForImportGeneration) {
let localImportStrategy: ReferenceEmitStrategy;
// The strategy used for local, in-project imports depends on whether TS has been configured
// with rootDirs. If so, then multiple directories may be mapped in the same "module
// namespace" and the logic of `LogicalProjectStrategy` is required to generate correct
// imports which may cross these multiple directories. Otherwise, plain relative imports are
// sufficient.
if (this.options.rootDir !== undefined ||
(this.options.rootDirs !== undefined && this.options.rootDirs.length > 0)) {
// rootDirs logic is in effect - use the `LogicalProjectStrategy` for in-project relative
// imports.
localImportStrategy =
new LogicalProjectStrategy(this.reflector, new LogicalFileSystem(this.rootDirs));
} else {
// Plain relative imports are all that's needed.
localImportStrategy = new RelativePathStrategy(this.reflector);
}
// The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference
// resolution strategy.
this.refEmitter = new ReferenceEmitter([
// First, try to use local identifiers if available.
new LocalIdentifierStrategy(),
// Next, attempt to use an absolute import.
new AbsoluteModuleStrategy(this.tsProgram, checker, this.moduleResolver, this.reflector),
// Finally, check if the reference is being written into a file within the project's .ts
// sources, and use a relative import if so. If this fails, ReferenceEmitter will throw
// an error.
localImportStrategy,
]);
// If an entrypoint is present, then all user imports should be directed through the
// entrypoint and private exports are not needed. The compiler will validate that all publicly
// visible directives/pipes are importable via this entrypoint.
if (this.entryPoint === null && this.options.generateDeepReexports === true) {
// No entrypoint is present and deep re-exports were requested, so configure the aliasing
// system to generate them.
this.aliasingHost = new PrivateExportAliasingHost(this.reflector);
}
} else {
// The CompilerHost supports fileNameToModuleName, so use that to emit imports.
this.refEmitter = new ReferenceEmitter([
// First, try to use local identifiers if available.
new LocalIdentifierStrategy(),
// Then use aliased references (this is a workaround to StrictDeps checks).
new AliasStrategy(),
// Then use fileNameToModuleName to emit imports.
new FileToModuleStrategy(this.reflector, this.fileToModuleHost),
]);
this.aliasingHost = new FileToModuleAliasingHost(this.fileToModuleHost);
}
const evaluator =
new PartialEvaluator(this.reflector, checker, this.incrementalDriver.depGraph);
const dtsReader = new DtsMetadataReader(checker, this.reflector);
const localMetaRegistry = new LocalMetadataRegistry();
const localMetaReader: MetadataReader = localMetaRegistry;
const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, this.aliasingHost);
this.scopeRegistry = new LocalModuleScopeRegistry(
localMetaReader, depScopeReader, this.refEmitter, this.aliasingHost);
const scopeReader: ComponentScopeReader = this.scopeRegistry;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, this.scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(this.reflector);
this.metaReader = new CompoundMetadataReader([localMetaReader, dtsReader]);
// If a flat module entrypoint was specified, then track references via a `ReferenceGraph` in
// order to produce proper diagnostics for incorrectly exported directives/pipes/etc. If there
// is no flat module entrypoint then don't pay the cost of tracking references.
let referencesRegistry: ReferencesRegistry;
if (this.entryPoint !== null) {
this.exportReferenceGraph = new ReferenceGraph();
referencesRegistry = new ReferenceGraphAdapter(this.exportReferenceGraph);
} else {
referencesRegistry = new NoopReferencesRegistry();
}
this.routeAnalyzer = new NgModuleRouteAnalyzer(this.moduleResolver, evaluator);
this.dtsTransforms = new DtsTransformRegistry();
this.mwpScanner = new ModuleWithProvidersScanner(this.reflector, evaluator, this.refEmitter);
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.metaReader !, scopeReader,
this.scopeRegistry, this.isCore, this.resourceManager, this.rootDirs,
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver,
this.cycleAnalyzer, this.refEmitter, this.defaultImportTracker,
this.incrementalDriver.depGraph, injectableRegistry, this.closureCompilerEnabled),
// TODO(alxhub): understand why the cast here is necessary (something to do with `null` not
// being assignable to `unknown` when wrapped in `Readonly`).
// clang-format off
new DirectiveDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.scopeRegistry, this.metaReader,
this.defaultImportTracker, injectableRegistry, this.isCore, this.closureCompilerEnabled
) as Readonly<DecoratorHandler<unknown, unknown, unknown>>,
// clang-format on
// Pipe handler must be before injectable handler in list so pipe factories are printed
// before injectable factories (so injectable factories can delegate to them)
new PipeDecoratorHandler(
this.reflector, evaluator, metaRegistry, this.scopeRegistry, this.defaultImportTracker,
injectableRegistry, this.isCore),
new InjectableDecoratorHandler(
this.reflector, this.defaultImportTracker, this.isCore,
this.options.strictInjectionParameters || false, injectableRegistry),
new NgModuleDecoratorHandler(
this.reflector, evaluator, this.metaReader, metaRegistry, this.scopeRegistry,
referencesRegistry, this.isCore, this.routeAnalyzer, this.refEmitter, this.factoryTracker,
this.defaultImportTracker, this.closureCompilerEnabled, injectableRegistry,
this.options.i18nInLocale),
];
return new TraitCompiler(
handlers, this.reflector, this.perfRecorder, this.incrementalDriver,
this.options.compileNonExportedClasses !== false, this.dtsTransforms);
getLibrarySummaries(): Map<string, api.LibrarySummary> {
throw new Error('Method not implemented.');
}
/**
* Reifies the inter-dependencies of NgModules and the components within their compilation scopes
* into the `IncrementalDriver`'s dependency graph.
*/
private recordNgModuleScopeDependencies() {
const recordSpan = this.perfRecorder.start('recordDependencies');
const depGraph = this.incrementalDriver.depGraph;
for (const scope of this.scopeRegistry !.getCompilationScopes()) {
const file = scope.declaration.getSourceFile();
const ngModuleFile = scope.ngModule.getSourceFile();
// A change to any dependency of the declaration causes the declaration to be invalidated,
// which requires the NgModule to be invalidated as well.
depGraph.addTransitiveDependency(ngModuleFile, file);
// A change to the NgModule file should cause the declaration itself to be invalidated.
depGraph.addDependency(file, ngModuleFile);
const meta = this.metaReader !.getDirectiveMetadata(new Reference(scope.declaration));
if (meta !== null && meta.isComponent) {
// If a component's template changes, it might have affected the import graph, and thus the
// remote scoping feature which is activated in the event of potential import cycles. Thus,
// the module depends not only on the transitive dependencies of the component, but on its
// resources as well.
depGraph.addTransitiveResources(ngModuleFile, file);
// A change to any directive/pipe in the compilation scope should cause the component to be
// invalidated.
for (const directive of scope.directives) {
// When a directive in scope is updated, the component needs to be recompiled as e.g. a
// selector may have changed.
depGraph.addTransitiveDependency(file, directive.ref.node.getSourceFile());
}
for (const pipe of scope.pipes) {
// When a pipe in scope is updated, the component needs to be recompiled as e.g. the
// pipe's name may have changed.
depGraph.addTransitiveDependency(file, pipe.ref.node.getSourceFile());
}
}
}
this.perfRecorder.stop(recordSpan);
getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
throw new Error('Method not implemented.');
}
private get reflector(): TypeScriptReflectionHost {
if (this._reflector === undefined) {
this._reflector = new TypeScriptReflectionHost(this.tsProgram.getTypeChecker());
}
return this._reflector;
}
private get coreImportsFrom(): ts.SourceFile|null {
if (this._coreImportsFrom === undefined) {
this._coreImportsFrom = this.isCore && getR3SymbolsFile(this.tsProgram) || null;
}
return this._coreImportsFrom;
}
private get isCore(): boolean {
if (this._isCore === undefined) {
this._isCore = isAngularCorePackage(this.tsProgram);
}
return this._isCore;
}
private get importRewriter(): ImportRewriter {
if (this._importRewriter === undefined) {
const coreImportsFrom = this.coreImportsFrom;
this._importRewriter = coreImportsFrom !== null ?
new R3SymbolsImportRewriter(coreImportsFrom.fileName) :
new NoopImportRewriter();
}
return this._importRewriter;
getEmittedSourceFiles(): Map<string, ts.SourceFile> {
throw new Error('Method not implemented.');
}
}
@ -799,100 +255,3 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
return {diagnostics, emitSkipped, emittedFiles};
}
/**
* Find the 'r3_symbols.ts' file in the given `Program`, or return `null` if it wasn't there.
*/
function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null {
return program.getSourceFiles().find(file => file.fileName.indexOf('r3_symbols.ts') >= 0) || null;
}
/**
* Determine if the given `Program` is @angular/core.
*/
function isAngularCorePackage(program: ts.Program): boolean {
// Look for its_just_angular.ts somewhere in the program.
const r3Symbols = getR3SymbolsFile(program);
if (r3Symbols === null) {
return false;
}
// Look for the constant ITS_JUST_ANGULAR in that file.
return r3Symbols.statements.some(stmt => {
// The statement must be a variable declaration statement.
if (!ts.isVariableStatement(stmt)) {
return false;
}
// It must be exported.
if (stmt.modifiers === undefined ||
!stmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) {
return false;
}
// It must declare ITS_JUST_ANGULAR.
return stmt.declarationList.declarations.some(decl => {
// The declaration must match the name.
if (!ts.isIdentifier(decl.name) || decl.name.text !== 'ITS_JUST_ANGULAR') {
return false;
}
// It must initialize the variable to true.
if (decl.initializer === undefined || decl.initializer.kind !== ts.SyntaxKind.TrueKeyword) {
return false;
}
// This definition matches.
return true;
});
});
}
/**
* Since "strictTemplates" is a true superset of type checking capabilities compared to
* "strictTemplateTypeCheck", it is required that the latter is not explicitly disabled if the
* former is enabled.
*/
function verifyCompatibleTypeCheckOptions(options: api.CompilerOptions): ts.Diagnostic|null {
if (options.fullTemplateTypeCheck === false && options.strictTemplates === true) {
return {
category: ts.DiagnosticCategory.Error,
code: ngErrorCode(ErrorCode.CONFIG_STRICT_TEMPLATES_IMPLIES_FULL_TEMPLATE_TYPECHECK),
file: undefined,
start: undefined,
length: undefined,
messageText:
`Angular compiler option "strictTemplates" is enabled, however "fullTemplateTypeCheck" is disabled.
Having the "strictTemplates" flag enabled implies that "fullTemplateTypeCheck" is also enabled, so
the latter can not be explicitly disabled.
One of the following actions is required:
1. Remove the "fullTemplateTypeCheck" option.
2. Remove "strictTemplates" or set it to 'false'.
More information about the template type checking compiler options can be found in the documentation:
https://v9.angular.io/guide/template-typecheck#template-type-checking`,
};
}
return null;
}
export class ReferenceGraphAdapter implements ReferencesRegistry {
constructor(private graph: ReferenceGraph) {}
add(source: ts.Declaration, ...references: Reference<ts.Declaration>[]): void {
for (const {node} of references) {
let sourceFile = node.getSourceFile();
if (sourceFile === undefined) {
sourceFile = ts.getOriginalNode(node).getSourceFile();
}
// Only record local references (not references into .d.ts files).
if (sourceFile === undefined || !isDtsPath(sourceFile.fileName)) {
this.graph.add(source, node);
}
}
}
}
function shouldEnablePerfTracing(options: api.CompilerOptions): boolean {
return options.tracePerformance !== undefined;
}