refactor(compiler): remove old ngtools api and add listLazyRoutes to new api (#19836)

Usages of `NgTools_InternalApi_NG_2` from `@angular/compiler-cli` will now
throw an error.

Adds `listLazyRoutes` to `@angular/compiler-cli/ngtools2.ts` for getting
the lazy routes of a `ng.Program`.
PR Close #19836
This commit is contained in:
Tobias Bosch
2017-10-20 09:46:41 -07:00
committed by Matias Niemelä
parent 5da96c75a2
commit 8d45fefc31
41 changed files with 1128 additions and 1837 deletions

View File

@ -229,6 +229,12 @@ export interface LibrarySummary {
sourceFile?: ts.SourceFile;
}
export interface LazyRoute {
route: string;
module: {name: string, filePath: string};
referencedModule: {name: string, filePath: string};
}
export interface Program {
/**
* Retrieve the TypeScript program used to produce semantic diagnostics and emit the sources.
@ -293,6 +299,14 @@ export interface Program {
*/
loadNgStructureAsync(): Promise<void>;
/**
* Returns the lazy routes in the program.
* @param entryRoute A reference to an NgModule like `someModule#name`. If given,
* will recursively analyze routes starting from this symbol only.
* Otherwise will list all routes for all NgModules in the program/
*/
listLazyRoutes(entryRoute?: string): LazyRoute[];
/**
* Emit the files requested by emitFlags implied by the program.
*

View File

@ -6,19 +6,18 @@
* found in the LICENSE file at https://angular.io/license
*/
import {EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
import {AotCompilerHost, EmitterVisitorContext, ExternalReference, GeneratedFile, ParseSourceSpan, TypeScriptEmitter, collectExternalReferences, syntaxError} from '@angular/compiler';
import * as path from 'path';
import * as ts from 'typescript';
import {BaseAotCompilerHost} from '../compiler_host';
import {TypeCheckHost} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata} from '../metadata/index';
import {METADATA_VERSION, ModuleMetadata} from '../metadata/index';
import {CompilerHost, CompilerOptions, LibrarySummary} from './api';
import {GENERATED_FILES} from './util';
import {MetadataReaderHost, createMetadataReaderCache, readMetadata} from './metadata_reader';
import {DTS, GENERATED_FILES} from './util';
const NODE_MODULES_PACKAGE_NAME = /node_modules\/((\w|-)+|(@(\w|-)+\/(\w|-)+))/;
const DTS = /\.d\.ts$/;
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
export function createCompilerHost(
@ -48,9 +47,12 @@ export interface CodeGenerator {
* - AotCompilerHost for @angular/compiler
* - TypeCheckHost for mapping ts errors to ng errors (via translateDiagnostics)
*/
export class TsCompilerAotCompilerTypeCheckHostAdapter extends
BaseAotCompilerHost<CompilerHost> implements ts.CompilerHost,
export class TsCompilerAotCompilerTypeCheckHostAdapter implements ts.CompilerHost, AotCompilerHost,
TypeCheckHost {
private metadataReaderCache = createMetadataReaderCache();
private flatModuleIndexCache = new Map<string, boolean>();
private flatModuleIndexNames = new Set<string>();
private flatModuleIndexRedirectNames = new Set<string>();
private rootDirs: string[];
private moduleResolutionCache: ts.ModuleResolutionCache;
private originalSourceFiles = new Map<string, ts.SourceFile|undefined>();
@ -58,6 +60,8 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
private generatedSourceFiles = new Map<string, GenSourceFile>();
private generatedCodeFor = new Map<string, string[]>();
private emitter = new TypeScriptEmitter();
private metadataReaderHost: MetadataReaderHost;
getCancellationToken: () => ts.CancellationToken;
getDefaultLibLocation: () => string;
trace: (s: string) => void;
@ -65,10 +69,9 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
directoryExists?: (directoryName: string) => boolean;
constructor(
private rootFiles: string[], options: CompilerOptions, context: CompilerHost,
private rootFiles: string[], private options: CompilerOptions, private context: CompilerHost,
private metadataProvider: MetadataProvider, private codeGenerator: CodeGenerator,
private librarySummaries = new Map<string, LibrarySummary>()) {
super(options, context);
this.moduleResolutionCache = ts.createModuleResolutionCache(
this.context.getCurrentDirectory !(), this.context.getCanonicalFileName.bind(this.context));
const basePath = this.options.basePath !;
@ -103,6 +106,15 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
if (context.fromSummaryFileName) {
this.fromSummaryFileName = context.fromSummaryFileName.bind(context);
}
this.metadataReaderHost = {
cacheMetadata: () => true,
getSourceFileMetadata: (filePath) => {
const sf = this.getOriginalSourceFile(filePath);
return sf ? this.metadataProvider.getMetadata(sf) : undefined;
},
fileExists: (filePath) => this.originalFileExists(filePath),
readFile: (filePath) => this.context.readFile(filePath),
};
}
private resolveModuleName(moduleName: string, containingFile: string): ts.ResolvedModule
@ -251,14 +263,6 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
return sf;
}
getMetadataForSourceFile(filePath: string): ModuleMetadata|undefined {
const sf = this.getOriginalSourceFile(filePath);
if (!sf) {
return undefined;
}
return this.metadataProvider.getMetadata(sf);
}
updateGeneratedFile(genFile: GeneratedFile): ts.SourceFile {
if (!genFile.stmts) {
throw new Error(
@ -415,7 +419,10 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
if (summary) {
return summary.text;
}
return super.loadSummary(filePath);
if (this.originalFileExists(filePath)) {
return this.context.readFile(filePath);
}
return null;
}
isSourceFile(filePath: string): boolean {
@ -424,7 +431,21 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
if (this.librarySummaries.has(filePath)) {
return false;
}
return super.isSourceFile(filePath);
if (GENERATED_FILES.test(filePath)) {
return false;
}
if (this.options.generateCodeForLibraries === false && DTS.test(filePath)) {
return false;
}
if (DTS.test(filePath)) {
// Check for a bundle index.
if (this.hasBundleIndex(filePath)) {
const normalFilePath = path.normalize(filePath);
return this.flatModuleIndexNames.has(normalFilePath) ||
this.flatModuleIndexRedirectNames.has(normalFilePath);
}
}
return true;
}
readFile(fileName: string) {
@ -434,6 +455,76 @@ export class TsCompilerAotCompilerTypeCheckHostAdapter extends
}
return this.context.readFile(fileName);
}
getMetadataFor(filePath: string): ModuleMetadata[]|undefined {
return readMetadata(filePath, this.metadataReaderHost, this.metadataReaderCache);
}
loadResource(filePath: string): Promise<string>|string {
if (this.context.readResource) return this.context.readResource(filePath);
if (!this.originalFileExists(filePath)) {
throw syntaxError(`Error: Resource file not found: ${filePath}`);
}
return this.context.readFile(filePath);
}
private hasBundleIndex(filePath: string): boolean {
const checkBundleIndex = (directory: string): boolean => {
let result = this.flatModuleIndexCache.get(directory);
if (result == null) {
if (path.basename(directory) == 'node_module') {
// Don't look outside the node_modules this package is installed in.
result = false;
} else {
// A bundle index exists if the typings .d.ts file has a metadata.json that has an
// importAs.
try {
const packageFile = path.join(directory, 'package.json');
if (this.originalFileExists(packageFile)) {
// Once we see a package.json file, assume false until it we find the bundle index.
result = false;
const packageContent: any = JSON.parse(this.context.readFile(packageFile));
if (packageContent.typings) {
const typings = path.normalize(path.join(directory, packageContent.typings));
if (DTS.test(typings)) {
const metadataFile = typings.replace(DTS, '.metadata.json');
if (this.originalFileExists(metadataFile)) {
const metadata = JSON.parse(this.context.readFile(metadataFile));
if (metadata.flatModuleIndexRedirect) {
this.flatModuleIndexRedirectNames.add(typings);
// Note: don't set result = true,
// as this would mark this folder
// as having a bundleIndex too early without
// filling the bundleIndexNames.
} else if (metadata.importAs) {
this.flatModuleIndexNames.add(typings);
result = true;
}
}
}
}
} else {
const parent = path.dirname(directory);
if (parent != directory) {
// Try the parent directory.
result = checkBundleIndex(parent);
} else {
result = false;
}
}
} catch (e) {
// If we encounter any errors assume we this isn't a bundle index.
result = false;
}
}
this.flatModuleIndexCache.set(directory, result);
}
return result;
};
return checkBundleIndex(path.dirname(filePath));
}
getDefaultLibFileName = (options: ts.CompilerOptions) =>
this.context.getDefaultLibFileName(options)
getCurrentDirectory = () => this.context.getCurrentDirectory();

View File

@ -0,0 +1,128 @@
/**
* @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 {METADATA_VERSION, ModuleMetadata} from '../metadata';
import {DTS} from './util';
export interface MetadataReaderHost {
getSourceFileMetadata(filePath: string): ModuleMetadata|undefined;
cacheMetadata?(fileName: string): boolean;
fileExists(filePath: string): boolean;
readFile(filePath: string): string;
}
export interface MetadataReaderCache {
/**
* @internal
*/
data: Map<string, ModuleMetadata[]|undefined>;
}
export function createMetadataReaderCache(): MetadataReaderCache {
const data = new Map<string, ModuleMetadata[]|undefined>();
return {data};
}
export function readMetadata(
filePath: string, host: MetadataReaderHost, cache?: MetadataReaderCache): ModuleMetadata[]|
undefined {
let metadatas = cache && cache.data.get(filePath);
if (metadatas) {
return metadatas;
}
if (host.fileExists(filePath)) {
// If the file doesn't exists then we cannot return metadata for the file.
// This will occur if the user referenced a declared module for which no file
// exists for the module (i.e. jQuery or angularjs).
if (DTS.test(filePath)) {
metadatas = readMetadataFile(host, filePath);
if (!metadatas) {
// If there is a .d.ts file but no metadata file we need to produce a
// metadata from the .d.ts file as metadata files capture reexports
// (starting with v3).
metadatas = [upgradeMetadataWithDtsData(
host, {'__symbolic': 'module', 'version': 1, 'metadata': {}}, filePath)];
}
} else {
const metadata = host.getSourceFileMetadata(filePath);
metadatas = metadata ? [metadata] : [];
}
}
if (cache && (!host.cacheMetadata || host.cacheMetadata(filePath))) {
cache.data.set(filePath, metadatas);
}
return metadatas;
}
function readMetadataFile(host: MetadataReaderHost, dtsFilePath: string): ModuleMetadata[]|
undefined {
const metadataPath = dtsFilePath.replace(DTS, '.metadata.json');
if (!host.fileExists(metadataPath)) {
return undefined;
}
try {
const metadataOrMetadatas = JSON.parse(host.readFile(metadataPath));
const metadatas: ModuleMetadata[] = metadataOrMetadatas ?
(Array.isArray(metadataOrMetadatas) ? metadataOrMetadatas : [metadataOrMetadatas]) :
[];
if (metadatas.length) {
let maxMetadata = metadatas.reduce((p, c) => p.version > c.version ? p : c);
if (maxMetadata.version < METADATA_VERSION) {
metadatas.push(upgradeMetadataWithDtsData(host, maxMetadata, dtsFilePath));
}
}
return metadatas;
} catch (e) {
console.error(`Failed to read JSON file ${metadataPath}`);
throw e;
}
}
function upgradeMetadataWithDtsData(
host: MetadataReaderHost, oldMetadata: ModuleMetadata, dtsFilePath: string): ModuleMetadata {
// patch v1 to v3 by adding exports and the `extends` clause.
// patch v3 to v4 by adding `interface` symbols for TypeAlias
let newMetadata: ModuleMetadata = {
'__symbolic': 'module',
'version': METADATA_VERSION,
'metadata': {...oldMetadata.metadata},
};
if (oldMetadata.exports) {
newMetadata.exports = oldMetadata.exports;
}
if (oldMetadata.importAs) {
newMetadata.importAs = oldMetadata.importAs;
}
if (oldMetadata.origins) {
newMetadata.origins = oldMetadata.origins;
}
const dtsMetadata = host.getSourceFileMetadata(dtsFilePath);
if (dtsMetadata) {
for (let prop in dtsMetadata.metadata) {
if (!newMetadata.metadata[prop]) {
newMetadata.metadata[prop] = dtsMetadata.metadata[prop];
}
}
// Only copy exports from exports from metadata prior to version 3.
// Starting with version 3 the collector began collecting exports and
// this should be redundant. Also, with bundler will rewrite the exports
// which will hoist the exports from modules referenced indirectly causing
// the imports to be different than the .d.ts files and using the .d.ts file
// exports would cause the StaticSymbolResolver to redirect symbols to the
// incorrect location.
if ((!oldMetadata.version || oldMetadata.version < 3) && dtsMetadata.exports) {
newMetadata.exports = dtsMetadata.exports;
}
}
return newMetadata;
}

View File

@ -14,12 +14,13 @@ import * as ts from 'typescript';
import {TypeCheckHost, translateDiagnostics} from '../diagnostics/translate_diagnostics';
import {ModuleMetadata, createBundleIndexHost} from '../metadata/index';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {CompilerHost, CompilerOptions, CustomTransformers, DEFAULT_ERROR_CODE, Diagnostic, EmitFlags, LazyRoute, LibrarySummary, Program, SOURCE, TsEmitArguments, TsEmitCallback} from './api';
import {CodeGenerator, TsCompilerAotCompilerTypeCheckHostAdapter, getOriginalReferences} from './compiler_host';
import {LowerMetadataCache, getExpressionLoweringTransformFactory} from './lower_expressions';
import {getAngularEmitterTransformFactory} from './node_emitter_transform';
import {GENERATED_FILES, StructureIsReused, createMessageDiagnostic, tsStructureIsReused} from './util';
/**
* Maximum number of files that are emitable via calling ts.Program.emit
* passing individual targetSourceFiles.
@ -50,8 +51,8 @@ class AngularCompilerProgram implements Program {
private emittedSourceFiles: ts.SourceFile[]|undefined;
// Lazily initialized fields
private _typeCheckHost: TypeCheckHost;
private _compiler: AotCompiler;
private _hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter;
private _tsProgram: ts.Program;
private _analyzedModules: NgAnalyzedModules|undefined;
private _structuralDiagnostics: Diagnostic[]|undefined;
@ -167,7 +168,7 @@ class AngularCompilerProgram implements Program {
diags.push(...this.tsProgram.getSemanticDiagnostics(sf, cancellationToken));
}
});
const {ng} = translateDiagnostics(this.typeCheckHost, diags);
const {ng} = translateDiagnostics(this.hostAdapter, diags);
return ng;
}
@ -175,18 +176,23 @@ class AngularCompilerProgram implements Program {
if (this._analyzedModules) {
throw new Error('Angular structure already loaded');
}
const {tmpProgram, sourceFiles, hostAdapter, rootNames} = this._createProgramWithBasicStubs();
return this._compiler.loadFilesAsync(sourceFiles)
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
return this.compiler.loadFilesAsync(sourceFiles)
.catch(this.catchAnalysisError.bind(this))
.then(analyzedModules => {
if (this._analyzedModules) {
throw new Error('Angular structure loaded both synchronously and asynchronsly');
}
this._updateProgramWithTypeCheckStubs(
tmpProgram, analyzedModules, hostAdapter, rootNames);
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
});
}
listLazyRoutes(route?: string): LazyRoute[] {
// Note: Don't analyzedModules if a route is given
// to be fast enough.
return this.compiler.listLazyRoutes(route, route ? undefined : this.analyzedModules);
}
emit(
{emitFlags = EmitFlags.Default, cancellationToken, customTransformers,
emitCallback = defaultEmitCallback}: {
@ -341,11 +347,18 @@ class AngularCompilerProgram implements Program {
// Private members
private get compiler(): AotCompiler {
if (!this._compiler) {
this.initSync();
this._createCompiler();
}
return this._compiler !;
}
private get hostAdapter(): TsCompilerAotCompilerTypeCheckHostAdapter {
if (!this._hostAdapter) {
this._createCompiler();
}
return this._hostAdapter !;
}
private get analyzedModules(): NgAnalyzedModules {
if (!this._analyzedModules) {
this.initSync();
@ -367,13 +380,6 @@ class AngularCompilerProgram implements Program {
return this._tsProgram !;
}
private get typeCheckHost(): TypeCheckHost {
if (!this._typeCheckHost) {
this.initSync();
}
return this._typeCheckHost !;
}
private calculateTransforms(
genFiles: Map<string, GeneratedFile>,
customTransformers?: CustomTransformers): ts.CustomTransformers {
@ -393,19 +399,41 @@ class AngularCompilerProgram implements Program {
if (this._analyzedModules) {
return;
}
const {tmpProgram, sourceFiles, hostAdapter, rootNames} = this._createProgramWithBasicStubs();
const {tmpProgram, sourceFiles, rootNames} = this._createProgramWithBasicStubs();
let analyzedModules: NgAnalyzedModules|null;
try {
analyzedModules = this._compiler.loadFilesSync(sourceFiles);
analyzedModules = this.compiler.loadFilesSync(sourceFiles);
} catch (e) {
analyzedModules = this.catchAnalysisError(e);
}
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, hostAdapter, rootNames);
this._updateProgramWithTypeCheckStubs(tmpProgram, analyzedModules, rootNames);
}
private _createCompiler() {
const codegen: CodeGenerator = {
generateFile: (genFileName, baseFileName) =>
this._compiler.emitBasicStub(genFileName, baseFileName),
findGeneratedFileNames: (fileName) => this._compiler.findGeneratedFileNames(fileName),
};
this._hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
this.rootNames, this.options, this.host, this.metadataCache, codegen,
this.oldProgramLibrarySummaries);
const aotOptions = getAotCompilerOptions(this.options);
this._structuralDiagnostics = [];
const errorCollector = (err: any) => {
this._structuralDiagnostics !.push({
messageText: err.toString(),
category: ts.DiagnosticCategory.Error,
source: SOURCE,
code: DEFAULT_ERROR_CODE
});
};
this._compiler = createAotCompiler(this._hostAdapter, aotOptions, errorCollector).compiler;
}
private _createProgramWithBasicStubs(): {
tmpProgram: ts.Program,
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter,
rootNames: string[],
sourceFiles: string[],
} {
@ -418,58 +446,56 @@ class AngularCompilerProgram implements Program {
const codegen: CodeGenerator = {
generateFile: (genFileName, baseFileName) =>
this._compiler.emitBasicStub(genFileName, baseFileName),
findGeneratedFileNames: (fileName) => this._compiler.findGeneratedFileNames(fileName),
this.compiler.emitBasicStub(genFileName, baseFileName),
findGeneratedFileNames: (fileName) => this.compiler.findGeneratedFileNames(fileName),
};
const hostAdapter = new TsCompilerAotCompilerTypeCheckHostAdapter(
this.rootNames, this.options, this.host, this.metadataCache, codegen,
this.oldProgramLibrarySummaries);
const aotOptions = getAotCompilerOptions(this.options);
this._compiler = createAotCompiler(hostAdapter, aotOptions).compiler;
this._typeCheckHost = hostAdapter;
this._structuralDiagnostics = [];
let rootNames =
this.rootNames.filter(fn => !GENERATED_FILES.test(fn) || !hostAdapter.isSourceFile(fn));
let rootNames = this.rootNames;
if (this.options.generateCodeForLibraries !== false) {
// if we should generateCodeForLibraries, enver include
// generated files in the program as otherwise we will
// ovewrite them and typescript will report the error
// TS5055: Cannot write file ... because it would overwrite input file.
rootNames = this.rootNames.filter(fn => !GENERATED_FILES.test(fn));
}
if (this.options.noResolve) {
this.rootNames.forEach(rootName => {
if (hostAdapter.shouldGenerateFilesFor(rootName)) {
rootNames.push(...this._compiler.findGeneratedFileNames(rootName));
if (this.hostAdapter.shouldGenerateFilesFor(rootName)) {
rootNames.push(...this.compiler.findGeneratedFileNames(rootName));
}
});
}
const tmpProgram = ts.createProgram(rootNames, this.options, hostAdapter, oldTsProgram);
const tmpProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, oldTsProgram);
const sourceFiles: string[] = [];
tmpProgram.getSourceFiles().forEach(sf => {
if (hostAdapter.isSourceFile(sf.fileName)) {
if (this.hostAdapter.isSourceFile(sf.fileName)) {
sourceFiles.push(sf.fileName);
}
});
return {tmpProgram, sourceFiles, hostAdapter, rootNames};
return {tmpProgram, sourceFiles, rootNames};
}
private _updateProgramWithTypeCheckStubs(
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules|null,
hostAdapter: TsCompilerAotCompilerTypeCheckHostAdapter, rootNames: string[]) {
tmpProgram: ts.Program, analyzedModules: NgAnalyzedModules|null, rootNames: string[]) {
this._analyzedModules = analyzedModules || emptyModules;
if (analyzedModules) {
tmpProgram.getSourceFiles().forEach(sf => {
if (sf.fileName.endsWith('.ngfactory.ts')) {
const {generate, baseFileName} = hostAdapter.shouldGenerateFile(sf.fileName);
const {generate, baseFileName} = this.hostAdapter.shouldGenerateFile(sf.fileName);
if (generate) {
// Note: ! is ok as hostAdapter.shouldGenerateFile will always return a basefileName
// for .ngfactory.ts files.
const genFile = this._compiler.emitTypeCheckStub(sf.fileName, baseFileName !);
const genFile = this.compiler.emitTypeCheckStub(sf.fileName, baseFileName !);
if (genFile) {
hostAdapter.updateGeneratedFile(genFile);
this.hostAdapter.updateGeneratedFile(genFile);
}
}
}
});
}
this._tsProgram = ts.createProgram(rootNames, this.options, hostAdapter, tmpProgram);
this._tsProgram = ts.createProgram(rootNames, this.options, this.hostAdapter, tmpProgram);
// Note: the new ts program should be completely reusable by TypeScript as:
// - we cache all the files in the hostAdapter
// - new new stubs use the exactly same imports/exports as the old once (we assert that in

View File

@ -11,6 +11,7 @@ import * as ts from 'typescript';
import {DEFAULT_ERROR_CODE, Diagnostic, SOURCE} from './api';
export const GENERATED_FILES = /(.*?)\.(ngfactory|shim\.ngstyle|ngstyle|ngsummary)\.(js|d\.ts|ts)$/;
export const DTS = /\.d\.ts$/;
export const enum StructureIsReused {Not = 0, SafeModules = 1, Completely = 2}