refactor(ivy): move ngcc into a higher level folder (#29092)
PR Close #29092
This commit is contained in:

committed by
Matias Niemelä

parent
cf4718c366
commit
a770aa231d
221
packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts
Normal file
221
packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
@ -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; }
|
||||
}
|
@ -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};
|
||||
});
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
9
packages/compiler-cli/ngcc/src/constants.ts
Normal file
9
packages/compiler-cli/ngcc/src/constants.ts
Normal 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';
|
26
packages/compiler-cli/ngcc/src/host/decorated_class.ts
Normal file
26
packages/compiler-cli/ngcc/src/host/decorated_class.ts
Normal 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[], ) {}
|
||||
}
|
1347
packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Normal file
1347
packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Normal file
File diff suppressed because it is too large
Load Diff
608
packages/compiler-cli/ngcc/src/host/esm5_host.ts
Normal file
608
packages/compiler-cli/ngcc/src/host/esm5_host.ts
Normal 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);
|
||||
}
|
72
packages/compiler-cli/ngcc/src/host/ngcc_host.ts
Normal file
72
packages/compiler-cli/ngcc/src/host/ngcc_host.ts
Normal 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[];
|
||||
}
|
87
packages/compiler-cli/ngcc/src/main.ts
Normal file
87
packages/compiler-cli/ngcc/src/main.ts
Normal 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;
|
||||
}
|
41
packages/compiler-cli/ngcc/src/packages/build_marker.ts
Normal file
41
packages/compiler-cli/ngcc/src/packages/build_marker.ts
Normal 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');
|
||||
}
|
74
packages/compiler-cli/ngcc/src/packages/bundle_program.ts
Normal file
74
packages/compiler-cli/ngcc/src/packages/bundle_program.ts
Normal 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;
|
||||
}
|
148
packages/compiler-cli/ngcc/src/packages/dependency_host.ts
Normal file
148
packages/compiler-cli/ngcc/src/packages/dependency_host.ts
Normal 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);
|
||||
}
|
||||
}
|
138
packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts
Normal file
138
packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
142
packages/compiler-cli/ngcc/src/packages/entry_point.ts
Normal file
142
packages/compiler-cli/ngcc/src/packages/entry_point.ts
Normal 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;
|
||||
}
|
@ -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};
|
||||
}
|
111
packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts
Normal file
111
packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts
Normal 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);
|
||||
});
|
||||
}
|
147
packages/compiler-cli/ngcc/src/packages/transformer.ts
Normal file
147
packages/compiler-cli/ngcc/src/packages/transformer.ts
Normal 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;
|
||||
}
|
44
packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts
Normal file
44
packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts
Normal 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);
|
||||
}
|
||||
}
|
125
packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts
Normal file
125
packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
518
packages/compiler-cli/ngcc/src/rendering/renderer.ts
Normal file
518
packages/compiler-cli/ngcc/src/rendering/renderer.ts
Normal 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}`;
|
||||
}
|
52
packages/compiler-cli/ngcc/src/utils.ts
Normal file
52
packages/compiler-cli/ngcc/src/utils.ts
Normal 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);
|
||||
}
|
Reference in New Issue
Block a user