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

@ -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

@ -0,0 +1,186 @@
/**
* @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 {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)$/;
/**
* `ResourceLoader` which delegates to a `CompilerHost` resource loading method.
*/
export class HostResourceLoader implements ResourceLoader {
private cache = new Map<string, string>();
private fetching = new Map<string, Promise<void>>();
private rootDirs: AbsoluteFsPath[];
canPreload = !!this.host.readResource;
constructor(private host: ExtendedTsCompilerHost, private options: ts.CompilerOptions) {
this.rootDirs = getRootDirs(host, options);
}
/**
* Resolve the url of a resource relative to the file that contains the reference to it.
* The return value of this method can be used in the `load()` and `preload()` methods.
*
* Uses the provided CompilerHost if it supports mapping resources to filenames.
* Otherwise, uses a fallback mechanism that searches the module resolution candidates.
*
* @param url The, possibly relative, url of the resource.
* @param fromFile The path to the file that contains the URL of the resource.
* @returns A resolved url of resource.
* @throws An error if the resource cannot be resolved.
*/
resolve(url: string, fromFile: string): string {
let resolvedUrl: string|null = null;
if (this.host.resourceNameToFileName) {
resolvedUrl = this.host.resourceNameToFileName(url, fromFile);
} else {
resolvedUrl = this.fallbackResolve(url, fromFile);
}
if (resolvedUrl === null) {
throw new Error(`HostResourceResolver: could not resolve ${url} in context of ${fromFile})`);
}
return resolvedUrl;
}
/**
* Preload the specified resource, asynchronously.
*
* Once the resource is loaded, its value is cached so it can be accessed synchronously via the
* `load()` method.
*
* @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to preload.
* @returns A Promise that is resolved once the resource has been loaded or `undefined` if the
* file has already been loaded.
* @throws An Error if pre-loading is not available.
*/
preload(resolvedUrl: string): Promise<void>|undefined {
if (!this.host.readResource) {
throw new Error(
'HostResourceLoader: the CompilerHost provided does not support pre-loading resources.');
}
if (this.cache.has(resolvedUrl)) {
return undefined;
} else if (this.fetching.has(resolvedUrl)) {
return this.fetching.get(resolvedUrl);
}
const result = this.host.readResource(resolvedUrl);
if (typeof result === 'string') {
this.cache.set(resolvedUrl, result);
return undefined;
} else {
const fetchCompletion = result.then(str => {
this.fetching.delete(resolvedUrl);
this.cache.set(resolvedUrl, str);
});
this.fetching.set(resolvedUrl, fetchCompletion);
return fetchCompletion;
}
}
/**
* Load the resource at the given url, synchronously.
*
* The contents of the resource may have been cached by a previous call to `preload()`.
*
* @param resolvedUrl The url (resolved by a call to `resolve()`) of the resource to load.
* @returns The contents of the resource.
*/
load(resolvedUrl: string): string {
if (this.cache.has(resolvedUrl)) {
return this.cache.get(resolvedUrl) !;
}
const result = this.host.readResource ? this.host.readResource(resolvedUrl) :
this.host.readFile(resolvedUrl);
if (typeof result !== 'string') {
throw new Error(`HostResourceLoader: loader(${resolvedUrl}) returned a Promise`);
}
this.cache.set(resolvedUrl, result);
return result;
}
/**
* Attempt to resolve `url` in the context of `fromFile`, while respecting the rootDirs
* option from the tsconfig. First, normalize the file name.
*/
private fallbackResolve(url: string, fromFile: string): string|null {
let candidateLocations: string[];
if (url.startsWith('/')) {
// This path is not really an absolute path, but instead the leading '/' means that it's
// rooted in the project rootDirs. So look for it according to the rootDirs.
candidateLocations = this.getRootedCandidateLocations(url);
} else {
// This path is a "relative" path and can be resolved as such. To make this easier on the
// downstream resolver, the './' prefix is added if missing to distinguish these paths from
// absolute node_modules paths.
if (!url.startsWith('.')) {
url = `./${url}`;
}
candidateLocations = this.getResolvedCandidateLocations(url, fromFile);
}
for (const candidate of candidateLocations) {
if (this.host.fileExists(candidate)) {
return candidate;
} else if (CSS_PREPROCESSOR_EXT.test(candidate)) {
/**
* If the user specified styleUrl points to *.scss, but the Sass compiler was run before
* Angular, then the resource may have been generated as *.css. Simply try the resolution
* again.
*/
const cssFallbackUrl = candidate.replace(CSS_PREPROCESSOR_EXT, '.css');
if (this.host.fileExists(cssFallbackUrl)) {
return cssFallbackUrl;
}
}
}
return null;
}
private getRootedCandidateLocations(url: string): AbsoluteFsPath[] {
// The path already starts with '/', so add a '.' to make it relative.
const segment: PathSegment = ('.' + url) as PathSegment;
return this.rootDirs.map(rootDir => join(rootDir, segment));
}
/**
* TypeScript provides utilities to resolve module names, but not resource files (which aren't
* a part of the ts.Program). However, TypeScript's module resolution can be used creatively
* to locate where resource files should be expected to exist. Since module resolution returns
* a list of file names that were considered, the loader can enumerate the possible locations
* for the file by setting up a module resolution for it that will fail.
*/
private getResolvedCandidateLocations(url: string, fromFile: string): string[] {
// `failedLookupLocations` is in the name of the type ts.ResolvedModuleWithFailedLookupLocations
// but is marked @internal in TypeScript. See
// https://github.com/Microsoft/TypeScript/issues/28770.
type ResolvedModuleWithFailedLookupLocations =
ts.ResolvedModuleWithFailedLookupLocations & {failedLookupLocations: ReadonlyArray<string>};
// clang-format off
const failedLookup = ts.resolveModuleName(url + '.$ngresource$', fromFile, this.options, this.host) as ResolvedModuleWithFailedLookupLocations;
// clang-format on
if (failedLookup.failedLookupLocations === undefined) {
throw new Error(
`Internal error: expected to find failedLookupLocations during resolution of resource '${url}' in context of ${fromFile}`);
}
return failedLookup.failedLookupLocations
.filter(candidate => candidate.endsWith('.$ngresource$.ts'))
.map(candidate => candidate.replace(/\.\$ngresource\$\.ts$/, ''));
}
}