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

@ -23,26 +23,12 @@ ts_library(
tsconfig = ":tsconfig", tsconfig = ":tsconfig",
deps = [ deps = [
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations", "//packages/compiler-cli/src/ngtsc/core",
"//packages/compiler-cli/src/ngtsc/cycles", "//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/entry_point",
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer", "//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/modulewithproviders",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/perf", "//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/switch",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@bazel/typescript", "@npm//@bazel/typescript",
"@npm//@types/chokidar", "@npm//@types/chokidar",
"@npm//@types/node", "@npm//@types/node",

View File

@ -79,11 +79,11 @@ export class ComponentDecoratorHandler implements
private reflector: ReflectionHost, private evaluator: PartialEvaluator, private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private metaReader: MetadataReader, private metaRegistry: MetadataRegistry, private metaReader: MetadataReader,
private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry, private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry,
private isCore: boolean, private resourceLoader: ResourceLoader, private rootDirs: string[], private isCore: boolean, private resourceLoader: ResourceLoader,
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean, private rootDirs: ReadonlyArray<string>, private defaultPreserveWhitespaces: boolean,
private enableI18nLegacyMessageIdFormat: boolean, private moduleResolver: ModuleResolver, private i18nUseExternalIds: boolean, private enableI18nLegacyMessageIdFormat: boolean,
private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter, private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
private defaultImportRecorder: DefaultImportRecorder, private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder,
private depTracker: DependencyTracker|null, private depTracker: DependencyTracker|null,
private injectableRegistry: InjectableClassRegistry, private injectableRegistry: InjectableClassRegistry,
private annotateForClosureCompiler: boolean) {} private annotateForClosureCompiler: boolean) {}

View File

@ -0,0 +1,46 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "core",
srcs = ["index.ts"] + glob([
"src/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/core",
deps = [
":api",
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/entry_point",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/metadata",
"//packages/compiler-cli/src/ngtsc/modulewithproviders",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/resource",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/switch",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
)
ts_library(
name = "api",
srcs = ["api.ts"],
deps = [
"@npm//typescript",
],
)

View File

@ -0,0 +1,51 @@
# What is the 'core' package?
This package contains the core functionality of the Angular compiler. It provides APIs for the implementor of a TypeScript compiler to provide Angular compilation as well.
It supports the 'ngc' command-line tool and the Angular CLI (via the `NgtscProgram`).
# Angular compilation
Angular compilation involves the translation of Angular decorators into static definition fields. At build time, this is done during the overall process of TypeScript compilation, where TypeScript code is type-checked and then downleveled to JavaScript code. Along the way, diagnostics specific to Angular can also be produced.
## Compilation flow
Any use of the TypeScript compiler APIs follows a multi-step process:
1. A `ts.CompilerHost` is created.
2. That `ts.CompilerHost`, plus a set of "root files", is used to create a `ts.Program`.
3. The `ts.Program` is used to gather various kinds of diagnostics.
4. Eventually, the `ts.Program` is asked to `emit`, and JavaScript code is produced.
A compiler which integrates Angular compilation into this process follows a very similar flow, with a few extra steps:
1. A `ts.CompilerHost` is created.
2. That `ts.CompilerHost` is wrapped in an `NgCompilerHost`, which adds Angular specific files to the compilation.
3. A `ts.Program` is created from the `NgCompilerHost` and its augmented set of root files.
4. An `NgCompiler` is created using the `ts.Program`.
5. Diagnostics can be gathered from the `ts.Program` as normal, as well as from the `NgCompiler`.
6. Prior to `emit`, `NgCompiler.prepareEmit` is called to retrieve the Angular transformers which need to be fed to `ts.Program.emit`.
7. `emit` is called on the `ts.Program` with the Angular transformers from above, which produces JavaScript code with Angular extensions.
## Asynchronous compilation
In some compilation environments (such as the Webpack-driven compilation inside the Angular CLI), various inputs to the compilation are only producible in an asynchronous fashion. For example, SASS compilation of `styleUrls` that link to SASS files requires spawning a child Webpack compilation. To support this, Angular has an asynchronous interface for loading such resources.
If this interface is used, an additional asynchronous step after `NgCompiler` creation is to call `NgCompiler.analyzeAsync` and await its `Promise`. After this operation completes, all resources have been loaded and the rest of the `NgCompiler` API can be used synchronously.
# Wrapping the `ts.CompilerHost`
Angular compilation generates a number of synthetic files (files which did not exist originally as inputs), depending on configuration. Such files can include:
* `.ngfactory` shim files, if requested.
* `.ngsummary` shim files, if requested.
* A flat module index file, if requested.
* The `__ng_typecheck__.ts` file, which supports template type-checking code.
These files don't exist on disk, but need to appear as such to the `ts.Program`. This is accomplished by wrapping the `ts.CompilerHost` (which abstracts the outside world to the `ts.Program`) in an implementation which provides these synthetic files. This is the primary function of `NgCompilerHost`.
# API definitions
The `core` package contains separate API definitions, which are used across the compiler. Of note is the interface `NgCompilerOptions`, which unifies various supported compilation options across Angular and TypeScript itself. It's assignable to `ts.CompilerOptions`, and implemented by the legacy `CompilerOptions` type in `//packages/compiler-cli/src/transformers/api.ts`.
The various types of options are split out into distinct interfaces according to their purpose and level of support.

View File

@ -0,0 +1,418 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
/**
* A host backed by a build system which has a unified view of the module namespace.
*
* Such a build system supports the `fileNameToModuleName` method provided by certain build system
* integrations (such as the integration with Bazel). See the docs on `fileNameToModuleName` for
* more details.
*/
export interface UnifiedModulesHost {
/**
* Converts a file path to a module name that can be used as an `import ...`.
*
* For example, such a host might determine that `/absolute/path/to/monorepo/lib/importedFile.ts`
* should be imported using a module specifier of `monorepo/lib/importedFile`.
*/
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string;
}
/**
* A host which additionally tracks and produces "resources" (HTML templates, CSS
* files, etc).
*/
export interface ResourceHost {
/**
* Converts a file path for a resource that is used in a source file or another resource
* into a filepath.
*/
resourceNameToFileName(resourceName: string, containingFilePath: string): string|null;
/**
* Load a referenced resource either statically or asynchronously. If the host returns a
* `Promise<string>` it is assumed the user of the corresponding `Program` will call
* `loadNgStructureAsync()`. Returning `Promise<string>` outside `loadNgStructureAsync()` will
* cause a diagnostics diagnostic error or an exception to be thrown.
*/
readResource(fileName: string): Promise<string>|string;
/**
* Get the absolute paths to the changed files that triggered the current compilation
* or `undefined` if this is not an incremental build.
*/
getModifiedResourceFiles?(): Set<string>|undefined;
}
/**
* A `ts.CompilerHost` interface which supports some number of optional methods in addition to the
* core interface.
*/
export interface ExtendedTsCompilerHost extends ts.CompilerHost, Partial<ResourceHost>,
Partial<UnifiedModulesHost> {}
/**
* Options supported by the legacy View Engine compiler, which are still consumed by the Angular Ivy
* compiler for backwards compatibility.
*
* These are expected to be removed at some point in the future.
*/
export interface LegacyNgcOptions {
/** generate all possible generated files */
allowEmptyCodegenFiles?: boolean;
/**
* Whether to type check the entire template.
*
* This flag currently controls a couple aspects of template type-checking, including
* whether embedded views are checked.
*
* For maximum type-checking, set this to `true`, and set `strictTemplates` to `true`.
*
* It is an error for this flag to be `false`, while `strictTemplates` is set to `true`.
*/
fullTemplateTypeCheck?: boolean;
/**
* Whether to generate a flat module index of the given name and the corresponding
* flat module metadata. This option is intended to be used when creating flat
* modules similar to how `@angular/core` and `@angular/common` are packaged.
* When this option is used the `package.json` for the library should refer to the
* generated flat module index instead of the library index file. When using this
* option only one .metadata.json file is produced that contains all the metadata
* necessary for symbols exported from the library index.
* In the generated .ngfactory.ts files flat module index is used to import symbols
* including both the public API from the library index as well as shrowded internal
* symbols.
* By default the .ts file supplied in the `files` field is assumed to be the
* library index. If more than one is specified, uses `libraryIndex` to select the
* file to use. If more than one .ts file is supplied and no `libraryIndex` is supplied
* an error is produced.
* A flat module index .d.ts and .js will be created with the given `flatModuleOutFile`
* name in the same location as the library index .d.ts file is emitted.
* For example, if a library uses `public_api.ts` file as the library index of the
* module the `tsconfig.json` `files` field would be `["public_api.ts"]`. The
* `flatModuleOutFile` options could then be set to, for example `"index.js"`, which
* produces `index.d.ts` and `index.metadata.json` files. The library's
* `package.json`'s `module` field would be `"index.js"` and the `typings` field would
* be `"index.d.ts"`.
*/
flatModuleOutFile?: string;
/**
* Preferred module id to use for importing flat module. References generated by `ngc`
* will use this module name when importing symbols from the flat module. This is only
* meaningful when `flatModuleOutFile` is also supplied. It is otherwise ignored.
*/
flatModuleId?: string;
/**
* Always report errors a parameter is supplied whose injection type cannot
* be determined. When this value option is not provided or is `false`, constructor
* parameters of classes marked with `@Injectable` whose type cannot be resolved will
* produce a warning. With this option `true`, they produce an error. When this option is
* not provided is treated as if it were `false`.
*/
strictInjectionParameters?: boolean;
/**
* Whether to remove blank text nodes from compiled templates. It is `false` by default starting
* from Angular 6.
*/
preserveWhitespaces?: boolean;
}
/**
* Options which were added to the Angular Ivy compiler to support backwards compatibility with
* existing View Engine applications.
*
* These are expected to be removed at some point in the future.
*/
export interface NgcCompatibilityOptions {
/**
* Controls whether ngtsc will emit `.ngfactory.js` shims for each compiled `.ts` file.
*
* These shims support legacy imports from `ngfactory` files, by exporting a factory shim
* for each component or NgModule in the original `.ts` file.
*/
generateNgFactoryShims?: boolean;
/**
* Controls whether ngtsc will emit `.ngsummary.js` shims for each compiled `.ts` file.
*
* These shims support legacy imports from `ngsummary` files, by exporting an empty object
* for each NgModule in the original `.ts` file. The only purpose of summaries is to feed them to
* `TestBed`, which is a no-op in Ivy.
*/
generateNgSummaryShims?: boolean;
/**
* Tells the compiler to generate definitions using the Render3 style code generation.
* This option defaults to `true`.
*
* Acceptable values are as follows:
*
* `false` - run ngc normally
* `true` - run the ngtsc compiler instead of the normal ngc compiler
* `ngtsc` - alias for `true`
*/
enableIvy?: boolean|'ngtsc';
}
/**
* Options related to template type-checking and its strictness.
*/
export interface StrictTemplateOptions {
/**
* If `true`, implies all template strictness flags below (unless individually disabled).
*
* Has no effect unless `fullTemplateTypeCheck` is also enabled.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictTemplates?: boolean;
/**
* Whether to check the type of a binding to a directive/component input against the type of the
* field on the directive/component.
*
* For example, if this is `false` then the expression `[input]="expr"` will have `expr` type-
* checked, but not the assignment of the resulting type to the `input` property of whichever
* directive or component is receiving the binding. If set to `true`, both sides of the assignment
* are checked.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictInputTypes?: boolean;
/**
* Whether to use strict null types for input bindings for directives.
*
* If this is `true`, applications that are compiled with TypeScript's `strictNullChecks` enabled
* will produce type errors for bindings which can evaluate to `undefined` or `null` where the
* inputs's type does not include `undefined` or `null` in its type. If set to `false`, all
* binding expressions are wrapped in a non-null assertion operator to effectively disable strict
* null checks.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set. Note that if `strictInputTypes` is
* not set, or set to `false`, this flag has no effect.
*/
strictNullInputTypes?: boolean;
/**
* Whether to check text attributes that happen to be consumed by a directive or component.
*
* For example, in a template containing `<input matInput disabled>` the `disabled` attribute ends
* up being consumed as an input with type `boolean` by the `matInput` directive. At runtime, the
* input will be set to the attribute's string value, which is an empty string for attributes
* without a value, so with this flag set to `true`, an error would be reported. If set to
* `false`, text attributes will never report an error.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set. Note that if `strictInputTypes` is
* not set, or set to `false`, this flag has no effect.
*/
strictAttributeTypes?: boolean;
/**
* Whether to use a strict type for null-safe navigation operations.
*
* If this is `false`, then the return type of `a?.b` or `a?()` will be `any`. If set to `true`,
* then the return type of `a?.b` for example will be the same as the type of the ternary
* expression `a != null ? a.b : a`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictSafeNavigationTypes?: boolean;
/**
* Whether to infer the type of local references.
*
* If this is `true`, the type of a `#ref` variable on a DOM node in the template will be
* determined by the type of `document.createElement` for the given DOM node. If set to `false`,
* the type of `ref` for DOM nodes will be `any`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictDomLocalRefTypes?: boolean;
/**
* Whether to infer the type of the `$event` variable in event bindings for directive outputs or
* animation events.
*
* If this is `true`, the type of `$event` will be inferred based on the generic type of
* `EventEmitter`/`Subject` of the output. If set to `false`, the `$event` variable will be of
* type `any`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictOutputEventTypes?: boolean;
/**
* Whether to infer the type of the `$event` variable in event bindings to DOM events.
*
* If this is `true`, the type of `$event` will be inferred based on TypeScript's
* `HTMLElementEventMap`, with a fallback to the native `Event` type. If set to `false`, the
* `$event` variable will be of type `any`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictDomEventTypes?: boolean;
/**
* Whether to include the generic type of components when type-checking the template.
*
* If no component has generic type parameters, this setting has no effect.
*
* If a component has generic type parameters and this setting is `true`, those generic parameters
* will be included in the context type for the template. If `false`, any generic parameters will
* be set to `any` in the template context type.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictContextGenerics?: boolean;
}
/**
* Options which control behavior useful for "monorepo" build cases using Bazel (such as the
* internal Google monorepo, g3).
*/
export interface BazelAndG3Options {
/**
* Enables the generation of alias re-exports of directives/pipes that are visible from an
* NgModule from that NgModule's file.
*
* This option should be disabled for application builds or for Angular Package Format libraries
* (where NgModules along with their directives/pipes are exported via a single entrypoint).
*
* For other library compilations which are intended to be path-mapped into an application build
* (or another library), enabling this option enables the resulting deep imports to work
* correctly.
*
* A consumer of such a path-mapped library will write an import like:
*
* ```typescript
* import {LibModule} from 'lib/deep/path/to/module';
* ```
*
* The compiler will attempt to generate imports of directives/pipes from that same module
* specifier (the compiler does not rewrite the user's given import path, unlike View Engine).
*
* ```typescript
* import {LibDir, LibCmp, LibPipe} from 'lib/deep/path/to/module';
* ```
*
* It would be burdensome for users to have to re-export all directives/pipes alongside each
* NgModule to support this import model. Enabling this option tells the compiler to generate
* private re-exports alongside the NgModule of all the directives/pipes it makes available, to
* support these future imports.
*/
generateDeepReexports?: boolean;
/**
* Insert JSDoc type annotations needed by Closure Compiler
*/
annotateForClosureCompiler?: boolean;
}
/**
* Options related to i18n compilation support.
*/
export interface I18nOptions {
/**
* Locale of the imported translations
*/
i18nInLocale?: string;
/**
* Render `$localize` messages with legacy format ids.
*
* This is only active if we are building with `enableIvy: true`.
* The default value for now is `true`.
*
* Use this option when use are using the `$localize` based localization messages but
* have not migrated the translation files to use the new `$localize` message id format.
*/
enableI18nLegacyMessageIdFormat?: boolean;
/**
* Whether translation variable name should contain external message id
* (used by Closure Compiler's output of `goog.getMsg` for transition period)
*/
i18nUseExternalIds?: boolean;
}
/**
* Non-public options which are useful during testing of the compiler.
*/
export interface TestOnlyOptions {
/**
* Whether to use the CompilerHost's fileNameToModuleName utility (if available) to generate
* import module specifiers. This is false by default, and exists to support running ngtsc
* within Google. This option is internal and is used by the ng_module.bzl rule to switch
* behavior between Bazel and Blaze.
*
* @internal
*/
_useHostForImportGeneration?: boolean;
/**
* Turn on template type-checking in the Ivy compiler.
*
* This is an internal flag being used to roll out template type-checking in ngtsc. Turning it on
* by default before it's ready might break other users attempting to test the new compiler's
* behavior.
*
* @internal
*/
ivyTemplateTypeCheck?: boolean;
}
/**
* A merged interface of all of the various Angular compiler options, as well as the standard
* `ts.CompilerOptions`.
*
* Also includes a few miscellaneous options.
*/
export interface NgCompilerOptions extends ts.CompilerOptions, LegacyNgcOptions, BazelAndG3Options,
NgcCompatibilityOptions, StrictTemplateOptions, TestOnlyOptions, I18nOptions {
/**
* Whether the compiler should avoid generating code for classes that haven't been exported.
* This is only active when building with `enableIvy: true`. Defaults to `true`.
*/
compileNonExportedClasses?: boolean;
/**
* Whether to remove blank text nodes from compiled templates. It is `false` by default starting
* from Angular 6.
*/
preserveWhitespaces?: boolean;
/**
* Disable TypeScript Version Check.
*/
disableTypeScriptVersionCheck?: boolean;
/** An option to enable ngtsc's internal performance tracing.
*
* This should be a path to a JSON file where trace information will be written. An optional 'ts:'
* prefix will cause the trace to be written via the TS host instead of directly to the filesystem
* (not all hosts support this mode of operation).
*
* This is currently not exposed to users as the trace format is still unstable.
*/
tracePerformance?: string;
}
export interface LazyRoute {
route: string;
module: {name: string, filePath: string};
referencedModule: {name: string, filePath: string};
}

View File

@ -0,0 +1,10 @@
/**
* @license
* Copyright Google Inc. 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
*/
export {NgCompiler} from './src/compiler';
export {NgCompilerHost} from './src/host';

View File

@ -0,0 +1,813 @@
/**
* @license
* Copyright Google Inc. 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 {Type} from '@angular/compiler';
import * as ts from 'typescript';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from '../../annotations';
import {CycleAnalyzer, ImportGraph} from '../../cycles';
import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {ReferenceGraph, checkForPrivateExports} from '../../entry_point';
import {LogicalFileSystem, getSourceFileOrError} from '../../file_system';
import {AbsoluteModuleStrategy, AliasStrategy, AliasingHost, DefaultImportTracker, ImportRewriter, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NoopImportRewriter, PrivateExportAliasingHost, R3SymbolsImportRewriter, Reference, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesAliasingHost, UnifiedModulesStrategy} from '../../imports';
import {IncrementalDriver} from '../../incremental';
import {IndexedComponent, IndexingContext, generateAnalysis} from '../../indexer';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry, MetadataReader} from '../../metadata';
import {ModuleWithProvidersScanner} from '../../modulewithproviders';
import {PartialEvaluator} from '../../partial_evaluator';
import {NOOP_PERF_RECORDER, PerfRecorder} from '../../perf';
import {TypeScriptReflectionHost} from '../../reflection';
import {HostResourceLoader} from '../../resource';
import {NgModuleRouteAnalyzer, entryPointKeyFor} from '../../routing';
import {ComponentScopeReader, LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../scope';
import {generatedFactoryTransform} from '../../shims';
import {ivySwitchTransform} from '../../switch';
import {DecoratorHandler, DtsTransformRegistry, TraitCompiler, aliasTransformFactory, declarationTransformFactory, ivyTransformFactory} from '../../transform';
import {TypeCheckContext, TypeCheckingConfig, isTemplateDiagnostic} from '../../typecheck';
import {getSourceFileOrNull, isDtsPath, resolveModuleName} from '../../util/src/typescript';
import {LazyRoute, NgCompilerOptions} from '../api';
import {NgCompilerHost} from './host';
/**
* State information about a compilation which is only generated once some data is requested from
* the `NgCompiler` (for example, by calling `getDiagnostics`).
*/
interface LazyCompilationState {
isCore: boolean;
traitCompiler: TraitCompiler;
reflector: TypeScriptReflectionHost;
metaReader: MetadataReader;
scopeRegistry: LocalModuleScopeRegistry;
exportReferenceGraph: ReferenceGraph|null;
routeAnalyzer: NgModuleRouteAnalyzer;
dtsTransforms: DtsTransformRegistry;
mwpScanner: ModuleWithProvidersScanner;
defaultImportTracker: DefaultImportTracker;
aliasingHost: AliasingHost|null;
refEmitter: ReferenceEmitter;
}
/**
* The heart of the Angular Ivy compiler.
*
* The `NgCompiler` provides an API for performing Angular compilation within a custom TypeScript
* compiler. Each instance of `NgCompiler` supports a single compilation, which might be
* incremental.
*
* `NgCompiler` is lazy, and does not perform any of the work of the compilation until one of its
* output methods (e.g. `getDiagnostics`) is called.
*
* See the README.md for more information.
*/
export class NgCompiler {
/**
* Lazily evaluated state of the compilation.
*
* This is created on demand by calling `ensureAnalyzed`.
*/
private compilation: LazyCompilationState|null = null;
/**
* Any diagnostics related to the construction of the compilation.
*
* These are diagnostics which arose during setup of the host and/or program.
*/
private constructionDiagnostics: ts.Diagnostic[] = [];
/**
* Semantic diagnostics related to the program itself.
*
* This is set by (and memoizes) `getDiagnostics`.
*/
private diagnostics: ts.Diagnostic[]|null = null;
private closureCompilerEnabled: boolean;
private typeCheckFile: ts.SourceFile;
private nextProgram: ts.Program;
private entryPoint: ts.SourceFile|null;
private moduleResolver: ModuleResolver;
private resourceManager: HostResourceLoader;
private cycleAnalyzer: CycleAnalyzer;
readonly incrementalDriver: IncrementalDriver;
constructor(
private host: NgCompilerHost, private options: NgCompilerOptions,
private tsProgram: ts.Program, oldProgram: ts.Program|null = null,
private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER) {
this.constructionDiagnostics.push(...this.host.diagnostics);
const incompatibleTypeCheckOptionsDiagnostic = verifyCompatibleTypeCheckOptions(this.options);
if (incompatibleTypeCheckOptionsDiagnostic !== null) {
this.constructionDiagnostics.push(incompatibleTypeCheckOptionsDiagnostic);
}
this.nextProgram = tsProgram;
this.closureCompilerEnabled = !!this.options.annotateForClosureCompiler;
this.entryPoint =
host.entryPoint !== null ? getSourceFileOrNull(tsProgram, host.entryPoint) : null;
this.typeCheckFile = getSourceFileOrError(tsProgram, host.typeCheckFile);
const moduleResolutionCache = ts.createModuleResolutionCache(
this.host.getCurrentDirectory(), fileName => this.host.getCanonicalFileName(fileName));
this.moduleResolver =
new ModuleResolver(tsProgram, this.options, this.host, moduleResolutionCache);
this.resourceManager = new HostResourceLoader(host, this.options);
this.cycleAnalyzer = new CycleAnalyzer(new ImportGraph(this.moduleResolver));
let modifiedResourceFiles: Set<string>|null = null;
if (this.host.getModifiedResourceFiles !== undefined) {
modifiedResourceFiles = this.host.getModifiedResourceFiles() || null;
}
if (oldProgram === null) {
this.incrementalDriver = IncrementalDriver.fresh(tsProgram);
} else {
const oldDriver = getIncrementalDriver(oldProgram);
if (oldDriver !== null) {
this.incrementalDriver =
IncrementalDriver.reconcile(oldProgram, oldDriver, tsProgram, modifiedResourceFiles);
} else {
// A previous ts.Program was used to create the current one, but it wasn't from an
// `NgCompiler`. That doesn't hurt anything, but the Angular analysis will have to start
// from a fresh state.
this.incrementalDriver = IncrementalDriver.fresh(tsProgram);
}
}
setIncrementalDriver(tsProgram, this.incrementalDriver);
}
/**
* Get all Angular-related diagnostics for this compilation.
*
* If a `ts.SourceFile` is passed, only diagnostics related to that file are returned.
*/
getDiagnostics(file?: ts.SourceFile): ts.Diagnostic[] {
if (this.diagnostics === null) {
const compilation = this.ensureAnalyzed();
this.diagnostics =
[...compilation.traitCompiler.diagnostics, ...this.getTemplateDiagnostics()];
if (this.entryPoint !== null && compilation.exportReferenceGraph !== null) {
this.diagnostics.push(...checkForPrivateExports(
this.entryPoint, this.tsProgram.getTypeChecker(), compilation.exportReferenceGraph));
}
}
if (file === undefined) {
return this.diagnostics;
} else {
return this.diagnostics.filter(diag => {
if (diag.file === file) {
return true;
} else if (isTemplateDiagnostic(diag) && diag.componentFile === file) {
// Template diagnostics are reported when diagnostics for the component file are
// requested (since no consumer of `getDiagnostics` would ever ask for diagnostics from
// the fake ts.SourceFile for templates).
return true;
} else {
return false;
}
});
}
}
/**
* Get all setup-related diagnostics for this compilation.
*/
getOptionDiagnostics(): ts.Diagnostic[] { return this.constructionDiagnostics; }
/**
* Get the `ts.Program` to use as a starting point when spawning a subsequent incremental
* compilation.
*
* The `NgCompiler` spawns an internal incremental TypeScript compilation (inheriting the
* consumer's `ts.Program` into a new one for the purposes of template type-checking). After this
* operation, the consumer's `ts.Program` is no longer usable for starting a new incremental
* compilation. `getNextProgram` retrieves the `ts.Program` which can be used instead.
*/
getNextProgram(): ts.Program { return this.nextProgram; }
/**
* Perform Angular's analysis step (as a precursor to `getDiagnostics` or `prepareEmit`)
* asynchronously.
*
* Normally, this operation happens lazily whenever `getDiagnostics` or `prepareEmit` are called.
* However, certain consumers may wish to allow for an asynchronous phase of analysis, where
* resources such as `styleUrls` are resolved asynchonously. In these cases `analyzeAsync` must be
* called first, and its `Promise` awaited prior to calling any other APIs of `NgCompiler`.
*/
async analyzeAsync(): Promise<void> {
if (this.compilation !== null) {
return;
}
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.traitCompiler.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.traitCompiler);
}
/**
* List lazy routes detected during analysis.
*
* This can be called for one specific route, or to retrieve all top-level routes.
*/
listLazyRoutes(entryRoute?: string): 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);
}
}
const compilation = this.ensureAnalyzed();
return compilation.routeAnalyzer.listLazyRoutes(entryRoute);
}
/**
* Fetch transformers and other information which is necessary for a consumer to `emit` the
* program with Angular-added definitions.
*/
prepareEmit(): {
transformers: ts.CustomTransformers,
ignoreFiles: Set<ts.SourceFile>,
} {
const compilation = this.ensureAnalyzed();
const coreImportsFrom = compilation.isCore ? getR3SymbolsFile(this.tsProgram) : null;
let importRewriter: ImportRewriter;
if (coreImportsFrom !== null) {
importRewriter = new R3SymbolsImportRewriter(coreImportsFrom.fileName);
} else {
importRewriter = new NoopImportRewriter();
}
const before = [
ivyTransformFactory(
compilation.traitCompiler, compilation.reflector, importRewriter,
compilation.defaultImportTracker, compilation.isCore, this.closureCompilerEnabled),
aliasTransformFactory(compilation.traitCompiler.exportStatements),
compilation.defaultImportTracker.importPreservingTransformer(),
];
const afterDeclarations: ts.TransformerFactory<ts.SourceFile>[] = [];
if (compilation.dtsTransforms !== null) {
afterDeclarations.push(
declarationTransformFactory(compilation.dtsTransforms, importRewriter));
}
// Only add aliasing re-exports to the .d.ts output if the `AliasingHost` requests it.
if (compilation.aliasingHost !== null && compilation.aliasingHost.aliasExportsInDts) {
afterDeclarations.push(aliasTransformFactory(compilation.traitCompiler.exportStatements));
}
if (this.host.factoryTracker !== null) {
before.push(generatedFactoryTransform(this.host.factoryTracker.sourceInfo, importRewriter));
}
before.push(ivySwitchTransform);
const ignoreFiles = new Set<ts.SourceFile>([this.typeCheckFile]);
return {transformers: {before, afterDeclarations} as ts.CustomTransformers, ignoreFiles};
}
/**
* Run the indexing process and return a `Map` of all indexed components.
*
* See the `indexing` package for more details.
*/
getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
const compilation = this.ensureAnalyzed();
const context = new IndexingContext();
compilation.traitCompiler.index(context);
return generateAnalysis(context);
}
private ensureAnalyzed(this: NgCompiler): LazyCompilationState {
if (this.compilation === null) {
this.analyzeSync();
}
return this.compilation !;
}
private analyzeSync(): void {
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.traitCompiler.analyzeSync(sf);
this.scanForMwp(sf);
this.perfRecorder.stop(analyzeFileSpan);
}
this.perfRecorder.stop(analyzeSpan);
this.resolveCompilation(this.compilation.traitCompiler);
}
private resolveCompilation(traitCompiler: TraitCompiler): void {
traitCompiler.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(traitCompiler);
}
private getTemplateDiagnostics(): ReadonlyArray<ts.Diagnostic> {
const host = this.host;
// 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, compilation.refEmitter !, compilation.reflector, host.typeCheckFile);
compilation.traitCompiler.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);
setIncrementalDriver(program, this.incrementalDriver);
this.nextProgram = program;
return diagnostics;
}
/**
* 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.compilation !.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.compilation !.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);
}
private scanForMwp(sf: ts.SourceFile): void {
this.compilation !.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.compilation !.dtsTransforms !.getReturnTypeTransform(sf).addTypeReplacement(
node, type);
}
});
}
private makeCompilation(): LazyCompilationState {
const checker = this.tsProgram.getTypeChecker();
const reflector = new TypeScriptReflectionHost(checker);
// Construct the ReferenceEmitter.
let refEmitter: ReferenceEmitter;
let aliasingHost: AliasingHost|null = null;
if (this.host.unifiedModulesHost === 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(reflector, new LogicalFileSystem([...this.host.rootDirs]));
} else {
// Plain relative imports are all that's needed.
localImportStrategy = new RelativePathStrategy(reflector);
}
// The CompilerHost doesn't have fileNameToModuleName, so build an NPM-centric reference
// resolution strategy.
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, 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.
aliasingHost = new PrivateExportAliasingHost(reflector);
}
} else {
// The CompilerHost supports fileNameToModuleName, so use that to emit imports.
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 UnifiedModulesStrategy(reflector, this.host.unifiedModulesHost),
]);
aliasingHost = new UnifiedModulesAliasingHost(this.host.unifiedModulesHost);
}
const evaluator = new PartialEvaluator(reflector, checker, this.incrementalDriver.depGraph);
const dtsReader = new DtsMetadataReader(checker, reflector);
const localMetaRegistry = new LocalMetadataRegistry();
const localMetaReader: MetadataReader = localMetaRegistry;
const depScopeReader = new MetadataDtsModuleScopeResolver(dtsReader, aliasingHost);
const scopeRegistry =
new LocalModuleScopeRegistry(localMetaReader, depScopeReader, refEmitter, aliasingHost);
const scopeReader: ComponentScopeReader = scopeRegistry;
const metaRegistry = new CompoundMetadataRegistry([localMetaRegistry, scopeRegistry]);
const injectableRegistry = new InjectableClassRegistry(reflector);
const 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;
let exportReferenceGraph: ReferenceGraph|null = null;
if (this.entryPoint !== null) {
exportReferenceGraph = new ReferenceGraph();
referencesRegistry = new ReferenceGraphAdapter(exportReferenceGraph);
} else {
referencesRegistry = new NoopReferencesRegistry();
}
const routeAnalyzer = new NgModuleRouteAnalyzer(this.moduleResolver, evaluator);
const dtsTransforms = new DtsTransformRegistry();
const mwpScanner = new ModuleWithProvidersScanner(reflector, evaluator, refEmitter);
const isCore = isAngularCorePackage(this.tsProgram);
const defaultImportTracker = new DefaultImportTracker();
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers: DecoratorHandler<unknown, unknown, unknown>[] = [
new ComponentDecoratorHandler(
reflector, evaluator, metaRegistry, metaReader, scopeReader, scopeRegistry, isCore,
this.resourceManager, this.host.rootDirs, this.options.preserveWhitespaces || false,
this.options.i18nUseExternalIds !== false,
this.options.enableI18nLegacyMessageIdFormat !== false, this.moduleResolver,
this.cycleAnalyzer, refEmitter, 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(
reflector, evaluator, metaRegistry, scopeRegistry, metaReader,
defaultImportTracker, injectableRegistry, 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(
reflector, evaluator, metaRegistry, scopeRegistry, defaultImportTracker,
injectableRegistry, isCore),
new InjectableDecoratorHandler(
reflector, defaultImportTracker, isCore, this.options.strictInjectionParameters || false,
injectableRegistry),
new NgModuleDecoratorHandler(
reflector, evaluator, metaReader, metaRegistry, scopeRegistry, referencesRegistry, isCore,
routeAnalyzer, refEmitter, this.host.factoryTracker, defaultImportTracker,
this.closureCompilerEnabled, injectableRegistry, this.options.i18nInLocale),
];
const traitCompiler = new TraitCompiler(
handlers, reflector, this.perfRecorder, this.incrementalDriver,
this.options.compileNonExportedClasses !== false, dtsTransforms);
return {
isCore, traitCompiler, reflector, scopeRegistry,
dtsTransforms, exportReferenceGraph, routeAnalyzer, mwpScanner,
metaReader, defaultImportTracker, aliasingHost, refEmitter,
};
}
}
/**
* 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;
});
});
}
/**
* 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;
}
/**
* Symbol under which the `IncrementalDriver` is stored on a `ts.Program`.
*
* The TS model of incremental compilation is based around reuse of a previous `ts.Program` in the
* construction of a new one. The `NgCompiler` follows this abstraction - passing in a previous
* `ts.Program` is sufficient to trigger incremental compilation. This previous `ts.Program` need
* not be from an Angular compilation (that is, it need not have been created from `NgCompiler`).
*
* If it is, though, Angular can benefit from reusing previous analysis work. This reuse is managed
* by the `IncrementalDriver`, which is inherited from the old program to the new program. To
* support this behind the API of passing an old `ts.Program`, the `IncrementalDriver` is stored on
* the `ts.Program` under this symbol.
*/
const SYM_INCREMENTAL_DRIVER = Symbol('NgIncrementalDriver');
/**
* Get an `IncrementalDriver` from the given `ts.Program` if one is present.
*
* See `SYM_INCREMENTAL_DRIVER` for more details.
*/
function getIncrementalDriver(program: ts.Program): IncrementalDriver|null {
const driver = (program as any)[SYM_INCREMENTAL_DRIVER];
if (driver === undefined || !(driver instanceof IncrementalDriver)) {
return null;
}
return driver;
}
/**
* Save the given `IncrementalDriver` onto the given `ts.Program`, for retrieval in a subsequent
* incremental compilation.
*
* See `SYM_INCREMENTAL_DRIVER` for more details.
*/
function setIncrementalDriver(program: ts.Program, driver: IncrementalDriver): void {
(program as any)[SYM_INCREMENTAL_DRIVER] = driver;
}
/**
* 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: NgCompilerOptions): 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;
}
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);
}
}
}
}

View File

@ -0,0 +1,246 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../diagnostics';
import {FlatIndexGenerator, findFlatIndexEntryPoint} from '../../entry_point';
import {AbsoluteFsPath, resolve} from '../../file_system';
import {FactoryGenerator, FactoryTracker, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator} from '../../shims';
import {typeCheckFilePath} from '../../typecheck';
import {normalizeSeparators} from '../../util/src/path';
import {getRootDirs} from '../../util/src/typescript';
import {ExtendedTsCompilerHost, NgCompilerOptions, UnifiedModulesHost} from '../api';
// A persistent source of bugs in CompilerHost delegation has been the addition by TS of new,
// optional methods on ts.CompilerHost. Since these methods are optional, it's not a type error that
// the delegating host doesn't implement or delegate them. This causes subtle runtime failures. No
// more. This infrastructure ensures that failing to delegate a method is a compile-time error.
/**
* Represents the `ExtendedTsCompilerHost` interface, with a transformation applied that turns all
* methods (even optional ones) into required fields (which may be `undefined`, if the method was
* optional).
*/
export type RequiredCompilerHostDelegations = {
[M in keyof Required<ExtendedTsCompilerHost>]: ExtendedTsCompilerHost[M];
};
/**
* Delegates all methods of `ExtendedTsCompilerHost` to a delegate, with the exception of
* `getSourceFile` and `fileExists` which are implemented in `NgCompilerHost`.
*
* If a new method is added to `ts.CompilerHost` which is not delegated, a type error will be
* generated for this class.
*/
export class DelegatingCompilerHost implements
Omit<RequiredCompilerHostDelegations, 'getSourceFile'|'fileExists'> {
constructor(protected delegate: ExtendedTsCompilerHost) {}
private delegateMethod<M extends keyof ExtendedTsCompilerHost>(name: M):
ExtendedTsCompilerHost[M] {
return this.delegate[name] !== undefined ? (this.delegate[name] as any).bind(this.delegate) :
undefined;
}
// Excluded are 'getSourceFile' and 'fileExists', which are actually implemented by NgCompilerHost
// below.
createHash = this.delegateMethod('createHash');
directoryExists = this.delegateMethod('directoryExists');
fileNameToModuleName = this.delegateMethod('fileNameToModuleName');
getCancellationToken = this.delegateMethod('getCancellationToken');
getCanonicalFileName = this.delegateMethod('getCanonicalFileName');
getCurrentDirectory = this.delegateMethod('getCurrentDirectory');
getDefaultLibFileName = this.delegateMethod('getDefaultLibFileName');
getDefaultLibLocation = this.delegateMethod('getDefaultLibLocation');
getDirectories = this.delegateMethod('getDirectories');
getEnvironmentVariable = this.delegateMethod('getEnvironmentVariable');
getModifiedResourceFiles = this.delegateMethod('getModifiedResourceFiles');
getNewLine = this.delegateMethod('getNewLine');
getParsedCommandLine = this.delegateMethod('getParsedCommandLine');
getSourceFileByPath = this.delegateMethod('getSourceFileByPath');
readDirectory = this.delegateMethod('readDirectory');
readFile = this.delegateMethod('readFile');
readResource = this.delegateMethod('readResource');
realpath = this.delegateMethod('realpath');
resolveModuleNames = this.delegateMethod('resolveModuleNames');
resolveTypeReferenceDirectives = this.delegateMethod('resolveTypeReferenceDirectives');
resourceNameToFileName = this.delegateMethod('resourceNameToFileName');
trace = this.delegateMethod('trace');
useCaseSensitiveFileNames = this.delegateMethod('useCaseSensitiveFileNames');
writeFile = this.delegateMethod('writeFile');
}
/**
* A wrapper around `ts.CompilerHost` (plus any extension methods from `ExtendedTsCompilerHost`).
*
* In order for a consumer to include Angular compilation in their TypeScript compiler, the
* `ts.Program` must be created with a host that adds Angular-specific files (e.g. factories,
* summaries, the template type-checking file, etc) to the compilation. `NgCompilerHost` is the
* host implementation which supports this.
*
* The interface implementations here ensure that `NgCompilerHost` fully delegates to
* `ExtendedTsCompilerHost` methods whenever present.
*/
export class NgCompilerHost extends DelegatingCompilerHost implements
RequiredCompilerHostDelegations,
ExtendedTsCompilerHost {
readonly factoryTracker: FactoryTracker|null = null;
readonly entryPoint: AbsoluteFsPath|null = null;
readonly diagnostics: ts.Diagnostic[];
readonly inputFiles: ReadonlyArray<string>;
readonly rootDirs: ReadonlyArray<AbsoluteFsPath>;
readonly typeCheckFile: AbsoluteFsPath;
constructor(
delegate: ExtendedTsCompilerHost, inputFiles: ReadonlyArray<string>,
rootDirs: ReadonlyArray<AbsoluteFsPath>, private shims: ShimGenerator[],
entryPoint: AbsoluteFsPath|null, typeCheckFile: AbsoluteFsPath,
factoryTracker: FactoryTracker|null, diagnostics: ts.Diagnostic[]) {
super(delegate);
this.factoryTracker = factoryTracker;
this.entryPoint = entryPoint;
this.typeCheckFile = typeCheckFile;
this.diagnostics = diagnostics;
this.inputFiles = inputFiles;
this.rootDirs = rootDirs;
}
/**
* Create an `NgCompilerHost` from a delegate host, an array of input filenames, and the full set
* of TypeScript and Angular compiler options.
*/
static wrap(
delegate: ts.CompilerHost, inputFiles: ReadonlyArray<string>,
options: NgCompilerOptions): NgCompilerHost {
// 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;
let rootFiles = [...inputFiles];
let normalizedInputFiles = inputFiles.map(n => resolve(n));
const generators: ShimGenerator[] = [];
let summaryGenerator: SummaryGenerator|null = null;
if (shouldGenerateSummaryShims) {
// Summary generation.
summaryGenerator = SummaryGenerator.forRootFiles(normalizedInputFiles);
generators.push(summaryGenerator);
}
let factoryTracker: FactoryTracker|null = null;
if (shouldGenerateFactoryShims) {
// Factory generation.
const factoryGenerator = FactoryGenerator.forRootFiles(normalizedInputFiles);
const factoryFileMap = factoryGenerator.factoryFileMap;
const factoryFileNames = Array.from(factoryFileMap.keys());
rootFiles.push(...factoryFileNames);
generators.push(factoryGenerator);
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 (summaryGenerator !== null) {
rootFiles.push(...summaryGenerator.getSummaryFileNames());
}
const rootDirs = getRootDirs(delegate, options as ts.CompilerOptions);
const typeCheckFile = typeCheckFilePath(rootDirs);
generators.push(new TypeCheckShimGenerator(typeCheckFile));
rootFiles.push(typeCheckFile);
let diagnostics: ts.Diagnostic[] = [];
let entryPoint: AbsoluteFsPath|null = null;
if (options.flatModuleOutFile != null && options.flatModuleOutFile !== '') {
entryPoint = findFlatIndexEntryPoint(normalizedInputFiles);
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.
diagnostics.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);
const flatIndexGenerator =
new FlatIndexGenerator(entryPoint, flatModuleOutFile, flatModuleId);
generators.push(flatIndexGenerator);
rootFiles.push(flatIndexGenerator.flatIndexPath);
}
}
return new NgCompilerHost(
delegate, rootFiles, rootDirs, generators, entryPoint, typeCheckFile, factoryTracker,
diagnostics);
}
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
for (let i = 0; i < this.shims.length; i++) {
const generator = this.shims[i];
// TypeScript internal paths are guaranteed to be POSIX-like absolute file paths.
const absoluteFsPath = resolve(fileName);
if (generator.recognize(absoluteFsPath)) {
const readFile = (originalFile: string) => {
return this.delegate.getSourceFile(
originalFile, languageVersion, onError, shouldCreateNewSourceFile) ||
null;
};
return generator.generate(absoluteFsPath, readFile) || undefined;
}
}
return this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile);
}
fileExists(fileName: string): boolean {
// Consider the file as existing whenever
// 1) it really does exist in the delegate host, or
// 2) at least one of the shim generators recognizes it
// Note that we can pass the file name as branded absolute fs path because TypeScript
// internally only passes POSIX-like paths.
return this.delegate.fileExists(fileName) ||
this.shims.some(shim => shim.recognize(resolve(fileName)));
}
get unifiedModulesHost(): UnifiedModulesHost|null {
return this.fileNameToModuleName !== undefined ? this as UnifiedModulesHost : null;
}
}

View File

@ -0,0 +1,27 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/core",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"@npm//typescript",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["//tools/testing:node_no_angular_es5"],
deps = [
":test_lib",
],
)

View File

@ -0,0 +1,56 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
import {FileSystem, NgtscCompilerHost, absoluteFrom as _, getFileSystem, getSourceFileOrError, setFileSystem} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {NgCompilerOptions} from '../api';
import {NgCompiler} from '../src/compiler';
import {NgCompilerHost} from '../src/host';
runInEachFileSystem(() => {
describe('NgCompiler', () => {
let fs: FileSystem;
beforeEach(() => {
fs = getFileSystem();
fs.ensureDir(_('/node_modules/@angular/core'));
fs.writeFile(_('/node_modules/@angular/core/index.d.ts'), `
export declare const Component: any;
`);
});
it('should also return template diagnostics when asked for component diagnostics', () => {
const COMPONENT = _('/cmp.ts');
fs.writeFile(COMPONENT, `
import {Component} from '@angular/core';
@Component({
selector: 'test-cmp',
templateUrl: './template.html',
})
export class Cmp {}
`);
fs.writeFile(_('/template.html'), `{{does_not_exist.foo}}`);
const options: NgCompilerOptions = {
strictTemplates: true,
};
const baseHost = new NgtscCompilerHost(getFileSystem(), options);
const host = NgCompilerHost.wrap(baseHost, [COMPONENT], options);
const program = ts.createProgram({host, options, rootNames: host.inputFiles});
const compiler = new NgCompiler(host, options, program);
const diags = compiler.getDiagnostics(getSourceFileOrError(program, COMPONENT));
expect(diags.length).toBe(1);
expect(diags[0].messageText).toContain('does_not_exist');
});
});
});

View File

@ -10,6 +10,7 @@ ts_library(
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection", "//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/src/ngtsc/util",

View File

@ -18,9 +18,9 @@ It's important to note that this logic is transitive. If the user instead import
This logic of course breaks down for non-Angular Package Format libraries, such as "internal" libraries within a monorepo, which frequently don't use `index.ts` files or entrypoints. In this case, the user will likely import NgModules directly from their declaration (e.g. via a 'lib/module' specifier), and the compiler cannot simply assume that the user has exported all of the directives/pipes from the NgModule via this same specifier. In this case a compiler feature called "aliasing" kicks in (see below) and generates private exports from the NgModule file. This logic of course breaks down for non-Angular Package Format libraries, such as "internal" libraries within a monorepo, which frequently don't use `index.ts` files or entrypoints. In this case, the user will likely import NgModules directly from their declaration (e.g. via a 'lib/module' specifier), and the compiler cannot simply assume that the user has exported all of the directives/pipes from the NgModule via this same specifier. In this case a compiler feature called "aliasing" kicks in (see below) and generates private exports from the NgModule file.
2. Using a `FileToModuleHost` 2. Using a `UnifiedModulesHost`
The `ts.CompilerHost` given to the compiler may optionally implement an interface called `FileToModuleHost`, which allows an absolute module specifier to be generated for any file. If a `FileToModuleHost` is present, the compiler will attempt to directly import all directives and pipes from the file which declares them, instead of going via the specifier of the NgModule as in the first mode described above. This logic is used internally in the Google monorepo. The `ts.CompilerHost` given to the compiler may optionally implement an interface called `UnifiedModulesHost`, which allows an absolute module specifier to be generated for any file. If a `UnifiedModulesHost` is present, the compiler will attempt to directly import all directives and pipes from the file which declares them, instead of going via the specifier of the NgModule as in the first mode described above. This logic is used internally in the Google monorepo.
This approach comes with a significant caveat: the build system may prevent importing from files which are not directly declared dependencies of the current compilation (this is known as "strict dependency checking"). This is a problem when attempting to consume a re-exported directive. For example, if the user depends only on '@angular/platform-browser', imports `BrowserModule` from '@angular/platform-browser' and attempts to use the re-exported `NgIf`, the compiler cannot import `NgIf` directly from its declaration within '@angular/common', which is a transitive (but not direct) dependency. This approach comes with a significant caveat: the build system may prevent importing from files which are not directly declared dependencies of the current compilation (this is known as "strict dependency checking"). This is a problem when attempting to consume a re-exported directive. For example, if the user depends only on '@angular/platform-browser', imports `BrowserModule` from '@angular/platform-browser' and attempts to use the re-exported `NgIf`, the compiler cannot import `NgIf` directly from its declaration within '@angular/common', which is a transitive (but not direct) dependency.
@ -86,19 +86,19 @@ This `ReferenceEmitStrategy` uses the `bestGuessOwningModule` of a `Reference` t
Note that the `bestGuessOwningModule` only gives the module specifier for the import, not the symbol name. The user may have renamed the class as part of re-exporting it from an entrypoint, so the `AbsoluteModuleStrategy` searches the exports of the target module and finds the symbol name by which the class is re-exported, if it exists. Note that the `bestGuessOwningModule` only gives the module specifier for the import, not the symbol name. The user may have renamed the class as part of re-exporting it from an entrypoint, so the `AbsoluteModuleStrategy` searches the exports of the target module and finds the symbol name by which the class is re-exported, if it exists.
### `FileToModuleStrategy` ### `UnifiedModulesStrategy`
This `ReferenceEmitStrategy` uses a `FileToModuleHost` to implement the major import mode #2 described at the beginning of this document. This `ReferenceEmitStrategy` uses a `UnifiedModulesHost` to implement the major import mode #2 described at the beginning of this document.
Under this strategy, direct imports to referenced classes are constructed using globally valid absolute module specifiers determined by the `FileToModuleHost`. Under this strategy, direct imports to referenced classes are constructed using globally valid absolute module specifiers determined by the `UnifiedModulesHost`.
Like with `AbsoluteModuleStrategy`, the `FileToModuleHost` only gives the module specifier and not the symbol name, so an appropriate symbol name must be determined by searching the exports of the module. Like with `AbsoluteModuleStrategy`, the `UnifiedModulesHost` only gives the module specifier and not the symbol name, so an appropriate symbol name must be determined by searching the exports of the module.
### `AliasStrategy` ### `AliasStrategy`
The `AliasStrategy` will choose the alias `Expression` of a `Reference`. This strategy is used before the `FileToModuleStrategy` to guarantee aliases are preferred to direct imports when available. The `AliasStrategy` will choose the alias `Expression` of a `Reference`. This strategy is used before the `UnifiedModulesStrategy` to guarantee aliases are preferred to direct imports when available.
See the description of aliasing in the case of `FileToModuleAliasingHost` below. See the description of aliasing in the case of `UnifiedModulesAliasingHost` below.
## Aliasing and re-exports ## Aliasing and re-exports
@ -120,14 +120,14 @@ Because the first import of an NgModule from a user library to a `.d.ts` is alwa
Aliasing is currently used in two cases: Aliasing is currently used in two cases:
1. To address strict dependency checking issues when using a `FileToModuleHost`. 1. To address strict dependency checking issues when using a `UnifiedModulesHost`.
2. To support dependening on non-Angular Package Format packages (e.g. private libraries in monorepos) which do not have an entrypoint file through which all directives/pipes/modules are exported. 2. To support dependening on non-Angular Package Format packages (e.g. private libraries in monorepos) which do not have an entrypoint file through which all directives/pipes/modules are exported.
In environments with "strict dependency checking" as described above, an NgModule which exports another NgModule from one of its dependencies needs to export its directives/pipes as well, in order to make them available to the downstream compiler. In environments with "strict dependency checking" as described above, an NgModule which exports another NgModule from one of its dependencies needs to export its directives/pipes as well, in order to make them available to the downstream compiler.
### Aliasing under `FileToModuleHost` ### Aliasing under `UnifiedModulesHost`
A `FileToModuleAliasingHost` implements `AliasingHost` and makes full use of the aliasing system in the case of a `FileToModuleHost`. A `UnifiedModulesAliasingHost` implements `AliasingHost` and makes full use of the aliasing system in the case of a `UnifiedModulesHost`.
When compiling an NgModule, re-exports are added under a stable name for each directive/pipe that's re-exported by the NgModule. When compiling an NgModule, re-exports are added under a stable name for each directive/pipe that's re-exported by the NgModule.

View File

@ -6,10 +6,10 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
export {AliasStrategy, AliasingHost, FileToModuleAliasingHost, PrivateExportAliasingHost} from './src/alias'; export {AliasStrategy, AliasingHost, PrivateExportAliasingHost, UnifiedModulesAliasingHost} from './src/alias';
export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core';
export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default'; export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default';
export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy} from './src/emitter'; export {AbsoluteModuleStrategy, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy, UnifiedModulesStrategy} from './src/emitter';
export {Reexport} from './src/reexport'; export {Reexport} from './src/reexport';
export {OwningModule, Reference} from './src/references'; export {OwningModule, Reference} from './src/references';
export {ModuleResolver} from './src/resolver'; export {ModuleResolver} from './src/resolver';

View File

@ -9,12 +9,14 @@
import {Expression, ExternalExpr} from '@angular/compiler'; import {Expression, ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {FileToModuleHost, ImportFlags, ReferenceEmitStrategy} from './emitter'; import {ImportFlags, ReferenceEmitStrategy} from './emitter';
import {Reference} from './references'; import {Reference} from './references';
// Escape anything that isn't alphanumeric, '/' or '_'. // Escape anything that isn't alphanumeric, '/' or '_'.
const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g; const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g;
@ -33,8 +35,8 @@ const CHARS_TO_ESCAPE = /[^a-zA-Z0-9/_]/g;
* *
* 1) It can be used to create "alias" re-exports from different files, which can be used when the * 1) It can be used to create "alias" re-exports from different files, which can be used when the
* user hasn't exported the directive(s) from the ES module containing the NgModule. These re- * user hasn't exported the directive(s) from the ES module containing the NgModule. These re-
* exports can also be helpful when using a `FileToModuleHost`, which overrides the import logic * exports can also be helpful when using a `UnifiedModulesHost`, which overrides the import
* described above. * logic described above.
* *
* 2) It can be used to get an alternative import expression for a directive or pipe, instead of * 2) It can be used to get an alternative import expression for a directive or pipe, instead of
* the import that the normal logic would apply. The alias used depends on the provenance of the * the import that the normal logic would apply. The alias used depends on the provenance of the
@ -85,16 +87,16 @@ export interface AliasingHost {
/** /**
* An `AliasingHost` which generates and consumes alias re-exports when module names for each file * An `AliasingHost` which generates and consumes alias re-exports when module names for each file
* are determined by a `FileToModuleHost`. * are determined by a `UnifiedModulesHost`.
* *
* When using a `FileToModuleHost`, aliasing prevents issues with transitive dependencies. See the * When using a `UnifiedModulesHost`, aliasing prevents issues with transitive dependencies. See the
* README.md for more details. * README.md for more details.
*/ */
export class FileToModuleAliasingHost implements AliasingHost { export class UnifiedModulesAliasingHost implements AliasingHost {
constructor(private fileToModuleHost: FileToModuleHost) {} constructor(private unifiedModulesHost: UnifiedModulesHost) {}
/** /**
* With a `FileToModuleHost`, aliases are chosen automatically without the need to look through * With a `UnifiedModulesHost`, aliases are chosen automatically without the need to look through
* the exports present in a .d.ts file, so we can avoid cluttering the .d.ts files. * the exports present in a .d.ts file, so we can avoid cluttering the .d.ts files.
*/ */
readonly aliasExportsInDts = false; readonly aliasExportsInDts = false;
@ -103,7 +105,8 @@ export class FileToModuleAliasingHost implements AliasingHost {
ref: Reference<ClassDeclaration>, context: ts.SourceFile, ngModuleName: string, ref: Reference<ClassDeclaration>, context: ts.SourceFile, ngModuleName: string,
isReExport: boolean): string|null { isReExport: boolean): string|null {
if (!isReExport) { if (!isReExport) {
// Aliasing is used with a FileToModuleHost to prevent transitive dependencies. Thus, aliases // Aliasing is used with a UnifiedModulesHost to prevent transitive dependencies. Thus,
// aliases
// only need to be created for directives/pipes which are not direct declarations of an // only need to be created for directives/pipes which are not direct declarations of an
// NgModule which exports them. // NgModule which exports them.
return null; return null;
@ -122,7 +125,7 @@ export class FileToModuleAliasingHost implements AliasingHost {
return null; return null;
} }
// viaModule is the module it'll actually be imported from. // viaModule is the module it'll actually be imported from.
const moduleName = this.fileToModuleHost.fileNameToModuleName(via.fileName, via.fileName); const moduleName = this.unifiedModulesHost.fileNameToModuleName(via.fileName, via.fileName);
return new ExternalExpr({moduleName, name: this.aliasName(decl, via)}); return new ExternalExpr({moduleName, name: this.aliasName(decl, via)});
} }
@ -132,8 +135,8 @@ export class FileToModuleAliasingHost implements AliasingHost {
*/ */
private aliasName(decl: ClassDeclaration, context: ts.SourceFile): string { private aliasName(decl: ClassDeclaration, context: ts.SourceFile): string {
// The declared module is used to get the name of the alias. // The declared module is used to get the name of the alias.
const declModule = const declModule = this.unifiedModulesHost.fileNameToModuleName(
this.fileToModuleHost.fileNameToModuleName(decl.getSourceFile().fileName, context.fileName); decl.getSourceFile().fileName, context.fileName);
const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$'); const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$');
return 'ɵng$' + replaced + '$$' + decl.name.text; return 'ɵng$' + replaced + '$$' + decl.name.text;

View File

@ -8,6 +8,7 @@
import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler'; import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {LogicalFileSystem, LogicalProjectPath, PathSegment, absoluteFromSourceFile, dirname, relative} from '../../file_system'; import {LogicalFileSystem, LogicalProjectPath, PathSegment, absoluteFromSourceFile, dirname, relative} from '../../file_system';
import {stripExtension} from '../../file_system/src/util'; import {stripExtension} from '../../file_system/src/util';
import {ReflectionHost} from '../../reflection'; import {ReflectionHost} from '../../reflection';
@ -18,17 +19,6 @@ import {Reference} from './references';
import {ModuleResolver} from './resolver'; import {ModuleResolver} from './resolver';
/**
* A host which supports an operation to convert a file name into a module name.
*
* This operation is typically implemented as part of the compiler host passed to ngtsc when running
* under a build tool like Bazel or Blaze.
*/
export interface FileToModuleHost {
fileNameToModuleName(importedFilePath: string, containingFilePath: string): string;
}
/** /**
* Flags which alter the imports generated by the `ReferenceEmitter`. * Flags which alter the imports generated by the `ReferenceEmitter`.
*/ */
@ -277,10 +267,11 @@ export class RelativePathStrategy implements ReferenceEmitStrategy {
} }
/** /**
* A `ReferenceEmitStrategy` which uses a `FileToModuleHost` to generate absolute import references. * A `ReferenceEmitStrategy` which uses a `UnifiedModulesHost` to generate absolute import
* references.
*/ */
export class FileToModuleStrategy implements ReferenceEmitStrategy { export class UnifiedModulesStrategy implements ReferenceEmitStrategy {
constructor(private reflector: ReflectionHost, private fileToModuleHost: FileToModuleHost) {} constructor(private reflector: ReflectionHost, private unifiedModulesHost: UnifiedModulesHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null { emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null {
const destSf = getSourceFile(ref.node); const destSf = getSourceFile(ref.node);
@ -290,7 +281,7 @@ export class FileToModuleStrategy implements ReferenceEmitStrategy {
} }
const moduleName = const moduleName =
this.fileToModuleHost.fileNameToModuleName(destSf.fileName, context.fileName); this.unifiedModulesHost.fileNameToModuleName(destSf.fileName, context.fileName);
return new ExternalExpr({moduleName, name}); return new ExternalExpr({moduleName, name});
} }

View File

@ -8,3 +8,4 @@
export * from './src/api'; export * from './src/api';
export {IndexingContext} from './src/context'; export {IndexingContext} from './src/context';
export {generateAnalysis} from './src/transform';

View File

@ -6,371 +6,147 @@
* found in the LICENSE file at https://angular.io/license * 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 ts from 'typescript';
import * as api from '../transformers/api'; import * as api from '../transformers/api';
import {nocollapseHack} from '../transformers/nocollapse_hack'; import {nocollapseHack} from '../transformers/nocollapse_hack';
import {verifySupportedTypeScriptVersion} from '../typescript_support'; import {verifySupportedTypeScriptVersion} from '../typescript_support';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ReferencesRegistry} from './annotations'; import {NgCompilerHost} from './core';
import {CycleAnalyzer, ImportGraph} from './cycles'; import {NgCompilerOptions} from './core/api';
import {ErrorCode, ngErrorCode} from './diagnostics'; import {NgCompiler} from './core/src/compiler';
import {FlatIndexGenerator, ReferenceGraph, checkForPrivateExports, findFlatIndexEntryPoint} from './entry_point'; import {IndexedComponent} from './indexer';
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 {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf'; 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 { export class NgtscProgram implements api.Program {
private compiler: NgCompiler;
/**
* The primary TypeScript program, which is used for analysis and emit.
*/
private tsProgram: ts.Program; 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 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 closureCompilerEnabled: boolean;
private entryPoint: ts.SourceFile|null; private host: NgCompilerHost;
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 perfRecorder: PerfRecorder = NOOP_PERF_RECORDER; private perfRecorder: PerfRecorder = NOOP_PERF_RECORDER;
private perfTracker: PerfTracker|null = null; 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( constructor(
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions, rootNames: ReadonlyArray<string>, private options: NgCompilerOptions,
private host: api.CompilerHost, oldProgram?: NgtscProgram) { delegateHost: api.CompilerHost, oldProgram?: NgtscProgram) {
// First, check whether the current TS version is supported.
if (!options.disableTypeScriptVersionCheck) { if (!options.disableTypeScriptVersionCheck) {
verifySupportedTypeScriptVersion(); verifySupportedTypeScriptVersion();
} }
const incompatibleTypeCheckOptionsDiagnostic = verifyCompatibleTypeCheckOptions(options); if (options.tracePerformance !== undefined) {
if (incompatibleTypeCheckOptionsDiagnostic !== null) {
this.constructionDiagnostics.push(incompatibleTypeCheckOptionsDiagnostic);
}
if (shouldEnablePerfTracing(options)) {
this.perfTracker = PerfTracker.zeroedToNow(); this.perfTracker = PerfTracker.zeroedToNow();
this.perfRecorder = this.perfTracker; this.perfRecorder = this.perfTracker;
} }
this.modifiedResourceFiles =
this.host.getModifiedResourceFiles && this.host.getModifiedResourceFiles() || null;
this.rootDirs = getRootDirs(host, options);
this.closureCompilerEnabled = !!options.annotateForClosureCompiler; 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[] = []; this.host = NgCompilerHost.wrap(delegateHost, rootNames, options);
let summaryGenerator: SummaryGenerator|null = null;
if (shouldGenerateSummaryShims) {
// Summary generation.
summaryGenerator = SummaryGenerator.forRootFiles(normalizedRootNames);
generators.push(summaryGenerator);
}
if (shouldGenerateFactoryShims) { const reuseProgram = oldProgram && oldProgram.reuseTsProgram;
// Factory generation. this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram);
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);
this.reuseTsProgram = this.tsProgram; this.reuseTsProgram = this.tsProgram;
this.entryPoint = entryPoint !== null ? getSourceFileOrNull(this.tsProgram, entryPoint) : null; // Create the NgCompiler which will drive the rest of the compilation.
const moduleResolutionCache = ts.createModuleResolutionCache( this.compiler =
this.host.getCurrentDirectory(), fileName => this.host.getCanonicalFileName(fileName)); new NgCompiler(this.host, options, this.tsProgram, reuseProgram, this.perfRecorder);
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);
}
} }
getTsProgram(): ts.Program { return this.tsProgram; } getTsProgram(): ts.Program { return this.tsProgram; }
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken| getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic> { undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getOptionsDiagnostics(cancellationToken); return this.tsProgram.getOptionsDiagnostics(cancellationToken);
} }
getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<ts.Diagnostic> {
return this.constructionDiagnostics;
}
getTsSyntacticDiagnostics( getTsSyntacticDiagnostics(
sourceFile?: ts.SourceFile|undefined, sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> { cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken); return this.tsProgram.getSyntacticDiagnostics(sourceFile, cancellationToken);
} }
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): ReadonlyArray<api.Diagnostic> {
return [];
}
getTsSemanticDiagnostics( getTsSemanticDiagnostics(
sourceFile?: ts.SourceFile|undefined, sourceFile?: ts.SourceFile|undefined,
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> { cancellationToken?: ts.CancellationToken|undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken); return this.tsProgram.getSemanticDiagnostics(sourceFile, cancellationToken);
} }
getNgSemanticDiagnostics( getNgOptionDiagnostics(cancellationToken?: ts.CancellationToken|
fileName?: string|undefined, undefined): readonly(ts.Diagnostic|api.Diagnostic)[] {
cancellationToken?: ts.CancellationToken|undefined): ReadonlyArray<ts.Diagnostic> { return this.compiler.getOptionDiagnostics();
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));
} }
getNgStructuralDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly api.Diagnostic[] {
return [];
}
getNgSemanticDiagnostics(
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; return diagnostics;
} }
async loadNgStructureAsync(): Promise<void> { /**
if (this.compilation === undefined) { * Ensure that the `NgCompiler` has properly analyzed the program, and allow for the asynchronous
this.compilation = this.makeCompilation(); * loading of any resources during the process.
} *
const analyzeSpan = this.perfRecorder.start('analyze'); * This is used by the Angular CLI to allow for spawning (async) child compilations for things
const promises: Promise<void>[] = []; * like SASS files used in `styleUrls`.
for (const sf of this.tsProgram.getSourceFiles()) { */
if (sf.isDeclarationFile) { loadNgStructureAsync(): Promise<void> { return this.compiler.analyzeAsync(); }
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);
}
listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] { listLazyRoutes(entryRoute?: string|undefined): api.LazyRoute[] {
if (entryRoute) { return this.compiler.listLazyRoutes(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);
} }
emit(opts?: { emit(opts?: {
emitFlags?: api.EmitFlags, emitFlags?: api.EmitFlags | undefined; cancellationToken?: ts.CancellationToken | undefined;
cancellationToken?: ts.CancellationToken, customTransformers?: api.CustomTransformers | undefined;
customTransformers?: api.CustomTransformers, emitCallback?: api.TsEmitCallback | undefined;
emitCallback?: api.TsEmitCallback, mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback | undefined;
mergeEmitResultsCallback?: api.TsMergeEmitResultsCallback }|undefined): ts.EmitResult {
}): ts.EmitResult { const {transformers, ignoreFiles} = this.compiler.prepareEmit();
const emitCallback = opts && opts.emitCallback || defaultEmitCallback; const emitCallback = opts && opts.emitCallback || defaultEmitCallback;
const compilation = this.ensureAnalyzed();
const writeFile: ts.WriteFileCallback = const writeFile: ts.WriteFileCallback =
(fileName: string, data: string, writeByteOrderMark: boolean, (fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void) | undefined, onError: ((message: string) => void) | undefined,
@ -383,9 +159,16 @@ export class NgtscProgram implements api.Program {
continue; 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')) { if (this.closureCompilerEnabled && fileName.endsWith('.js')) {
data = nocollapseHack(data); data = nocollapseHack(data);
} }
@ -393,46 +176,22 @@ export class NgtscProgram implements api.Program {
}; };
const customTransforms = opts && opts.customTransformers; const customTransforms = opts && opts.customTransformers;
const beforeTransforms = transformers.before || [];
const afterDeclarationsTransforms = transformers.afterDeclarations;
const beforeTransforms = [ if (customTransforms !== undefined && customTransforms.beforeTs !== undefined) {
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) {
beforeTransforms.push(...customTransforms.beforeTs); beforeTransforms.push(...customTransforms.beforeTs);
} }
const emitSpan = this.perfRecorder.start('emit'); const emitSpan = this.perfRecorder.start('emit');
const emitResults: ts.EmitResult[] = []; const emitResults: ts.EmitResult[] = [];
const typeCheckFile = getSourceFileOrNull(this.tsProgram, this.typeCheckFilePath);
for (const targetSourceFile of this.tsProgram.getSourceFiles()) { for (const targetSourceFile of this.tsProgram.getSourceFiles()) {
if (targetSourceFile.isDeclarationFile || targetSourceFile === typeCheckFile) { if (targetSourceFile.isDeclarationFile || ignoreFiles.has(targetSourceFile)) {
continue; continue;
} }
if (this.incrementalDriver.safeToSkipEmit(targetSourceFile)) { if (this.compiler.incrementalDriver.safeToSkipEmit(targetSourceFile)) {
continue; continue;
} }
@ -447,7 +206,7 @@ export class NgtscProgram implements api.Program {
before: beforeTransforms, before: beforeTransforms,
after: customTransforms && customTransforms.afterTs, after: customTransforms && customTransforms.afterTs,
afterDeclarations: afterDeclarationsTransforms, afterDeclarations: afterDeclarationsTransforms,
}, } as any,
})); }));
this.perfRecorder.stop(fileEmitSpan); this.perfRecorder.stop(fileEmitSpan);
} }
@ -461,323 +220,20 @@ export class NgtscProgram implements api.Program {
return ((opts && opts.mergeEmitResultsCallback) || mergeEmitResults)(emitResults); 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> { getIndexedComponents(): Map<ts.Declaration, IndexedComponent> {
const compilation = this.ensureAnalyzed(); return this.compiler.getIndexedComponents();
const context = new IndexingContext();
compilation.index(context);
return generateAnalysis(context);
} }
private makeCompilation(): TraitCompiler { getLibrarySummaries(): Map<string, api.LibrarySummary> {
const checker = this.tsProgram.getTypeChecker(); throw new Error('Method not implemented.');
// 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 getEmittedGeneratedFiles(): Map<string, GeneratedFile> {
// resolution strategy. throw new Error('Method not implemented.');
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 = getEmittedSourceFiles(): Map<string, ts.SourceFile> {
new PartialEvaluator(this.reflector, checker, this.incrementalDriver.depGraph); throw new Error('Method not implemented.');
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);
}
/**
* 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);
}
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;
} }
} }
@ -799,100 +255,3 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
return {diagnostics, emitSkipped, emittedFiles}; 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;
}

View File

@ -0,0 +1,19 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "resource",
srcs = ["index.ts"] + glob([
"src/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/resource",
deps = [
"//packages:types",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript",
],
)

View File

@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. 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
*/
export {HostResourceLoader} from './src/loader';

View File

@ -8,11 +8,10 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {CompilerHost} from '../transformers/api'; import {ResourceLoader} from '../../annotations';
import {ExtendedTsCompilerHost} from '../../core/api';
import {ResourceLoader} from './annotations'; import {AbsoluteFsPath, PathSegment, join} from '../../file_system';
import {AbsoluteFsPath, PathSegment, join} from './file_system'; import {getRootDirs} from '../../util/src/typescript';
import {getRootDirs} from './util/src/typescript';
const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/; const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/;
@ -27,7 +26,7 @@ export class HostResourceLoader implements ResourceLoader {
canPreload = !!this.host.readResource; canPreload = !!this.host.readResource;
constructor(private host: CompilerHost, private options: ts.CompilerOptions) { constructor(private host: ExtendedTsCompilerHost, private options: ts.CompilerOptions) {
this.rootDirs = getRootDirs(host, options); this.rootDirs = getRootDirs(host, options);
} }

View File

@ -11,6 +11,7 @@ ts_library(
deps = [ deps = [
"//packages:types", "//packages:types",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/file_system", "//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/file_system/testing", "//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",

View File

@ -8,9 +8,10 @@
import {ExternalExpr, ExternalReference} from '@angular/compiler'; import {ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {absoluteFrom} from '../../file_system'; import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing'; import {runInEachFileSystem} from '../../file_system/testing';
import {AliasingHost, FileToModuleAliasingHost, FileToModuleHost, Reference} from '../../imports'; import {AliasingHost, Reference, UnifiedModulesAliasingHost} from '../../imports';
import {DtsMetadataReader} from '../../metadata'; import {DtsMetadataReader} from '../../metadata';
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
import {makeProgram} from '../../testing'; import {makeProgram} from '../../testing';
@ -19,7 +20,7 @@ import {MetadataDtsModuleScopeResolver} from '../src/dependency';
const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/; const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/;
const testHost: FileToModuleHost = { const testHost: UnifiedModulesHost = {
fileNameToModuleName: function(imported: string): string { fileNameToModuleName: function(imported: string): string {
const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported) !; const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported) !;
return 'root/' + res[1]; return 'root/' + res[1];
@ -183,7 +184,7 @@ runInEachFileSystem(() => {
} }
`, `,
}, },
new FileToModuleAliasingHost(testHost)); new UnifiedModulesAliasingHost(testHost));
const {ShallowModule} = refs; const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !; const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
@ -233,7 +234,7 @@ runInEachFileSystem(() => {
} }
`, `,
}, },
new FileToModuleAliasingHost(testHost)); new UnifiedModulesAliasingHost(testHost));
const {ShallowModule} = refs; const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !; const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
@ -266,7 +267,7 @@ runInEachFileSystem(() => {
} }
`, `,
}, },
new FileToModuleAliasingHost(testHost)); new UnifiedModulesAliasingHost(testHost));
const {DeepExportModule} = refs; const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !; const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope); const [DeepDir] = scopeToRefs(scope);

View File

@ -8,8 +8,8 @@
/// <reference types="node" /> /// <reference types="node" />
export {ShimGenerator} from './src/api';
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator'; export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {FactoryTracker} from './src/factory_tracker'; export {FactoryTracker} from './src/factory_tracker';
export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host';
export {SummaryGenerator} from './src/summary_generator'; export {SummaryGenerator} from './src/summary_generator';
export {TypeCheckShimGenerator} from './src/typecheck_shim'; export {TypeCheckShimGenerator} from './src/typecheck_shim';

View File

@ -0,0 +1,27 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
export interface ShimGenerator {
/**
* Returns `true` if this generator is intended to handle the given file.
*/
recognize(fileName: AbsoluteFsPath): boolean;
/**
* Generate a shim's `ts.SourceFile` for the given original file.
*
* `readFile` is a function which allows the generator to look up the contents of existing source
* files. It returns null if the requested file doesn't exist.
*
* If `generate` returns null, then the shim generator declines to generate the file after all.
*/
generate(genFileName: AbsoluteFsPath, readFile: (fileName: string) => ts.SourceFile | null):
ts.SourceFile|null;
}

View File

@ -11,7 +11,7 @@ import {AbsoluteFsPath, absoluteFrom, basename} from '../../file_system';
import {ImportRewriter} from '../../imports'; import {ImportRewriter} from '../../imports';
import {isNonDeclarationTsPath} from '../../util/src/typescript'; import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {ShimGenerator} from './host'; import {ShimGenerator} from './api';
import {generatedModuleName} from './util'; import {generatedModuleName} from './util';
const TS_DTS_SUFFIX = /(\.d)?\.ts$/; const TS_DTS_SUFFIX = /(\.d)?\.ts$/;

View File

@ -1,129 +0,0 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom, resolve} from '../../file_system';
export interface ShimGenerator {
/**
* Returns `true` if this generator is intended to handle the given file.
*/
recognize(fileName: AbsoluteFsPath): boolean;
/**
* Generate a shim's `ts.SourceFile` for the given original file.
*
* `readFile` is a function which allows the generator to look up the contents of existing source
* files. It returns null if the requested file doesn't exist.
*
* If `generate` returns null, then the shim generator declines to generate the file after all.
*/
generate(genFileName: AbsoluteFsPath, readFile: (fileName: string) => ts.SourceFile | null):
ts.SourceFile|null;
}
/**
* A wrapper around a `ts.CompilerHost` which supports generated files.
*/
export class GeneratedShimsHostWrapper implements ts.CompilerHost {
constructor(private delegate: ts.CompilerHost, private shimGenerators: ShimGenerator[]) {
if (delegate.resolveModuleNames !== undefined) {
this.resolveModuleNames =
(moduleNames: string[], containingFile: string, reusedNames: string[],
redirectedReference: ts.ResolvedProjectReference, options?: ts.CompilerOptions) =>
// FIXME: Additional parameters are required in TS3.6, but ignored in 3.5.
// Remove the any cast once google3 is fully on TS3.6.
(delegate.resolveModuleNames as any) !(
moduleNames, containingFile, reusedNames, redirectedReference, options);
}
if (delegate.resolveTypeReferenceDirectives) {
// Backward compatibility with TypeScript 2.9 and older since return
// type has changed from (ts.ResolvedTypeReferenceDirective | undefined)[]
// to ts.ResolvedTypeReferenceDirective[] in Typescript 3.0
type ts3ResolveTypeReferenceDirectives = (names: string[], containingFile: string) =>
ts.ResolvedTypeReferenceDirective[];
this.resolveTypeReferenceDirectives = (names: string[], containingFile: string) =>
(delegate.resolveTypeReferenceDirectives as ts3ResolveTypeReferenceDirectives) !(
names, containingFile);
}
if (delegate.directoryExists !== undefined) {
this.directoryExists = (directoryName: string) => delegate.directoryExists !(directoryName);
}
if (delegate.getDirectories !== undefined) {
this.getDirectories = (path: string) => delegate.getDirectories !(path);
}
}
// FIXME: Additional options param is needed in TS3.6, but not alloowed in 3.5.
// Make the options param non-optional once google3 is fully on TS3.6.
resolveModuleNames?:
(moduleNames: string[], containingFile: string, reusedNames: string[],
redirectedReference: ts.ResolvedProjectReference,
options?: ts.CompilerOptions) => (ts.ResolvedModule | undefined)[];
resolveTypeReferenceDirectives?:
(names: string[], containingFile: string) => ts.ResolvedTypeReferenceDirective[];
directoryExists?: (directoryName: string) => boolean;
getSourceFile(
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
for (let i = 0; i < this.shimGenerators.length; i++) {
const generator = this.shimGenerators[i];
// TypeScript internal paths are guaranteed to be POSIX-like absolute file paths.
const absoluteFsPath = resolve(fileName);
if (generator.recognize(absoluteFsPath)) {
const readFile = (originalFile: string) => {
return this.delegate.getSourceFile(
originalFile, languageVersion, onError, shouldCreateNewSourceFile) ||
null;
};
return generator.generate(absoluteFsPath, readFile) || undefined;
}
}
return this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile);
}
getDefaultLibFileName(options: ts.CompilerOptions): string {
return this.delegate.getDefaultLibFileName(options);
}
writeFile(
fileName: string, data: string, writeByteOrderMark: boolean,
onError: ((message: string) => void)|undefined,
sourceFiles: ReadonlyArray<ts.SourceFile>|undefined): void {
return this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
}
getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); }
getDirectories?: (path: string) => string[];
getCanonicalFileName(fileName: string): string {
return this.delegate.getCanonicalFileName(fileName);
}
useCaseSensitiveFileNames(): boolean { return this.delegate.useCaseSensitiveFileNames(); }
getNewLine(): string { return this.delegate.getNewLine(); }
fileExists(fileName: string): boolean {
// Consider the file as existing whenever
// 1) it really does exist in the delegate host, or
// 2) at least one of the shim generators recognizes it
// Note that we can pass the file name as branded absolute fs path because TypeScript
// internally only passes POSIX-like paths.
return this.delegate.fileExists(fileName) ||
this.shimGenerators.some(gen => gen.recognize(absoluteFrom(fileName)));
}
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
}

View File

@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom} from '../../file_system'; import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
import {isNonDeclarationTsPath} from '../../util/src/typescript'; import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {ShimGenerator} from './host'; import {ShimGenerator} from './api';
import {generatedModuleName} from './util'; import {generatedModuleName} from './util';
export class SummaryGenerator implements ShimGenerator { export class SummaryGenerator implements ShimGenerator {

View File

@ -10,7 +10,7 @@ import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system'; import {AbsoluteFsPath} from '../../file_system';
import {ShimGenerator} from './host'; import {ShimGenerator} from './api';
/** /**
* A `ShimGenerator` which adds a type-checking file to the `ts.Program`. * A `ShimGenerator` which adds a type-checking file to the `ts.Program`.

View File

@ -1,37 +0,0 @@
/**
* @license
* Copyright Google Inc. 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 * as ts from 'typescript';
import {GeneratedShimsHostWrapper} from '../src/host';
describe('shim host', () => {
it('should not have optional methods when delegate does not have them', function() {
const delegate = {} as unknown as ts.CompilerHost;
const shimsHost = new GeneratedShimsHostWrapper(delegate, []);
expect(shimsHost.resolveModuleNames).not.toBeDefined();
expect(shimsHost.resolveTypeReferenceDirectives).not.toBeDefined();
expect(shimsHost.directoryExists).not.toBeDefined();
expect(shimsHost.getDirectories).not.toBeDefined();
});
it('should delegate optional methods if available', function() {
const delegate = {
resolveModuleNames: () => undefined,
resolveTypeReferenceDirectives: () => undefined,
directoryExists: () => undefined,
getDirectories: () => undefined,
} as unknown as ts.CompilerHost;
const shimsHost = new GeneratedShimsHostWrapper(delegate, []);
expect(shimsHost.resolveModuleNames).toBeDefined();
expect(shimsHost.resolveTypeReferenceDirectives).toBeDefined();
expect(shimsHost.directoryExists).toBeDefined();
expect(shimsHost.getDirectories).toBeDefined();
});
});

View File

@ -9,6 +9,7 @@ ts_library(
]), ]),
deps = [ deps = [
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics", "//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api", "//packages/compiler-cli/src/ngtsc/incremental:api",

View File

@ -7,6 +7,7 @@
*/ */
export * from './src/api'; export * from './src/api';
export {aliasTransformFactory} from './src/alias';
export {ClassRecord, TraitCompiler} from './src/compilation'; export {ClassRecord, TraitCompiler} from './src/compilation';
export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform, ReturnTypeTransform} from './src/declaration'; export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform, ReturnTypeTransform} from './src/declaration';
export {AnalyzedTrait, ErroredTrait, PendingTrait, ResolvedTrait, SkippedTrait, Trait, TraitState} from './src/trait'; export {AnalyzedTrait, ErroredTrait, PendingTrait, ResolvedTrait, SkippedTrait, Trait, TraitState} from './src/trait';

View File

@ -9,9 +9,9 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
export function aliasTransformFactory(exportStatements: Map<string, Map<string, [string, string]>>): export function aliasTransformFactory(exportStatements: Map<string, Map<string, [string, string]>>):
ts.TransformerFactory<ts.Bundle|ts.SourceFile> { ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => { return (context: ts.TransformationContext) => {
return (file: ts.SourceFile | ts.Bundle) => { return (file: ts.SourceFile) => {
if (ts.isBundle(file) || !exportStatements.has(file.fileName)) { if (ts.isBundle(file) || !exportStatements.has(file.fileName)) {
return file; return file;
} }

View File

@ -67,7 +67,7 @@ export class DtsTransformRegistry {
export function declarationTransformFactory( export function declarationTransformFactory(
transformRegistry: DtsTransformRegistry, importRewriter: ImportRewriter, transformRegistry: DtsTransformRegistry, importRewriter: ImportRewriter,
importPrefix?: string): ts.TransformerFactory<ts.Bundle|ts.SourceFile> { importPrefix?: string): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext) => { return (context: ts.TransformationContext) => {
const transformer = new DtsTransformer(context, importRewriter, importPrefix); const transformer = new DtsTransformer(context, importRewriter, importPrefix);
return (fileOrBundle) => { return (fileOrBundle) => {

View File

@ -8,5 +8,6 @@
export * from './src/api'; export * from './src/api';
export {TypeCheckContext} from './src/context'; export {TypeCheckContext} from './src/context';
export {TemplateDiagnostic, isTemplateDiagnostic} from './src/diagnostics';
export {TypeCheckProgramHost} from './src/host'; export {TypeCheckProgramHost} from './src/host';
export {typeCheckFilePath} from './src/type_check_file'; export {typeCheckFilePath} from './src/type_check_file';

View File

@ -13,6 +13,16 @@ import {getTokenAtPosition} from '../../util/src/typescript';
import {ExternalTemplateSourceMapping, TemplateId, TemplateSourceMapping} from './api'; import {ExternalTemplateSourceMapping, TemplateId, TemplateSourceMapping} from './api';
/**
* A `ts.Diagnostic` with additional information about the diagnostic related to template
* type-checking.
*/
export interface TemplateDiagnostic extends ts.Diagnostic {
/**
* The component with the template that resulted in this diagnostic.
*/
componentFile: ts.SourceFile;
}
/** /**
* Adapter interface which allows the template type-checking diagnostics code to interpret offsets * Adapter interface which allows the template type-checking diagnostics code to interpret offsets
@ -139,7 +149,7 @@ export function makeTemplateDiagnostic(
code: ErrorCode, messageText: string | ts.DiagnosticMessageChain, relatedMessage?: { code: ErrorCode, messageText: string | ts.DiagnosticMessageChain, relatedMessage?: {
text: string, text: string,
span: ParseSourceSpan, span: ParseSourceSpan,
}): ts.Diagnostic { }): TemplateDiagnostic {
if (mapping.type === 'direct') { if (mapping.type === 'direct') {
let relatedInformation: ts.DiagnosticRelatedInformation[]|undefined = undefined; let relatedInformation: ts.DiagnosticRelatedInformation[]|undefined = undefined;
if (relatedMessage !== undefined) { if (relatedMessage !== undefined) {
@ -159,6 +169,7 @@ export function makeTemplateDiagnostic(
source: 'ngtsc', source: 'ngtsc',
code: ngErrorCode(code), category, messageText, code: ngErrorCode(code), category, messageText,
file: mapping.node.getSourceFile(), file: mapping.node.getSourceFile(),
componentFile: mapping.node.getSourceFile(),
start: span.start.offset, start: span.start.offset,
length: span.end.offset - span.start.offset, relatedInformation, length: span.end.offset - span.start.offset, relatedInformation,
}; };
@ -207,6 +218,7 @@ export function makeTemplateDiagnostic(
category, category,
code: ngErrorCode(code), messageText, code: ngErrorCode(code), messageText,
file: sf, file: sf,
componentFile: componentSf,
start: span.start.offset, start: span.start.offset,
length: span.end.offset - span.start.offset, length: span.end.offset - span.start.offset,
// Show a secondary message indicating the component whose template contains the error. // Show a secondary message indicating the component whose template contains the error.
@ -303,3 +315,8 @@ function hasIgnoreMarker(node: ts.Node, sourceFile: ts.SourceFile): boolean {
return commentText === IGNORE_MARKER; return commentText === IGNORE_MARKER;
}) === true; }) === true;
} }
export function isTemplateDiagnostic(diagnostic: ts.Diagnostic): diagnostic is TemplateDiagnostic {
return diagnostic.hasOwnProperty('componentFile') &&
ts.isSourceFile((diagnostic as any).componentFile);
}

View File

@ -9,6 +9,8 @@
import {GeneratedFile, ParseSourceSpan, Position} from '@angular/compiler'; import {GeneratedFile, ParseSourceSpan, Position} from '@angular/compiler';
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ExtendedTsCompilerHost, NgCompilerOptions} from '../ngtsc/core/api';
export const DEFAULT_ERROR_CODE = 100; export const DEFAULT_ERROR_CODE = 100;
export const UNKNOWN_ERROR_CODE = 500; export const UNKNOWN_ERROR_CODE = 500;
export const SOURCE = 'angular' as 'angular'; export const SOURCE = 'angular' as 'angular';
@ -37,7 +39,7 @@ export function isNgDiagnostic(diagnostic: any): diagnostic is Diagnostic {
return diagnostic != null && diagnostic.source === 'angular'; return diagnostic != null && diagnostic.source === 'angular';
} }
export interface CompilerOptions extends ts.CompilerOptions { export interface CompilerOptions extends NgCompilerOptions, ts.CompilerOptions {
// NOTE: These comments and aio/content/guides/aot-compiler.md should be kept in sync. // NOTE: These comments and aio/content/guides/aot-compiler.md should be kept in sync.
// Write statistics about compilation (e.g. total time, ...) // Write statistics about compilation (e.g. total time, ...)
@ -62,42 +64,6 @@ export interface CompilerOptions extends ts.CompilerOptions {
// Don't produce .ngfactory.js or .ngstyle.js files // Don't produce .ngfactory.js or .ngstyle.js files
skipTemplateCodegen?: boolean; skipTemplateCodegen?: boolean;
// Always report errors when the type of a parameter supplied whose injection type cannot
// be determined. When this value option is not provided or is `false`, constructor
// parameters of classes marked with `@Injectable` whose type cannot be resolved will
// produce a warning. With this option `true`, they produce an error. When this option is
// not provided is treated as if it were `false`.
strictInjectionParameters?: boolean;
// Whether to generate a flat module index of the given name and the corresponding
// flat module metadata. This option is intended to be used when creating flat
// modules similar to how `@angular/core` and `@angular/common` are packaged.
// When this option is used the `package.json` for the library should referred to the
// generated flat module index instead of the library index file. When using this
// option only one .metadata.json file is produced that contains all the metadata
// necessary for symbols exported from the library index.
// In the generated .ngfactory.ts files flat module index is used to import symbols
// includes both the public API from the library index as well as shrowded internal
// symbols.
// By default the .ts file supplied in the `files` files field is assumed to be
// library index. If more than one is specified, uses `libraryIndex` to select the
// file to use. If more than on .ts file is supplied and no `libraryIndex` is supplied
// an error is produced.
// A flat module index .d.ts and .js will be created with the given `flatModuleOutFile`
// name in the same location as the library index .d.ts file is emitted.
// For example, if a library uses `public_api.ts` file as the library index of the
// module the `tsconfig.json` `files` field would be `["public_api.ts"]`. The
// `flatModuleOutFile` options could then be set to, for example `"index.js"`, which
// produces `index.d.ts` and `index.metadata.json` files. The library's
// `package.json`'s `module` field would be `"index.js"` and the `typings` field would
// be `"index.d.ts"`.
flatModuleOutFile?: string;
// Preferred module id to use for importing flat module. References generated by `ngc`
// will use this module name when importing symbols from the flat module. This is only
// meaningful when `flatModuleOutFile` is also supplied. It is otherwise ignored.
flatModuleId?: string;
// A prefix to insert in generated private symbols, e.g. for "my_prefix_" we // A prefix to insert in generated private symbols, e.g. for "my_prefix_" we
// would generate private symbols named like `ɵmy_prefix_a`. // would generate private symbols named like `ɵmy_prefix_a`.
flatModulePrivateSymbolPrefix?: string; flatModulePrivateSymbolPrefix?: string;
@ -107,136 +73,6 @@ export interface CompilerOptions extends ts.CompilerOptions {
// Default is true. // Default is true.
generateCodeForLibraries?: boolean; generateCodeForLibraries?: boolean;
/**
* Whether to type check the entire template.
*
* This flag currently controls a couple aspects of template type-checking, including
* whether embedded views are checked.
*
* For maximum type-checking, set this to `true`, and set `strictTemplates` to `true`.
*
* It is an error for this flag to be `false`, while `strictTemplates` is set to `true`.
*/
fullTemplateTypeCheck?: boolean;
/**
* If `true`, implies all template strictness flags below (unless individually disabled).
*
* Has no effect unless `fullTemplateTypeCheck` is also enabled.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictTemplates?: boolean;
/**
* Whether to check the type of a binding to a directive/component input against the type of the
* field on the directive/component.
*
* For example, if this is `false` then the expression `[input]="expr"` will have `expr` type-
* checked, but not the assignment of the resulting type to the `input` property of whichever
* directive or component is receiving the binding. If set to `true`, both sides of the assignment
* are checked.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictInputTypes?: boolean;
/**
* Whether to use strict null types for input bindings for directives.
*
* If this is `true`, applications that are compiled with TypeScript's `strictNullChecks` enabled
* will produce type errors for bindings which can evaluate to `undefined` or `null` where the
* inputs's type does not include `undefined` or `null` in its type. If set to `false`, all
* binding expressions are wrapped in a non-null assertion operator to effectively disable strict
* null checks.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set. Note that if `strictInputTypes` is
* not set, or set to `false`, this flag has no effect.
*/
strictNullInputTypes?: boolean;
/**
* Whether to check text attributes that happen to be consumed by a directive or component.
*
* For example, in a template containing `<input matInput disabled>` the `disabled` attribute ends
* up being consumed as an input with type `boolean` by the `matInput` directive. At runtime, the
* input will be set to the attribute's string value, which is an empty string for attributes
* without a value, so with this flag set to `true`, an error would be reported. If set to
* `false`, text attributes will never report an error.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set. Note that if `strictInputTypes` is
* not set, or set to `false`, this flag has no effect.
*/
strictAttributeTypes?: boolean;
/**
* Whether to use a strict type for null-safe navigation operations.
*
* If this is `false`, then the return type of `a?.b` or `a?()` will be `any`. If set to `true`,
* then the return type of `a?.b` for example will be the same as the type of the ternary
* expression `a != null ? a.b : a`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictSafeNavigationTypes?: boolean;
/**
* Whether to infer the type of local references.
*
* If this is `true`, the type of a `#ref` variable on a DOM node in the template will be
* determined by the type of `document.createElement` for the given DOM node. If set to `false`,
* the type of `ref` for DOM nodes will be `any`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictDomLocalRefTypes?: boolean;
/**
* Whether to infer the type of the `$event` variable in event bindings for directive outputs or
* animation events.
*
* If this is `true`, the type of `$event` will be inferred based on the generic type of
* `EventEmitter`/`Subject` of the output. If set to `false`, the `$event` variable will be of
* type `any`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictOutputEventTypes?: boolean;
/**
* Whether to infer the type of the `$event` variable in event bindings to DOM events.
*
* If this is `true`, the type of `$event` will be inferred based on TypeScript's
* `HTMLElementEventMap`, with a fallback to the native `Event` type. If set to `false`, the
* `$event` variable will be of type `any`.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictDomEventTypes?: boolean;
/**
* Whether to include the generic type of components when type-checking the template.
*
* If no component has generic type parameters, this setting has no effect.
*
* If a component has generic type parameters and this setting is `true`, those generic parameters
* will be included in the context type for the template. If `false`, any generic parameters will
* be set to `any` in the template context type.
*
* Defaults to `false`, even if "fullTemplateTypeCheck" is set.
*/
strictContextGenerics?: boolean;
// Whether to use the CompilerHost's fileNameToModuleName utility (if available) to generate
// import module specifiers. This is false by default, and exists to support running ngtsc
// within Google. This option is internal and is used by the ng_module.bzl rule to switch
// behavior between Bazel and Blaze.
_useHostForImportGeneration?: boolean;
// Insert JSDoc type annotations needed by Closure Compiler
annotateForClosureCompiler?: boolean;
// Modify how angular annotations are emitted to improve tree-shaking. // Modify how angular annotations are emitted to improve tree-shaking.
// Default is static fields. // Default is static fields.
// decorators: Leave the Decorators in-place. This makes compilation faster. // decorators: Leave the Decorators in-place. This makes compilation faster.
@ -255,9 +91,6 @@ export interface CompilerOptions extends ts.CompilerOptions {
// position. // position.
disableExpressionLowering?: boolean; disableExpressionLowering?: boolean;
// Disable TypeScript Version Check.
disableTypeScriptVersionCheck?: boolean;
// Locale of the application // Locale of the application
i18nOutLocale?: string; i18nOutLocale?: string;
// Export format (xlf, xlf2 or xmb) // Export format (xlf, xlf2 or xmb)
@ -267,33 +100,10 @@ export interface CompilerOptions extends ts.CompilerOptions {
// Import format if different from `i18nFormat` // Import format if different from `i18nFormat`
i18nInFormat?: string; i18nInFormat?: string;
// Locale of the imported translations
i18nInLocale?: string;
// Path to the translation file // Path to the translation file
i18nInFile?: string; i18nInFile?: string;
// How to handle missing messages // How to handle missing messages
i18nInMissingTranslations?: 'error'|'warning'|'ignore'; i18nInMissingTranslations?: 'error'|'warning'|'ignore';
// Whether translation variable name should contain external message id
// (used by Closure Compiler's output of `goog.getMsg` for transition period)
i18nUseExternalIds?: boolean;
/**
* Render `$localize` messages with legacy format ids.
*
* This is only active if we are building with `enableIvy: true`.
* The default value for now is `true`.
*
* Use this option when use are using the `$localize` based localization messages but
* have not migrated the translation files to use the new `$localize` message id format.
*/
enableI18nLegacyMessageIdFormat?: boolean;
// Whether to remove blank text nodes from compiled templates. It is `false` by default starting
// from Angular 6.
preserveWhitespaces?: boolean;
/** generate all possible generated files */
allowEmptyCodegenFiles?: boolean;
/** /**
* Whether to generate .ngsummary.ts files that allow to use AOTed artifacts * Whether to generate .ngsummary.ts files that allow to use AOTed artifacts
@ -311,51 +121,9 @@ export interface CompilerOptions extends ts.CompilerOptions {
*/ */
enableResourceInlining?: boolean; enableResourceInlining?: boolean;
/**
* Controls whether ngtsc will emit `.ngfactory.js` shims for each compiled `.ts` file.
*
* These shims support legacy imports from `ngfactory` files, by exporting a factory shim
* for each component or NgModule in the original `.ts` file.
*/
generateNgFactoryShims?: boolean;
/**
* Controls whether ngtsc will emit `.ngsummary.js` shims for each compiled `.ts` file.
*
* These shims support legacy imports from `ngsummary` files, by exporting an empty object
* for each NgModule in the original `.ts` file. The only purpose of summaries is to feed them to
* `TestBed`, which is a no-op in Ivy.
*/
generateNgSummaryShims?: boolean;
/**
* Tells the compiler to generate definitions using the Render3 style code generation.
* This option defaults to `true`.
*
* Acceptable values are as follows:
*
* `false` - run ngc normally
* `true` - run the ngtsc compiler instead of the normal ngc compiler
* `ngtsc` - alias for `true`
*
* @publicApi
*/
enableIvy?: boolean|'ngtsc';
/** @internal */ /** @internal */
collectAllErrors?: boolean; collectAllErrors?: boolean;
/** An option to enable ngtsc's internal performance tracing.
*
* This should be a path to a JSON file where trace information will be written. An optional 'ts:'
* prefix will cause the trace to be written via the TS host instead of directly to the filesystem
* (not all hosts support this mode of operation).
*
* This is currently not exposed to users as the trace format is still unstable.
*
* @internal */
tracePerformance?: string;
/** /**
* Whether NGC should generate re-exports for external symbols which are referenced * Whether NGC should generate re-exports for external symbols which are referenced
* in Angular metadata (e.g. @Component, @Inject, @ViewChild). This can be enabled in * in Angular metadata (e.g. @Component, @Inject, @ViewChild). This can be enabled in
@ -364,72 +132,14 @@ export interface CompilerOptions extends ts.CompilerOptions {
* Read more about this here: https://github.com/angular/angular/issues/25644. * Read more about this here: https://github.com/angular/angular/issues/25644.
*/ */
createExternalSymbolFactoryReexports?: boolean; createExternalSymbolFactoryReexports?: boolean;
/**
* Turn on template type-checking in the Ivy compiler.
*
* This is an internal flag being used to roll out template type-checking in ngtsc. Turning it on
* by default before it's ready might break other users attempting to test the new compiler's
* behavior.
*
* @internal
*/
ivyTemplateTypeCheck?: boolean;
/**
* Enables the generation of alias re-exports of directives/pipes that are visible from an
* NgModule from that NgModule's file.
*
* This option should be disabled for application builds or for Angular Package Format libraries
* (where NgModules along with their directives/pipes are exported via a single entrypoint).
*
* For other library compilations which are intended to be path-mapped into an application build
* (or another library), enabling this option enables the resulting deep imports to work
* correctly.
*
* A consumer of such a path-mapped library will write an import like:
*
* ```typescript
* import {LibModule} from 'lib/deep/path/to/module';
* ```
*
* The compiler will attempt to generate imports of directives/pipes from that same module
* specifier (the compiler does not rewrite the user's given import path, unlike View Engine).
*
* ```typescript
* import {LibDir, LibCmp, LibPipe} from 'lib/deep/path/to/module';
* ```
*
* It would be burdensome for users to have to re-export all directives/pipes alongside each
* NgModule to support this import model. Enabling this option tells the compiler to generate
* private re-exports alongside the NgModule of all the directives/pipes it makes available, to
* support these future imports.
*/
generateDeepReexports?: boolean;
/**
* Whether the compiler should avoid generating code for classes that haven't been exported.
* This is only active when building with `enableIvy: true`. Defaults to `true`.
*/
compileNonExportedClasses?: boolean;
} }
export interface CompilerHost extends ts.CompilerHost { export interface CompilerHost extends ts.CompilerHost, ExtendedTsCompilerHost {
/** /**
* Converts a module name that is used in an `import` to a file path. * Converts a module name that is used in an `import` to a file path.
* I.e. `path/to/containingFile.ts` containing `import {...} from 'module-name'`. * I.e. `path/to/containingFile.ts` containing `import {...} from 'module-name'`.
*/ */
moduleNameToFileName?(moduleName: string, containingFile: string): string|null; moduleNameToFileName?(moduleName: string, containingFile: string): string|null;
/**
* Converts a file path to a module name that can be used as an `import ...`
* I.e. `path/to/importedFile.ts` should be imported by `path/to/containingFile.ts`.
*/
fileNameToModuleName?(importedFilePath: string, containingFilePath: string): string;
/**
* Converts a file path for a resource that is used in a source file or another resource
* into a filepath.
*/
resourceNameToFileName?(resourceName: string, containingFilePath: string): string|null;
/** /**
* Converts a file name into a representation that should be stored in a summary file. * Converts a file name into a representation that should be stored in a summary file.
* This has to include changing the suffix as well. * This has to include changing the suffix as well.
@ -444,13 +154,6 @@ export interface CompilerHost extends ts.CompilerHost {
* given the fileName of the library that is referrig to it. * given the fileName of the library that is referrig to it.
*/ */
fromSummaryFileName?(fileName: string, referringLibFileName: string): string; fromSummaryFileName?(fileName: string, referringLibFileName: string): string;
/**
* Load a referenced resource either statically or asynchronously. If the host returns a
* `Promise<string>` it is assumed the user of the corresponding `Program` will call
* `loadNgStructureAsync()`. Returning `Promise<string>` outside `loadNgStructureAsync()` will
* cause a diagnostics diagnostic error or an exception to be thrown.
*/
readResource?(fileName: string): Promise<string>|string;
/** /**
* Produce an AMD module name for the source file. Used in Bazel. * Produce an AMD module name for the source file. Used in Bazel.
* *
@ -458,12 +161,6 @@ export interface CompilerHost extends ts.CompilerHost {
* rather than by path. See http://requirejs.org/docs/whyamd.html#namedmodules * rather than by path. See http://requirejs.org/docs/whyamd.html#namedmodules
*/ */
amdModuleName?(sf: ts.SourceFile): string|undefined; amdModuleName?(sf: ts.SourceFile): string|undefined;
/**
* Get the absolute paths to the changed files that triggered the current compilation
* or `undefined` if this is not an incremental build.
*/
getModifiedResourceFiles?(): Set<string>|undefined;
} }
export enum EmitFlags { export enum EmitFlags {

View File

@ -860,7 +860,7 @@ export function createProgram({rootNames, options, host, oldProgram}: {
host: CompilerHost, oldProgram?: Program host: CompilerHost, oldProgram?: Program
}): Program { }): Program {
if (options.enableIvy !== false) { if (options.enableIvy !== false) {
return new NgtscProgram(rootNames, options, host, oldProgram as NgtscProgram); return new NgtscProgram(rootNames, options, host, oldProgram as NgtscProgram | undefined);
} else { } else {
return new AngularCompilerProgram(rootNames, options, host, oldProgram); return new AngularCompilerProgram(rootNames, options, host, oldProgram);
} }

View File

@ -1177,7 +1177,7 @@ export declare class AnimationEvent {
it('should still type-check when fileToModuleName aliasing is enabled, but alias exports are not in the .d.ts file', it('should still type-check when fileToModuleName aliasing is enabled, but alias exports are not in the .d.ts file',
() => { () => {
// The template type-checking file imports directives/pipes in order to type-check their // The template type-checking file imports directives/pipes in order to type-check their
// usage. When `FileToModuleHost` aliasing is enabled, these imports would ordinarily use // usage. When `UnifiedModulesHost` aliasing is enabled, these imports would ordinarily use
// aliased values. However, such aliases are not guaranteed to exist in the .d.ts files, // aliased values. However, such aliases are not guaranteed to exist in the .d.ts files,
// and so feeding such imports back into TypeScript does not work. // and so feeding such imports back into TypeScript does not work.
// //