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

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

View File

@ -1,35 +0,0 @@
load("//tools:defaults.bzl", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "ngcc",
srcs = glob([
"*.ts",
"**/*.ts",
]),
deps = [
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/annotations",
"//packages/compiler-cli/src/ngtsc/cycles",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/scope",
"//packages/compiler-cli/src/ngtsc/transform",
"//packages/compiler-cli/src/ngtsc/translator",
"//packages/compiler-cli/src/ngtsc/util",
"@npm//@types/convert-source-map",
"@npm//@types/node",
"@npm//@types/shelljs",
"@npm//@types/source-map",
"@npm//@types/yargs",
"@npm//canonical-path",
"@npm//dependency-graph",
"@npm//magic-string",
"@npm//source-map",
"@npm//typescript",
],
)

View File

@ -1,30 +0,0 @@
# Angular Compatibility Compiler (ngcc)
This compiler will convert `node_modules` compiled with `ngc`, into `node_modules` which
appear to have been compiled with `ngtsc`.
This conversion will allow such "legacy" packages to be used by the Ivy rendering engine.
## Building
The project is built using Bazel:
```bash
yarn bazel build //packages/compiler-cli/src/ngcc
```
## Unit Testing
The unit tests are built and run using Bazel:
```bash
yarn bazel test //packages/compiler-cli/src/ngcc/test
```
## Integration Testing
There are tests that check the behavior of the overall executable:
```bash
yarn bazel test //packages/compiler-cli/test/ngcc
```

View File

@ -1,9 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export {mainNgcc} from './src/main';

View File

@ -1,16 +0,0 @@
#!/usr/bin/env node
/**
* @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 {mainNgcc} from './src/main';
// CLI entry point
if (require.main === module) {
const args = process.argv.slice(2);
process.exitCode = mainNgcc(args);
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,43 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
export interface SwitchMarkerAnalysis {
sourceFile: ts.SourceFile;
declarations: SwitchableVariableDeclaration[];
}
export type SwitchMarkerAnalyses = Map<ts.SourceFile, SwitchMarkerAnalysis>;
export const SwitchMarkerAnalyses = Map;
/**
* This Analyzer will analyse the files that have an R3 switch marker in them
* that will be replaced.
*/
export class SwitchMarkerAnalyzer {
constructor(private host: NgccReflectionHost) {}
/**
* Analyze the files in the program to identify declarations that contain R3
* switch markers.
* @param program The program to analyze.
* @return A map of source files to analysis objects. The map will contain only the
* source files that had switch markers, and the analysis will contain an array of
* the declarations in that source file that contain the marker.
*/
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
const analyzedFiles = new SwitchMarkerAnalyses();
program.getSourceFiles().forEach(sourceFile => {
const declarations = this.host.getSwitchableDeclarations(sourceFile);
if (declarations.length) {
analyzedFiles.set(sourceFile, {sourceFile, declarations});
}
});
return analyzedFiles;
}
}

View File

@ -1,9 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export const IMPORT_PREFIX = 'ɵngcc';

View File

@ -1,26 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {Decorator} from '../../../ngtsc/reflection';
/**
* A simple container that holds the details of a decorated class that has been
* found in a `DecoratedFile`.
*/
export class DecoratedClass {
/**
* Initialize a `DecoratedClass` that was found in a `DecoratedFile`.
* @param name The name of the class that has been found. This is mostly used
* for informational purposes.
* @param declaration The TypeScript AST node where this class is declared
* @param decorators The collection of decorators that have been found on this class.
*/
constructor(
public name: string, public declaration: ts.Declaration, public decorators: Decorator[], ) {}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

@ -1,41 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {resolve} from 'canonical-path';
import {existsSync, readFileSync, writeFileSync} from 'fs';
import {EntryPoint, EntryPointFormat} from './entry_point';
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
function getMarkerPath(entryPointPath: string, format: EntryPointFormat) {
return resolve(entryPointPath, `__modified_by_ngcc_for_${format}__`);
}
/**
* Check whether there is a build marker for the given entry point and format.
* @param entryPoint the entry point to check for a marker.
* @param format the format for which we are checking for a marker.
*/
export function checkMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat): boolean {
const markerPath = getMarkerPath(entryPoint.path, format);
const markerExists = existsSync(markerPath);
if (markerExists) {
const previousVersion = readFileSync(markerPath, 'utf8');
if (previousVersion !== NGCC_VERSION) {
throw new Error(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
}
}
return markerExists;
}
export function writeMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat) {
const markerPath = getMarkerPath(entryPoint.path, format);
writeFileSync(markerPath, NGCC_VERSION, 'utf8');
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,61 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {BundleProgram, makeBundleProgram} from './bundle_program';
import {EntryPoint, EntryPointFormat} from './entry_point';
/**
* A bundle of files and paths (and TS programs) that correspond to a particular
* format of a package entry-point.
*/
export interface EntryPointBundle {
format: EntryPointFormat;
isFlat: boolean;
rootDirs: AbsoluteFsPath[];
src: BundleProgram;
dts: BundleProgram|null;
}
/**
* Get an object that describes a formatted bundle for an entry-point.
* @param entryPoint The entry-point that contains the bundle.
* @param format The format of the bundle.
* @param transformDts True if processing this bundle should also process its `.d.ts` files.
*/
export function makeEntryPointBundle(
entryPoint: EntryPoint, isCore: boolean, format: EntryPointFormat,
transformDts: boolean): EntryPointBundle|null {
// Bail out if the entry-point does not have this format.
const path = entryPoint[format];
if (!path) {
return null;
}
// Create the TS program and necessary helpers.
const options: ts.CompilerOptions = {
allowJs: true,
maxNodeModuleJsDepth: Infinity,
rootDir: entryPoint.path,
};
const host = ts.createCompilerHost(options);
const rootDirs = [AbsoluteFsPath.from(entryPoint.path)];
// Create the bundle programs, as necessary.
const src = makeBundleProgram(isCore, path, 'r3_symbols.js', options, host);
const dts = transformDts ?
makeBundleProgram(isCore, entryPoint.typings, 'r3_symbols.d.ts', options, host) :
null;
const isFlat = src.r3SymbolsFile === null;
return {format, rootDirs, isFlat, src, dts};
}

View File

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

View File

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

View File

@ -1,44 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import MagicString from 'magic-string';
import {NgccReflectionHost} from '../host/ngcc_host';
import {CompiledClass} from '../analysis/decoration_analyzer';
import {EsmRenderer} from './esm_renderer';
import {EntryPointBundle} from '../packages/entry_point_bundle';
export class Esm5Renderer extends EsmRenderer {
constructor(
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string,
targetPath: string) {
super(host, isCore, bundle, sourcePath, targetPath);
}
/**
* Add the definitions to each decorated class
*/
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
if (!classSymbol) {
throw new Error(
`Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent;
if (!parent || !ts.isBlock(parent)) {
throw new Error(
`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement));
if (!returnStatement) {
throw new Error(
`Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
}
const insertionPoint = returnStatement.getFullStart();
output.appendLeft(insertionPoint, '\n' + definitions);
}
}

View File

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

View File

@ -1,34 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {ImportRewriter, validateAndRewriteCoreSymbol} from '../../../ngtsc/imports';
export class NgccFlatImportRewriter implements ImportRewriter {
shouldImportSymbol(symbol: string, specifier: string): boolean {
if (specifier === '@angular/core') {
// Don't use imports for @angular/core symbols in a flat bundle, as they'll be visible
// directly.
return false;
} else {
return true;
}
}
rewriteSymbol(symbol: string, specifier: string): string {
if (specifier === '@angular/core') {
return validateAndRewriteCoreSymbol(symbol);
} else {
return symbol;
}
}
rewriteSpecifier(originalModulePath: string, inContextOfFile: string): string {
return originalModulePath;
}
}

View File

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

View File

@ -1,52 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
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);
}

View File

@ -1,36 +0,0 @@
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
package(default_visibility = ["//visibility:public"])
ts_library(
name = "test_lib",
testonly = True,
srcs = glob([
"**/*.ts",
]),
deps = [
"//packages/compiler-cli/src/ngcc",
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/path",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/transform",
"@npm//@types/convert-source-map",
"@npm//@types/mock-fs",
"@npm//canonical-path",
"@npm//magic-string",
"@npm//typescript",
],
)
jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
deps = [
":test_lib",
"//tools/testing:node_no_angular",
"@npm//canonical-path",
"@npm//convert-source-map",
],
)

View File

@ -1,231 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {Decorator} from '../../../ngtsc/reflection';
import {DecoratorHandler, DetectResult} from '../../../ngtsc/transform';
import {CompiledClass, DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {makeTestBundleProgram} from '../helpers/utils';
const TEST_PROGRAM = [
{
name: 'test.js',
contents: `
import {Component, Directive, Injectable} from '@angular/core';
export class MyComponent {}
MyComponent.decorators = [{type: Component}];
export class MyDirective {}
MyDirective.decorators = [{type: Directive}];
export class MyService {}
MyService.decorators = [{type: Injectable}];
`,
},
{
name: 'other.js',
contents: `
import {Component} from '@angular/core';
export class MyOtherComponent {}
MyOtherComponent.decorators = [{type: Component}];
`,
},
];
const INTERNAL_COMPONENT_PROGRAM = [
{
name: 'entrypoint.js',
contents: `
import {Component, NgModule} from '@angular/core';
import {ImportedComponent} from './component';
export class LocalComponent {}
LocalComponent.decorators = [{type: Component}];
export class MyModule {}
MyModule.decorators = [{type: NgModule, args: [{
declarations: [ImportedComponent, LocalComponent],
exports: [ImportedComponent, LocalComponent],
},] }];
`
},
{
name: 'component.js',
contents: `
import {Component} from '@angular/core';
export class ImportedComponent {}
ImportedComponent.decorators = [{type: Component}];
`,
isRoot: false,
}
];
type DecoratorHandlerWithResolve = DecoratorHandler<any, any>& {
resolve: NonNullable<DecoratorHandler<any, any>['resolve']>;
};
describe('DecorationAnalyzer', () => {
describe('analyzeProgram()', () => {
let logs: string[];
let program: ts.Program;
let testHandler: jasmine.SpyObj<DecoratorHandlerWithResolve>;
let result: DecorationAnalyses;
// Helpers
const createTestHandler = () => {
const handler = jasmine.createSpyObj<DecoratorHandlerWithResolve>('TestDecoratorHandler', [
'detect',
'analyze',
'resolve',
'compile',
]);
// Only detect the Component and Directive decorators
handler.detect.and.callFake(
(node: ts.Declaration, decorators: Decorator[]): DetectResult<any>| undefined => {
logs.push(`detect: ${(node as any).name.text}@${decorators.map(d => d.name)}`);
if (!decorators) {
return undefined;
}
const metadata = decorators.find(d => d.name === 'Component' || d.name === 'Directive');
if (metadata === undefined) {
return undefined;
} else {
return {
metadata,
trigger: metadata.node,
};
}
});
// The "test" analysis is an object with the name of the decorator being analyzed
handler.analyze.and.callFake((decl: ts.Declaration, dec: Decorator) => {
logs.push(`analyze: ${(decl as any).name.text}@${dec.name}`);
return {analysis: {decoratorName: dec.name}, diagnostics: undefined};
});
// The "test" resolution is just setting `resolved: true` on the analysis
handler.resolve.and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(`resolve: ${(decl as any).name.text}@${analysis.decoratorName}`);
analysis.resolved = true;
});
// The "test" compilation result is just the name of the decorator being compiled
// (suffixed with `(compiled)`)
handler.compile.and.callFake((decl: ts.Declaration, analysis: any) => {
logs.push(
`compile: ${(decl as any).name.text}@${analysis.decoratorName} (resolved: ${analysis.resolved})`);
return `@${analysis.decoratorName} (compiled)`;
});
return handler;
};
const setUpAndAnalyzeProgram = (...progArgs: Parameters<typeof makeTestBundleProgram>) => {
logs = [];
const {options, host, ...bundle} = makeTestBundleProgram(...progArgs);
program = bundle.program;
const reflectionHost = new Esm2015ReflectionHost(false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const analyzer = new DecorationAnalyzer(
program, options, host, program.getTypeChecker(), reflectionHost, referencesRegistry,
[AbsoluteFsPath.fromUnchecked('/')], false);
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram();
};
describe('basic usage', () => {
beforeEach(() => setUpAndAnalyzeProgram(TEST_PROGRAM));
it('should return an object containing a reference to the original source file', () => {
TEST_PROGRAM.forEach(({name}) => {
const file = program.getSourceFile(name) !;
expect(result.get(file) !.sourceFile).toBe(file);
});
});
it('should call detect on the decorator handlers with each class from the parsed file',
() => {
expect(testHandler.detect).toHaveBeenCalledTimes(4);
expect(testHandler.detect.calls.allArgs().map(args => args[1][0])).toEqual([
jasmine.objectContaining({name: 'Component'}),
jasmine.objectContaining({name: 'Directive'}),
jasmine.objectContaining({name: 'Injectable'}),
jasmine.objectContaining({name: 'Component'}),
]);
});
it('should return an object containing the classes that were analyzed', () => {
const file1 = program.getSourceFile(TEST_PROGRAM[0].name) !;
const compiledFile1 = result.get(file1) !;
expect(compiledFile1.compiledClasses.length).toEqual(2);
expect(compiledFile1.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
expect(compiledFile1.compiledClasses[1]).toEqual(jasmine.objectContaining({
name: 'MyDirective', compilation: ['@Directive (compiled)'],
} as unknown as CompiledClass));
const file2 = program.getSourceFile(TEST_PROGRAM[1].name) !;
const compiledFile2 = result.get(file2) !;
expect(compiledFile2.compiledClasses.length).toEqual(1);
expect(compiledFile2.compiledClasses[0]).toEqual(jasmine.objectContaining({
name: 'MyOtherComponent', compilation: ['@Component (compiled)'],
} as unknown as CompiledClass));
});
it('should analyze, resolve and compile the classes that are detected', () => {
expect(logs).toEqual([
// First detect and (potentially) analyze.
'detect: MyComponent@Component',
'analyze: MyComponent@Component',
'detect: MyDirective@Directive',
'analyze: MyDirective@Directive',
'detect: MyService@Injectable',
'detect: MyOtherComponent@Component',
'analyze: MyOtherComponent@Component',
// The resolve.
'resolve: MyComponent@Component',
'resolve: MyDirective@Directive',
'resolve: MyOtherComponent@Component',
// Finally compile.
'compile: MyComponent@Component (resolved: true)',
'compile: MyDirective@Directive (resolved: true)',
'compile: MyOtherComponent@Component (resolved: true)',
]);
});
});
describe('internal components', () => {
beforeEach(() => setUpAndAnalyzeProgram(INTERNAL_COMPONENT_PROGRAM));
// The problem of exposing the type of these internal components in the .d.ts typing files
// is not yet solved.
it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => {
const file = program.getSourceFile('component.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const ImportedComponent =
analysis.compiledClasses.find(f => f.name === 'ImportedComponent') !;
expect(ImportedComponent).toBeDefined();
});
it('should analyze an internally defined component, which is not exported at all', () => {
const file = program.getSourceFile('entrypoint.js') !;
const analysis = result.get(file) !;
expect(analysis).toBeDefined();
const LocalComponent = analysis.compiledClasses.find(f => f.name === 'LocalComponent') !;
expect(LocalComponent).toBeDefined();
});
});
});
});

View File

@ -1,401 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {BundleProgram} from '../../src/packages/bundle_program';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
const TEST_PROGRAM = [
{
name: '/src/entry-point.js',
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: '/src/explicit.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ExplicitInternalModule {}
export function explicitInternalFunction() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
export function explicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function explicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class ExplicitClass {
static explicitInternalMethod() {
return {
ngModule: ExplicitInternalModule,
providers: []
};
}
static explicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static explicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: '/src/any.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class AnyInternalModule {}
export function anyInternalFunction() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
export function anyExternalFunction() {
return {
ngModule: ExternalModule,
providers: []
};
}
export function anyLibraryFunction() {
return {
ngModule: LibraryModule,
providers: []
};
}
export class AnyClass {
static anyInternalMethod() {
return {
ngModule: AnyInternalModule,
providers: []
};
}
static anyExternalMethod() {
return {
ngModule: ExternalModule,
providers: []
};
}
static anyLibraryMethod() {
return {
ngModule: LibraryModule,
providers: []
};
}
}
`
},
{
name: '/src/implicit.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class ImplicitInternalModule {}
export function implicitInternalFunction() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
export function implicitExternalFunction() {
return {
ngModule: ExternalModule,
providers: [],
};
}
export function implicitLibraryFunction() {
return {
ngModule: LibraryModule,
providers: [],
};
}
export class ImplicitClass {
static implicitInternalMethod() {
return {
ngModule: ImplicitInternalModule,
providers: [],
};
}
static implicitExternalMethod() {
return {
ngModule: ExternalModule,
providers: [],
};
}
static implicitLibraryMethod() {
return {
ngModule: LibraryModule,
providers: [],
};
}
}
`
},
{
name: '/src/no-providers.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class NoProvidersInternalModule {}
export function noProvExplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvExplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvExplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvAnyInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvAnyExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvAnyLibraryFunction() {
return {ngModule: LibraryModule};
}
export function noProvImplicitInternalFunction() {
return {ngModule: NoProvidersInternalModule};
}
export function noProvImplicitExternalFunction() {
return {ngModule: ExternalModule};
}
export function noProvImplicitLibraryFunction() {
return {ngModule: LibraryModule};
}
`
},
{
name: '/src/module.js',
contents: `
export class ExternalModule {}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
const TEST_DTS_PROGRAM = [
{
name: '/typings/entry-point.d.ts',
contents: `
export * from './explicit';
export * from './any';
export * from './implicit';
export * from './no-providers';
export * from './module';
`
},
{
name: '/typings/explicit.d.ts',
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ExplicitInternalModule {}
export declare function explicitInternalFunction(): ModuleWithProviders<ExplicitInternalModule>;
export declare function explicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function explicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare class ExplicitClass {
static explicitInternalMethod(): ModuleWithProviders<ExplicitInternalModule>;
static explicitExternalMethod(): ModuleWithProviders<ExternalModule>;
static explicitLibraryMethod(): ModuleWithProviders<LibraryModule>;
}
`
},
{
name: '/typings/any.d.ts',
contents: `
import {ModuleWithProviders} from './core';
export declare class AnyInternalModule {}
export declare function anyInternalFunction(): ModuleWithProviders<any>;
export declare function anyExternalFunction(): ModuleWithProviders<any>;
export declare function anyLibraryFunction(): ModuleWithProviders<any>;
export declare class AnyClass {
static anyInternalMethod(): ModuleWithProviders<any>;
static anyExternalMethod(): ModuleWithProviders<any>;
static anyLibraryMethod(): ModuleWithProviders<any>;
}
`
},
{
name: '/typings/implicit.d.ts',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class ImplicitInternalModule {}
export declare function implicitInternalFunction(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
export declare function implicitExternalFunction(): { ngModule: typeof ExternalModule; providers: never[]; };
export declare function implicitLibraryFunction(): { ngModule: typeof LibraryModule; providers: never[]; };
export declare class ImplicitClass {
static implicitInternalMethod(): { ngModule: typeof ImplicitInternalModule; providers: never[]; };
static implicitExternalMethod(): { ngModule: typeof ExternalModule; providers: never[]; };
static implicitLibraryMethod(): { ngModule: typeof LibraryModule; providers: never[]; };
}
`
},
{
name: '/typings/no-providers.d.ts',
contents: `
import {ModuleWithProviders} from './core';
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export declare class NoProvidersInternalModule {}
export declare function noProvExplicitInternalFunction(): ModuleWithProviders<NoProvidersInternalModule>;
export declare function noProvExplicitExternalFunction(): ModuleWithProviders<ExternalModule>;
export declare function noProvExplicitLibraryFunction(): ModuleWithProviders<LibraryModule>;
export declare function noProvAnyInternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyExternalFunction(): ModuleWithProviders<any>;
export declare function noProvAnyLibraryFunction(): ModuleWithProviders<any>;
export declare function noProvImplicitInternalFunction(): { ngModule: typeof NoProvidersInternalModule; };
export declare function noProvImplicitExternalFunction(): { ngModule: typeof ExternalModule; };
export declare function noProvImplicitLibraryFunction(): { ngModule: typeof LibraryModule; };
`
},
{
name: '/typings/module.d.ts',
contents: `
export declare class ExternalModule {}
`
},
{
name: '/typings/core.d.ts',
contents: `
export declare interface Type<T> {
new (...args: any[]): T
}
export declare type Provider = any;
export declare interface ModuleWithProviders<T> {
ngModule: Type<T>
providers?: Provider[]
}
`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
describe('ModuleWithProvidersAnalyzer', () => {
describe('analyzeProgram()', () => {
let analyses: ModuleWithProvidersAnalyses;
let program: ts.Program;
let dtsProgram: BundleProgram;
let referencesRegistry: NgccReferencesRegistry;
beforeAll(() => {
program = makeTestProgram(...TEST_PROGRAM);
dtsProgram = makeTestBundleProgram(TEST_DTS_PROGRAM);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dtsProgram);
referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new ModuleWithProvidersAnalyzer(host, referencesRegistry);
analyses = analyzer.analyzeProgram(program);
});
it('should ignore declarations that already have explicit NgModule type params',
() => { expect(getAnalysisDescription(analyses, '/typings/explicit.d.ts')).toEqual([]); });
it('should find declarations that use `any` for the NgModule type param', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/any.d.ts');
expect(anyAnalysis).toContain(['anyInternalFunction', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['anyInternalMethod', 'AnyInternalModule', null]);
expect(anyAnalysis).toContain(['anyExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['anyLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should track internal module references in the references registry', () => {
const declarations = referencesRegistry.getDeclarationMap();
const externalModuleDeclaration =
getDeclaration(program, '/src/module.js', 'ExternalModule', ts.isClassDeclaration);
const libraryModuleDeclaration = getDeclaration(
program, '/node_modules/some-library/index.d.ts', 'LibraryModule', ts.isClassDeclaration);
expect(declarations.has(externalModuleDeclaration.name !)).toBe(true);
expect(declarations.has(libraryModuleDeclaration.name !)).toBe(false);
});
it('should find declarations that have implicit return types', () => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/implicit.d.ts');
expect(anyAnalysis).toContain(['implicitInternalFunction', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryFunction', 'LibraryModule', 'some-library']);
expect(anyAnalysis).toContain(['implicitInternalMethod', 'ImplicitInternalModule', null]);
expect(anyAnalysis).toContain(['implicitExternalMethod', 'ExternalModule', null]);
expect(anyAnalysis).toContain(['implicitLibraryMethod', 'LibraryModule', 'some-library']);
});
it('should find declarations that do not specify a `providers` property in the return type',
() => {
const anyAnalysis = getAnalysisDescription(analyses, '/typings/no-providers.d.ts');
expect(anyAnalysis).not.toContain([
'noProvExplicitInternalFunction', 'NoProvidersInternalModule'
]);
expect(anyAnalysis).not.toContain([
'noProvExplicitExternalFunction', 'ExternalModule', null
]);
expect(anyAnalysis).toContain([
'noProvAnyInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvAnyExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvAnyLibraryFunction', 'LibraryModule', 'some-library'
]);
expect(anyAnalysis).toContain([
'noProvImplicitInternalFunction', 'NoProvidersInternalModule', null
]);
expect(anyAnalysis).toContain(['noProvImplicitExternalFunction', 'ExternalModule', null]);
expect(anyAnalysis).toContain([
'noProvImplicitLibraryFunction', 'LibraryModule', 'some-library'
]);
});
function getAnalysisDescription(analyses: ModuleWithProvidersAnalyses, fileName: string) {
const file = dtsProgram.program.getSourceFile(fileName) !;
const analysis = analyses.get(file);
return analysis ?
analysis.map(
info =>
[info.declaration.name !.getText(),
(info.ngModule.node as ts.ClassDeclaration).name !.getText(),
info.ngModule.viaModule]) :
[];
}
});
});

View File

@ -1,241 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {Reference} from '../../../ngtsc/imports';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {getDeclaration, makeTestBundleProgram, makeTestProgram} from '../helpers/utils';
describe('PrivateDeclarationsAnalyzer', () => {
describe('analyzeProgram()', () => {
const TEST_PROGRAM = [
{
name: '/src/entry_point.js',
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/src/a.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class PublicComponent {}
PublicComponent.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
},
{
name: '/src/b.js',
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
class PrivateComponent1 {}
PrivateComponent1.decorators = [
{type: Component, args: [{selectors: 'b', template: ''}]}
];
class PrivateComponent2 {}
PrivateComponent2.decorators = [
{type: Component, args: [{selectors: 'c', template: ''}]}
];
export class ModuleB {}
ModuleB.decorators = [
{type: NgModule, args: [{declarations: [PrivateComponent1]}]}
];
`
},
{
name: '/src/c.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class InternalComponent1 {}
InternalComponent1.decorators = [
{type: Component, args: [{selectors: 'd', template: ''}]}
];
export class InternalComponent2 {}
InternalComponent2.decorators = [
{type: Component, args: [{selectors: 'e', template: ''}]}
];
`
},
{
name: '/src/mod.js',
isRoot: false,
contents: `
import {Component, NgModule} from '@angular/core';
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent1} from './c';
export class ModuleA {}
ModuleA.decorators = [
{type: NgModule, args: [{
declarations: [PublicComponent, InternalComponent1],
imports: [ModuleB]
}]}
];
`
}
];
const TEST_DTS_PROGRAM = [
{
name: '/typings/entry_point.d.ts',
isRoot: true,
contents: `
export {PublicComponent} from './a';
export {ModuleA} from './mod';
export {ModuleB} from './b';
`
},
{
name: '/typings/a.d.ts',
isRoot: false,
contents: `
export declare class PublicComponent {}
`
},
{
name: '/typings/b.d.ts',
isRoot: false,
contents: `
export declare class ModuleB {}
`
},
{
name: '/typings/c.d.ts',
isRoot: false,
contents: `
export declare class InternalComponent1 {}
`
},
{
name: '/typings/mod.d.ts',
isRoot: false,
contents: `
import {PublicComponent} from './a';
import {ModuleB} from './b';
import {InternalComponent1} from './c';
export declare class ModuleA {}
`
},
];
it('should find all NgModule declarations that were not publicly exported from the entry-point',
() => {
const {program, referencesRegistry, analyzer} = setup(TEST_PROGRAM, TEST_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'PublicComponent');
addToReferencesRegistry(program, referencesRegistry, '/src/b.js', 'PrivateComponent1');
addToReferencesRegistry(program, referencesRegistry, '/src/c.js', 'InternalComponent1');
const analyses = analyzer.analyzeProgram(program);
// Note that `PrivateComponent2` and `InternalComponent2` are not found because they are
// not added to the ReferencesRegistry (i.e. they were not declared in an NgModule).
expect(analyses.length).toEqual(2);
expect(analyses).toEqual([
{identifier: 'PrivateComponent1', from: '/src/b.js', dtsFrom: null, alias: null},
{
identifier: 'InternalComponent1',
from: '/src/c.js',
dtsFrom: '/typings/c.d.ts',
alias: null
},
]);
});
const ALIASED_EXPORTS_PROGRAM = [
{
name: '/src/entry_point.js',
isRoot: true,
contents: `
// This component is only exported as an alias.
export {ComponentOne as aliasedComponentOne} from './a';
// This component is exported both as itself and an alias.
export {ComponentTwo as aliasedComponentTwo, ComponentTwo} from './a';
`
},
{
name: '/src/a.js',
isRoot: false,
contents: `
import {Component} from '@angular/core';
export class ComponentOne {}
ComponentOne.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
export class ComponentTwo {}
Component.decorators = [
{type: Component, args: [{selectors: 'a', template: ''}]}
];
`
}
];
const ALIASED_EXPORTS_DTS_PROGRAM = [
{
name: '/typings/entry_point.d.ts',
isRoot: true,
contents: `
export declare class aliasedComponentOne {}
export declare class ComponentTwo {}
export {ComponentTwo as aliasedComponentTwo}
`
},
];
it('should find all non-public declarations that were aliased', () => {
const {program, referencesRegistry, analyzer} =
setup(ALIASED_EXPORTS_PROGRAM, ALIASED_EXPORTS_DTS_PROGRAM);
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentOne');
addToReferencesRegistry(program, referencesRegistry, '/src/a.js', 'ComponentTwo');
const analyses = analyzer.analyzeProgram(program);
expect(analyses).toEqual([{
identifier: 'ComponentOne',
from: '/src/a.js',
dtsFrom: null,
alias: 'aliasedComponentOne',
}]);
});
});
});
type Files = {
name: string,
contents: string, isRoot?: boolean | undefined
}[];
function setup(jsProgram: Files, dtsProgram: Files) {
const program = makeTestProgram(...jsProgram);
const dts = makeTestBundleProgram(dtsProgram);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker(), dts);
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new PrivateDeclarationsAnalyzer(host, referencesRegistry);
return {program, referencesRegistry, analyzer};
}
/**
* Add up the named component to the references registry.
*
* This would normally be done by the decoration handlers in the `DecorationAnalyzer`.
*/
function addToReferencesRegistry(
program: ts.Program, registry: NgccReferencesRegistry, fileName: string,
componentName: string) {
const declaration = getDeclaration(program, fileName, componentName, ts.isClassDeclaration);
registry.add(null !, new Reference(declaration));
}

View File

@ -1,55 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {Reference} from '../../../ngtsc/imports';
import {PartialEvaluator} from '../../../ngtsc/partial_evaluator';
import {TypeScriptReflectionHost} from '../../../ngtsc/reflection';
import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
describe('NgccReferencesRegistry', () => {
it('should return a mapping from resolved reference identifiers to their declarations', () => {
const {program, options, host} = makeProgram([{
name: 'index.ts',
contents: `
export class SomeClass {}
export function someFunction() {}
export const someVariable = 42;
export const testArray = [SomeClass, someFunction, someVariable];
`
}]);
const checker = program.getTypeChecker();
const testArrayDeclaration =
getDeclaration(program, 'index.ts', 'testArray', ts.isVariableDeclaration);
const someClassDecl = getDeclaration(program, 'index.ts', 'SomeClass', ts.isClassDeclaration);
const someFunctionDecl =
getDeclaration(program, 'index.ts', 'someFunction', ts.isFunctionDeclaration);
const someVariableDecl =
getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration);
const testArrayExpression = testArrayDeclaration.initializer !;
const reflectionHost = new TypeScriptReflectionHost(checker);
const evaluator = new PartialEvaluator(reflectionHost, checker);
const registry = new NgccReferencesRegistry(reflectionHost);
const references = (evaluator.evaluate(testArrayExpression) as any[])
.filter(ref => ref instanceof Reference) as Reference<ts.Declaration>[];
registry.add(null !, ...references);
const map = registry.getDeclarationMap();
expect(map.size).toEqual(2);
expect(map.get(someClassDecl.name !) !.node).toBe(someClassDecl);
expect(map.get(someFunctionDecl.name !) !.node).toBe(someFunctionDecl);
expect(map.has(someVariableDecl.name as ts.Identifier)).toBe(false);
});
});

View File

@ -1,76 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {makeTestProgram} from '../helpers/utils';
const TEST_PROGRAM = [
{
name: 'entrypoint.js',
contents: `
import {a} from './a';
import {b} from './b';
`
},
{
name: 'a.js',
contents: `
import {c} from './c';
export const a = 1;
`
},
{
name: 'b.js',
contents: `
export const b = 42;
var factoryB = factory__PRE_R3__;
`
},
{
name: 'c.js',
contents: `
export const c = 'So long, and thanks for all the fish!';
var factoryC = factory__PRE_R3__;
var factoryD = factory__PRE_R3__;
`
},
];
describe('SwitchMarkerAnalyzer', () => {
describe('analyzeProgram()', () => {
it('should check for switchable markers in all the files of the program', () => {
const program = makeTestProgram(...TEST_PROGRAM);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const analyzer = new SwitchMarkerAnalyzer(host);
const analysis = analyzer.analyzeProgram(program);
const entrypoint = program.getSourceFile('entrypoint.js') !;
const a = program.getSourceFile('a.js') !;
const b = program.getSourceFile('b.js') !;
const c = program.getSourceFile('c.js') !;
expect(analysis.size).toEqual(2);
expect(analysis.has(entrypoint)).toBe(false);
expect(analysis.has(a)).toBe(false);
expect(analysis.has(b)).toBe(true);
expect(analysis.get(b) !.sourceFile).toBe(b);
expect(analysis.get(b) !.declarations.map(decl => decl.getText())).toEqual([
'factoryB = factory__PRE_R3__'
]);
expect(analysis.has(c)).toBe(true);
expect(analysis.get(c) !.sourceFile).toBe(c);
expect(analysis.get(c) !.declarations.map(decl => decl.getText())).toEqual([
'factoryC = factory__PRE_R3__',
'factoryD = factory__PRE_R3__',
]);
});
});
});

View File

@ -1,123 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
import {BundleProgram} from '../../src/packages/bundle_program';
import {EntryPointFormat} from '../../src/packages/entry_point';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
export {getDeclaration} from '../../../ngtsc/testing/in_memory_typescript';
/**
*
* @param format The format of the bundle.
* @param files The source files to include in the bundle.
* @param dtsFiles The typings files to include the bundle.
*/
export function makeTestEntryPointBundle(
format: EntryPointFormat, files: {name: string, contents: string, isRoot?: boolean}[],
dtsFiles?: {name: string, contents: string, isRoot?: boolean}[]): EntryPointBundle {
const src = makeTestBundleProgram(files);
const dts = dtsFiles ? makeTestBundleProgram(dtsFiles) : null;
const isFlat = src.r3SymbolsFile === null;
return {format, rootDirs: [AbsoluteFsPath.fromUnchecked('/')], src, dts, isFlat};
}
/**
* Create a bundle program for testing.
* @param files The source files of the bundle program.
*/
export function makeTestBundleProgram(files: {name: string, contents: string}[]): BundleProgram {
const {program, options, host} = makeTestProgramInternal(...files);
const path = files[0].name;
const file = program.getSourceFile(path) !;
const r3SymbolsInfo = files.find(file => file.name.indexOf('r3_symbols') !== -1) || null;
const r3SymbolsPath = r3SymbolsInfo && r3SymbolsInfo.name;
const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null;
return {program, options, host, path, file, r3SymbolsPath, r3SymbolsFile};
}
function makeTestProgramInternal(
...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): {
program: ts.Program,
host: ts.CompilerHost,
options: ts.CompilerOptions,
} {
return makeProgram([getFakeCore(), getFakeTslib(), ...files], {allowJs: true, checkJs: false});
}
export function makeTestProgram(
...files: {name: string, contents: string, isRoot?: boolean | undefined}[]): ts.Program {
return makeTestProgramInternal(...files).program;
}
// TODO: unify this with the //packages/compiler-cli/test/ngtsc/fake_core package
export function getFakeCore() {
return {
name: 'node_modules/@angular/core/index.ts',
contents: `
type FnWithArg<T> = (arg?: any) => T;
function callableClassDecorator(): FnWithArg<(clazz: any) => any> {
return null !;
}
function callableParamDecorator(): FnWithArg<(a: any, b: any, c: any) => void> {
return null !;
}
function makePropDecorator(): any {
}
export const Component = callableClassDecorator();
export const Directive = callableClassDecorator();
export const Injectable = callableClassDecorator();
export const NgModule = callableClassDecorator();
export const Input = makePropDecorator();
export const Inject = callableParamDecorator();
export const Self = callableParamDecorator();
export const SkipSelf = callableParamDecorator();
export const Optional = callableParamDecorator();
export class InjectionToken {
constructor(name: string) {}
}
export interface ModuleWithProviders<T = any> {}
`
};
}
export function getFakeTslib() {
return {
name: 'node_modules/tslib/index.ts',
contents: `
export function __decorate(decorators: any[], target: any, key?: string | symbol, desc?: any) {}
export function __param(paramIndex: number, decorator: any) {}
export function __metadata(metadataKey: any, metadataValue: any) {}
`
};
}
export function convertToDirectTsLibImport(filesystem: {name: string, contents: string}[]) {
return filesystem.map(file => {
const contents =
file.contents
.replace(
`import * as tslib_1 from 'tslib';`,
`import { __decorate, __metadata, __read, __values, __param, __extends, __assign } from 'tslib';`)
.replace(/tslib_1\./g, '');
return {...file, contents};
});
}

View File

@ -1,387 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ClassMemberKind, Import} from '../../../ngtsc/reflection';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const FILES = [
{
name: '/some_directive.js',
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
const INJECTED_TOKEN = new InjectionToken('injected');
class ViewContainerRef {
}
class TemplateRef {
}
let SomeDirective = class SomeDirective {
constructor(_viewContainer, _template, injected) {
this.instanceProperty = 'instance';
this.input1 = '';
this.input2 = 0;
}
instanceMethod() { }
static staticMethod() { }
};
SomeDirective.staticProperty = 'static';
tslib_1.__decorate([
Input(),
tslib_1.__metadata("design:type", String)
], SomeDirective.prototype, "input1", void 0);
tslib_1.__decorate([
Input(),
tslib_1.__metadata("design:type", Number)
], SomeDirective.prototype, "input2", void 0);
SomeDirective = tslib_1.__decorate([
Directive({ selector: '[someDirective]' }),
tslib_1.__param(2, Inject(INJECTED_TOKEN)),
tslib_1.__metadata("design:paramtypes", [ViewContainerRef,
TemplateRef, String])
], SomeDirective);
export { SomeDirective };
`,
},
{
name: '/node_modules/@angular/core/some_directive.js',
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Input } from './directives';
let SomeDirective = class SomeDirective {
constructor() { this.input1 = ''; }
};
tslib_1.__decorate([
Input(),
tslib_1.__metadata("design:type", String)
], SomeDirective.prototype, "input1", void 0);
SomeDirective = tslib_1.__decorate([
Directive({ selector: '[someDirective]' }),
], SomeDirective);
export { SomeDirective };
`,
},
{
name: 'ngmodule.js',
contents: `
import * as tslib_1 from 'tslib';
import { NgModule } from './directives';
var HttpClientXsrfModule_1;
let HttpClientXsrfModule = HttpClientXsrfModule_1 = class HttpClientXsrfModule {
static withOptions(options = {}) {
return {
ngModule: HttpClientXsrfModule_1,
providers: [],
};
}
};
HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([
NgModule({
providers: [],
})
], HttpClientXsrfModule);
let missingValue;
let nonDecoratedVar;
nonDecoratedVar = 43;
export { HttpClientXsrfModule };
`
},
];
describe('Fesm2015ReflectionHost [import helper style]', () => {
[{files: FILES, label: 'namespaced'},
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
].forEach(fileSystem => {
describe(`[${fileSystem.label}]`, () => {
describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm2015ReflectionHost(true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
ts.isVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
});
describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
it('should find non decorated properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
expect(instanceProperty.isStatic).toEqual(false);
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
});
it('should find static methods on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !;
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
expect(staticMethod.isStatic).toEqual(true);
expect(ts.isMethodDeclaration(staticMethod.implementation !)).toEqual(true);
});
it('should find static properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm2015ReflectionHost(true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
});
describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined();
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm2015ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
});
});
});
describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;
const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', ts.isClassDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe(null);
});
it('should return the declaration of an externally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node;
const identifierOfDirective =
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
decoratorNode.expression :
null;
const expectedDeclarationNode = getDeclaration(
program, 'node_modules/@angular/core/index.ts', 'Directive',
ts.isVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
});
describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null);
if (!value || !ts.isClassExpression(value)) {
throw new Error(
`Expected value to be a class expression: ${value && value.getText()}.`);
}
expect(value.name !.text).toBe('HttpClientXsrfModule');
});
it('should return null if the variable has no assignment', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const missingValue = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue');
const value = host.getVariableValue(missingValue !);
expect(value).toBe(null);
});
it('should return null if the variable is not assigned from a call to __decorate', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null);
});
});
});
});
function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) {
return;
}
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
node.name.text === variableName) {
return node;
}
return node.forEachChild(node => findVariableDeclaration(node, variableName));
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,424 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {ClassMemberKind, Import} from '../../../ngtsc/reflection';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {convertToDirectTsLibImport, getDeclaration, makeTestProgram} from '../helpers/utils';
import {expectTypeValueReferencesForParameters} from './util';
const FILES = [
{
name: '/some_directive.js',
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Inject, InjectionToken, Input } from '@angular/core';
var INJECTED_TOKEN = new InjectionToken('injected');
var ViewContainerRef = /** @class */ (function () {
function ViewContainerRef() {
}
return ViewContainerRef;
}());
var TemplateRef = /** @class */ (function () {
function TemplateRef() {
}
return TemplateRef;
}());
var SomeDirective = /** @class */ (function () {
function SomeDirective(_viewContainer, _template, injected) {
this.instanceProperty = 'instance';
this.input1 = '';
this.input2 = 0;
}
SomeDirective.prototype.instanceMethod = function () { };
SomeDirective.staticMethod = function () { };
SomeDirective.staticProperty = 'static';
tslib_1.__decorate([
Input(),
tslib_1.__metadata("design:type", String)
], SomeDirective.prototype, "input1", void 0);
tslib_1.__decorate([
Input(),
tslib_1.__metadata("design:type", Number)
], SomeDirective.prototype, "input2", void 0);
SomeDirective = tslib_1.__decorate([
Directive({ selector: '[someDirective]' }),
tslib_1.__param(2, Inject(INJECTED_TOKEN)),
tslib_1.__metadata("design:paramtypes", [ViewContainerRef,
TemplateRef, String])
], SomeDirective);
return SomeDirective;
}());
export { SomeDirective };
`,
},
{
name: '/node_modules/@angular/core/some_directive.js',
contents: `
import * as tslib_1 from 'tslib';
import { Directive, Input } from './directives';
var SomeDirective = /** @class */ (function () {
function SomeDirective() {
this.input1 = '';
}
tslib_1.__decorate([
Input(),
tslib_1.__metadata("design:type", String)
], SomeDirective.prototype, "input1", void 0);
SomeDirective = tslib_1.__decorate([
Directive({ selector: '[someDirective]' }),
], SomeDirective);
return SomeDirective;
}());
export { SomeDirective };
`,
},
{
name: '/ngmodule.js',
contents: `
import * as tslib_1 from 'tslib';
import { NgModule } from '@angular/core';
var HttpClientXsrfModule = /** @class */ (function () {
function HttpClientXsrfModule() {
}
HttpClientXsrfModule_1 = HttpClientXsrfModule;
HttpClientXsrfModule.withOptions = function (options) {
if (options === void 0) { options = {}; }
return {
ngModule: HttpClientXsrfModule_1,
providers: [],
};
};
var HttpClientXsrfModule_1;
HttpClientXsrfModule = HttpClientXsrfModule_1 = tslib_1.__decorate([
NgModule({
providers: [],
})
], HttpClientXsrfModule);
return HttpClientXsrfModule;
}());
var missingValue;
var nonDecoratedVar;
nonDecoratedVar = 43;
export { HttpClientXsrfModule };
`
},
];
describe('Esm5ReflectionHost [import helper style]', () => {
[{files: FILES, label: 'namespaced'},
{files: convertToDirectTsLibImport(FILES), label: 'direct import'},
].forEach(fileSystem => {
describe(`[${fileSystem.label}]`, () => {
describe('getDecoratorsOfDeclaration()', () => {
it('should find the decorators on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: '@angular/core'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.callFake(
(identifier: ts.Identifier) => identifier.getText() === 'Directive' ?
{from: '@angular/core', name: 'Directive'} :
{});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toEqual({from: '@angular/core', name: 'Directive'});
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Directive')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm5ReflectionHost(true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
ts.isVariableDeclaration);
const decorators = host.getDecoratorsOfDeclaration(classNode) !;
expect(decorators).toBeDefined();
expect(decorators.length).toEqual(1);
const decorator = decorators[0];
expect(decorator.name).toEqual('Directive');
expect(decorator.import).toEqual({name: 'Directive', from: './directives'});
expect(decorator.args !.map(arg => arg.getText())).toEqual([
'{ selector: \'[someDirective]\' }',
]);
});
});
describe('getMembersOfClass()', () => {
it('should find decorated members on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
const input2 = members.find(member => member.name === 'input2') !;
expect(input2.kind).toEqual(ClassMemberKind.Property);
expect(input2.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
it('should find non decorated properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const instanceProperty = members.find(member => member.name === 'instanceProperty') !;
expect(instanceProperty.kind).toEqual(ClassMemberKind.Property);
expect(instanceProperty.isStatic).toEqual(false);
expect(ts.isBinaryExpression(instanceProperty.implementation !)).toEqual(true);
expect(instanceProperty.value !.getText()).toEqual(`'instance'`);
});
it('should find static methods on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticMethod = members.find(member => member.name === 'staticMethod') !;
expect(staticMethod.kind).toEqual(ClassMemberKind.Method);
expect(staticMethod.isStatic).toEqual(true);
expect(ts.isFunctionExpression(staticMethod.implementation !)).toEqual(true);
});
it('should find static properties on a class', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const staticProperty = members.find(member => member.name === 'staticProperty') !;
expect(staticProperty.kind).toEqual(ClassMemberKind.Property);
expect(staticProperty.isStatic).toEqual(true);
expect(ts.isPropertyAccessExpression(staticProperty.implementation !)).toEqual(true);
expect(staticProperty.value !.getText()).toEqual(`'static'`);
});
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const spy =
spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier').and.returnValue({});
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
host.getMembersOfClass(classNode);
const identifiers = spy.calls.all().map(call => (call.args[0] as ts.Identifier).text);
expect(identifiers.some(identifier => identifier === 'Input')).toBeTruthy();
});
it('should support decorators being used inside @angular/core', () => {
const program = makeTestProgram(fileSystem.files[1]);
const host = new Esm5ReflectionHost(true, program.getTypeChecker());
const classNode = getDeclaration(
program, '/node_modules/@angular/core/some_directive.js', 'SomeDirective',
ts.isVariableDeclaration);
const members = host.getMembersOfClass(classNode);
const input1 = members.find(member => member.name === 'input1') !;
expect(input1.kind).toEqual(ClassMemberKind.Property);
expect(input1.isStatic).toEqual(false);
expect(input1.decorators !.map(d => d.name)).toEqual(['Input']);
});
});
describe('getConstructorParameters', () => {
it('should find the decorated constructor parameters', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
expect(parameters).toBeDefined();
expect(parameters !.map(parameter => parameter.name)).toEqual([
'_viewContainer', '_template', 'injected'
]);
expectTypeValueReferencesForParameters(parameters !, [
'ViewContainerRef',
'TemplateRef',
'String',
]);
});
describe('(returned parameters `decorators`)', () => {
it('should use `getImportOfIdentifier()` to retrieve import info', () => {
const mockImportInfo = {} as Import;
const spy = spyOn(Esm5ReflectionHost.prototype, 'getImportOfIdentifier')
.and.returnValue(mockImportInfo);
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const parameters = host.getConstructorParameters(classNode);
const decorators = parameters ![2].decorators !;
expect(decorators.length).toEqual(1);
expect(decorators[0].import).toBe(mockImportInfo);
const typeIdentifier = spy.calls.mostRecent().args[0] as ts.Identifier;
expect(typeIdentifier.text).toBe('Inject');
});
});
});
describe('findDecoratedClasses', () => {
it('should return an array of all decorated classes in the given source file', () => {
const program = makeTestProgram(...fileSystem.files);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const ngModuleFile = program.getSourceFile('/ngmodule.js') !;
const ngModuleClasses = host.findDecoratedClasses(ngModuleFile);
expect(ngModuleClasses.length).toEqual(1);
const ngModuleClass = ngModuleClasses.find(c => c.name === 'HttpClientXsrfModule') !;
expect(ngModuleClass.decorators.map(decorator => decorator.name)).toEqual(['NgModule']);
const someDirectiveFile = program.getSourceFile('/some_directive.js') !;
const someDirectiveClasses = host.findDecoratedClasses(someDirectiveFile);
expect(someDirectiveClasses.length).toEqual(1);
const someDirectiveClass = someDirectiveClasses.find(c => c.name === 'SomeDirective') !;
expect(someDirectiveClass.decorators.map(decorator => decorator.name)).toEqual([
'Directive'
]);
});
});
describe('getDeclarationOfIdentifier', () => {
it('should return the declaration of a locally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const ctrDecorators = host.getConstructorParameters(classNode) !;
const identifierOfViewContainerRef = (ctrDecorators[0].typeValueReference !as{
local: true,
expression: ts.Identifier,
defaultImportStatement: null,
}).expression;
const expectedDeclarationNode = getDeclaration(
program, '/some_directive.js', 'ViewContainerRef', ts.isVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfViewContainerRef);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe(null);
});
it('should return the declaration of an externally defined identifier', () => {
const program = makeTestProgram(fileSystem.files[0]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const classNode = getDeclaration(
program, '/some_directive.js', 'SomeDirective', ts.isVariableDeclaration);
const classDecorators = host.getDecoratorsOfDeclaration(classNode) !;
const decoratorNode = classDecorators[0].node;
const identifierOfDirective =
ts.isCallExpression(decoratorNode) && ts.isIdentifier(decoratorNode.expression) ?
decoratorNode.expression :
null;
const expectedDeclarationNode = getDeclaration(
program, 'node_modules/@angular/core/index.ts', 'Directive',
ts.isVariableDeclaration);
const actualDeclaration = host.getDeclarationOfIdentifier(identifierOfDirective !);
expect(actualDeclaration).not.toBe(null);
expect(actualDeclaration !.node).toBe(expectedDeclarationNode);
expect(actualDeclaration !.viaModule).toBe('@angular/core');
});
});
});
describe('getVariableValue', () => {
it('should find the "actual" declaration of an aliased variable identifier', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const ngModuleRef = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'HttpClientXsrfModule_1');
const value = host.getVariableValue(ngModuleRef !);
expect(value).not.toBe(null);
if (!value || !ts.isFunctionDeclaration(value.parent)) {
throw new Error(
`Expected result to be a function declaration: ${value && value.getText()}.`);
}
expect(value.getText()).toBe('HttpClientXsrfModule');
});
it('should return undefined if the variable has no assignment', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const missingValue = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'missingValue');
const value = host.getVariableValue(missingValue !);
expect(value).toBe(null);
});
it('should return null if the variable is not assigned from a call to __decorate', () => {
const program = makeTestProgram(fileSystem.files[2]);
const host = new Esm5ReflectionHost(false, program.getTypeChecker());
const nonDecoratedVar = findVariableDeclaration(
program.getSourceFile(fileSystem.files[2].name) !, 'nonDecoratedVar');
const value = host.getVariableValue(nonDecoratedVar !);
expect(value).toBe(null);
});
});
});
function findVariableDeclaration(
node: ts.Node | undefined, variableName: string): ts.VariableDeclaration|undefined {
if (!node) {
return;
}
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) &&
node.name.text === variableName) {
return node;
}
return node.forEachChild(node => findVariableDeclaration(node, variableName));
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {CtorParameter} from '../../../ngtsc/reflection';
/**
* Check that a given list of `CtorParameter`s has `typeValueReference`s of specific `ts.Identifier`
* names.
*/
export function expectTypeValueReferencesForParameters(
parameters: CtorParameter[], expectedParams: (string | null)[]) {
parameters !.forEach((param, idx) => {
const expected = expectedParams[idx];
if (expected !== null) {
if (param.typeValueReference === null || !param.typeValueReference.local ||
!ts.isIdentifier(param.typeValueReference.expression)) {
fail(`Incorrect typeValueReference generated, expected ${expected}`);
} else {
expect(param.typeValueReference.expression.text).toEqual(expected);
}
}
});
}

View File

@ -1,154 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {existsSync, readFileSync, writeFileSync} from 'fs';
import * as mockFs from 'mock-fs';
import {checkMarkerFile, writeMarkerFile} from '../../src/packages/build_marker';
import {EntryPoint} from '../../src/packages/entry_point';
function createMockFileSystem() {
mockFs({
'/node_modules/@angular/common': {
'package.json': `{
"fesm2015": "./fesm2015/common.js",
"fesm5": "./fesm5/common.js",
"typings": "./common.d.ts"
}`,
'fesm2015': {
'common.js': 'DUMMY CONTENT',
'http.js': 'DUMMY CONTENT',
'http/testing.js': 'DUMMY CONTENT',
'testing.js': 'DUMMY CONTENT',
},
'http': {
'package.json': `{
"fesm2015": "../fesm2015/http.js",
"fesm5": "../fesm5/http.js",
"typings": "./http.d.ts"
}`,
'testing': {
'package.json': `{
"fesm2015": "../../fesm2015/http/testing.js",
"fesm5": "../../fesm5/http/testing.js",
"typings": "../http/testing.d.ts"
}`,
},
},
'other': {
'package.json': '{ }',
},
'testing': {
'package.json': `{
"fesm2015": "../fesm2015/testing.js",
"fesm5": "../fesm5/testing.js",
"typings": "../testing.d.ts"
}`,
},
'node_modules': {
'tslib': {
'package.json': '{ }',
'node_modules': {
'other-lib': {
'package.json': '{ }',
},
},
},
},
},
'/node_modules/@angular/no-typings': {
'package.json': `{
"fesm2015": "./fesm2015/index.js"
}`,
'fesm2015': {
'index.js': 'DUMMY CONTENT',
'index.d.ts': 'DUMMY CONTENT',
},
},
'/node_modules/@angular/other': {
'not-package.json': '{ "fesm2015": "./fesm2015/other.js" }',
'package.jsonot': '{ "fesm5": "./fesm5/other.js" }',
},
'/node_modules/@angular/other2': {
'node_modules_not': {
'lib1': {
'package.json': '{ }',
},
},
'not_node_modules': {
'lib2': {
'package.json': '{ }',
},
},
},
});
}
function restoreRealFileSystem() {
mockFs.restore();
}
function createEntryPoint(path: string): EntryPoint {
return {name: 'some-package', path, package: '', typings: ''};
}
describe('Marker files', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
describe('writeMarkerFile', () => {
it('should write a file containing the version placeholder', () => {
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(false);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(true);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(false);
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
writeMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'esm5');
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__'))
.toBe(true);
expect(existsSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__')).toBe(true);
expect(
readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
expect(readFileSync('/node_modules/@angular/common/__modified_by_ngcc_for_esm5__', 'utf8'))
.toEqual('0.0.0-PLACEHOLDER');
});
});
describe('checkMarkerFile', () => {
it('should return false if the marker file does not exist', () => {
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
.toBe(false);
});
it('should return true if the marker file exists and contains the correct version', () => {
writeFileSync(
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', '0.0.0-PLACEHOLDER',
'utf8');
expect(checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
.toBe(true);
});
it('should throw if the marker file exists but contains the wrong version', () => {
writeFileSync(
'/node_modules/@angular/common/__modified_by_ngcc_for_fesm2015__', 'WRONG_VERSION',
'utf8');
expect(() => checkMarkerFile(createEntryPoint('/node_modules/@angular/common'), 'fesm2015'))
.toThrowError(
'The ngcc compiler has changed since the last ngcc build.\n' +
'Please completely remove `node_modules` and try again.');
});
});
});

View File

@ -1,254 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'canonical-path';
import * as mockFs from 'mock-fs';
import * as ts from 'typescript';
import {DependencyHost} from '../../src/packages/dependency_host';
const Module = require('module');
interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
describe('DependencyHost', () => {
let host: DependencyHost;
beforeEach(() => host = new DependencyHost());
describe('getDependencies()', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
it('should not generate a TS AST if the source does not contain any imports or re-exports',
() => {
spyOn(ts, 'createSourceFile');
host.computeDependencies('/no/imports/or/re-exports.js', new Set(), new Set(), new Set());
expect(ts.createSourceFile).not.toHaveBeenCalled();
});
it('should resolve all the external imports of the source file', () => {
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies('/external/imports.js', resolved, missing, deepImports);
expect(resolved.size).toBe(2);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
it('should resolve all the external re-exports of the source file', () => {
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies('/external/re-exports.js', resolved, missing, deepImports);
expect(resolved.size).toBe(2);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
it('should capture missing external imports', () => {
spyOn(host, 'tryResolveEntryPoint')
.and.callFake(
(from: string, importPath: string) =>
importPath === 'missing' ? null : `RESOLVED/${importPath}`);
spyOn(host, 'tryResolve').and.callFake(() => null);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies('/external/imports-missing.js', resolved, missing, deepImports);
expect(resolved.size).toBe(1);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(missing.size).toBe(1);
expect(missing.has('missing')).toBe(true);
expect(deepImports.size).toBe(0);
});
it('should not register deep imports as missing', () => {
// This scenario verifies the behavior of the dependency analysis when an external import
// is found that does not map to an entry-point but still exists on disk, i.e. a deep import.
// Such deep imports are captured for diagnostics purposes.
const tryResolveEntryPoint = (from: string, importPath: string) =>
importPath === 'deep/import' ? null : `RESOLVED/${importPath}`;
spyOn(host, 'tryResolveEntryPoint').and.callFake(tryResolveEntryPoint);
spyOn(host, 'tryResolve')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies('/external/deep-import.js', resolved, missing, deepImports);
expect(resolved.size).toBe(0);
expect(missing.size).toBe(0);
expect(deepImports.size).toBe(1);
expect(deepImports.has('deep/import')).toBe(true);
});
it('should recurse into internal dependencies', () => {
spyOn(host, 'resolveInternal')
.and.callFake(
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const getDependenciesSpy = spyOn(host, 'computeDependencies').and.callThrough();
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies('/internal/outer.js', resolved, missing, deepImports);
expect(getDependenciesSpy)
.toHaveBeenCalledWith('/internal/outer.js', resolved, missing, deepImports);
expect(getDependenciesSpy)
.toHaveBeenCalledWith(
'/internal/inner.js', resolved, missing, deepImports, jasmine.any(Set));
expect(resolved.size).toBe(1);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
it('should handle circular internal dependencies', () => {
spyOn(host, 'resolveInternal')
.and.callFake(
(from: string, importPath: string) => path.join('/internal', importPath + '.js'));
spyOn(host, 'tryResolveEntryPoint')
.and.callFake((from: string, importPath: string) => `RESOLVED/${importPath}`);
const resolved = new Set();
const missing = new Set();
const deepImports = new Set();
host.computeDependencies('/internal/circular-a.js', resolved, missing, deepImports);
expect(resolved.size).toBe(2);
expect(resolved.has('RESOLVED/path/to/x')).toBe(true);
expect(resolved.has('RESOLVED/path/to/y')).toBe(true);
});
function createMockFileSystem() {
mockFs({
'/no/imports/or/re-exports.js': 'some text but no import-like statements',
'/external/imports.js': `import {X} from 'path/to/x';\nimport {Y} from 'path/to/y';`,
'/external/re-exports.js': `export {X} from 'path/to/x';\nexport {Y} from 'path/to/y';`,
'/external/imports-missing.js': `import {X} from 'path/to/x';\nimport {Y} from 'missing';`,
'/external/deep-import.js': `import {Y} from 'deep/import';`,
'/internal/outer.js': `import {X} from './inner';`,
'/internal/inner.js': `import {Y} from 'path/to/y';`,
'/internal/circular-a.js': `import {B} from './circular-b'; import {X} from 'path/to/x';`,
'/internal/circular-b.js': `import {A} from './circular-a'; import {Y} from 'path/to/y';`,
});
}
});
describe('resolveInternal', () => {
it('should resolve the dependency via `Module._resolveFilename`', () => {
spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
const result = host.resolveInternal('/SOURCE/PATH/FILE', '../TARGET/PATH/FILE');
expect(result).toEqual('RESOLVED_PATH');
});
it('should first resolve the `to` on top of the `from` directory', () => {
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
host.resolveInternal('/SOURCE/PATH/FILE', '../TARGET/PATH/FILE');
expect(resolveSpy)
.toHaveBeenCalledWith('/SOURCE/TARGET/PATH/FILE', jasmine.any(Object), false, undefined);
});
});
describe('tryResolveExternal', () => {
it('should call `tryResolve`, appending `package.json` to the target path', () => {
const tryResolveSpy = spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED');
host.tryResolveEntryPoint('SOURCE_PATH', 'TARGET_PATH');
expect(tryResolveSpy).toHaveBeenCalledWith('SOURCE_PATH', 'TARGET_PATH/package.json');
});
it('should return the directory containing the result from `tryResolve', () => {
spyOn(host, 'tryResolve').and.returnValue('PATH/TO/RESOLVED');
expect(host.tryResolveEntryPoint('SOURCE_PATH', 'TARGET_PATH')).toEqual('PATH/TO');
});
it('should return null if `tryResolve` returns null', () => {
spyOn(host, 'tryResolve').and.returnValue(null);
expect(host.tryResolveEntryPoint('SOURCE_PATH', 'TARGET_PATH')).toEqual(null);
});
});
describe('tryResolve()', () => {
it('should resolve the dependency via `Module._resolveFilename`, passing the `from` path to the `paths` option',
() => {
const resolveSpy = spyOn(Module, '_resolveFilename').and.returnValue('RESOLVED_PATH');
const result = host.tryResolve('SOURCE_PATH', 'TARGET_PATH');
expect(resolveSpy).toHaveBeenCalledWith('TARGET_PATH', jasmine.any(Object), false, {
paths: ['SOURCE_PATH']
});
expect(result).toEqual('RESOLVED_PATH');
});
it('should return null if `Module._resolveFilename` throws an error', () => {
const resolveSpy =
spyOn(Module, '_resolveFilename').and.throwError(`Cannot find module 'TARGET_PATH'`);
const result = host.tryResolve('SOURCE_PATH', 'TARGET_PATH');
expect(result).toBe(null);
});
});
describe('isStringImportOrReexport', () => {
it('should return true if the statement is an import', () => {
expect(host.isStringImportOrReexport(createStatement('import {X} from "some/x";')))
.toBe(true);
expect(host.isStringImportOrReexport(createStatement('import * as X from "some/x";')))
.toBe(true);
});
it('should return true if the statement is a re-export', () => {
expect(host.isStringImportOrReexport(createStatement('export {X} from "some/x";')))
.toBe(true);
expect(host.isStringImportOrReexport(createStatement('export * from "some/x";'))).toBe(true);
});
it('should return false if the statement is not an import or a re-export', () => {
expect(host.isStringImportOrReexport(createStatement('class X {}'))).toBe(false);
expect(host.isStringImportOrReexport(createStatement('export function foo() {}')))
.toBe(false);
expect(host.isStringImportOrReexport(createStatement('export const X = 10;'))).toBe(false);
});
function createStatement(source: string) {
return ts
.createSourceFile('source.js', source, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS)
.statements[0];
}
});
describe('hasImportOrReexportStatements', () => {
it('should return true if there is an import statement', () => {
expect(host.hasImportOrReexportStatements('import {X} from "some/x";')).toBe(true);
expect(host.hasImportOrReexportStatements('import * as X from "some/x";')).toBe(true);
expect(
host.hasImportOrReexportStatements('blah blah\n\n import {X} from "some/x";\nblah blah'))
.toBe(true);
expect(host.hasImportOrReexportStatements('\t\timport {X} from "some/x";')).toBe(true);
});
it('should return true if there is a re-export statement', () => {
expect(host.hasImportOrReexportStatements('export {X} from "some/x";')).toBe(true);
expect(
host.hasImportOrReexportStatements('blah blah\n\n export {X} from "some/x";\nblah blah'))
.toBe(true);
expect(host.hasImportOrReexportStatements('\t\texport {X} from "some/x";')).toBe(true);
expect(host.hasImportOrReexportStatements(
'blah blah\n\n export * from "@angular/core;\nblah blah'))
.toBe(true);
});
it('should return false if there is no import nor re-export statement', () => {
expect(host.hasImportOrReexportStatements('blah blah')).toBe(false);
expect(host.hasImportOrReexportStatements('export function moo() {}')).toBe(false);
expect(
host.hasImportOrReexportStatements('Some text that happens to include the word import'))
.toBe(false);
});
});
function restoreRealFileSystem() { mockFs.restore(); }
});

View File

@ -1,109 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {DependencyHost} from '../../src/packages/dependency_host';
import {DependencyResolver} from '../../src/packages/dependency_resolver';
import {EntryPoint} from '../../src/packages/entry_point';
describe('DependencyResolver', () => {
let host: DependencyHost;
let resolver: DependencyResolver;
beforeEach(() => {
host = new DependencyHost();
resolver = new DependencyResolver(host);
});
describe('sortEntryPointsByDependency()', () => {
const first = { path: 'first', fesm2015: 'first/index.ts' } as EntryPoint;
const second = { path: 'second', esm2015: 'second/index.ts' } as EntryPoint;
const third = { path: 'third', fesm2015: 'third/index.ts' } as EntryPoint;
const fourth = { path: 'fourth', esm2015: 'fourth/index.ts' } as EntryPoint;
const fifth = { path: 'fifth', fesm2015: 'fifth/index.ts' } as EntryPoint;
const dependencies = {
'first/index.ts': {resolved: ['second', 'third', 'ignored-1'], missing: []},
'second/index.ts': {resolved: ['third', 'fifth'], missing: []},
'third/index.ts': {resolved: ['fourth', 'ignored-2'], missing: []},
'fourth/index.ts': {resolved: ['fifth'], missing: []},
'fifth/index.ts': {resolved: [], missing: []},
};
it('should order the entry points by their dependency on each other', () => {
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.entryPoints).toEqual([fifth, fourth, third, second, first]);
});
it('should remove entry-points that have missing direct dependencies', () => {
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
'first/index.ts': {resolved: [], missing: ['missing']},
'second/index.ts': {resolved: [], missing: []},
}));
const result = resolver.sortEntryPointsByDependency([first, second]);
expect(result.entryPoints).toEqual([second]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: first, missingDependencies: ['missing']},
]);
});
it('should remove entry points that depended upon an invalid entry-point', () => {
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
'first/index.ts': {resolved: ['second'], missing: []},
'second/index.ts': {resolved: [], missing: ['missing']},
'third/index.ts': {resolved: [], missing: []},
}));
// Note that we will process `first` before `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([first, second, third]);
expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: ['missing']},
{entryPoint: first, missingDependencies: ['missing']},
]);
});
it('should remove entry points that will depend upon an invalid entry-point', () => {
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies({
'first/index.ts': {resolved: ['second'], missing: []},
'second/index.ts': {resolved: [], missing: ['missing']},
'third/index.ts': {resolved: [], missing: []},
}));
// Note that we will process `first` after `second`, which has the missing dependency.
const result = resolver.sortEntryPointsByDependency([second, first, third]);
expect(result.entryPoints).toEqual([third]);
expect(result.invalidEntryPoints).toEqual([
{entryPoint: second, missingDependencies: ['missing']},
{entryPoint: first, missingDependencies: ['second']},
]);
});
it('should error if the entry point does not have either the fesm2015 nor esm2015 formats',
() => {
expect(() => resolver.sortEntryPointsByDependency([{ path: 'first' } as EntryPoint]))
.toThrowError(`ESM2015 format (flat and non-flat) missing in 'first' entry-point.`);
});
it('should capture any dependencies that were ignored', () => {
spyOn(host, 'computeDependencies').and.callFake(createFakeComputeDependencies(dependencies));
const result = resolver.sortEntryPointsByDependency([fifth, first, fourth, second, third]);
expect(result.ignoredDependencies).toEqual([
{entryPoint: first, dependencyPath: 'ignored-1'},
{entryPoint: third, dependencyPath: 'ignored-2'},
]);
});
interface DepMap {
[path: string]: {resolved: string[], missing: string[]};
}
function createFakeComputeDependencies(dependencies: DepMap) {
return (entryPoint: string, resolved: Set<string>, missing: Set<string>) => {
dependencies[entryPoint].resolved.forEach(dep => resolved.add(dep));
dependencies[entryPoint].missing.forEach(dep => missing.add(dep));
};
}
});
});

View File

@ -1,165 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as mockFs from 'mock-fs';
import {DependencyHost} from '../../src/packages/dependency_host';
import {DependencyResolver} from '../../src/packages/dependency_resolver';
import {EntryPoint} from '../../src/packages/entry_point';
import {EntryPointFinder} from '../../src/packages/entry_point_finder';
describe('findEntryPoints()', () => {
let resolver: DependencyResolver;
let finder: EntryPointFinder;
beforeEach(() => {
resolver = new DependencyResolver(new DependencyHost());
spyOn(resolver, 'sortEntryPointsByDependency').and.callFake((entryPoints: EntryPoint[]) => {
return {entryPoints, ignoredEntryPoints: [], ignoredDependencies: []};
});
finder = new EntryPointFinder(resolver);
});
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
it('should find sub-entry-points within a package', () => {
const {entryPoints} = finder.findEntryPoints('/sub_entry_points');
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
['/sub_entry_points/common', '/sub_entry_points/common'],
['/sub_entry_points/common', '/sub_entry_points/common/http'],
['/sub_entry_points/common', '/sub_entry_points/common/http/testing'],
['/sub_entry_points/common', '/sub_entry_points/common/testing'],
]);
});
it('should find packages inside a namespace', () => {
const {entryPoints} = finder.findEntryPoints('/namespaced');
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
['/namespaced/@angular/common', '/namespaced/@angular/common'],
['/namespaced/@angular/common', '/namespaced/@angular/common/http'],
['/namespaced/@angular/common', '/namespaced/@angular/common/http/testing'],
['/namespaced/@angular/common', '/namespaced/@angular/common/testing'],
]);
});
it('should return an empty array if there are no packages', () => {
const {entryPoints} = finder.findEntryPoints('/no_packages');
expect(entryPoints).toEqual([]);
});
it('should return an empty array if there are no valid entry-points', () => {
const {entryPoints} = finder.findEntryPoints('/no_valid_entry_points');
expect(entryPoints).toEqual([]);
});
it('should ignore folders starting with .', () => {
const {entryPoints} = finder.findEntryPoints('/dotted_folders');
expect(entryPoints).toEqual([]);
});
it('should ignore folders that are symlinked', () => {
const {entryPoints} = finder.findEntryPoints('/symlinked_folders');
expect(entryPoints).toEqual([]);
});
it('should handle nested node_modules folders', () => {
const {entryPoints} = finder.findEntryPoints('/nested_node_modules');
const entryPointPaths = entryPoints.map(x => [x.package, x.path]);
expect(entryPointPaths).toEqual([
['/nested_node_modules/outer', '/nested_node_modules/outer'],
// Note that the inner entry point does not get included as part of the outer package
[
'/nested_node_modules/outer/node_modules/inner',
'/nested_node_modules/outer/node_modules/inner'
],
]);
});
function createMockFileSystem() {
mockFs({
'/sub_entry_points': {
'common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
'http': {
'package.json': createPackageJson('http'),
'http.metadata.json': 'metadata info',
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
},
'/namespaced': {
'@angular': {
'common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
'http': {
'package.json': createPackageJson('http'),
'http.metadata.json': 'metadata info',
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
'testing': {
'package.json': createPackageJson('testing'),
'testing.metadata.json': 'metadata info',
},
},
},
},
'/no_packages': {'should_not_be_found': {}},
'/no_valid_entry_points': {
'some_package': {
'package.json': '{}',
},
},
'/dotted_folders': {
'.common': {
'package.json': createPackageJson('common'),
'common.metadata.json': 'metadata info',
},
},
'/symlinked_folders': {
'common': mockFs.symlink({path: '/sub_entry_points/common'}),
},
'/nested_node_modules': {
'outer': {
'package.json': createPackageJson('outer'),
'outer.metadata.json': 'metadata info',
'node_modules': {
'inner': {
'package.json': createPackageJson('inner'),
'inner.metadata.json': 'metadata info',
},
},
},
},
});
}
function restoreRealFileSystem() { mockFs.restore(); }
});
function createPackageJson(packageName: string): string {
const packageJson: any = {
typings: `./${packageName}.d.ts`,
fesm2015: `./fesm2015/${packageName}.js`,
esm2015: `./esm2015/${packageName}.js`,
fesm5: `./fesm2015/${packageName}.js`,
esm5: `./esm2015/${packageName}.js`,
main: `./bundles/${packageName}.umd.js`,
};
return JSON.stringify(packageJson);
}

View File

@ -1,156 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as mockFs from 'mock-fs';
import {getEntryPointInfo} from '../../src/packages/entry_point';
describe('getEntryPointInfo()', () => {
beforeEach(createMockFileSystem);
afterEach(restoreRealFileSystem);
it('should return an object containing absolute paths to the formats of the specified entry-point',
() => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/valid_entry_point');
expect(entryPoint).toEqual({
name: 'some-package/valid_entry_point',
package: '/some_package',
path: '/some_package/valid_entry_point',
typings: `/some_package/valid_entry_point/valid_entry_point.d.ts`,
fesm2015: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
esm2015: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
fesm5: `/some_package/valid_entry_point/fesm2015/valid_entry_point.js`,
esm5: `/some_package/valid_entry_point/esm2015/valid_entry_point.js`,
umd: `/some_package/valid_entry_point/bundles/valid_entry_point.umd.js`,
});
});
it('should return null if there is no package.json at the entry-point path', () => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_package_json');
expect(entryPoint).toBe(null);
});
it('should return null if there is no typings or types field in the package.json', () => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_typings');
expect(entryPoint).toBe(null);
});
it('should return null if there is no esm2015 nor fesm2015 field in the package.json', () => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_esm2015');
expect(entryPoint).toBe(null);
});
it('should return null if there is no metadata.json file next to the typing file', () => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/missing_metadata.json');
expect(entryPoint).toBe(null);
});
it('should work if the typings field is named `types', () => {
const entryPoint =
getEntryPointInfo('/some_package', '/some_package/types_rather_than_typings');
expect(entryPoint).toEqual({
name: 'some-package/types_rather_than_typings',
package: '/some_package',
path: '/some_package/types_rather_than_typings',
typings: `/some_package/types_rather_than_typings/types_rather_than_typings.d.ts`,
fesm2015: `/some_package/types_rather_than_typings/fesm2015/types_rather_than_typings.js`,
esm2015: `/some_package/types_rather_than_typings/esm2015/types_rather_than_typings.js`,
fesm5: `/some_package/types_rather_than_typings/fesm2015/types_rather_than_typings.js`,
esm5: `/some_package/types_rather_than_typings/esm2015/types_rather_than_typings.js`,
umd: `/some_package/types_rather_than_typings/bundles/types_rather_than_typings.umd.js`,
});
});
it('should work with Angular Material style package.json', () => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/material_style');
expect(entryPoint).toEqual({
name: 'some_package/material_style',
package: '/some_package',
path: '/some_package/material_style',
typings: `/some_package/material_style/material_style.d.ts`,
fesm2015: `/some_package/material_style/esm2015/material_style.js`,
fesm5: `/some_package/material_style/esm5/material_style.es5.js`,
umd: `/some_package/material_style/bundles/material_style.umd.js`,
});
});
it('should return null if the package.json is not valid JSON', () => {
const entryPoint = getEntryPointInfo('/some_package', '/some_package/unexpected_symbols');
expect(entryPoint).toBe(null);
});
});
function createMockFileSystem() {
mockFs({
'/some_package': {
'valid_entry_point': {
'package.json': createPackageJson('valid_entry_point'),
'valid_entry_point.metadata.json': 'some meta data',
},
'missing_package_json': {
// no package.json!
'missing_package_json.metadata.json': 'some meta data',
},
'missing_typings': {
'package.json': createPackageJson('missing_typings', {excludes: ['typings']}),
'missing_typings.metadata.json': 'some meta data',
},
'types_rather_than_typings': {
'package.json': createPackageJson('types_rather_than_typings', {}, 'types'),
'types_rather_than_typings.metadata.json': 'some meta data',
},
'missing_esm2015': {
'package.json': createPackageJson('missing_fesm2015', {excludes: ['esm2015', 'fesm2015']}),
'missing_esm2015.metadata.json': 'some meta data',
},
'missing_metadata': {
'package.json': createPackageJson('missing_metadata'),
// no metadata.json!
},
'material_style': {
'package.json': `{
"name": "some_package/material_style",
"typings": "./material_style.d.ts",
"main": "./bundles/material_style.umd.js",
"module": "./esm5/material_style.es5.js",
"es2015": "./esm2015/material_style.js"
}`,
'material_style.metadata.json': 'some meta data',
},
'unexpected_symbols': {
// package.json might not be a valid JSON
// for example, @schematics/angular contains a package.json blueprint
// with unexpected symbols
'package.json':
'{"devDependencies": {<% if (!minimal) { %>"@types/jasmine": "~2.8.8" <% } %>}}',
},
}
});
}
function restoreRealFileSystem() {
mockFs.restore();
}
function createPackageJson(
packageName: string, {excludes}: {excludes?: string[]} = {},
typingsProp: string = 'typings'): string {
const packageJson: any = {
name: `some-package/${packageName}`,
[typingsProp]: `./${packageName}.d.ts`,
fesm2015: `./fesm2015/${packageName}.js`,
esm2015: `./esm2015/${packageName}.js`,
fesm5: `./fesm2015/${packageName}.js`,
esm5: `./esm2015/${packageName}.js`,
main: `./bundles/${packageName}.umd.js`,
};
if (excludes) {
excludes.forEach(exclude => delete packageJson[exclude]);
}
return JSON.stringify(packageJson);
}

View File

@ -1,335 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {dirname} from 'canonical-path';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {EsmRenderer} from '../../src/rendering/esm_renderer';
import {makeTestEntryPointBundle} from '../helpers/utils';
function setup(file: {name: string, contents: string}) {
const dir = dirname(file.name);
const bundle = makeTestEntryPointBundle('esm2015', [file]) !;
const typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses =
new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new EsmRenderer(host, false, bundle, dir, dir);
return {
host,
program: bundle.src.program,
sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses
};
}
const PROGRAM = {
name: '/some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
export class A {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: OtherA }
];
export class B {}
B.decorators = [
{ type: OtherB },
{ type: Directive, args: [{ selector: '[b]' }] }
];
export class C {}
C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;
let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;
function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {
const compilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
// Some other content`
};
const PROGRAM_DECORATE_HELPER = {
name: '/some/file.js',
contents: `
import * as tslib_1 from "tslib";
var D_1;
/* A copyright notice */
import { Directive } from '@angular/core';
const OtherA = () => (node) => { };
const OtherB = () => (node) => { };
let A = class A {
};
A = tslib_1.__decorate([
Directive({ selector: '[a]' }),
OtherA()
], A);
export { A };
let B = class B {
};
B = tslib_1.__decorate([
OtherB(),
Directive({ selector: '[b]' })
], B);
export { B };
let C = class C {
};
C = tslib_1.__decorate([
Directive({ selector: '[c]' })
], C);
export { C };
let D = D_1 = class D {
};
D = D_1 = tslib_1.__decorate([
Directive({ selector: '[d]', providers: [D_1] })
], D);
export { D };
// Some other content`
};
describe('Esm2015Renderer', () => {
describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addImports(output, [
{specifier: '@angular/core', qualifier: 'i0'},
{specifier: '@angular/common', qualifier: 'i1'}
]);
expect(output.toString()).toContain(`import * as i0 from '@angular/core';
import * as i1 from '@angular/common';
/* A copyright notice */`);
});
});
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
{from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA1'},
{from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA2'},
{from: '/some/foo/b.js', dtsFrom: '/some/foo/b.d.ts', identifier: 'ComponentB'},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
]);
expect(output.toString()).toContain(`
// Some other content
export {ComponentA1} from './a';
export {ComponentA2} from './a';
export {ComponentB} from './foo/b';
export {TopLevelComponent};`);
});
it('should not insert alias exports in js output', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
{from: '/some/a.js', alias: 'eComponentA1', identifier: 'ComponentA1'},
{from: '/some/a.js', alias: 'eComponentA2', identifier: 'ComponentA2'},
{from: '/some/foo/b.js', alias: 'eComponentB', identifier: 'ComponentB'},
{from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'},
]);
const outputString = output.toString();
expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`);
expect(outputString).not.toContain(`{eComponentB as ComponentB}`);
expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`);
});
});
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
expect(output.toString()).toContain(`
import {Directive} from '@angular/core';
const x = 3;
export class A {}`);
});
});
describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => {
const {renderer, program, switchMarkerAnalyses, sourceFile} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
expect(output.toString())
.not.toContain(`let compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
expect(output.toString())
.toContain(`let badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
expect(output.toString())
.toContain(`let compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString())
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
});
});
describe('addDefinitions', () => {
it('should insert the definitions directly after the class declaration', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`
export class A {}
SOME DEFINITION TEXT
A.decorators = [
`);
});
});
describe('removeDecorators', () => {
describe('[static property declaration]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
});
it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis',
() => {
const {decorationAnalyses, sourceFile, renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
expect(output.toString()).not.toContain(`C.decorators = [`);
});
});
});
describe('[__decorate declarations]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`let C = class C {\n};\nexport { C };`);
});
});
});

View File

@ -1,405 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {dirname} from 'canonical-path';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../ngtsc/path';
import {DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm5ReflectionHost} from '../../src/host/esm5_host';
import {Esm5Renderer} from '../../src/rendering/esm5_renderer';
import {makeTestEntryPointBundle, getDeclaration} from '../helpers/utils';
function setup(file: {name: string, contents: string}) {
const dir = dirname(file.name);
const bundle = makeTestEntryPointBundle('esm5', [file]);
const typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm5ReflectionHost(false, typeChecker);
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses =
new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, host,
referencesRegistry, [AbsoluteFsPath.fromUnchecked('/')], false)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const renderer = new Esm5Renderer(host, false, bundle, dir, dir);
return {
host,
program: bundle.src.program,
sourceFile: bundle.src.file, renderer, decorationAnalyses, switchMarkerAnalyses
};
}
const PROGRAM = {
name: '/some/file.js',
contents: `
/* A copyright notice */
import {Directive} from '@angular/core';
var A = (function() {
function A() {}
A.decorators = [
{ type: Directive, args: [{ selector: '[a]' }] },
{ type: OtherA }
];
A.prototype.ngDoCheck = function() {
//
};
return A;
}());
var B = (function() {
function B() {}
B.decorators = [
{ type: OtherB },
{ type: Directive, args: [{ selector: '[b]' }] }
];
return B;
}());
var C = (function() {
function C() {}
C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
return C;
}());
function NoIife() {}
var BadIife = (function() {
function BadIife() {}
BadIife.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];
}());
var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;
var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;
function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {
const compilerFactory = injector.get(CompilerFactory);
const compiler = compilerFactory.createCompiler([options]);
return compiler.compileModuleAsync(moduleType);
}
function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {
ngDevMode && assertNgModuleType(moduleType);
return Promise.resolve(new R3NgModuleFactory(moduleType));
}
// Some other content
export {A, B, C, NoIife, BadIife};`
};
const PROGRAM_DECORATE_HELPER = {
name: '/some/file.js',
contents: `
import * as tslib_1 from "tslib";
/* A copyright notice */
import { Directive } from '@angular/core';
var OtherA = function () { return function (node) { }; };
var OtherB = function () { return function (node) { }; };
var A = /** @class */ (function () {
function A() {
}
A = tslib_1.__decorate([
Directive({ selector: '[a]' }),
OtherA()
], A);
return A;
}());
export { A };
var B = /** @class */ (function () {
function B() {
}
B = tslib_1.__decorate([
OtherB(),
Directive({ selector: '[b]' })
], B);
return B;
}());
export { B };
var C = /** @class */ (function () {
function C() {
}
C = tslib_1.__decorate([
Directive({ selector: '[c]' })
], C);
return C;
}());
export { C };
var D = /** @class */ (function () {
function D() {
}
D_1 = D;
var D_1;
D = D_1 = tslib_1.__decorate([
Directive({ selector: '[d]', providers: [D_1] })
], D);
return D;
}());
export { D };
// Some other content`
};
describe('Esm5Renderer', () => {
describe('addImports', () => {
it('should insert the given imports at the start of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addImports(output, [
{specifier: '@angular/core', qualifier: 'i0'},
{specifier: '@angular/common', qualifier: 'i1'}
]);
expect(output.toString()).toContain(`import * as i0 from '@angular/core';
import * as i1 from '@angular/common';
/* A copyright notice */`);
});
});
describe('addExports', () => {
it('should insert the given exports at the end of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
{from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA1'},
{from: '/some/a.js', dtsFrom: '/some/a.d.ts', identifier: 'ComponentA2'},
{from: '/some/foo/b.js', dtsFrom: '/some/foo/b.d.ts', identifier: 'ComponentB'},
{from: PROGRAM.name, dtsFrom: PROGRAM.name, identifier: 'TopLevelComponent'},
]);
expect(output.toString()).toContain(`
export {A, B, C, NoIife, BadIife};
export {ComponentA1} from './a';
export {ComponentA2} from './a';
export {ComponentB} from './foo/b';
export {TopLevelComponent};`);
});
it('should not insert alias exports in js output', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addExports(output, PROGRAM.name.replace(/\.js$/, ''), [
{from: '/some/a.js', alias: 'eComponentA1', identifier: 'ComponentA1'},
{from: '/some/a.js', alias: 'eComponentA2', identifier: 'ComponentA2'},
{from: '/some/foo/b.js', alias: 'eComponentB', identifier: 'ComponentB'},
{from: PROGRAM.name, alias: 'eTopLevelComponent', identifier: 'TopLevelComponent'},
]);
const outputString = output.toString();
expect(outputString).not.toContain(`{eComponentA1 as ComponentA1}`);
expect(outputString).not.toContain(`{eComponentB as ComponentB}`);
expect(outputString).not.toContain(`{eTopLevelComponent as TopLevelComponent}`);
});
});
describe('addConstants', () => {
it('should insert the given constants after imports in the source file', () => {
const {renderer, program} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.addConstants(output, 'const x = 3;', file);
expect(output.toString()).toContain(`
import {Directive} from '@angular/core';
const x = 3;
var A = (function() {`);
});
});
describe('rewriteSwitchableDeclarations', () => {
it('should switch marked declaration initializers', () => {
const {renderer, program, sourceFile, switchMarkerAnalyses} = setup(PROGRAM);
const file = program.getSourceFile('some/file.js');
if (file === undefined) {
throw new Error(`Could not find source file`);
}
const output = new MagicString(PROGRAM.contents);
renderer.rewriteSwitchableDeclarations(
output, file, switchMarkerAnalyses.get(sourceFile) !.declarations);
expect(output.toString())
.not.toContain(`var compileNgModuleFactory = compileNgModuleFactory__PRE_R3__;`);
expect(output.toString())
.toContain(`var badlyFormattedVariable = __PRE_R3__badlyFormattedVariable;`);
expect(output.toString())
.toContain(`var compileNgModuleFactory = compileNgModuleFactory__POST_R3__;`);
expect(output.toString())
.toContain(`function compileNgModuleFactory__PRE_R3__(injector, options, moduleType) {`);
expect(output.toString())
.toContain(`function compileNgModuleFactory__POST_R3__(injector, options, moduleType) {`);
});
});
describe('addDefinitions', () => {
it('should insert the definitions directly before the return statement of the class IIFE',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`
A.prototype.ngDoCheck = function() {
//
};
SOME DEFINITION TEXT
return A;
`);
});
it('should error if the compiledClass is not valid', () => {
const {renderer, host, sourceFile, program} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const badSymbolDeclaration =
getDeclaration(program, sourceFile.fileName, 'A', ts.isVariableDeclaration);
const badSymbol: any = {name: 'BadSymbol', declaration: badSymbolDeclaration};
const hostSpy = spyOn(host, 'getClassSymbol').and.returnValue(null);
expect(() => renderer.addDefinitions(output, badSymbol, 'SOME DEFINITION TEXT'))
.toThrowError('Compiled class does not have a valid symbol: BadSymbol in /some/file.js');
const noIifeDeclaration =
getDeclaration(program, sourceFile.fileName, 'NoIife', ts.isFunctionDeclaration);
const mockNoIifeClass: any = {declaration: noIifeDeclaration, name: 'NoIife'};
hostSpy.and.returnValue({valueDeclaration: noIifeDeclaration});
expect(() => renderer.addDefinitions(output, mockNoIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(
'Compiled class declaration is not inside an IIFE: NoIife in /some/file.js');
const badIifeWrapper: any =
getDeclaration(program, sourceFile.fileName, 'BadIife', ts.isVariableDeclaration);
const badIifeDeclaration =
badIifeWrapper.initializer.expression.expression.body.statements[0];
const mockBadIifeClass: any = {declaration: badIifeDeclaration, name: 'BadIife'};
hostSpy.and.returnValue({valueDeclaration: badIifeDeclaration});
expect(() => renderer.addDefinitions(output, mockBadIifeClass, 'SOME DEFINITION TEXT'))
.toThrowError(
'Compiled class wrapper IIFE does not have a return statement: BadIife in /some/file.js');
});
});
describe('removeDecorators', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString())
.not.toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[c]' }] }`);
});
it('should delete the decorator (and its container if there are not other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators[0];
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
renderer.addDefinitions(output, compiledClass, 'SOME DEFINITION TEXT');
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[a]' }] },`);
expect(output.toString()).toContain(`{ type: OtherA }`);
expect(output.toString()).toContain(`{ type: Directive, args: [{ selector: '[b]' }] }`);
expect(output.toString()).toContain(`{ type: OtherB }`);
expect(output.toString()).toContain(`function C() {}\nSOME DEFINITION TEXT\n return C;`);
expect(output.toString()).not.toContain(`C.decorators = [
{ type: Directive, args: [{ selector: '[c]' }] },
];`);
});
});
describe('[__decorate declarations]', () => {
it('should delete the decorator (and following comma) that was matched in the analysis', () => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'A') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).not.toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (but cope with no trailing comma) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'B') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).toContain(`Directive({ selector: '[c]' })`);
});
it('should delete the decorator (and its container if there are no other decorators left) that was matched in the analysis',
() => {
const {renderer, decorationAnalyses, sourceFile} = setup(PROGRAM_DECORATE_HELPER);
const output = new MagicString(PROGRAM_DECORATE_HELPER.contents);
const compiledClass =
decorationAnalyses.get(sourceFile) !.compiledClasses.find(c => c.name === 'C') !;
const decorator = compiledClass.decorators.find(d => d.name === 'Directive') !;
const decoratorsToRemove = new Map<ts.Node, ts.Node[]>();
decoratorsToRemove.set(decorator.node.parent !, [decorator.node]);
renderer.removeDecorators(output, decoratorsToRemove);
expect(output.toString()).toContain(`Directive({ selector: '[a]' }),`);
expect(output.toString()).toContain(`OtherA()`);
expect(output.toString()).toContain(`Directive({ selector: '[b]' })`);
expect(output.toString()).toContain(`OtherB()`);
expect(output.toString()).not.toContain(`Directive({ selector: '[c]' })`);
expect(output.toString()).not.toContain(`C = tslib_1.__decorate([`);
expect(output.toString()).toContain(`function C() {\n }\n return C;`);
});
});
});

View File

@ -1,522 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as fs from 'fs';
import MagicString from 'magic-string';
import * as ts from 'typescript';
import {fromObject, generateMapFileComment} from 'convert-source-map';
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {ModuleWithProvidersAnalyzer} from '../../src/analysis/module_with_providers_analyzer';
import {PrivateDeclarationsAnalyzer} from '../../src/analysis/private_declarations_analyzer';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {RedundantDecoratorMap, Renderer} from '../../src/rendering/renderer';
import {EntryPointBundle} from '../../src/packages/entry_point_bundle';
import {makeTestEntryPointBundle} from '../helpers/utils';
class TestRenderer extends Renderer {
constructor(host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) {
super(host, isCore, bundle, '/src', '/dist');
}
addImports(output: MagicString, imports: {specifier: string, qualifier: string}[]) {
output.prepend('\n// ADD IMPORTS\n');
}
addExports(output: MagicString, baseEntryPointPath: string, exports: {
identifier: string,
from: string
}[]) {
output.prepend('\n// ADD EXPORTS\n');
}
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
output.prepend('\n// ADD CONSTANTS\n');
}
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string) {
output.prepend('\n// ADD DEFINITIONS\n');
}
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap) {
output.prepend('\n// REMOVE DECORATORS\n');
}
rewriteSwitchableDeclarations(output: MagicString, sourceFile: ts.SourceFile): void {
output.prepend('\n// REWRITTEN DECLARATIONS\n');
}
}
function createTestRenderer(
packageName: string, files: {name: string, contents: string}[],
dtsFiles?: {name: string, contents: string}[]) {
const isCore = packageName === '@angular/core';
const bundle = makeTestEntryPointBundle('esm2015', files, dtsFiles);
const typeChecker = bundle.src.program.getTypeChecker();
const host = new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts);
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses = new DecorationAnalyzer(
bundle.src.program, bundle.src.options, bundle.src.host,
typeChecker, host, referencesRegistry, bundle.rootDirs, isCore)
.analyzeProgram();
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(bundle.src.program);
const moduleWithProvidersAnalyses =
new ModuleWithProvidersAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
const privateDeclarationsAnalyses =
new PrivateDeclarationsAnalyzer(host, referencesRegistry).analyzeProgram(bundle.src.program);
const renderer = new TestRenderer(host, isCore, bundle);
spyOn(renderer, 'addImports').and.callThrough();
spyOn(renderer, 'addDefinitions').and.callThrough();
spyOn(renderer, 'removeDecorators').and.callThrough();
return {renderer, decorationAnalyses, switchMarkerAnalyses, moduleWithProvidersAnalyses,
privateDeclarationsAnalyses};
}
describe('Renderer', () => {
const INPUT_PROGRAM = {
name: '/src/file.js',
contents:
`import { Directive } from '@angular/core';\nexport class A {\n foo(x) {\n return x;\n }\n}\nA.decorators = [\n { type: Directive, args: [{ selector: '[a]' }] }\n];\n`
};
const INPUT_DTS_PROGRAM = {
name: '/typings/file.d.ts',
contents: `export declare class A {\nfoo(x: number): number;\n}\n`
};
const COMPONENT_PROGRAM = {
name: '/src/component.js',
contents:
`import { Component } from '@angular/core';\nexport class A {}\nA.decorators = [\n { type: Component, args: [{ selector: 'a', template: '{{ person!.name }}' }] }\n];\n`
};
const INPUT_PROGRAM_MAP = fromObject({
'version': 3,
'file': '/src/file.js',
'sourceRoot': '',
'sources': ['/src/file.ts'],
'names': [],
'mappings':
'AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC1C,MAAM;IACF,GAAG,CAAC,CAAS;QACT,OAAO,CAAC,CAAC;IACb,CAAC;;AACM,YAAU,GAAG;IAChB,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,EAAE;CACnD,CAAC',
'sourcesContent': [INPUT_PROGRAM.contents]
});
const RENDERED_CONTENTS =
`\n// ADD EXPORTS\n\n// ADD IMPORTS\n\n// ADD CONSTANTS\n\n// ADD DEFINITIONS\n\n// REMOVE DECORATORS\n` +
INPUT_PROGRAM.contents;
const OUTPUT_PROGRAM_MAP = fromObject({
'version': 3,
'file': '/dist/file.js',
'sources': ['/src/file.js'],
'sourcesContent': [INPUT_PROGRAM.contents],
'names': [],
'mappings': ';;;;;;;;;;AAAA;;;;;;;;;'
});
const MERGED_OUTPUT_PROGRAM_MAP = fromObject({
'version': 3,
'sources': ['/src/file.ts'],
'names': [],
'mappings': ';;;;;;;;;;AAAA',
'file': '/dist/file.js',
'sourcesContent': [INPUT_PROGRAM.contents]
});
describe('renderProgram()', () => {
it('should render the modified contents; and a new map file, if the original provided no map file.',
() => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
expect(result[1].path).toEqual('/dist/file.js.map');
expect(result[1].contents).toEqual(OUTPUT_PROGRAM_MAP.toJSON());
});
it('should render as JavaScript', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = createTestRenderer('test-package', [COMPONENT_PROGRAM]);
renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2])
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
type: Component,
args: [{ selector: 'a', template: '{{ person!.name }}' }]
}], null, null);
A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], factory: function A_Factory(t) { return new (t || A)(); }, consts: 1, vars: 1, template: function A_Template(rf, ctx) { if (rf & 1) {
ɵngcc0.ɵtext(0);
} if (rf & 2) {
ɵngcc0.ɵtextBinding(0, ɵngcc0.ɵinterpolation1("", ctx.person.name, ""));
} }, encapsulation: 2 });`);
});
describe('calling abstract methods', () => {
it('should call addImports with the source code and info about the core Angular library.',
() => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addImportsSpy.calls.first().args[1]).toEqual([
{specifier: '@angular/core', qualifier: 'ɵngcc0'}
]);
});
it('should call addDefinitions with the source code, the analyzed class and the rendered definitions.',
() => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addDefinitionsSpy.calls.first().args[1]).toEqual(jasmine.objectContaining({
name: 'A',
decorators: [jasmine.objectContaining({name: 'Directive'})],
}));
expect(addDefinitionsSpy.calls.first().args[2])
.toEqual(`/*@__PURE__*/ ɵngcc0.ɵsetClassMetadata(A, [{
type: Directive,
args: [{ selector: '[a]' }]
}], null, { foo: [] });
A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""]], factory: function A_Factory(t) { return new (t || A)(); } });`);
});
it('should call removeDecorators with the source code, a map of class decorators that have been analyzed',
() => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const removeDecoratorsSpy = renderer.removeDecorators as jasmine.Spy;
expect(removeDecoratorsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
// Each map key is the TS node of the decorator container
// Each map value is an array of TS nodes that are the decorators to remove
const map = removeDecoratorsSpy.calls.first().args[1] as Map<ts.Node, ts.Node[]>;
const keys = Array.from(map.keys());
expect(keys.length).toEqual(1);
expect(keys[0].getText())
.toEqual(`[\n { type: Directive, args: [{ selector: '[a]' }] }\n]`);
const values = Array.from(map.values());
expect(values.length).toEqual(1);
expect(values[0].length).toEqual(1);
expect(values[0][0].getText())
.toEqual(`{ type: Directive, args: [{ selector: '[a]' }] }`);
});
});
describe('source map merging', () => {
it('should merge any inline source map from the original file and write the output as an inline source map',
() => {
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer(
'test-package', [{
...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n' + INPUT_PROGRAM_MAP.toComment()
}]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + MERGED_OUTPUT_PROGRAM_MAP.toComment());
expect(result[1]).toBeUndefined();
});
it('should merge any external source map from the original file and write the output to an external source map',
() => {
// Mock out reading the map file from disk
spyOn(fs, 'readFileSync').and.returnValue(INPUT_PROGRAM_MAP.toJSON());
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer(
'test-package', [{
...INPUT_PROGRAM,
contents: INPUT_PROGRAM.contents + '\n//# sourceMappingURL=file.js.map'
}]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
expect(result[0].path).toEqual('/dist/file.js');
expect(result[0].contents)
.toEqual(RENDERED_CONTENTS + '\n' + generateMapFileComment('/dist/file.js.map'));
expect(result[1].path).toEqual('/dist/file.js.map');
expect(result[1].contents).toEqual(MERGED_OUTPUT_PROGRAM_MAP.toJSON());
});
});
describe('@angular/core support', () => {
it('should render relative imports in ESM bundles', () => {
const CORE_FILE = {
name: '/src/core.js',
contents:
`import { NgModule } from './ng_module';\nexport class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
};
const R3_SYMBOLS_FILE = {
// r3_symbols in the file name indicates that this is the path to rewrite core imports to
name: '/src/r3_symbols.js',
contents: `export const NgModule = () => null;`
};
// The package name of `@angular/core` indicates that we are compiling the core library.
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('@angular/core', [CORE_FILE, R3_SYMBOLS_FILE]);
renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2])
.toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`);
const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[1]).toEqual([
{specifier: './r3_symbols', qualifier: 'ɵngcc0'}
]);
});
it('should render no imports in FESM bundles', () => {
const CORE_FILE = {
name: '/src/core.js',
contents: `export const NgModule = () => null;
export class MyModule {}\nMyModule.decorators = [\n { type: NgModule, args: [] }\n];\n`
};
const {decorationAnalyses, renderer, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} = createTestRenderer('@angular/core', [CORE_FILE]);
renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const addDefinitionsSpy = renderer.addDefinitions as jasmine.Spy;
expect(addDefinitionsSpy.calls.first().args[2])
.toContain(`/*@__PURE__*/ setClassMetadata(`);
const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[1]).toEqual([]);
});
});
describe('rendering typings', () => {
it('should render extract types into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents)
.toContain(
'foo(x: number): number;\n static ngDirectiveDef: ɵngcc0.ɵDirectiveDefWithMeta');
});
it('should render imports into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents).toContain(`// ADD IMPORTS\nexport declare class A`);
});
it('should render exports into typings files', () => {
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer('test-package', [INPUT_PROGRAM], [INPUT_DTS_PROGRAM]);
// Add a mock export to trigger export rendering
privateDeclarationsAnalyses.push(
{identifier: 'ComponentB', from: '/src/file.js', dtsFrom: '/typings/b.d.ts'});
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/file.d.ts') !;
expect(typingsFile.contents)
.toContain(`// ADD EXPORTS\n\n// ADD IMPORTS\nexport declare class A`);
});
it('should fixup functions/methods that return ModuleWithProviders structures', () => {
const MODULE_WITH_PROVIDERS_PROGRAM = [
{
name: '/src/index.js',
contents: `
import {ExternalModule} from './module';
import {LibraryModule} from 'some-library';
export class SomeClass {}
export class SomeModule {
static withProviders1() {
return {ngModule: SomeModule};
}
static withProviders2() {
return {ngModule: SomeModule};
}
static withProviders3() {
return {ngModule: SomeClass};
}
static withProviders4() {
return {ngModule: ExternalModule};
}
static withProviders5() {
return {ngModule: ExternalModule};
}
static withProviders6() {
return {ngModule: LibraryModule};
}
static withProviders7() {
return {ngModule: SomeModule, providers: []};
};
static withProviders8() {
return {ngModule: SomeModule};
}
}
export function withProviders1() {
return {ngModule: SomeModule};
}
export function withProviders2() {
return {ngModule: SomeModule};
}
export function withProviders3() {
return {ngModule: SomeClass};
}
export function withProviders4() {
return {ngModule: ExternalModule};
}
export function withProviders5() {
return {ngModule: ExternalModule};
}
export function withProviders6() {
return {ngModule: LibraryModule};
}
export function withProviders7() {
return {ngModule: SomeModule, providers: []};
};
export function withProviders8() {
return {ngModule: SomeModule};
}`,
},
{
name: '/src/module.js',
contents: `
export class ExternalModule {
static withProviders1() {
return {ngModule: ExternalModule};
}
static withProviders2() {
return {ngModule: ExternalModule};
}
}`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
const MODULE_WITH_PROVIDERS_DTS_PROGRAM = [
{
name: '/typings/index.d.ts',
contents: `
import {ModuleWithProviders} from '@angular/core';
export declare class SomeClass {}
export interface MyModuleWithProviders extends ModuleWithProviders {}
export declare class SomeModule {
static withProviders1(): ModuleWithProviders;
static withProviders2(): ModuleWithProviders<any>;
static withProviders3(): ModuleWithProviders<SomeClass>;
static withProviders4(): ModuleWithProviders;
static withProviders5();
static withProviders6(): ModuleWithProviders;
static withProviders7(): {ngModule: SomeModule, providers: any[]};
static withProviders8(): MyModuleWithProviders;
}
export declare function withProviders1(): ModuleWithProviders;
export declare function withProviders2(): ModuleWithProviders<any>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
export declare function withProviders4(): ModuleWithProviders;
export declare function withProviders5();
export declare function withProviders6(): ModuleWithProviders;
export declare function withProviders7(): {ngModule: SomeModule, providers: any[]};
export declare function withProviders8(): MyModuleWithProviders;`
},
{
name: '/typings/module.d.ts',
contents: `
export interface ModuleWithProviders {}
export declare class ExternalModule {
static withProviders1(): ModuleWithProviders;
static withProviders2(): ModuleWithProviders;
}`
},
{
name: '/node_modules/some-library/index.d.ts',
contents: 'export declare class LibraryModule {}'
},
];
const {renderer, decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses} =
createTestRenderer(
'test-package', MODULE_WITH_PROVIDERS_PROGRAM, MODULE_WITH_PROVIDERS_DTS_PROGRAM);
const result = renderer.renderProgram(
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
moduleWithProvidersAnalyses);
const typingsFile = result.find(f => f.path === '/typings/index.d.ts') !;
expect(typingsFile.contents).toContain(`
static withProviders1(): ModuleWithProviders<SomeModule>;
static withProviders2(): ModuleWithProviders<SomeModule>;
static withProviders3(): ModuleWithProviders<SomeClass>;
static withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>;
static withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>;
static withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>;
static withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
static withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
expect(typingsFile.contents).toContain(`
export declare function withProviders1(): ModuleWithProviders<SomeModule>;
export declare function withProviders2(): ModuleWithProviders<SomeModule>;
export declare function withProviders3(): ModuleWithProviders<SomeClass>;
export declare function withProviders4(): ModuleWithProviders<ɵngcc0.ExternalModule>;
export declare function withProviders5(): ɵngcc1.ModuleWithProviders<ɵngcc0.ExternalModule>;
export declare function withProviders6(): ModuleWithProviders<ɵngcc2.LibraryModule>;
export declare function withProviders7(): ({ngModule: SomeModule, providers: any[]})&{ngModule:SomeModule};
export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
expect(renderer.addImports).toHaveBeenCalledWith(jasmine.any(MagicString), [
{specifier: './module', qualifier: 'ɵngcc0'},
{specifier: '@angular/core', qualifier: 'ɵngcc1'},
{specifier: 'some-library', qualifier: 'ɵngcc2'},
]);
// The following expectation checks that we do not mistake `ModuleWithProviders` types
// that are not imported from `@angular/core`.
const typingsFile2 = result.find(f => f.path === '/typings/module.d.ts') !;
expect(typingsFile2.contents).toContain(`
static withProviders1(): (ModuleWithProviders)&{ngModule:ExternalModule};
static withProviders2(): (ModuleWithProviders)&{ngModule:ExternalModule};`);
});
});
});
});