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

@ -10,6 +10,7 @@ ts_library(
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/compiler-cli/src/ngtsc/reflection",
"//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.
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.
@ -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.
### `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`
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
@ -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:
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.
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.

View File

@ -6,10 +6,10 @@
* 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 {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 {OwningModule, Reference} from './src/references';
export {ModuleResolver} from './src/resolver';

View File

@ -9,12 +9,14 @@
import {Expression, ExternalExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection';
import {FileToModuleHost, ImportFlags, ReferenceEmitStrategy} from './emitter';
import {ImportFlags, ReferenceEmitStrategy} from './emitter';
import {Reference} from './references';
// Escape anything that isn't alphanumeric, '/' or '_'.
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
* 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
* described above.
* exports can also be helpful when using a `UnifiedModulesHost`, which overrides the import
* logic described above.
*
* 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
@ -85,16 +87,16 @@ export interface AliasingHost {
/**
* 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.
*/
export class FileToModuleAliasingHost implements AliasingHost {
constructor(private fileToModuleHost: FileToModuleHost) {}
export class UnifiedModulesAliasingHost implements AliasingHost {
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.
*/
readonly aliasExportsInDts = false;
@ -103,7 +105,8 @@ export class FileToModuleAliasingHost implements AliasingHost {
ref: Reference<ClassDeclaration>, context: ts.SourceFile, ngModuleName: string,
isReExport: boolean): string|null {
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
// NgModule which exports them.
return null;
@ -122,7 +125,7 @@ export class FileToModuleAliasingHost implements AliasingHost {
return null;
}
// 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)});
}
@ -132,8 +135,8 @@ export class FileToModuleAliasingHost implements AliasingHost {
*/
private aliasName(decl: ClassDeclaration, context: ts.SourceFile): string {
// The declared module is used to get the name of the alias.
const declModule =
this.fileToModuleHost.fileNameToModuleName(decl.getSourceFile().fileName, context.fileName);
const declModule = this.unifiedModulesHost.fileNameToModuleName(
decl.getSourceFile().fileName, context.fileName);
const replaced = declModule.replace(CHARS_TO_ESCAPE, '_').replace(/\//g, '$');
return 'ɵng$' + replaced + '$$' + decl.name.text;

View File

@ -8,6 +8,7 @@
import {Expression, ExternalExpr, ExternalReference, WrappedNodeExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {LogicalFileSystem, LogicalProjectPath, PathSegment, absoluteFromSourceFile, dirname, relative} from '../../file_system';
import {stripExtension} from '../../file_system/src/util';
import {ReflectionHost} from '../../reflection';
@ -18,17 +19,6 @@ import {Reference} from './references';
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`.
*/
@ -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 {
constructor(private reflector: ReflectionHost, private fileToModuleHost: FileToModuleHost) {}
export class UnifiedModulesStrategy implements ReferenceEmitStrategy {
constructor(private reflector: ReflectionHost, private unifiedModulesHost: UnifiedModulesHost) {}
emit(ref: Reference<ts.Node>, context: ts.SourceFile): Expression|null {
const destSf = getSourceFile(ref.node);
@ -290,7 +281,7 @@ export class FileToModuleStrategy implements ReferenceEmitStrategy {
}
const moduleName =
this.fileToModuleHost.fileNameToModuleName(destSf.fileName, context.fileName);
this.unifiedModulesHost.fileNameToModuleName(destSf.fileName, context.fileName);
return new ExternalExpr({moduleName, name});
}