refactor(ivy): move ngcc into a higher level folder (#29092)

PR Close #29092
This commit is contained in:
Pete Bacon Darwin
2019-03-20 13:47:58 +00:00
committed by Matias Niemelä
parent cf4718c366
commit a770aa231d
55 changed files with 108 additions and 95 deletions

View File

@ -0,0 +1,221 @@
/**
* @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 {ConstantPool} from '@angular/compiler';
import * as path from 'canonical-path';
import * as fs from 'fs';
import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path';
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
export interface AnalyzedFile {
sourceFile: ts.SourceFile;
analyzedClasses: AnalyzedClass[];
}
export interface AnalyzedClass extends DecoratedClass {
diagnostics?: ts.Diagnostic[];
matches: {handler: DecoratorHandler<any, any>; analysis: any;}[];
}
export interface CompiledClass extends AnalyzedClass { compilation: CompileResult[]; }
export interface CompiledFile {
compiledClasses: CompiledClass[];
sourceFile: ts.SourceFile;
constantPool: ConstantPool;
}
export type DecorationAnalyses = Map<ts.SourceFile, CompiledFile>;
export const DecorationAnalyses = Map;
export interface MatchingHandler<A, M> {
handler: DecoratorHandler<A, M>;
detected: M;
}
/**
* Simple class that resolves and loads files directly from the filesystem.
*/
class NgccResourceLoader implements ResourceLoader {
canPreload = false;
preload(): undefined|Promise<void> { throw new Error('Not implemented.'); }
load(url: string): string { return fs.readFileSync(url, 'utf8'); }
resolve(url: string, containingFile: string): string {
return path.resolve(path.dirname(containingFile), url);
}
}
/**
* This Analyzer will analyze the files that have decorated classes that need to be transformed.
*/
export class DecorationAnalyzer {
resourceManager = new NgccResourceLoader();
refEmitter = new ReferenceEmitter([
new LocalIdentifierStrategy(),
new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host),
// TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc
// projects only ever have one rootDir. Instead, ngcc should just switch its emitted imort based
// on whether a bestGuessOwningModule is present in the Reference.
new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)),
]);
dtsModuleScopeResolver = new MetadataDtsModuleScopeResolver(
this.typeChecker, this.reflectionHost, /* aliasGenerator */ null);
scopeRegistry = new LocalModuleScopeRegistry(
this.dtsModuleScopeResolver, this.refEmitter, /* aliasGenerator */ null);
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker);
moduleResolver = new ModuleResolver(this.program, this.options, this.host);
importGraph = new ImportGraph(this.moduleResolver);
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
handlers: DecoratorHandler<any, any>[] = [
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator, this.isCore),
new ComponentDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager,
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
new DirectiveDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
this.isCore),
new InjectableDecoratorHandler(
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, /* strictCtorDeps */ false),
new NgModuleDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry,
this.isCore, /* routeAnalyzer */ null, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
new PipeDecoratorHandler(
this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
this.isCore),
];
constructor(
private program: ts.Program, private options: ts.CompilerOptions,
private host: ts.CompilerHost, private typeChecker: ts.TypeChecker,
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
private rootDirs: AbsoluteFsPath[], private isCore: boolean) {}
/**
* Analyze a program to find all the decorated files should be transformed.
*
* @returns a map of the source files to the analysis for those files.
*/
analyzeProgram(): DecorationAnalyses {
const decorationAnalyses = new DecorationAnalyses();
const analysedFiles = this.program.getSourceFiles()
.map(sourceFile => this.analyzeFile(sourceFile))
.filter(isDefined);
analysedFiles.forEach(analysedFile => this.resolveFile(analysedFile));
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
compiledFiles.forEach(
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
return decorationAnalyses;
}
protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined {
const decoratedClasses = this.reflectionHost.findDecoratedClasses(sourceFile);
return decoratedClasses.length ? {
sourceFile,
analyzedClasses: decoratedClasses.map(clazz => this.analyzeClass(clazz)).filter(isDefined)
} :
undefined;
}
protected analyzeClass(clazz: DecoratedClass): AnalyzedClass|null {
const matchingHandlers = this.handlers
.map(handler => {
const detected =
handler.detect(clazz.declaration, clazz.decorators);
return {handler, detected};
})
.filter(isMatchingHandler);
if (matchingHandlers.length === 0) {
return null;
}
const detections: {handler: DecoratorHandler<any, any>, detected: DetectResult<any>}[] = [];
let hasWeakHandler: boolean = false;
let hasNonWeakHandler: boolean = false;
let hasPrimaryHandler: boolean = false;
for (const {handler, detected} of matchingHandlers) {
if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) {
continue;
} else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) {
// Clear all the WEAK handlers from the list of matches.
detections.length = 0;
}
if (hasPrimaryHandler && handler.precedence === HandlerPrecedence.PRIMARY) {
throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`);
}
detections.push({handler, detected});
if (handler.precedence === HandlerPrecedence.WEAK) {
hasWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.SHARED) {
hasNonWeakHandler = true;
} else if (handler.precedence === HandlerPrecedence.PRIMARY) {
hasNonWeakHandler = true;
hasPrimaryHandler = true;
}
}
const matches: {handler: DecoratorHandler<any, any>, analysis: any}[] = [];
const allDiagnostics: ts.Diagnostic[] = [];
for (const {handler, detected} of detections) {
const {analysis, diagnostics} = handler.analyze(clazz.declaration, detected.metadata);
if (diagnostics !== undefined) {
allDiagnostics.push(...diagnostics);
}
matches.push({handler, analysis});
}
return {...clazz, matches, diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined};
}
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
const constantPool = new ConstantPool();
const compiledClasses: CompiledClass[] = analyzedFile.analyzedClasses.map(analyzedClass => {
const compilation = this.compileClass(analyzedClass, constantPool);
return {...analyzedClass, compilation};
});
return {constantPool, sourceFile: analyzedFile.sourceFile, compiledClasses};
}
protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] {
const compilations: CompileResult[] = [];
for (const {handler, analysis} of clazz.matches) {
const result = handler.compile(clazz.declaration, analysis, constantPool);
if (Array.isArray(result)) {
compilations.push(...result);
} else {
compilations.push(result);
}
}
return compilations;
}
protected resolveFile(analyzedFile: AnalyzedFile): void {
analyzedFile.analyzedClasses.forEach(({declaration, matches}) => {
matches.forEach(({handler, analysis}) => {
if ((handler.resolve !== undefined) && analysis) {
handler.resolve(declaration, analysis);
}
});
});
}
}
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
handler is MatchingHandler<A, M> {
return !!handler.detected;
}

View File

@ -0,0 +1,120 @@
/**
* @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 {ReferencesRegistry} from '../../../src/ngtsc/annotations';
import {Reference} from '../../../src/ngtsc/imports';
import {Declaration} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
export interface ModuleWithProvidersInfo {
/**
* The declaration (in the .d.ts file) of the function that returns
* a `ModuleWithProviders object, but has a signature that needs
* a type parameter adding.
*/
declaration: ts.MethodDeclaration|ts.FunctionDeclaration;
/**
* The NgModule class declaration (in the .d.ts file) to add as a type parameter.
*/
ngModule: Declaration;
}
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
export const ModuleWithProvidersAnalyses = Map;
export class ModuleWithProvidersAnalyzer {
constructor(private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry) {}
analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
const analyses = new ModuleWithProvidersAnalyses();
const rootFiles = this.getRootFiles(program);
rootFiles.forEach(f => {
const fns = this.host.getModuleWithProvidersFunctions(f);
fns && fns.forEach(fn => {
const dtsFn = this.getDtsDeclaration(fn.declaration);
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
null;
if (!typeParam || isAnyKeyword(typeParam)) {
// Either we do not have a parameterized type or the type is `any`.
let ngModule = this.host.getDeclarationOfIdentifier(fn.ngModule);
if (!ngModule) {
throw new Error(
`Cannot find a declaration for NgModule ${fn.ngModule.text} referenced in ${fn.declaration.getText()}`);
}
// For internal (non-library) module references, redirect the module's value declaration
// to its type declaration.
if (ngModule.viaModule === null) {
const dtsNgModule = this.host.getDtsDeclaration(ngModule.node);
if (!dtsNgModule) {
throw new Error(
`No typings declaration can be found for the referenced NgModule class in ${fn.declaration.getText()}.`);
}
if (!ts.isClassDeclaration(dtsNgModule)) {
throw new Error(
`The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`);
}
// Record the usage of the internal module as it needs to become an exported symbol
const reference = new Reference(ngModule.node);
reference.addIdentifier(fn.ngModule);
this.referencesRegistry.add(ngModule.node, reference);
ngModule = {node: dtsNgModule, viaModule: null};
}
const dtsFile = dtsFn.getSourceFile();
const analysis = analyses.get(dtsFile) || [];
analysis.push({declaration: dtsFn, ngModule});
analyses.set(dtsFile, analysis);
}
});
});
return analyses;
}
private getRootFiles(program: ts.Program): ts.SourceFile[] {
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
}
private getDtsDeclaration(fn: ts.SignatureDeclaration) {
let dtsFn: ts.Declaration|null = null;
const containerClass = this.host.getClassSymbol(fn.parent);
const fnName = fn.name && ts.isIdentifier(fn.name) && fn.name.text;
if (containerClass && fnName) {
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
// Get the declaration of the matching static method
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
dtsClass.members
.find(
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
member.name.text === fnName) as ts.Declaration :
null;
} else {
dtsFn = this.host.getDtsDeclaration(fn);
}
if (!dtsFn) {
throw new Error(`Matching type declaration for ${fn.getText()} is missing`);
}
if (!isFunctionOrMethod(dtsFn)) {
throw new Error(
`Matching type declaration for ${fn.getText()} is not a function: ${dtsFn.getText()}`);
}
return dtsFn;
}
}
function isFunctionOrMethod(declaration: ts.Declaration): declaration is ts.FunctionDeclaration|
ts.MethodDeclaration {
return ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration);
}
function isAnyKeyword(typeParam: ts.TypeNode): typeParam is ts.KeywordTypeNode {
return typeParam.kind === ts.SyntaxKind.AnyKeyword;
}

View File

@ -0,0 +1,49 @@
/**
* @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 {ReferencesRegistry} from '../../../src/ngtsc/annotations';
import {Reference} from '../../../src/ngtsc/imports';
import {Declaration, ReflectionHost} from '../../../src/ngtsc/reflection';
import {hasNameIdentifier} from '../utils';
/**
* This is a place for DecoratorHandlers to register references that they
* find in their analysis of the code.
*
* This registry is used to ensure that these references are publicly exported
* from libraries that are compiled by ngcc.
*/
export class NgccReferencesRegistry implements ReferencesRegistry {
private map = new Map<ts.Identifier, Declaration>();
constructor(private host: ReflectionHost) {}
/**
* Register one or more references in the registry.
* Only `ResolveReference` references are stored. Other types are ignored.
* @param references A collection of references to register.
*/
add(source: ts.Declaration, ...references: Reference<ts.Declaration>[]): void {
references.forEach(ref => {
// Only store relative references. We are not interested in literals.
if (ref.bestGuessOwningModule === null && hasNameIdentifier(ref.node)) {
const declaration = this.host.getDeclarationOfIdentifier(ref.node.name);
if (declaration && hasNameIdentifier(declaration.node)) {
this.map.set(declaration.node.name, declaration);
}
}
});
}
/**
* Create and return a mapping for the registered resolved references.
* @returns A map of reference identifiers to reference declarations.
*/
getDeclarationMap(): Map<ts.Identifier, Declaration> { return this.map; }
}

View File

@ -0,0 +1,105 @@
/**
* @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 {Declaration} from '../../../src/ngtsc/reflection';
import {NgccReflectionHost} from '../host/ngcc_host';
import {hasNameIdentifier, isDefined} from '../utils';
import {NgccReferencesRegistry} from './ngcc_references_registry';
export interface ExportInfo {
identifier: string;
from: string;
dtsFrom?: string|null;
alias?: string|null;
}
export type PrivateDeclarationsAnalyses = ExportInfo[];
/**
* This class will analyze a program to find all the declared classes
* (i.e. on an NgModule) that are not publicly exported via an entry-point.
*/
export class PrivateDeclarationsAnalyzer {
constructor(
private host: NgccReflectionHost, private referencesRegistry: NgccReferencesRegistry) {}
analyzeProgram(program: ts.Program): PrivateDeclarationsAnalyses {
const rootFiles = this.getRootFiles(program);
return this.getPrivateDeclarations(rootFiles, this.referencesRegistry.getDeclarationMap());
}
private getRootFiles(program: ts.Program): ts.SourceFile[] {
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
}
private getPrivateDeclarations(
rootFiles: ts.SourceFile[],
declarations: Map<ts.Identifier, Declaration>): PrivateDeclarationsAnalyses {
const privateDeclarations: Map<ts.Identifier, Declaration> = new Map(declarations);
const exportAliasDeclarations: Map<ts.Identifier, string> = new Map();
rootFiles.forEach(f => {
const exports = this.host.getExportsOfModule(f);
if (exports) {
exports.forEach((declaration, exportedName) => {
if (hasNameIdentifier(declaration.node)) {
const privateDeclaration = privateDeclarations.get(declaration.node.name);
if (privateDeclaration) {
if (privateDeclaration.node !== declaration.node) {
throw new Error(`${declaration.node.name.text} is declared multiple times.`);
}
if (declaration.node.name.text === exportedName) {
// This declaration is public so we can remove it from the list
privateDeclarations.delete(declaration.node.name);
} else if (!this.host.getDtsDeclaration(declaration.node)) {
// The referenced declaration is exported publicly but via an alias.
// In some cases the original declaration is missing from the dts program, such as
// when rolling up (flattening) the dts files.
// This is because the original declaration gets renamed to the exported alias.
// There is a constraint on this which we cannot handle. Consider the following
// code:
//
// /src/entry_point.js:
// export {MyComponent as aliasedMyComponent} from './a';
// export {MyComponent} from './b';`
//
// /src/a.js:
// export class MyComponent {}
//
// /src/b.js:
// export class MyComponent {}
//
// //typings/entry_point.d.ts:
// export declare class aliasedMyComponent {}
// export declare class MyComponent {}
//
// In this case we would end up matching the `MyComponent` from `/src/a.js` to the
// `MyComponent` declared in `/typings/entry_point.d.ts` even though that
// declaration is actually for the `MyComponent` in `/src/b.js`.
exportAliasDeclarations.set(declaration.node.name, exportedName);
}
}
}
});
}
});
return Array.from(privateDeclarations.keys()).map(id => {
const from = id.getSourceFile().fileName;
const declaration = privateDeclarations.get(id) !;
const alias = exportAliasDeclarations.get(id) || null;
const dtsDeclaration = this.host.getDtsDeclaration(declaration.node);
const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName;
return {identifier: id.text, from, dtsFrom, alias};
});
}
}

View File

@ -0,0 +1,43 @@
/**
* @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 {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
export interface SwitchMarkerAnalysis {
sourceFile: ts.SourceFile;
declarations: SwitchableVariableDeclaration[];
}
export type SwitchMarkerAnalyses = Map<ts.SourceFile, SwitchMarkerAnalysis>;
export const SwitchMarkerAnalyses = Map;
/**
* This Analyzer will analyse the files that have an R3 switch marker in them
* that will be replaced.
*/
export class SwitchMarkerAnalyzer {
constructor(private host: NgccReflectionHost) {}
/**
* Analyze the files in the program to identify declarations that contain R3
* switch markers.
* @param program The program to analyze.
* @return A map of source files to analysis objects. The map will contain only the
* source files that had switch markers, and the analysis will contain an array of
* the declarations in that source file that contain the marker.
*/
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
const analyzedFiles = new SwitchMarkerAnalyses();
program.getSourceFiles().forEach(sourceFile => {
const declarations = this.host.getSwitchableDeclarations(sourceFile);
if (declarations.length) {
analyzedFiles.set(sourceFile, {sourceFile, declarations});
}
});
return analyzedFiles;
}
}

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 const IMPORT_PREFIX = 'ɵngcc';

View File

@ -0,0 +1,26 @@
/**
* @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 {Decorator} from '../../../src/ngtsc/reflection';
/**
* A simple container that holds the details of a decorated class that has been
* found in a `DecoratedFile`.
*/
export class DecoratedClass {
/**
* Initialize a `DecoratedClass` that was found in a `DecoratedFile`.
* @param name The name of the class that has been found. This is mostly used
* for informational purposes.
* @param declaration The TypeScript AST node where this class is declared
* @param decorators The collection of decorators that have been found on this class.
*/
constructor(
public name: string, public declaration: ts.Declaration, public decorators: Decorator[], ) {}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,608 @@
/**
* @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 {ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
import {getNameText, hasNameIdentifier} from '../utils';
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
/**
* ESM5 packages contain ECMAScript IIFE functions that act like classes. For example:
*
* ```
* var CommonModule = (function () {
* function CommonModule() {
* }
* CommonModule.decorators = [ ... ];
* ```
*
* * "Classes" are decorated if they have a static property called `decorators`.
* * Members are decorated if there is a matching key on a static property
* called `propDecorators`.
* * Constructor parameters decorators are found on an object returned from
* a static method called `ctorParameters`.
*
*/
export class Esm5ReflectionHost extends Esm2015ReflectionHost {
/**
* Check whether the given node actually represents a class.
*/
isClass(node: ts.Node): node is ts.NamedDeclaration {
return super.isClass(node) || !!this.getClassSymbol(node);
}
/**
* Determines whether the given declaration has a base class.
*
* In ES5, we need to determine if the IIFE wrapper takes a `_super` parameter .
*/
hasBaseClass(node: ts.Declaration): boolean {
const classSymbol = this.getClassSymbol(node);
if (!classSymbol) return false;
const iifeBody = classSymbol.valueDeclaration.parent;
if (!iifeBody || !ts.isBlock(iifeBody)) return false;
const iife = iifeBody.parent;
if (!iife || !ts.isFunctionExpression(iife)) return false;
return iife.parameters.length === 1 && isSuperIdentifier(iife.parameters[0].name);
}
/**
* Find a symbol for a node that we think is a class.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
* So we might need to dig around inside to get hold of the "class" symbol.
*
* `node` might be one of:
* - A class declaration (from a declaration file).
* - The declaration of the outer variable, which is assigned the result of the IIFE.
* - The function declaration inside the IIFE, which is eventually returned and assigned to the
* outer variable.
*
* @param node the top level declaration that represents an exported class or the function
* expression inside the IIFE.
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
*/
getClassSymbol(node: ts.Node): ts.Symbol|undefined {
const symbol = super.getClassSymbol(node);
if (symbol) return symbol;
if (ts.isVariableDeclaration(node)) {
const iifeBody = getIifeBody(node);
if (!iifeBody) return undefined;
const innerClassIdentifier = getReturnIdentifier(iifeBody);
if (!innerClassIdentifier) return undefined;
return this.checker.getSymbolAtLocation(innerClassIdentifier);
}
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(node);
return outerClassNode && this.getClassSymbol(outerClassNode);
}
/**
* Trace an identifier to its declaration, if possible.
*
* This method attempts to resolve the declaration of the given identifier, tracing back through
* imports and re-exports until the original declaration statement is found. A `Declaration`
* object is returned if the original declaration is found, or `null` is returned otherwise.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
* If we are looking for the declaration of the identifier of the inner function expression, we
* will get hold of the outer "class" variable declaration and return its identifier instead. See
* `getClassDeclarationFromInnerFunctionDeclaration()` for more info.
*
* @param id a TypeScript `ts.Identifier` to trace back to a declaration.
*
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
* otherwise.
*/
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
// Get the identifier for the outer class node (if any).
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(id.parent);
if (outerClassNode && hasNameIdentifier(outerClassNode)) {
id = outerClassNode.name;
}
// Resolve the identifier to a Symbol, and return the declaration of that.
return super.getDeclarationOfIdentifier(id);
}
/**
* Parse a function declaration to find the relevant metadata about it.
*
* In ESM5 we need to do special work with optional arguments to the function, since they get
* their own initializer statement that needs to be parsed and then not included in the "body"
* statements of the function.
*
* @param node the function declaration to parse.
* @returns an object containing the node, statements and parameters of the function.
*/
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
const parameters =
node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null}));
let lookingForParamInitializers = true;
const statements = node.body && node.body.statements.filter(s => {
lookingForParamInitializers =
lookingForParamInitializers && reflectParamInitializer(s, parameters);
// If we are no longer looking for parameter initializers then we include this statement
return !lookingForParamInitializers;
});
return {node, body: statements || null, parameters};
}
///////////// Protected Helpers /////////////
/**
* Find the declarations of the constructor parameters of a class identified by its symbol.
*
* In ESM5 there is no "class" so the constructor that we want is actually the declaration
* function itself.
*
* @param classSymbol the class whose parameters we want to find.
* @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in
* the class's constructor or null if there is no constructor.
*/
protected getConstructorParameterDeclarations(classSymbol: ts.Symbol):
ts.ParameterDeclaration[]|null {
const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration;
if (constructor.parameters.length > 0) {
return Array.from(constructor.parameters);
}
if (isSynthesizedConstructor(constructor)) {
return null;
}
return [];
}
/**
* Get the parameter type and decorators for the constructor of a class,
* where the information is stored on a static method of the class.
*
* In this case the decorators are stored in the body of a method
* (`ctorParatemers`) attached to the constructor function.
*
* Note that unlike ESM2015 this is a function expression rather than an arrow
* function:
*
* ```
* SomeDirective.ctorParameters = function() { return [
* { type: ViewContainerRef, },
* { type: TemplateRef, },
* { type: IterableDiffers, },
* { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] },
* ]; };
* ```
*
* @param paramDecoratorsProperty the property that holds the parameter info we want to get.
* @returns an array of objects containing the type and decorators for each parameter.
*/
protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null {
const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty);
const returnStatement = getReturnStatement(paramDecorators);
const expression = returnStatement && returnStatement.expression;
if (expression && ts.isArrayLiteralExpression(expression)) {
const elements = expression.elements;
return elements.map(reflectArrayElement).map(paramInfo => {
const typeExpression = paramInfo && paramInfo.get('type') || null;
const decoratorInfo = paramInfo && paramInfo.get('decorators') || null;
const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo);
return {typeExpression, decorators};
});
}
return null;
}
/**
* Reflect over a symbol and extract the member information, combining it with the
* provided decorator information, and whether it is a static member.
*
* If a class member uses accessors (e.g getters and/or setters) then it gets downleveled
* in ES5 to a single `Object.defineProperty()` call. In that case we must parse this
* call to extract the one or two ClassMember objects that represent the accessors.
*
* @param symbol the symbol for the member to reflect over.
* @param decorators an array of decorators associated with the member.
* @param isStatic true if this member is static, false if it is an instance property.
* @returns the reflected member information, or null if the symbol is not a member.
*/
protected reflectMembers(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean):
ClassMember[]|null {
const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
const propertyDefinition = node && getPropertyDefinition(node);
if (propertyDefinition) {
const members: ClassMember[] = [];
if (propertyDefinition.setter) {
members.push({
node,
implementation: propertyDefinition.setter,
kind: ClassMemberKind.Setter,
type: null,
name: symbol.name,
nameNode: null,
value: null,
isStatic: isStatic || false,
decorators: decorators || [],
});
// Prevent attaching the decorators to a potential getter. In ES5, we can't tell where the
// decorators were originally attached to, however we only want to attach them to a single
// `ClassMember` as otherwise ngtsc would handle the same decorators twice.
decorators = undefined;
}
if (propertyDefinition.getter) {
members.push({
node,
implementation: propertyDefinition.getter,
kind: ClassMemberKind.Getter,
type: null,
name: symbol.name,
nameNode: null,
value: null,
isStatic: isStatic || false,
decorators: decorators || [],
});
}
return members;
}
const members = super.reflectMembers(symbol, decorators, isStatic);
members && members.forEach(member => {
if (member && member.kind === ClassMemberKind.Method && member.isStatic && member.node &&
ts.isPropertyAccessExpression(member.node) && member.node.parent &&
ts.isBinaryExpression(member.node.parent) &&
ts.isFunctionExpression(member.node.parent.right)) {
// Recompute the implementation for this member:
// ES5 static methods are variable declarations so the declaration is actually the
// initializer of the variable assignment
member.implementation = member.node.parent.right;
}
});
return members;
}
/**
* Find statements related to the given class that may contain calls to a helper.
*
* In ESM5 code the helper calls are hidden inside the class's IIFE.
*
* @param classSymbol the class whose helper calls we are interested in. We expect this symbol
* to reference the inner identifier inside the IIFE.
* @returns an array of statements that may contain helper calls.
*/
protected getStatementsForClass(classSymbol: ts.Symbol): ts.Statement[] {
const classDeclaration = classSymbol.valueDeclaration;
return ts.isBlock(classDeclaration.parent) ? Array.from(classDeclaration.parent.statements) :
[];
}
}
///////////// Internal Helpers /////////////
/**
* Represents the details about property definitions that were set using `Object.defineProperty`.
*/
interface PropertyDefinition {
setter: ts.FunctionExpression|null;
getter: ts.FunctionExpression|null;
}
/**
* In ES5, getters and setters have been downleveled into call expressions of
* `Object.defineProperty`, such as
*
* ```
* Object.defineProperty(Clazz.prototype, "property", {
* get: function () {
* return 'value';
* },
* set: function (value) {
* this.value = value;
* },
* enumerable: true,
* configurable: true
* });
* ```
*
* This function inspects the given node to determine if it corresponds with such a call, and if so
* extracts the `set` and `get` function expressions from the descriptor object, if they exist.
*
* @param node The node to obtain the property definition from.
* @returns The property definition if the node corresponds with accessor, null otherwise.
*/
function getPropertyDefinition(node: ts.Node): PropertyDefinition|null {
if (!ts.isCallExpression(node)) return null;
const fn = node.expression;
if (!ts.isPropertyAccessExpression(fn) || !ts.isIdentifier(fn.expression) ||
fn.expression.text !== 'Object' || fn.name.text !== 'defineProperty')
return null;
const descriptor = node.arguments[2];
if (!descriptor || !ts.isObjectLiteralExpression(descriptor)) return null;
return {
setter: readPropertyFunctionExpression(descriptor, 'set'),
getter: readPropertyFunctionExpression(descriptor, 'get'),
};
}
function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name: string) {
const property = object.properties.find(
(p): p is ts.PropertyAssignment =>
ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === name);
return property && ts.isFunctionExpression(property.initializer) && property.initializer || null;
}
/**
* Get the actual (outer) declaration of a class.
*
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE and
* returned to be assigned to a variable outside the IIFE, which is what the rest of the program
* interacts with.
*
* Given the inner function declaration, we want to get to the declaration of the outer variable
* that represents the class.
*
* @param node a node that could be the function expression inside an ES5 class IIFE.
* @returns the outer variable declaration or `undefined` if it is not a "class".
*/
function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.VariableDeclaration|
undefined {
if (ts.isFunctionDeclaration(node)) {
// It might be the function expression inside the IIFE. We need to go 5 levels up...
// 1. IIFE body.
let outerNode = node.parent;
if (!outerNode || !ts.isBlock(outerNode)) return undefined;
// 2. IIFE function expression.
outerNode = outerNode.parent;
if (!outerNode || !ts.isFunctionExpression(outerNode)) return undefined;
// 3. IIFE call expression.
outerNode = outerNode.parent;
if (!outerNode || !ts.isCallExpression(outerNode)) return undefined;
// 4. Parenthesis around IIFE.
outerNode = outerNode.parent;
if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return undefined;
// 5. Outer variable declaration.
outerNode = outerNode.parent;
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined;
return outerNode;
}
return undefined;
}
function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined {
if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) {
return undefined;
}
const call = declaration.initializer;
return ts.isCallExpression(call.expression) &&
ts.isFunctionExpression(call.expression.expression) ?
call.expression.expression.body :
undefined;
}
function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined {
const returnStatement = body.statements.find(ts.isReturnStatement);
return returnStatement && returnStatement.expression &&
ts.isIdentifier(returnStatement.expression) ?
returnStatement.expression :
undefined;
}
function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnStatement|undefined {
return declaration && ts.isFunctionExpression(declaration) ?
declaration.body.statements.find(ts.isReturnStatement) :
undefined;
}
function reflectArrayElement(element: ts.Expression) {
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
}
/**
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
* in the case no user-defined constructor exists and e.g. property initializers are used.
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
* compiler generates a synthetic constructor.
*
* We need to identify such constructors as ngcc needs to be able to tell if a class did
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
* empty constructor apart from a synthesized constructor, but fortunately that does not
* matter for the code generated by ngtsc.
*
* When a class has a superclass however, a synthesized constructor must not be considered
* as a user-defined constructor as that prevents a base factory call from being created by
* ngtsc, resulting in a factory function that does not inject the dependencies of the
* superclass. Hence, we identify a default synthesized super call in the constructor body,
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
*
* @param constructor a constructor function to test
* @returns true if the constructor appears to have been synthesized
*/
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
if (!constructor.body) return false;
const firstStatement = constructor.body.statements[0];
if (!firstStatement) return false;
return isSynthesizedSuperThisAssignment(firstStatement) ||
isSynthesizedSuperReturnStatement(firstStatement);
}
/**
* Identifies a synthesized super call of the form:
*
* ```
* var _this = _super !== null && _super.apply(this, arguments) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
if (!ts.isVariableStatement(statement)) return false;
const variableDeclarations = statement.declarationList.declarations;
if (variableDeclarations.length !== 1) return false;
const variableDeclaration = variableDeclarations[0];
if (!ts.isIdentifier(variableDeclaration.name) ||
!variableDeclaration.name.text.startsWith('_this'))
return false;
const initializer = variableDeclaration.initializer;
if (!initializer) return false;
return isSynthesizedDefaultSuperCall(initializer);
}
/**
* Identifies a synthesized super call of the form:
*
* ```
* return _super !== null && _super.apply(this, arguments) || this;
* ```
*
* @param statement a statement that may be a synthesized super call
* @returns true if the statement looks like a synthesized super call
*/
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
if (!ts.isReturnStatement(statement)) return false;
const expression = statement.expression;
if (!expression) return false;
return isSynthesizedDefaultSuperCall(expression);
}
/**
* Tests whether the expression is of the form:
*
* ```
* _super !== null && _super.apply(this, arguments) || this;
* ```
*
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
const left = expression.left;
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
}
function isSuperNotNull(expression: ts.Expression): boolean {
return isBinaryExpr(expression, ts.SyntaxKind.ExclamationEqualsEqualsToken) &&
isSuperIdentifier(expression.left);
}
/**
* Tests whether the expression is of the form
*
* ```
* _super.apply(this, arguments)
* ```
*
* @param expression an expression that may represent a default super call
* @returns true if the expression corresponds with the above form
*/
function isSuperApplyCall(expression: ts.Expression): boolean {
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
const targetFn = expression.expression;
if (!ts.isPropertyAccessExpression(targetFn)) return false;
if (!isSuperIdentifier(targetFn.expression)) return false;
if (targetFn.name.text !== 'apply') return false;
const thisArgument = expression.arguments[0];
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
const argumentsArgument = expression.arguments[1];
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
}
function isBinaryExpr(
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
}
function isSuperIdentifier(node: ts.Node): boolean {
// Verify that the identifier is prefixed with `_super`. We don't test for equivalence
// as TypeScript may have suffixed the name, e.g. `_super_1` to avoid name conflicts.
// Requiring only a prefix should be sufficiently accurate.
return ts.isIdentifier(node) && node.text.startsWith('_super');
}
/**
* Parse the statement to extract the ESM5 parameter initializer if there is one.
* If one is found, add it to the appropriate parameter in the `parameters` collection.
*
* The form we are looking for is:
*
* ```
* if (arg === void 0) { arg = initializer; }
* ```
*
* @param statement a statement that may be initializing an optional parameter
* @param parameters the collection of parameters that were found in the function definition
* @returns true if the statement was a parameter initializer
*/
function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) {
if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) &&
ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) {
const ifStatementComparison = statement.expression; // (arg === void 0)
const thenStatement = statement.thenStatement.statements[0]; // arg = initializer;
if (isAssignmentStatement(thenStatement)) {
const comparisonName = ifStatementComparison.left.text;
const assignmentName = thenStatement.expression.left.text;
if (comparisonName === assignmentName) {
const parameter = parameters.find(p => p.name === comparisonName);
if (parameter) {
parameter.initializer = thenStatement.expression.right;
return true;
}
}
}
}
return false;
}
function isUndefinedComparison(expression: ts.Expression): expression is ts.Expression&
{left: ts.Identifier, right: ts.Expression} {
return ts.isBinaryExpression(expression) &&
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left);
}

View File

@ -0,0 +1,72 @@
/**
* @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 {ReflectionHost} from '../../../src/ngtsc/reflection';
import {DecoratedClass} from './decorated_class';
export const PRE_R3_MARKER = '__PRE_R3__';
export const POST_R3_MARKER = '__POST_R3__';
export type SwitchableVariableDeclaration = ts.VariableDeclaration & {initializer: ts.Identifier};
export function isSwitchableVariableDeclaration(node: ts.Node):
node is SwitchableVariableDeclaration {
return ts.isVariableDeclaration(node) && !!node.initializer &&
ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_R3_MARKER);
}
/**
* A structure returned from `getModuleWithProviderInfo` that describes functions
* that return ModuleWithProviders objects.
*/
export interface ModuleWithProvidersFunction {
/**
* The declaration of the function that returns the `ModuleWithProviders` object.
*/
declaration: ts.SignatureDeclaration;
/**
* The identifier of the `ngModule` property on the `ModuleWithProviders` object.
*/
ngModule: ts.Identifier;
}
/**
* A reflection host that has extra methods for looking at non-Typescript package formats
*/
export interface NgccReflectionHost extends ReflectionHost {
/**
* Find a symbol for a declaration that we think is a class.
* @param declaration The declaration whose symbol we are finding
* @returns the symbol for the declaration or `undefined` if it is not
* a "class" or has no symbol.
*/
getClassSymbol(node: ts.Node): ts.Symbol|undefined;
/**
* Search the given module for variable declarations in which the initializer
* is an identifier marked with the `PRE_R3_MARKER`.
* @param module The module in which to search for switchable declarations.
* @returns An array of variable declarations that match.
*/
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
/**
* Find all the classes that contain decorations in a given file.
* @param sourceFile The source file to search for decorated classes.
* @returns An array of decorated classes.
*/
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[];
/**
* Search the given source file for exported functions and static class methods that return
* ModuleWithProviders objects.
* @param f The source file to search for these functions
* @returns An array of info items about each of the functions that return ModuleWithProviders
* objects.
*/
getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersFunction[];
}

View File

@ -0,0 +1,87 @@
/**
* @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 path from 'canonical-path';
import * as yargs from 'yargs';
import {checkMarkerFile, writeMarkerFile} from './packages/build_marker';
import {DependencyHost} from './packages/dependency_host';
import {DependencyResolver} from './packages/dependency_resolver';
import {EntryPointFormat} from './packages/entry_point';
import {makeEntryPointBundle} from './packages/entry_point_bundle';
import {EntryPointFinder} from './packages/entry_point_finder';
import {Transformer} from './packages/transformer';
export function mainNgcc(args: string[]): number {
const options =
yargs
.option('s', {
alias: 'source',
describe: 'A path to the root folder to compile.',
default: './node_modules'
})
.option('f', {
alias: 'formats',
array: true,
describe: 'An array of formats to compile.',
default: ['fesm2015', 'esm2015', 'fesm5', 'esm5']
})
.option('t', {
alias: 'target',
describe: 'A path to a root folder where the compiled files will be written.',
defaultDescription: 'The `source` folder.'
})
.help()
.parse(args);
const sourcePath: string = path.resolve(options['s']);
const formats: EntryPointFormat[] = options['f'];
const targetPath: string = options['t'] || sourcePath;
const transformer = new Transformer(sourcePath, targetPath);
const host = new DependencyHost();
const resolver = new DependencyResolver(host);
const finder = new EntryPointFinder(resolver);
try {
const {entryPoints} = finder.findEntryPoints(sourcePath);
entryPoints.forEach(entryPoint => {
// Are we compiling the Angular core?
const isCore = entryPoint.name === '@angular/core';
// We transform the d.ts typings files while transforming one of the formats.
// This variable decides with which of the available formats to do this transform.
// It is marginally faster to process via the flat file if available.
const dtsTransformFormat: EntryPointFormat = entryPoint.fesm2015 ? 'fesm2015' : 'esm2015';
formats.forEach(format => {
if (checkMarkerFile(entryPoint, format)) {
console.warn(`Skipping ${entryPoint.name} : ${format} (already built).`);
return;
}
const bundle =
makeEntryPointBundle(entryPoint, isCore, format, format === dtsTransformFormat);
if (bundle === null) {
console.warn(
`Skipping ${entryPoint.name} : ${format} (no entry point file for this format).`);
} else {
transformer.transform(entryPoint, isCore, bundle);
}
// Write the built-with-ngcc marker
writeMarkerFile(entryPoint, format);
});
});
} catch (e) {
console.error(e.stack || e.message);
return 1;
}
return 0;
}

View File

@ -0,0 +1,41 @@
/**
* @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 {resolve} from 'canonical-path';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {EntryPoint, EntryPointFormat} from './entry_point';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
function getMarkerPath(entryPointPath: string, format: EntryPointFormat) {
return resolve(entryPointPath, `__modified_by_ngcc_for_${format}__`);
}
/**
* Check whether there is a build marker for the given entry point and format.
* @param entryPoint the entry point to check for a marker.
* @param format the format for which we are checking for a marker.
*/
export function checkMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat): boolean {
const markerPath = getMarkerPath(entryPoint.path, format);
const markerExists = existsSync(markerPath);
if (markerExists) {
const previousVersion = readFileSync(markerPath, 'utf8');
if (previousVersion !== NGCC_VERSION) {
throw new Error(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
}
}
return markerExists;
}
export function writeMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat) {
const markerPath = getMarkerPath(entryPoint.path, format);
writeFileSync(markerPath, NGCC_VERSION, 'utf8');
}

View File

@ -0,0 +1,74 @@
/**
* @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 {dirname, resolve} from 'canonical-path';
import {existsSync, lstatSync, readdirSync} from 'fs';
import * as ts from 'typescript';
/**
* An entry point bundle contains one or two programs, e.g. `src` and `dts`,
* that are compiled via TypeScript.
*
* To aid with processing the program, this interface exposes the program itself,
* as well as path and TS file of the entry-point to the program and the r3Symbols
* file, if appropriate.
*/
export interface BundleProgram {
program: ts.Program;
options: ts.CompilerOptions;
host: ts.CompilerHost;
path: string;
file: ts.SourceFile;
r3SymbolsPath: string|null;
r3SymbolsFile: ts.SourceFile|null;
}
/**
* Create a bundle program.
*/
export function makeBundleProgram(
isCore: boolean, path: string, r3FileName: string, options: ts.CompilerOptions,
host: ts.CompilerHost): BundleProgram {
const r3SymbolsPath = isCore ? findR3SymbolsPath(dirname(path), r3FileName) : null;
const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path];
const program = ts.createProgram(rootPaths, options, host);
const file = program.getSourceFile(path) !;
const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null;
return {program, options, host, path, file, r3SymbolsPath, r3SymbolsFile};
}
/**
* Search the given directory hierarchy to find the path to the `r3_symbols` file.
*/
export function findR3SymbolsPath(directory: string, filename: string): string|null {
const r3SymbolsFilePath = resolve(directory, filename);
if (existsSync(r3SymbolsFilePath)) {
return r3SymbolsFilePath;
}
const subDirectories =
readdirSync(directory)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = lstatSync(resolve(directory, p));
return stat.isDirectory() && !stat.isSymbolicLink();
});
for (const subDirectory of subDirectories) {
const r3SymbolsFilePath = findR3SymbolsPath(resolve(directory, subDirectory, ), filename);
if (r3SymbolsFilePath) {
return r3SymbolsFilePath;
}
}
return null;
}

View File

@ -0,0 +1,148 @@
/**
* @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 path from 'canonical-path';
import * as fs from 'fs';
import * as ts from 'typescript';
/**
* Helper functions for computing dependencies.
*/
export class DependencyHost {
/**
* Get a list of the resolved paths to all the dependencies of this entry point.
* @param from An absolute path to the file whose dependencies we want to get.
* @param resolved A set that will have the absolute paths of resolved entry points added to it.
* @param missing A set that will have the dependencies that could not be found added to it.
* @param deepImports A set that will have the import paths that exist but cannot be mapped to
* entry-points, i.e. deep-imports.
* @param internal A set that is used to track internal dependencies to prevent getting stuck in a
* circular dependency loop.
*/
computeDependencies(
from: string, resolved: Set<string>, missing: Set<string>, deepImports: Set<string>,
internal: Set<string> = new Set()): void {
const fromContents = fs.readFileSync(from, 'utf8');
if (!this.hasImportOrReexportStatements(fromContents)) {
return;
}
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
const sf =
ts.createSourceFile(from, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
sf.statements
// filter out statements that are not imports or reexports
.filter(this.isStringImportOrReexport)
// Grab the id of the module that is being imported
.map(stmt => stmt.moduleSpecifier.text)
// Resolve this module id into an absolute path
.forEach(importPath => {
if (importPath.startsWith('.')) {
// This is an internal import so follow it
const internalDependency = this.resolveInternal(from, importPath);
// Avoid circular dependencies
if (!internal.has(internalDependency)) {
internal.add(internalDependency);
this.computeDependencies(
internalDependency, resolved, missing, deepImports, internal);
}
} else {
const resolvedEntryPoint = this.tryResolveEntryPoint(from, importPath);
if (resolvedEntryPoint !== null) {
resolved.add(resolvedEntryPoint);
} else {
// If the import could not be resolved as entry point, it either does not exist
// at all or is a deep import.
const deeplyImportedFile = this.tryResolve(from, importPath);
if (deeplyImportedFile !== null) {
deepImports.add(importPath);
} else {
missing.add(importPath);
}
}
}
});
}
/**
* Resolve an internal module import.
* @param from the absolute file path from where to start trying to resolve this module
* @param to the module specifier of the internal dependency to resolve
* @returns the resolved path to the import.
*/
resolveInternal(from: string, to: string): string {
const fromDirectory = path.dirname(from);
// `fromDirectory` is absolute so we don't need to worry about telling `require.resolve`
// about it - unlike `tryResolve` below.
return require.resolve(path.resolve(fromDirectory, to));
}
/**
* We don't want to resolve external dependencies directly because if it is a path to a
* sub-entry-point (e.g. @angular/animations/browser rather than @angular/animations)
* then `require.resolve()` may return a path to a UMD bundle, which may actually live
* in the folder containing the sub-entry-point
* (e.g. @angular/animations/bundles/animations-browser.umd.js).
*
* Instead we try to resolve it as a package, which is what we would need anyway for it to be
* compilable by ngcc.
*
* If `to` is actually a path to a file then this will fail, which is what we want.
*
* @param from the file path from where to start trying to resolve this module
* @param to the module specifier of the dependency to resolve
* @returns the resolved path to the entry point directory of the import or null
* if it cannot be resolved.
*/
tryResolveEntryPoint(from: string, to: string): string|null {
const entryPoint = this.tryResolve(from, `${to}/package.json`);
return entryPoint && path.dirname(entryPoint);
}
/**
* Resolve the absolute path of a module from a particular starting point.
*
* @param from the file path from where to start trying to resolve this module
* @param to the module specifier of the dependency to resolve
* @returns an absolute path to the entry-point of the dependency or null if it could not be
* resolved.
*/
tryResolve(from: string, to: string): string|null {
try {
return require.resolve(to, {paths: [from]});
} catch (e) {
return null;
}
}
/**
* Check whether the given statement is an import with a string literal module specifier.
* @param stmt the statement node to check.
* @returns true if the statement is an import with a string literal module specifier.
*/
isStringImportOrReexport(stmt: ts.Statement): stmt is ts.ImportDeclaration&
{moduleSpecifier: ts.StringLiteral} {
return ts.isImportDeclaration(stmt) ||
ts.isExportDeclaration(stmt) && !!stmt.moduleSpecifier &&
ts.isStringLiteral(stmt.moduleSpecifier);
}
/**
* Check whether a source file needs to be parsed for imports.
* This is a performance short-circuit, which saves us from creating
* a TypeScript AST unnecessarily.
*
* @param source The content of the source file to check.
*
* @returns false if there are definitely no import or re-export statements
* in this file, true otherwise.
*/
hasImportOrReexportStatements(source: string): boolean {
return /(import|export)\s.+from/.test(source);
}
}

View File

@ -0,0 +1,138 @@
/**
* @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 {DepGraph} from 'dependency-graph';
import {DependencyHost} from './dependency_host';
import {EntryPoint} from './entry_point';
/**
* Holds information about entry points that are removed because
* they have dependencies that are missing (directly or transitively).
*
* This might not be an error, because such an entry point might not actually be used
* in the application. If it is used then the `ngc` application compilation would
* fail also, so we don't need ngcc to catch this.
*
* For example, consider an application that uses the `@angular/router` package.
* This package includes an entry-point called `@angular/router/upgrade`, which has a dependency
* on the `@angular/upgrade` package.
* If the application never uses code from `@angular/router/upgrade` then there is no need for
* `@angular/upgrade` to be installed.
* In this case the ngcc tool should just ignore the `@angular/router/upgrade` end-point.
*/
export interface InvalidEntryPoint {
entryPoint: EntryPoint;
missingDependencies: string[];
}
/**
* Holds information about dependencies of an entry-point that do not need to be processed
* by the ngcc tool.
*
* For example, the `rxjs` package does not contain any Angular decorators that need to be
* compiled and so this can be safely ignored by ngcc.
*/
export interface IgnoredDependency {
entryPoint: EntryPoint;
dependencyPath: string;
}
/**
* The result of sorting the entry-points by their dependencies.
*
* The `entryPoints` array will be ordered so that no entry point depends upon an entry point that
* appears later in the array.
*
* Some entry points or their dependencies may be have been ignored. These are captured for
* diagnostic purposes in `invalidEntryPoints` and `ignoredDependencies` respectively.
*/
export interface SortedEntryPointsInfo {
entryPoints: EntryPoint[];
invalidEntryPoints: InvalidEntryPoint[];
ignoredDependencies: IgnoredDependency[];
}
/**
* A class that resolves dependencies between entry-points.
*/
export class DependencyResolver {
constructor(private host: DependencyHost) {}
/**
* Sort the array of entry points so that the dependant entry points always come later than
* their dependencies in the array.
* @param entryPoints An array entry points to sort.
* @returns the result of sorting the entry points.
*/
sortEntryPointsByDependency(entryPoints: EntryPoint[]): SortedEntryPointsInfo {
const invalidEntryPoints: InvalidEntryPoint[] = [];
const ignoredDependencies: IgnoredDependency[] = [];
const graph = new DepGraph<EntryPoint>();
// Add the entry points to the graph as nodes
entryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint));
// Now add the dependencies between them
entryPoints.forEach(entryPoint => {
const entryPointPath = entryPoint.fesm2015 || entryPoint.esm2015;
if (!entryPointPath) {
throw new Error(
`ESM2015 format (flat and non-flat) missing in '${entryPoint.path}' entry-point.`);
}
const dependencies = new Set<string>();
const missing = new Set<string>();
const deepImports = new Set<string>();
this.host.computeDependencies(entryPointPath, dependencies, missing, deepImports);
if (missing.size > 0) {
// This entry point has dependencies that are missing
// so remove it from the graph.
removeNodes(entryPoint, Array.from(missing));
} else {
dependencies.forEach(dependencyPath => {
if (graph.hasNode(dependencyPath)) {
// The dependency path maps to an entry point that exists in the graph
// so add the dependency.
graph.addDependency(entryPoint.path, dependencyPath);
} else if (invalidEntryPoints.some(i => i.entryPoint.path === dependencyPath)) {
// The dependency path maps to an entry-point that was previously removed
// from the graph, so remove this entry-point as well.
removeNodes(entryPoint, [dependencyPath]);
} else {
// The dependency path points to a package that ngcc does not care about.
ignoredDependencies.push({entryPoint, dependencyPath});
}
});
}
if (deepImports.size) {
const imports = Array.from(deepImports).map(i => `'${i}'`).join(', ');
console.warn(
`Entry point '${entryPoint.name}' contains deep imports into ${imports}. ` +
`This is probably not a problem, but may cause the compilation of entry points to be out of order.`);
}
});
// The map now only holds entry-points that ngcc cares about and whose dependencies
// (direct and transitive) all exist.
return {
entryPoints: graph.overallOrder().map(path => graph.getNodeData(path)),
invalidEntryPoints,
ignoredDependencies
};
function removeNodes(entryPoint: EntryPoint, missingDependencies: string[]) {
const nodesToRemove = [entryPoint.path, ...graph.dependantsOf(entryPoint.path)];
nodesToRemove.forEach(node => {
invalidEntryPoints.push({entryPoint: graph.getNodeData(node), missingDependencies});
graph.removeNode(node);
});
}
}
}

View File

@ -0,0 +1,142 @@
/**
* @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 path from 'canonical-path';
import * as fs from 'fs';
/**
* An object containing paths to the entry-points for each format.
*/
export interface EntryPointPaths {
esm5?: string;
fesm5?: string;
esm2015?: string;
fesm2015?: string;
umd?: string;
}
/**
* The possible values for the format of an entry-point.
*/
export type EntryPointFormat = keyof(EntryPointPaths);
/**
* An object containing information about an entry-point, including paths
* to each of the possible entry-point formats.
*/
export interface EntryPoint extends EntryPointPaths {
/** The name of the package (e.g. `@angular/core`). */
name: string;
/** The path to the package that contains this entry-point. */
package: string;
/** The path to this entry point. */
path: string;
/** The path to a typings (.d.ts) file for this entry-point. */
typings: string;
}
/**
* The properties that may be loaded from the `package.json` file.
*/
interface EntryPointPackageJson {
name: string;
fesm2015?: string;
fesm5?: string;
es2015?: string; // if exists then it is actually FESM2015
esm2015?: string;
esm5?: string;
main?: string; // UMD
module?: string; // if exists then it is actually FESM5
types?: string; // Synonymous to `typings` property - see https://bit.ly/2OgWp2H
typings?: string; // TypeScript .d.ts files
}
/**
* Parses the JSON from a package.json file.
* @param packageJsonPath the absolute path to the package.json file.
* @returns JSON from the package.json file if it is valid, `null` otherwise.
*/
function loadEntryPointPackage(packageJsonPath: string): EntryPointPackageJson|null {
try {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
} catch (e) {
// We may have run into a package.json with unexpected symbols
console.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`);
return null;
}
}
/**
* Try to get an entry point from the given path.
* @param packagePath the absolute path to the containing npm package
* @param entryPointPath the absolute path to the potential entry point.
* @returns Info about the entry point if it is valid, `null` otherwise.
*/
export function getEntryPointInfo(packagePath: string, entryPointPath: string): EntryPoint|null {
const packageJsonPath = path.resolve(entryPointPath, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return null;
}
const entryPointPackageJson = loadEntryPointPackage(packageJsonPath);
if (!entryPointPackageJson) {
return null;
}
// If there is `esm2015` then `es2015` will be FESM2015, otherwise ESM2015.
// If there is `esm5` then `module` will be FESM5, otherwise it will be ESM5.
const {
name,
module: modulePath,
types,
typings = types, // synonymous
es2015,
fesm2015 = es2015, // synonymous
fesm5 = modulePath, // synonymous
esm2015,
esm5,
main
} = entryPointPackageJson;
// Minimum requirement is that we have typings and one of esm2015 or fesm2015 formats.
if (!typings || !(fesm2015 || esm2015)) {
return null;
}
// Also there must exist a `metadata.json` file next to the typings entry-point.
const metadataPath =
path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
if (!fs.existsSync(metadataPath)) {
return null;
}
const entryPointInfo: EntryPoint = {
name,
package: packagePath,
path: entryPointPath,
typings: path.resolve(entryPointPath, typings),
};
if (esm2015) {
entryPointInfo.esm2015 = path.resolve(entryPointPath, esm2015);
}
if (fesm2015) {
entryPointInfo.fesm2015 = path.resolve(entryPointPath, fesm2015);
}
if (fesm5) {
entryPointInfo.fesm5 = path.resolve(entryPointPath, fesm5);
}
if (esm5) {
entryPointInfo.esm5 = path.resolve(entryPointPath, esm5);
}
if (main) {
entryPointInfo.umd = path.resolve(entryPointPath, main);
}
return entryPointInfo;
}

View File

@ -0,0 +1,61 @@
/**
* @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 '../../../src/ngtsc/path';
import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPoint, EntryPointFormat} from './entry_point';
/**
* A bundle of files and paths (and TS programs) that correspond to a particular
* format of a package entry-point.
*/
export interface EntryPointBundle {
format: EntryPointFormat;
isFlat: boolean;
rootDirs: AbsoluteFsPath[];
src: BundleProgram;
dts: BundleProgram|null;
}
/**
* Get an object that describes a formatted bundle for an entry-point.
* @param entryPoint The entry-point that contains the bundle.
* @param format The format of the bundle.
* @param transformDts True if processing this bundle should also process its `.d.ts` files.
*/
export function makeEntryPointBundle(
entryPoint: EntryPoint, isCore: boolean, format: EntryPointFormat,
transformDts: boolean): EntryPointBundle|null {
// Bail out if the entry-point does not have this format.
const path = entryPoint[format];
if (!path) {
return null;
}
// Create the TS program and necessary helpers.
const options: ts.CompilerOptions = {
allowJs: true,
maxNodeModuleJsDepth: Infinity,
rootDir: entryPoint.path,
};
const host = ts.createCompilerHost(options);
const rootDirs = [AbsoluteFsPath.from(entryPoint.path)];
// Create the bundle programs, as necessary.
const src = makeBundleProgram(isCore, path, 'r3_symbols.js', options, host);
const dts = transformDts ?
makeBundleProgram(isCore, entryPoint.typings, 'r3_symbols.d.ts', options, host) :
null;
const isFlat = src.r3SymbolsFile === null;
return {format, rootDirs, isFlat, src, dts};
}

View File

@ -0,0 +1,111 @@
/**
* @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 path from 'canonical-path';
import * as fs from 'fs';
import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver';
import {EntryPoint, getEntryPointInfo} from './entry_point';
export class EntryPointFinder {
constructor(private resolver: DependencyResolver) {}
/**
* Search the given directory, and sub-directories, for Angular package entry points.
* @param sourceDirectory An absolute path to the directory to search for entry points.
*/
findEntryPoints(sourceDirectory: string): SortedEntryPointsInfo {
const unsortedEntryPoints = walkDirectoryForEntryPoints(sourceDirectory);
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints);
}
}
/**
* Look for entry points that need to be compiled, starting at the source directory.
* The function will recurse into directories that start with `@...`, e.g. `@angular/...`.
* @param sourceDirectory An absolute path to the root directory where searching begins.
*/
function walkDirectoryForEntryPoints(sourceDirectory: string): EntryPoint[] {
const entryPoints: EntryPoint[] = [];
fs.readdirSync(sourceDirectory)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = fs.lstatSync(path.resolve(sourceDirectory, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(p => {
// Either the directory is a potential package or a namespace containing packages (e.g
// `@angular`).
const packagePath = path.join(sourceDirectory, p);
if (p.startsWith('@')) {
entryPoints.push(...walkDirectoryForEntryPoints(packagePath));
} else {
entryPoints.push(...getEntryPointsForPackage(packagePath));
// Also check for any nested node_modules in this package
const nestedNodeModulesPath = path.resolve(packagePath, 'node_modules');
if (fs.existsSync(nestedNodeModulesPath)) {
entryPoints.push(...walkDirectoryForEntryPoints(nestedNodeModulesPath));
}
}
});
return entryPoints;
}
/**
* Recurse the folder structure looking for all the entry points
* @param packagePath The absolute path to an npm package that may contain entry points
* @returns An array of entry points that were discovered.
*/
function getEntryPointsForPackage(packagePath: string): EntryPoint[] {
const entryPoints: EntryPoint[] = [];
// Try to get an entry point from the top level package directory
const topLevelEntryPoint = getEntryPointInfo(packagePath, packagePath);
if (topLevelEntryPoint !== null) {
entryPoints.push(topLevelEntryPoint);
}
// Now search all the directories of this package for possible entry points
walkDirectory(packagePath, subdir => {
const subEntryPoint = getEntryPointInfo(packagePath, subdir);
if (subEntryPoint !== null) {
entryPoints.push(subEntryPoint);
}
});
return entryPoints;
}
/**
* Recursively walk a directory and its sub-directories, applying a given
* function to each directory.
* @param dir the directory to recursively walk.
* @param fn the function to apply to each directory.
*/
function walkDirectory(dir: string, fn: (dir: string) => void) {
return fs
.readdirSync(dir)
// Not interested in hidden files
.filter(p => !p.startsWith('.'))
// Ignore node_modules
.filter(p => p !== 'node_modules')
// Only interested in directories (and only those that are not symlinks)
.filter(p => {
const stat = fs.lstatSync(path.resolve(dir, p));
return stat.isDirectory() && !stat.isSymbolicLink();
})
.forEach(subdir => {
subdir = path.resolve(dir, subdir);
fn(subdir);
walkDirectory(subdir, fn);
});
}

View File

@ -0,0 +1,147 @@
/**
* @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 {dirname} from 'canonical-path';
import {existsSync, writeFileSync} from 'fs';
import {mkdir, mv} from 'shelljs';
import * as ts from 'typescript';
import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry';
import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../host/esm2015_host';
import {Esm5ReflectionHost} from '../host/esm5_host';
import {NgccReflectionHost} from '../host/ngcc_host';
import {Esm5Renderer} from '../rendering/esm5_renderer';
import {EsmRenderer} from '../rendering/esm_renderer';
import {FileInfo, Renderer} from '../rendering/renderer';
import {EntryPoint} from './entry_point';
import {EntryPointBundle} from './entry_point_bundle';
/**
* A Package is stored in a directory on disk and that directory can contain one or more package
* formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files).
*
* Each of these formats exposes one or more entry points, which are source files that need to be
* parsed to identify the decorated exported classes that need to be analyzed and compiled by one or
* more `DecoratorHandler` objects.
*
* Each entry point to a package is identified by a `package.json` which contains properties that
* indicate what formatted bundles are accessible via this end-point.
*
* Each bundle is identified by a root `SourceFile` that can be parsed and analyzed to
* identify classes that need to be transformed; and then finally rendered and written to disk.
*
* Along with the source files, the corresponding source maps (either inline or external) and
* `.d.ts` files are transformed accordingly.
*
* - Flat file packages have all the classes in a single file.
* - Other packages may re-export classes from other non-entry point files.
* - Some formats may contain multiple "modules" in a single file.
*/
export class Transformer {
constructor(private sourcePath: string, private targetPath: string) {}
/**
* Transform the source (and typings) files of a bundle.
* @param bundle the bundle to transform.
*/
transform(entryPoint: EntryPoint, isCore: boolean, bundle: EntryPointBundle): void {
console.warn(`Compiling ${entryPoint.name} - ${bundle.format}`);
const reflectionHost = this.getHost(isCore, bundle);
// Parse and analyze the files.
const {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle);
// Transform the source files and source maps.
const renderer = this.getRenderer(reflectionHost, isCore, bundle);
const renderedFiles = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
// Write out all the transformed files.
renderedFiles.forEach(file => this.writeFile(file));
}
getHost(isCore: boolean, bundle: EntryPointBundle): NgccReflectionHost {
const typeChecker = bundle.src.program.getTypeChecker();
switch (bundle.format) {
case 'esm2015':
case 'fesm2015':
return new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts);
case 'esm5':
case 'fesm5':
return new Esm5ReflectionHost(isCore, typeChecker, bundle.dts);
default:
throw new Error(`Reflection host for "${bundle.format}" not yet implemented.`);
}
}
getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer {
switch (bundle.format) {
case 'esm2015':
case 'fesm2015':
return new EsmRenderer(host, isCore, bundle, this.sourcePath, this.targetPath);
case 'esm5':
case 'fesm5':
return new Esm5Renderer(host, isCore, bundle, this.sourcePath, this.targetPath);
default:
throw new Error(`Renderer for "${bundle.format}" not yet implemented.`);
}
}
analyzeProgram(reflectionHost: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle):
ProgramAnalyses {
const typeChecker = bundle.src.program.getTypeChecker();
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const switchMarkerAnalyzer = new SwitchMarkerAnalyzer(reflectionHost);
const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program);
const decorationAnalyzer = new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, reflectionHost,
referencesRegistry, bundle.rootDirs, isCore);
const decorationAnalyses = decorationAnalyzer.analyzeProgram();
const moduleWithProvidersAnalyzer =
bundle.dts && new ModuleWithProvidersAnalyzer(reflectionHost, referencesRegistry);
const moduleWithProvidersAnalyses = moduleWithProvidersAnalyzer &&
moduleWithProvidersAnalyzer.analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyzer =
new PrivateDeclarationsAnalyzer(reflectionHost, referencesRegistry);
const privateDeclarationsAnalyses =
privateDeclarationsAnalyzer.analyzeProgram(bundle.src.program);
return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses};
}
writeFile(file: FileInfo): void {
mkdir('-p', dirname(file.path));
const backPath = file.path + '.bak';
if (existsSync(file.path) && !existsSync(backPath)) {
mv(file.path, backPath);
}
writeFileSync(file.path, file.contents, 'utf8');
}
}
interface ProgramAnalyses {
decorationAnalyses: Map<ts.SourceFile, CompiledFile>;
switchMarkerAnalyses: SwitchMarkerAnalyses;
privateDeclarationsAnalyses: ExportInfo[];
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null;
}

View File

@ -0,0 +1,44 @@
/**
* @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 MagicString from 'magic-string';
import {NgccReflectionHost} from '../host/ngcc_host';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {EsmRenderer} from './esm_renderer';
import {EntryPointBundle} from '../packages/entry_point_bundle';
export class Esm5Renderer extends EsmRenderer {
constructor(
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string,
targetPath: string) {
super(host, isCore, bundle, sourcePath, targetPath);
}
/**
* Add the definitions to each decorated class
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(
`Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent;
if (!parent || !ts.isBlock(parent)) {
throw new Error(
`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement));
if (!returnStatement) {
throw new Error(
`Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const insertionPoint = returnStatement.getFullStart();
output.appendLeft(insertionPoint, '\n' + definitions);
}
}

View File

@ -0,0 +1,125 @@
/**
* @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 {dirname, relative} from 'canonical-path';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer';
import {EntryPointBundle} from '../packages/entry_point_bundle';
import {ExportInfo} from '../analysis/private_declarations_analyzer';
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
export class EsmRenderer extends Renderer {
constructor(
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string,
targetPath: string) {
super(host, isCore, bundle, sourcePath, targetPath);
}
/**
* Add the imports at the top of the file
*/
addImports(output: MagicString, imports: {specifier: string; qualifier: string;}[]): void {
// The imports get inserted at the very top of the file.
imports.forEach(
i => { output.appendLeft(0, `import * as ${i.qualifier} from '${i.specifier}';\n`); });
}
addExports(output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void {
exports.forEach(e => {
let exportFrom = '';
const isDtsFile = isDtsPath(entryPointBasePath);
const from = isDtsFile ? e.dtsFrom : e.from;
if (from) {
const basePath = stripExtension(from);
const relativePath = './' + relative(dirname(entryPointBasePath), basePath);
exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : '';
}
// aliases should only be added in dts files as these are lost when rolling up dts file.
const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier;
const exportStr = `\nexport {${exportStatement}}${exportFrom};`;
output.append(exportStr);
});
}
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
if (constants === '') {
return;
}
const insertionPoint = file.statements.reduce((prev, stmt) => {
if (ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) ||
ts.isNamespaceImport(stmt)) {
return stmt.getEnd();
}
return prev;
}, 0);
output.appendLeft(insertionPoint, '\n' + constants + '\n');
}
/**
* Add the definitions to each decorated class
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
}
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
output.appendLeft(insertionPoint, '\n' + definitions);
}
/**
* Remove static decorator properties from classes
*/
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void {
decoratorsToRemove.forEach((nodesToRemove, containerNode) => {
if (ts.isArrayLiteralExpression(containerNode)) {
const items = containerNode.elements;
if (items.length === nodesToRemove.length) {
// Remove the entire statement
const statement = findStatement(containerNode);
if (statement) {
output.remove(statement.getFullStart(), statement.getEnd());
}
} else {
nodesToRemove.forEach(node => {
// remove any trailing comma
const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ?
node.getEnd() + 1 :
node.getEnd();
output.remove(node.getFullStart(), end);
});
}
}
});
}
rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile,
declarations: SwitchableVariableDeclaration[]): void {
declarations.forEach(declaration => {
const start = declaration.initializer.getStart();
const end = declaration.initializer.getEnd();
const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER);
outputText.overwrite(start, end, replacement);
});
}
}
function findStatement(node: ts.Node) {
while (node) {
if (ts.isExpressionStatement(node)) {
return node;
}
node = node.parent;
}
return undefined;
}

View File

@ -0,0 +1,34 @@
/**
* @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 {ImportRewriter, validateAndRewriteCoreSymbol} from '../../../src/ngtsc/imports';
export class NgccFlatImportRewriter implements ImportRewriter {
shouldImportSymbol(symbol: string, specifier: string): boolean {
if (specifier === '@angular/core') {
// Don't use imports for @angular/core symbols in a flat bundle, as they'll be visible
// directly.
return false;
} else {
return true;
}
}
rewriteSymbol(symbol: string, specifier: string): string {
if (specifier === '@angular/core') {
return validateAndRewriteCoreSymbol(symbol);
} else {
return symbol;
}
}
rewriteSpecifier(originalModulePath: string, inContextOfFile: string): string {
return originalModulePath;
}
}

View File

@ -0,0 +1,518 @@
/**
* @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 {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
import {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
import {readFileSync, statSync} from 'fs';
import MagicString from 'magic-string';
import {basename, dirname, relative, resolve} from 'canonical-path';
import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
import * as ts from 'typescript';
import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports';
import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
import {translateStatement, translateType, ImportManager} from '../../../src/ngtsc/translator';
import {NgccFlatImportRewriter} from './ngcc_import_rewriter';
import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer';
import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer';
import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
import {IMPORT_PREFIX} from '../constants';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {EntryPointBundle} from '../packages/entry_point_bundle';
interface SourceMapInfo {
source: string;
map: SourceMapConverter|null;
isInline: boolean;
}
/**
* Information about a file that has been rendered.
*/
export interface FileInfo {
/**
* Path to where the file should be written.
*/
path: string;
/**
* The contents of the file to be be written.
*/
contents: string;
}
interface DtsClassInfo {
dtsDeclaration: ts.Declaration;
compilation: CompileResult[];
}
/**
* A structure that captures information about what needs to be rendered
* in a typings file.
*
* It is created as a result of processing the analysis passed to the renderer.
*
* The `renderDtsFile()` method consumes it when rendering a typings file.
*/
class DtsRenderInfo {
classInfo: DtsClassInfo[] = [];
moduleWithProviders: ModuleWithProvidersInfo[] = [];
privateExports: ExportInfo[] = [];
}
/**
* The collected decorators that have become redundant after the compilation
* of Ivy static fields. The map is keyed by the container node, such that we
* can tell if we should remove the entire decorator property
*/
export type RedundantDecoratorMap = Map<ts.Node, ts.Node[]>;
export const RedundantDecoratorMap = Map;
/**
* A base-class for rendering an `AnalyzedFile`.
*
* Package formats have output files that must be rendered differently. Concrete sub-classes must
* implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods.
*/
export abstract class Renderer {
constructor(
protected host: NgccReflectionHost, protected isCore: boolean,
protected bundle: EntryPointBundle, protected sourcePath: string,
protected targetPath: string) {}
renderProgram(
decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] {
const renderedFiles: FileInfo[] = [];
// Transform the source files.
this.bundle.src.program.getSourceFiles().map(sourceFile => {
const compiledFile = decorationAnalyses.get(sourceFile);
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
if (compiledFile || switchMarkerAnalysis || sourceFile === this.bundle.src.file) {
renderedFiles.push(...this.renderFile(
sourceFile, compiledFile, switchMarkerAnalysis, privateDeclarationsAnalyses));
}
});
// Transform the .d.ts files
if (this.bundle.dts) {
const dtsFiles = this.getTypingsFilesToRender(
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
// If the dts entry-point is not already there (it did not have compiled classes)
// then add it now, to ensure it gets its extra exports rendered.
if (!dtsFiles.has(this.bundle.dts.file)) {
dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo());
}
dtsFiles.forEach(
(renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo)));
}
return renderedFiles;
}
/**
* Render the source code and source-map for an Analyzed file.
* @param compiledFile The analyzed file to render.
* @param targetPath The absolute path where the rendered file will be written.
*/
renderFile(
sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined,
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] {
const input = this.extractSourceMap(sourceFile);
const outputText = new MagicString(input.source);
if (switchMarkerAnalysis) {
this.rewriteSwitchableDeclarations(
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
}
if (compiledFile) {
const importManager = new ImportManager(
this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlat), IMPORT_PREFIX);
// TODO: remove constructor param metadata and property decorators (we need info from the
// handlers to do this)
const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses);
this.removeDecorators(outputText, decoratorsToRemove);
compiledFile.compiledClasses.forEach(clazz => {
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
this.addDefinitions(outputText, clazz, renderedDefinition);
});
this.addConstants(
outputText,
renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
compiledFile.sourceFile);
this.addImports(outputText, importManager.getAllImports(compiledFile.sourceFile.fileName));
}
// Add exports to the entry-point file
if (sourceFile === this.bundle.src.file) {
const entryPointBasePath = stripExtension(this.bundle.src.path);
this.addExports(outputText, entryPointBasePath, privateDeclarationsAnalyses);
}
return this.renderSourceAndMap(sourceFile, input, outputText);
}
renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] {
const input = this.extractSourceMap(dtsFile);
const outputText = new MagicString(input.source);
const printer = ts.createPrinter();
const importManager = new ImportManager(
this.getImportRewriter(this.bundle.dts !.r3SymbolsFile, false), IMPORT_PREFIX);
renderInfo.classInfo.forEach(dtsClass => {
const endOfClass = dtsClass.dtsDeclaration.getEnd();
dtsClass.compilation.forEach(declaration => {
const type = translateType(declaration.type, importManager);
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
outputText.appendRight(endOfClass - 1, newStatement);
});
});
this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager);
this.addImports(outputText, importManager.getAllImports(dtsFile.fileName));
this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports);
return this.renderSourceAndMap(dtsFile, input, outputText);
}
/**
* Add the type parameters to the appropriate functions that return `ModuleWithProviders`
* structures.
*
* This function only gets called on typings files, so it doesn't need different implementations
* for each bundle format.
*/
protected addModuleWithProvidersParams(
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
importManager: ImportManager): void {
moduleWithProviders.forEach(info => {
const ngModuleName = (info.ngModule.node as ts.ClassDeclaration).name !.text;
const declarationFile = info.declaration.getSourceFile().fileName;
const ngModuleFile = info.ngModule.node.getSourceFile().fileName;
const importPath = info.ngModule.viaModule ||
(declarationFile !== ngModuleFile ?
stripExtension(`./${relative(dirname(declarationFile), ngModuleFile)}`) :
null);
const ngModule = getImportString(importManager, importPath, ngModuleName);
if (info.declaration.type) {
const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ?
info.declaration.type.typeName :
null;
if (this.isCoreModuleWithProvidersType(typeName)) {
// The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type
// parameter adding.
outputText.overwrite(
info.declaration.type.getStart(), info.declaration.type.getEnd(),
`ModuleWithProviders<${ngModule}>`);
} else {
// The declaration returns an unknown type so we need to convert it to a union that
// includes the ngModule property.
const originalTypeString = info.declaration.type.getText();
outputText.overwrite(
info.declaration.type.getStart(), info.declaration.type.getEnd(),
`(${originalTypeString})&{ngModule:${ngModule}}`);
}
} else {
// The declaration has no return type so provide one.
const lastToken = info.declaration.getLastToken();
const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ?
lastToken.getStart() :
info.declaration.getEnd();
outputText.appendLeft(
insertPoint,
`: ${getImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`);
}
});
}
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
void;
protected abstract addImports(output: MagicString, imports: {specifier: string,
qualifier: string}[]): void;
protected abstract addExports(
output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void;
protected abstract addDefinitions(
output: MagicString, compiledClass: CompiledClass, definitions: string): void;
protected abstract removeDecorators(
output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void;
protected abstract rewriteSwitchableDeclarations(
outputText: MagicString, sourceFile: ts.SourceFile,
declarations: SwitchableVariableDeclaration[]): void;
/**
* From the given list of classes, computes a map of decorators that should be removed.
* The decorators to remove are keyed by their container node, such that we can tell if
* we should remove the entire decorator property.
* @param classes The list of classes that may have decorators to remove.
* @returns A map of decorators to remove, keyed by their container node.
*/
protected computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap {
const decoratorsToRemove = new RedundantDecoratorMap();
classes.forEach(clazz => {
clazz.decorators.forEach(dec => {
const decoratorArray = dec.node.parent !;
if (!decoratorsToRemove.has(decoratorArray)) {
decoratorsToRemove.set(decoratorArray, [dec.node]);
} else {
decoratorsToRemove.get(decoratorArray) !.push(dec.node);
}
});
});
return decoratorsToRemove;
}
/**
* Get the map from the source (note whether it is inline or external)
*/
protected extractSourceMap(file: ts.SourceFile): SourceMapInfo {
const inline = commentRegex.test(file.text);
const external = mapFileCommentRegex.test(file.text);
if (inline) {
const inlineSourceMap = fromSource(file.text);
return {
source: removeComments(file.text).replace(/\n\n$/, '\n'),
map: inlineSourceMap,
isInline: true,
};
} else if (external) {
let externalSourceMap: SourceMapConverter|null = null;
try {
externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName));
} catch (e) {
if (e.code === 'ENOENT') {
console.warn(
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
const mapPath = file.fileName + '.map';
if (basename(e.path) !== basename(mapPath) && statSync(mapPath).isFile()) {
console.warn(
`Guessing the map file name from the source file name: "${basename(mapPath)}"`);
try {
externalSourceMap = fromObject(JSON.parse(readFileSync(mapPath, 'utf8')));
} catch (e) {
console.error(e);
}
}
}
}
return {
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
map: externalSourceMap,
isInline: false,
};
} else {
return {source: file.text, map: null, isInline: false};
}
}
/**
* Merge the input and output source-maps, replacing the source-map comment in the output file
* with an appropriate source-map comment pointing to the merged source-map.
*/
protected renderSourceAndMap(
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] {
const outputPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName));
const outputMapPath = `${outputPath}.map`;
const outputMap = output.generateMap({
source: sourceFile.fileName,
includeContent: true,
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
// the merge algorithm.
});
// we must set this after generation as magic string does "manipulation" on the path
outputMap.file = outputPath;
const mergedMap =
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
const result: FileInfo[] = [];
if (input.isInline) {
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
} else {
result.push({
path: outputPath,
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`
});
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
}
return result;
}
protected getTypingsFilesToRender(
decorationAnalyses: DecorationAnalyses,
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|
null): Map<ts.SourceFile, DtsRenderInfo> {
const dtsMap = new Map<ts.SourceFile, DtsRenderInfo>();
// Capture the rendering info from the decoration analyses
decorationAnalyses.forEach(compiledFile => {
compiledFile.compiledClasses.forEach(compiledClass => {
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
if (dtsDeclaration) {
const dtsFile = dtsDeclaration.getSourceFile();
const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation});
dtsMap.set(dtsFile, renderInfo);
}
});
});
// Capture the ModuleWithProviders functions/methods that need updating
if (moduleWithProvidersAnalyses !== null) {
moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => {
const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
renderInfo.moduleWithProviders = moduleWithProvidersToFix;
dtsMap.set(dtsFile, renderInfo);
});
}
// Capture the private declarations that need to be re-exported
if (privateDeclarationsAnalyses.length) {
privateDeclarationsAnalyses.forEach(e => {
if (!e.dtsFrom && !e.alias) {
throw new Error(
`There is no typings path for ${e.identifier} in ${e.from}.\n` +
`We need to add an export for this class to a .d.ts typings file because ` +
`Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` +
`The simplest fix for this is to ensure that this class is exported from the package's entry-point.`);
}
});
const dtsEntryPoint = this.bundle.dts !.file;
const renderInfo = dtsMap.get(dtsEntryPoint) || new DtsRenderInfo();
renderInfo.privateExports = privateDeclarationsAnalyses;
dtsMap.set(dtsEntryPoint, renderInfo);
}
return dtsMap;
}
/**
* Check whether the given type is the core Angular `ModuleWithProviders` interface.
* @param typeName The type to check.
* @returns true if the type is the core Angular `ModuleWithProviders` interface.
*/
private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) {
const id =
typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null;
return (
id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core'));
}
private getImportRewriter(r3SymbolsFile: ts.SourceFile|null, isFlat: boolean): ImportRewriter {
if (this.isCore && isFlat) {
return new NgccFlatImportRewriter();
} else if (this.isCore) {
return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName);
} else {
return new NoopImportRewriter();
}
}
}
/**
* Merge the two specified source-maps into a single source-map that hides the intermediate
* source-map.
* E.g. Consider these mappings:
*
* ```
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
* ```
*
* this will be replaced with:
*
* ```
* OLD_SRC -> MERGED_MAP -> NEW_SRC
* ```
*/
export function mergeSourceMaps(
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
if (!oldMap) {
return fromObject(newMap);
}
const oldMapConsumer = new SourceMapConsumer(oldMap);
const newMapConsumer = new SourceMapConsumer(newMap);
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
mergedMapGenerator.applySourceMap(oldMapConsumer);
const merged = fromJSON(mergedMapGenerator.toString());
return merged;
}
/**
* Render the constant pool as source code for the given class.
*/
export function renderConstantPool(
sourceFile: ts.SourceFile, constantPool: ConstantPool, imports: ImportManager): string {
const printer = ts.createPrinter();
return constantPool.statements
.map(stmt => translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER))
.map(stmt => printer.printNode(ts.EmitHint.Unspecified, stmt, sourceFile))
.join('\n');
}
/**
* Render the definitions as source code for the given class.
* @param sourceFile The file containing the class to process.
* @param clazz The class whose definitions are to be rendered.
* @param compilation The results of analyzing the class - this is used to generate the rendered
* definitions.
* @param imports An object that tracks the imports that are needed by the rendered definitions.
*/
export function renderDefinitions(
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string {
const printer = ts.createPrinter();
const name = (compiledClass.declaration as ts.NamedDeclaration).name !;
const translate = (stmt: Statement) =>
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
const definitions =
compiledClass.compilation
.map(
c => c.statements.map(statement => translate(statement))
.concat(translate(createAssignmentStatement(name, c.name, c.initializer)))
.map(
statement =>
printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile))
.join('\n'))
.join('\n');
return definitions;
}
export function stripExtension(filePath: string): string {
return filePath.replace(/\.(js|d\.ts)$/, '');
}
/**
* Create an Angular AST statement node that contains the assignment of the
* compiled decorator to be applied to the class.
* @param analyzedClass The info about the class whose statement we want to create.
*/
function createAssignmentStatement(
receiverName: ts.DeclarationName, propName: string, initializer: Expression): Statement {
const receiver = new WrappedNodeExpr(receiverName);
return new WritePropExpr(receiver, propName, initializer).toStmt();
}
function getImportString(
importManager: ImportManager, importPath: string | null, importName: string) {
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null;
return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`;
}

View File

@ -0,0 +1,52 @@
/**
* @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';
export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol) => ts.Symbol {
return function(symbol: ts.Symbol) {
return ts.SymbolFlags.Alias & symbol.flags ? checker.getAliasedSymbol(symbol) : symbol;
};
}
export function isDefined<T>(value: T | undefined | null): value is T {
return (value !== undefined) && (value !== null);
}
export function getNameText(name: ts.PropertyName | ts.BindingName): string {
return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText();
}
/**
* Parse down the AST and capture all the nodes that satisfy the test.
* @param node The start node.
* @param test The function that tests whether a node should be included.
* @returns a collection of nodes that satisfy the test.
*/
export function findAll<T>(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T[] {
const nodes: T[] = [];
findAllVisitor(node);
return nodes;
function findAllVisitor(n: ts.Node) {
if (test(n)) {
nodes.push(n);
} else {
n.forEachChild(child => findAllVisitor(child));
}
}
}
/**
* Does the given declaration have a name which is an identifier?
* @param declaration The declaration to test.
* @returns true if the declaration has an identifer for a name.
*/
export function hasNameIdentifier(declaration: ts.Declaration): declaration is ts.Declaration&
{name: ts.Identifier} {
return ts.isIdentifier((declaration as any).name);
}