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-24 08:59:59 -08:00
committed by Andrew Kushnir
parent 31e9dda2c8
commit 24b2f1da2b
38 changed files with 1915 additions and 1300 deletions
+2 -16
View File
@@ -23,26 +23,12 @@ ts_library(
tsconfig = ":tsconfig",
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/core",
"//packages/compiler-cli/src/ngtsc/core:api",
"//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/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//@types/chokidar",
"@npm//@types/node",
@@ -79,11 +79,11 @@ export class ComponentDecoratorHandler implements
private reflector: ReflectionHost, private evaluator: PartialEvaluator,
private metaRegistry: MetadataRegistry, private metaReader: MetadataReader,
private scopeReader: ComponentScopeReader, private scopeRegistry: LocalModuleScopeRegistry,
private isCore: boolean, private resourceLoader: ResourceLoader, private rootDirs: string[],
private defaultPreserveWhitespaces: boolean, private i18nUseExternalIds: boolean,
private enableI18nLegacyMessageIdFormat: boolean, private moduleResolver: ModuleResolver,
private cycleAnalyzer: CycleAnalyzer, private refEmitter: ReferenceEmitter,
private defaultImportRecorder: DefaultImportRecorder,
private isCore: boolean, private resourceLoader: ResourceLoader,
private rootDirs: ReadonlyArray<string>, private defaultPreserveWhitespaces: boolean,
private i18nUseExternalIds: boolean, private enableI18nLegacyMessageIdFormat: boolean,
private moduleResolver: ModuleResolver, private cycleAnalyzer: CycleAnalyzer,
private refEmitter: ReferenceEmitter, private defaultImportRecorder: DefaultImportRecorder,
private depTracker: DependencyTracker|null,
private injectableRegistry: InjectableClassRegistry,
private annotateForClosureCompiler: boolean) {}
@@ -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",
],
)
@@ -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.
File diff suppressed because it is too large Load Diff
@@ -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';
File diff suppressed because it is too large Load Diff
@@ -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;
}
}
@@ -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",
],
)
@@ -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');
});
});
});
@@ -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",
@@ -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.
@@ -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';
@@ -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;
@@ -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});
}
@@ -8,3 +8,4 @@
export * from './src/api';
export {IndexingContext} from './src/context';
export {generateAnalysis} from './src/transform';
File diff suppressed because it is too large Load Diff
@@ -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",
],
)
@@ -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';
@@ -8,11 +8,10 @@
import * as ts from 'typescript';
import {CompilerHost} from '../transformers/api';
import {ResourceLoader} from './annotations';
import {AbsoluteFsPath, PathSegment, join} from './file_system';
import {getRootDirs} from './util/src/typescript';
import {ResourceLoader} from '../../annotations';
import {ExtendedTsCompilerHost} from '../../core/api';
import {AbsoluteFsPath, PathSegment, join} from '../../file_system';
import {getRootDirs} from '../../util/src/typescript';
const CSS_PREPROCESSOR_EXT = /(\.scss|\.less|\.styl)$/;
@@ -27,7 +26,7 @@ export class HostResourceLoader implements ResourceLoader {
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);
}
@@ -11,6 +11,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/file_system/testing",
"//packages/compiler-cli/src/ngtsc/imports",
@@ -8,9 +8,10 @@
import {ExternalExpr, ExternalReference} from '@angular/compiler';
import * as ts from 'typescript';
import {UnifiedModulesHost} from '../../core/api';
import {absoluteFrom} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {AliasingHost, FileToModuleAliasingHost, FileToModuleHost, Reference} from '../../imports';
import {AliasingHost, Reference, UnifiedModulesAliasingHost} from '../../imports';
import {DtsMetadataReader} from '../../metadata';
import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection';
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 testHost: FileToModuleHost = {
const testHost: UnifiedModulesHost = {
fileNameToModuleName: function(imported: string): string {
const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported) !;
return 'root/' + res[1];
@@ -183,7 +184,7 @@ runInEachFileSystem(() => {
}
`,
},
new FileToModuleAliasingHost(testHost));
new UnifiedModulesAliasingHost(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
@@ -233,7 +234,7 @@ runInEachFileSystem(() => {
}
`,
},
new FileToModuleAliasingHost(testHost));
new UnifiedModulesAliasingHost(testHost));
const {ShallowModule} = refs;
const scope = resolver.resolve(ShallowModule) !;
const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope);
@@ -266,7 +267,7 @@ runInEachFileSystem(() => {
}
`,
},
new FileToModuleAliasingHost(testHost));
new UnifiedModulesAliasingHost(testHost));
const {DeepExportModule} = refs;
const scope = resolver.resolve(DeepExportModule) !;
const [DeepDir] = scopeToRefs(scope);
@@ -8,8 +8,8 @@
/// <reference types="node" />
export {ShimGenerator} from './src/api';
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {FactoryTracker} from './src/factory_tracker';
export {GeneratedShimsHostWrapper, ShimGenerator} from './src/host';
export {SummaryGenerator} from './src/summary_generator';
export {TypeCheckShimGenerator} from './src/typecheck_shim';
@@ -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;
}
@@ -11,7 +11,7 @@ import {AbsoluteFsPath, absoluteFrom, basename} from '../../file_system';
import {ImportRewriter} from '../../imports';
import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {ShimGenerator} from './host';
import {ShimGenerator} from './api';
import {generatedModuleName} from './util';
const TS_DTS_SUFFIX = /(\.d)?\.ts$/;
@@ -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); }
}
@@ -11,7 +11,7 @@ import * as ts from 'typescript';
import {AbsoluteFsPath, absoluteFrom} from '../../file_system';
import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {ShimGenerator} from './host';
import {ShimGenerator} from './api';
import {generatedModuleName} from './util';
export class SummaryGenerator implements ShimGenerator {
@@ -10,7 +10,7 @@ import * as ts from 'typescript';
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`.
@@ -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();
});
});
@@ -9,6 +9,7 @@ ts_library(
]),
deps = [
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/diagnostics",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental:api",

Some files were not shown because too many files have changed in this diff Show More