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

committed by
Matias Niemelä

parent
cf4718c366
commit
a770aa231d
35
packages/compiler-cli/ngcc/BUILD.bazel
Normal file
35
packages/compiler-cli/ngcc/BUILD.bazel
Normal file
@ -0,0 +1,35 @@
|
||||
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",
|
||||
],
|
||||
)
|
30
packages/compiler-cli/ngcc/README.md
Normal file
30
packages/compiler-cli/ngcc/README.md
Normal file
@ -0,0 +1,30 @@
|
||||
# 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/ngcc
|
||||
```
|
||||
|
||||
## Unit Testing
|
||||
|
||||
The unit tests are built and run using Bazel:
|
||||
|
||||
```bash
|
||||
yarn bazel test //packages/compiler-cli/ngcc/test
|
||||
```
|
||||
|
||||
## Integration Testing
|
||||
|
||||
There are tests that check the behavior of the overall executable:
|
||||
|
||||
```bash
|
||||
yarn bazel test //packages/compiler-cli/ngcc/test:integration
|
||||
```
|
9
packages/compiler-cli/ngcc/index.ts
Normal file
9
packages/compiler-cli/ngcc/index.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export {mainNgcc} from './src/main';
|
16
packages/compiler-cli/ngcc/main-ngcc.ts
Normal file
16
packages/compiler-cli/ngcc/main-ngcc.ts
Normal file
@ -0,0 +1,16 @@
|
||||
#!/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);
|
||||
}
|
221
packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts
Normal file
221
packages/compiler-cli/ngcc/src/analysis/decoration_analyzer.ts
Normal file
@ -0,0 +1,221 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ConstantPool} from '@angular/compiler';
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
|
||||
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
|
||||
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, ReferenceEmitter} from '../../../src/ngtsc/imports';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
import {AbsoluteFsPath, LogicalFileSystem} from '../../../src/ngtsc/path';
|
||||
import {LocalModuleScopeRegistry, MetadataDtsModuleScopeResolver} from '../../../src/ngtsc/scope';
|
||||
import {CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from '../../../src/ngtsc/transform';
|
||||
import {DecoratedClass} from '../host/decorated_class';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {isDefined} from '../utils';
|
||||
|
||||
export interface AnalyzedFile {
|
||||
sourceFile: ts.SourceFile;
|
||||
analyzedClasses: AnalyzedClass[];
|
||||
}
|
||||
|
||||
export interface AnalyzedClass extends DecoratedClass {
|
||||
diagnostics?: ts.Diagnostic[];
|
||||
matches: {handler: DecoratorHandler<any, any>; analysis: any;}[];
|
||||
}
|
||||
|
||||
export interface CompiledClass extends AnalyzedClass { compilation: CompileResult[]; }
|
||||
|
||||
export interface CompiledFile {
|
||||
compiledClasses: CompiledClass[];
|
||||
sourceFile: ts.SourceFile;
|
||||
constantPool: ConstantPool;
|
||||
}
|
||||
|
||||
export type DecorationAnalyses = Map<ts.SourceFile, CompiledFile>;
|
||||
export const DecorationAnalyses = Map;
|
||||
|
||||
export interface MatchingHandler<A, M> {
|
||||
handler: DecoratorHandler<A, M>;
|
||||
detected: M;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simple class that resolves and loads files directly from the filesystem.
|
||||
*/
|
||||
class NgccResourceLoader implements ResourceLoader {
|
||||
canPreload = false;
|
||||
preload(): undefined|Promise<void> { throw new Error('Not implemented.'); }
|
||||
load(url: string): string { return fs.readFileSync(url, 'utf8'); }
|
||||
resolve(url: string, containingFile: string): string {
|
||||
return path.resolve(path.dirname(containingFile), url);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This Analyzer will analyze the files that have decorated classes that need to be transformed.
|
||||
*/
|
||||
export class DecorationAnalyzer {
|
||||
resourceManager = new NgccResourceLoader();
|
||||
refEmitter = new ReferenceEmitter([
|
||||
new LocalIdentifierStrategy(),
|
||||
new AbsoluteModuleStrategy(this.program, this.typeChecker, this.options, this.host),
|
||||
// TODO(alxhub): there's no reason why ngcc needs the "logical file system" logic here, as ngcc
|
||||
// projects only ever have one rootDir. Instead, ngcc should just switch its emitted imort based
|
||||
// on whether a bestGuessOwningModule is present in the Reference.
|
||||
new LogicalProjectStrategy(this.typeChecker, new LogicalFileSystem(this.rootDirs)),
|
||||
]);
|
||||
dtsModuleScopeResolver = new MetadataDtsModuleScopeResolver(
|
||||
this.typeChecker, this.reflectionHost, /* aliasGenerator */ null);
|
||||
scopeRegistry = new LocalModuleScopeRegistry(
|
||||
this.dtsModuleScopeResolver, this.refEmitter, /* aliasGenerator */ null);
|
||||
evaluator = new PartialEvaluator(this.reflectionHost, this.typeChecker);
|
||||
moduleResolver = new ModuleResolver(this.program, this.options, this.host);
|
||||
importGraph = new ImportGraph(this.moduleResolver);
|
||||
cycleAnalyzer = new CycleAnalyzer(this.importGraph);
|
||||
handlers: DecoratorHandler<any, any>[] = [
|
||||
new BaseDefDecoratorHandler(this.reflectionHost, this.evaluator, this.isCore),
|
||||
new ComponentDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.scopeRegistry, this.isCore, this.resourceManager,
|
||||
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true,
|
||||
this.moduleResolver, this.cycleAnalyzer, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
|
||||
new DirectiveDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||
this.isCore),
|
||||
new InjectableDecoratorHandler(
|
||||
this.reflectionHost, NOOP_DEFAULT_IMPORT_RECORDER, this.isCore, /* strictCtorDeps */ false),
|
||||
new NgModuleDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.scopeRegistry, this.referencesRegistry,
|
||||
this.isCore, /* routeAnalyzer */ null, this.refEmitter, NOOP_DEFAULT_IMPORT_RECORDER),
|
||||
new PipeDecoratorHandler(
|
||||
this.reflectionHost, this.evaluator, this.scopeRegistry, NOOP_DEFAULT_IMPORT_RECORDER,
|
||||
this.isCore),
|
||||
];
|
||||
|
||||
constructor(
|
||||
private program: ts.Program, private options: ts.CompilerOptions,
|
||||
private host: ts.CompilerHost, private typeChecker: ts.TypeChecker,
|
||||
private reflectionHost: NgccReflectionHost, private referencesRegistry: ReferencesRegistry,
|
||||
private rootDirs: AbsoluteFsPath[], private isCore: boolean) {}
|
||||
|
||||
/**
|
||||
* Analyze a program to find all the decorated files should be transformed.
|
||||
*
|
||||
* @returns a map of the source files to the analysis for those files.
|
||||
*/
|
||||
analyzeProgram(): DecorationAnalyses {
|
||||
const decorationAnalyses = new DecorationAnalyses();
|
||||
const analysedFiles = this.program.getSourceFiles()
|
||||
.map(sourceFile => this.analyzeFile(sourceFile))
|
||||
.filter(isDefined);
|
||||
analysedFiles.forEach(analysedFile => this.resolveFile(analysedFile));
|
||||
const compiledFiles = analysedFiles.map(analysedFile => this.compileFile(analysedFile));
|
||||
compiledFiles.forEach(
|
||||
compiledFile => decorationAnalyses.set(compiledFile.sourceFile, compiledFile));
|
||||
return decorationAnalyses;
|
||||
}
|
||||
|
||||
protected analyzeFile(sourceFile: ts.SourceFile): AnalyzedFile|undefined {
|
||||
const decoratedClasses = this.reflectionHost.findDecoratedClasses(sourceFile);
|
||||
return decoratedClasses.length ? {
|
||||
sourceFile,
|
||||
analyzedClasses: decoratedClasses.map(clazz => this.analyzeClass(clazz)).filter(isDefined)
|
||||
} :
|
||||
undefined;
|
||||
}
|
||||
|
||||
protected analyzeClass(clazz: DecoratedClass): AnalyzedClass|null {
|
||||
const matchingHandlers = this.handlers
|
||||
.map(handler => {
|
||||
const detected =
|
||||
handler.detect(clazz.declaration, clazz.decorators);
|
||||
return {handler, detected};
|
||||
})
|
||||
.filter(isMatchingHandler);
|
||||
|
||||
if (matchingHandlers.length === 0) {
|
||||
return null;
|
||||
}
|
||||
const detections: {handler: DecoratorHandler<any, any>, detected: DetectResult<any>}[] = [];
|
||||
let hasWeakHandler: boolean = false;
|
||||
let hasNonWeakHandler: boolean = false;
|
||||
let hasPrimaryHandler: boolean = false;
|
||||
|
||||
for (const {handler, detected} of matchingHandlers) {
|
||||
if (hasNonWeakHandler && handler.precedence === HandlerPrecedence.WEAK) {
|
||||
continue;
|
||||
} else if (hasWeakHandler && handler.precedence !== HandlerPrecedence.WEAK) {
|
||||
// Clear all the WEAK handlers from the list of matches.
|
||||
detections.length = 0;
|
||||
}
|
||||
if (hasPrimaryHandler && handler.precedence === HandlerPrecedence.PRIMARY) {
|
||||
throw new Error(`TODO.Diagnostic: Class has multiple incompatible Angular decorators.`);
|
||||
}
|
||||
|
||||
detections.push({handler, detected});
|
||||
if (handler.precedence === HandlerPrecedence.WEAK) {
|
||||
hasWeakHandler = true;
|
||||
} else if (handler.precedence === HandlerPrecedence.SHARED) {
|
||||
hasNonWeakHandler = true;
|
||||
} else if (handler.precedence === HandlerPrecedence.PRIMARY) {
|
||||
hasNonWeakHandler = true;
|
||||
hasPrimaryHandler = true;
|
||||
}
|
||||
}
|
||||
|
||||
const matches: {handler: DecoratorHandler<any, any>, analysis: any}[] = [];
|
||||
const allDiagnostics: ts.Diagnostic[] = [];
|
||||
for (const {handler, detected} of detections) {
|
||||
const {analysis, diagnostics} = handler.analyze(clazz.declaration, detected.metadata);
|
||||
if (diagnostics !== undefined) {
|
||||
allDiagnostics.push(...diagnostics);
|
||||
}
|
||||
matches.push({handler, analysis});
|
||||
}
|
||||
return {...clazz, matches, diagnostics: allDiagnostics.length > 0 ? allDiagnostics : undefined};
|
||||
}
|
||||
|
||||
protected compileFile(analyzedFile: AnalyzedFile): CompiledFile {
|
||||
const constantPool = new ConstantPool();
|
||||
const compiledClasses: CompiledClass[] = analyzedFile.analyzedClasses.map(analyzedClass => {
|
||||
const compilation = this.compileClass(analyzedClass, constantPool);
|
||||
return {...analyzedClass, compilation};
|
||||
});
|
||||
return {constantPool, sourceFile: analyzedFile.sourceFile, compiledClasses};
|
||||
}
|
||||
|
||||
protected compileClass(clazz: AnalyzedClass, constantPool: ConstantPool): CompileResult[] {
|
||||
const compilations: CompileResult[] = [];
|
||||
for (const {handler, analysis} of clazz.matches) {
|
||||
const result = handler.compile(clazz.declaration, analysis, constantPool);
|
||||
if (Array.isArray(result)) {
|
||||
compilations.push(...result);
|
||||
} else {
|
||||
compilations.push(result);
|
||||
}
|
||||
}
|
||||
return compilations;
|
||||
}
|
||||
|
||||
protected resolveFile(analyzedFile: AnalyzedFile): void {
|
||||
analyzedFile.analyzedClasses.forEach(({declaration, matches}) => {
|
||||
matches.forEach(({handler, analysis}) => {
|
||||
if ((handler.resolve !== undefined) && analysis) {
|
||||
handler.resolve(declaration, analysis);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function isMatchingHandler<A, M>(handler: Partial<MatchingHandler<A, M>>):
|
||||
handler is MatchingHandler<A, M> {
|
||||
return !!handler.detected;
|
||||
}
|
@ -0,0 +1,120 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
|
||||
import {Reference} from '../../../src/ngtsc/imports';
|
||||
import {Declaration} from '../../../src/ngtsc/reflection';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {isDefined} from '../utils';
|
||||
|
||||
export interface ModuleWithProvidersInfo {
|
||||
/**
|
||||
* The declaration (in the .d.ts file) of the function that returns
|
||||
* a `ModuleWithProviders object, but has a signature that needs
|
||||
* a type parameter adding.
|
||||
*/
|
||||
declaration: ts.MethodDeclaration|ts.FunctionDeclaration;
|
||||
/**
|
||||
* The NgModule class declaration (in the .d.ts file) to add as a type parameter.
|
||||
*/
|
||||
ngModule: Declaration;
|
||||
}
|
||||
|
||||
export type ModuleWithProvidersAnalyses = Map<ts.SourceFile, ModuleWithProvidersInfo[]>;
|
||||
export const ModuleWithProvidersAnalyses = Map;
|
||||
|
||||
export class ModuleWithProvidersAnalyzer {
|
||||
constructor(private host: NgccReflectionHost, private referencesRegistry: ReferencesRegistry) {}
|
||||
|
||||
analyzeProgram(program: ts.Program): ModuleWithProvidersAnalyses {
|
||||
const analyses = new ModuleWithProvidersAnalyses();
|
||||
const rootFiles = this.getRootFiles(program);
|
||||
rootFiles.forEach(f => {
|
||||
const fns = this.host.getModuleWithProvidersFunctions(f);
|
||||
fns && fns.forEach(fn => {
|
||||
const dtsFn = this.getDtsDeclaration(fn.declaration);
|
||||
const typeParam = dtsFn.type && ts.isTypeReferenceNode(dtsFn.type) &&
|
||||
dtsFn.type.typeArguments && dtsFn.type.typeArguments[0] ||
|
||||
null;
|
||||
if (!typeParam || isAnyKeyword(typeParam)) {
|
||||
// Either we do not have a parameterized type or the type is `any`.
|
||||
let ngModule = this.host.getDeclarationOfIdentifier(fn.ngModule);
|
||||
if (!ngModule) {
|
||||
throw new Error(
|
||||
`Cannot find a declaration for NgModule ${fn.ngModule.text} referenced in ${fn.declaration.getText()}`);
|
||||
}
|
||||
// For internal (non-library) module references, redirect the module's value declaration
|
||||
// to its type declaration.
|
||||
if (ngModule.viaModule === null) {
|
||||
const dtsNgModule = this.host.getDtsDeclaration(ngModule.node);
|
||||
if (!dtsNgModule) {
|
||||
throw new Error(
|
||||
`No typings declaration can be found for the referenced NgModule class in ${fn.declaration.getText()}.`);
|
||||
}
|
||||
if (!ts.isClassDeclaration(dtsNgModule)) {
|
||||
throw new Error(
|
||||
`The referenced NgModule in ${fn.declaration.getText()} is not a class declaration in the typings program; instead we get ${dtsNgModule.getText()}`);
|
||||
}
|
||||
// Record the usage of the internal module as it needs to become an exported symbol
|
||||
const reference = new Reference(ngModule.node);
|
||||
reference.addIdentifier(fn.ngModule);
|
||||
this.referencesRegistry.add(ngModule.node, reference);
|
||||
|
||||
ngModule = {node: dtsNgModule, viaModule: null};
|
||||
}
|
||||
const dtsFile = dtsFn.getSourceFile();
|
||||
const analysis = analyses.get(dtsFile) || [];
|
||||
analysis.push({declaration: dtsFn, ngModule});
|
||||
analyses.set(dtsFile, analysis);
|
||||
}
|
||||
});
|
||||
});
|
||||
return analyses;
|
||||
}
|
||||
|
||||
private getRootFiles(program: ts.Program): ts.SourceFile[] {
|
||||
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
|
||||
}
|
||||
|
||||
private getDtsDeclaration(fn: ts.SignatureDeclaration) {
|
||||
let dtsFn: ts.Declaration|null = null;
|
||||
const containerClass = this.host.getClassSymbol(fn.parent);
|
||||
const fnName = fn.name && ts.isIdentifier(fn.name) && fn.name.text;
|
||||
if (containerClass && fnName) {
|
||||
const dtsClass = this.host.getDtsDeclaration(containerClass.valueDeclaration);
|
||||
// Get the declaration of the matching static method
|
||||
dtsFn = dtsClass && ts.isClassDeclaration(dtsClass) ?
|
||||
dtsClass.members
|
||||
.find(
|
||||
member => ts.isMethodDeclaration(member) && ts.isIdentifier(member.name) &&
|
||||
member.name.text === fnName) as ts.Declaration :
|
||||
null;
|
||||
} else {
|
||||
dtsFn = this.host.getDtsDeclaration(fn);
|
||||
}
|
||||
if (!dtsFn) {
|
||||
throw new Error(`Matching type declaration for ${fn.getText()} is missing`);
|
||||
}
|
||||
if (!isFunctionOrMethod(dtsFn)) {
|
||||
throw new Error(
|
||||
`Matching type declaration for ${fn.getText()} is not a function: ${dtsFn.getText()}`);
|
||||
}
|
||||
return dtsFn;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function isFunctionOrMethod(declaration: ts.Declaration): declaration is ts.FunctionDeclaration|
|
||||
ts.MethodDeclaration {
|
||||
return ts.isFunctionDeclaration(declaration) || ts.isMethodDeclaration(declaration);
|
||||
}
|
||||
|
||||
function isAnyKeyword(typeParam: ts.TypeNode): typeParam is ts.KeywordTypeNode {
|
||||
return typeParam.kind === ts.SyntaxKind.AnyKeyword;
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {ReferencesRegistry} from '../../../src/ngtsc/annotations';
|
||||
import {Reference} from '../../../src/ngtsc/imports';
|
||||
import {Declaration, ReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {hasNameIdentifier} from '../utils';
|
||||
|
||||
/**
|
||||
* This is a place for DecoratorHandlers to register references that they
|
||||
* find in their analysis of the code.
|
||||
*
|
||||
* This registry is used to ensure that these references are publicly exported
|
||||
* from libraries that are compiled by ngcc.
|
||||
*/
|
||||
export class NgccReferencesRegistry implements ReferencesRegistry {
|
||||
private map = new Map<ts.Identifier, Declaration>();
|
||||
|
||||
constructor(private host: ReflectionHost) {}
|
||||
|
||||
/**
|
||||
* Register one or more references in the registry.
|
||||
* Only `ResolveReference` references are stored. Other types are ignored.
|
||||
* @param references A collection of references to register.
|
||||
*/
|
||||
add(source: ts.Declaration, ...references: Reference<ts.Declaration>[]): void {
|
||||
references.forEach(ref => {
|
||||
// Only store relative references. We are not interested in literals.
|
||||
if (ref.bestGuessOwningModule === null && hasNameIdentifier(ref.node)) {
|
||||
const declaration = this.host.getDeclarationOfIdentifier(ref.node.name);
|
||||
if (declaration && hasNameIdentifier(declaration.node)) {
|
||||
this.map.set(declaration.node.name, declaration);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and return a mapping for the registered resolved references.
|
||||
* @returns A map of reference identifiers to reference declarations.
|
||||
*/
|
||||
getDeclarationMap(): Map<ts.Identifier, Declaration> { return this.map; }
|
||||
}
|
@ -0,0 +1,105 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {Declaration} from '../../../src/ngtsc/reflection';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {hasNameIdentifier, isDefined} from '../utils';
|
||||
import {NgccReferencesRegistry} from './ngcc_references_registry';
|
||||
|
||||
export interface ExportInfo {
|
||||
identifier: string;
|
||||
from: string;
|
||||
dtsFrom?: string|null;
|
||||
alias?: string|null;
|
||||
}
|
||||
export type PrivateDeclarationsAnalyses = ExportInfo[];
|
||||
|
||||
/**
|
||||
* This class will analyze a program to find all the declared classes
|
||||
* (i.e. on an NgModule) that are not publicly exported via an entry-point.
|
||||
*/
|
||||
export class PrivateDeclarationsAnalyzer {
|
||||
constructor(
|
||||
private host: NgccReflectionHost, private referencesRegistry: NgccReferencesRegistry) {}
|
||||
|
||||
analyzeProgram(program: ts.Program): PrivateDeclarationsAnalyses {
|
||||
const rootFiles = this.getRootFiles(program);
|
||||
return this.getPrivateDeclarations(rootFiles, this.referencesRegistry.getDeclarationMap());
|
||||
}
|
||||
|
||||
private getRootFiles(program: ts.Program): ts.SourceFile[] {
|
||||
return program.getRootFileNames().map(f => program.getSourceFile(f)).filter(isDefined);
|
||||
}
|
||||
|
||||
private getPrivateDeclarations(
|
||||
rootFiles: ts.SourceFile[],
|
||||
declarations: Map<ts.Identifier, Declaration>): PrivateDeclarationsAnalyses {
|
||||
const privateDeclarations: Map<ts.Identifier, Declaration> = new Map(declarations);
|
||||
const exportAliasDeclarations: Map<ts.Identifier, string> = new Map();
|
||||
|
||||
rootFiles.forEach(f => {
|
||||
const exports = this.host.getExportsOfModule(f);
|
||||
if (exports) {
|
||||
exports.forEach((declaration, exportedName) => {
|
||||
if (hasNameIdentifier(declaration.node)) {
|
||||
const privateDeclaration = privateDeclarations.get(declaration.node.name);
|
||||
if (privateDeclaration) {
|
||||
if (privateDeclaration.node !== declaration.node) {
|
||||
throw new Error(`${declaration.node.name.text} is declared multiple times.`);
|
||||
}
|
||||
|
||||
if (declaration.node.name.text === exportedName) {
|
||||
// This declaration is public so we can remove it from the list
|
||||
privateDeclarations.delete(declaration.node.name);
|
||||
} else if (!this.host.getDtsDeclaration(declaration.node)) {
|
||||
// The referenced declaration is exported publicly but via an alias.
|
||||
// In some cases the original declaration is missing from the dts program, such as
|
||||
// when rolling up (flattening) the dts files.
|
||||
// This is because the original declaration gets renamed to the exported alias.
|
||||
|
||||
// There is a constraint on this which we cannot handle. Consider the following
|
||||
// code:
|
||||
//
|
||||
// /src/entry_point.js:
|
||||
// export {MyComponent as aliasedMyComponent} from './a';
|
||||
// export {MyComponent} from './b';`
|
||||
//
|
||||
// /src/a.js:
|
||||
// export class MyComponent {}
|
||||
//
|
||||
// /src/b.js:
|
||||
// export class MyComponent {}
|
||||
//
|
||||
// //typings/entry_point.d.ts:
|
||||
// export declare class aliasedMyComponent {}
|
||||
// export declare class MyComponent {}
|
||||
//
|
||||
// In this case we would end up matching the `MyComponent` from `/src/a.js` to the
|
||||
// `MyComponent` declared in `/typings/entry_point.d.ts` even though that
|
||||
// declaration is actually for the `MyComponent` in `/src/b.js`.
|
||||
|
||||
exportAliasDeclarations.set(declaration.node.name, exportedName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return Array.from(privateDeclarations.keys()).map(id => {
|
||||
const from = id.getSourceFile().fileName;
|
||||
const declaration = privateDeclarations.get(id) !;
|
||||
const alias = exportAliasDeclarations.get(id) || null;
|
||||
const dtsDeclaration = this.host.getDtsDeclaration(declaration.node);
|
||||
const dtsFrom = dtsDeclaration && dtsDeclaration.getSourceFile().fileName;
|
||||
|
||||
return {identifier: id.text, from, dtsFrom, alias};
|
||||
});
|
||||
}
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
||||
|
||||
export interface SwitchMarkerAnalysis {
|
||||
sourceFile: ts.SourceFile;
|
||||
declarations: SwitchableVariableDeclaration[];
|
||||
}
|
||||
|
||||
export type SwitchMarkerAnalyses = Map<ts.SourceFile, SwitchMarkerAnalysis>;
|
||||
export const SwitchMarkerAnalyses = Map;
|
||||
|
||||
/**
|
||||
* This Analyzer will analyse the files that have an R3 switch marker in them
|
||||
* that will be replaced.
|
||||
*/
|
||||
export class SwitchMarkerAnalyzer {
|
||||
constructor(private host: NgccReflectionHost) {}
|
||||
/**
|
||||
* Analyze the files in the program to identify declarations that contain R3
|
||||
* switch markers.
|
||||
* @param program The program to analyze.
|
||||
* @return A map of source files to analysis objects. The map will contain only the
|
||||
* source files that had switch markers, and the analysis will contain an array of
|
||||
* the declarations in that source file that contain the marker.
|
||||
*/
|
||||
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
|
||||
const analyzedFiles = new SwitchMarkerAnalyses();
|
||||
program.getSourceFiles().forEach(sourceFile => {
|
||||
const declarations = this.host.getSwitchableDeclarations(sourceFile);
|
||||
if (declarations.length) {
|
||||
analyzedFiles.set(sourceFile, {sourceFile, declarations});
|
||||
}
|
||||
});
|
||||
return analyzedFiles;
|
||||
}
|
||||
}
|
9
packages/compiler-cli/ngcc/src/constants.ts
Normal file
9
packages/compiler-cli/ngcc/src/constants.ts
Normal file
@ -0,0 +1,9 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
export const IMPORT_PREFIX = 'ɵngcc';
|
26
packages/compiler-cli/ngcc/src/host/decorated_class.ts
Normal file
26
packages/compiler-cli/ngcc/src/host/decorated_class.ts
Normal file
@ -0,0 +1,26 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
import {Decorator} from '../../../src/ngtsc/reflection';
|
||||
|
||||
/**
|
||||
* A simple container that holds the details of a decorated class that has been
|
||||
* found in a `DecoratedFile`.
|
||||
*/
|
||||
export class DecoratedClass {
|
||||
/**
|
||||
* Initialize a `DecoratedClass` that was found in a `DecoratedFile`.
|
||||
* @param name The name of the class that has been found. This is mostly used
|
||||
* for informational purposes.
|
||||
* @param declaration The TypeScript AST node where this class is declared
|
||||
* @param decorators The collection of decorators that have been found on this class.
|
||||
*/
|
||||
constructor(
|
||||
public name: string, public declaration: ts.Declaration, public decorators: Decorator[], ) {}
|
||||
}
|
1347
packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Normal file
1347
packages/compiler-cli/ngcc/src/host/esm2015_host.ts
Normal file
File diff suppressed because it is too large
Load Diff
608
packages/compiler-cli/ngcc/src/host/esm5_host.ts
Normal file
608
packages/compiler-cli/ngcc/src/host/esm5_host.ts
Normal file
@ -0,0 +1,608 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {ClassMember, ClassMemberKind, Declaration, Decorator, FunctionDefinition, Parameter, reflectObjectLiteral} from '../../../src/ngtsc/reflection';
|
||||
import {getNameText, hasNameIdentifier} from '../utils';
|
||||
|
||||
import {Esm2015ReflectionHost, ParamInfo, getPropertyValueFromSymbol, isAssignmentStatement} from './esm2015_host';
|
||||
|
||||
|
||||
/**
|
||||
* ESM5 packages contain ECMAScript IIFE functions that act like classes. For example:
|
||||
*
|
||||
* ```
|
||||
* var CommonModule = (function () {
|
||||
* function CommonModule() {
|
||||
* }
|
||||
* CommonModule.decorators = [ ... ];
|
||||
* ```
|
||||
*
|
||||
* * "Classes" are decorated if they have a static property called `decorators`.
|
||||
* * Members are decorated if there is a matching key on a static property
|
||||
* called `propDecorators`.
|
||||
* * Constructor parameters decorators are found on an object returned from
|
||||
* a static method called `ctorParameters`.
|
||||
*
|
||||
*/
|
||||
export class Esm5ReflectionHost extends Esm2015ReflectionHost {
|
||||
/**
|
||||
* Check whether the given node actually represents a class.
|
||||
*/
|
||||
isClass(node: ts.Node): node is ts.NamedDeclaration {
|
||||
return super.isClass(node) || !!this.getClassSymbol(node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the given declaration has a base class.
|
||||
*
|
||||
* In ES5, we need to determine if the IIFE wrapper takes a `_super` parameter .
|
||||
*/
|
||||
hasBaseClass(node: ts.Declaration): boolean {
|
||||
const classSymbol = this.getClassSymbol(node);
|
||||
if (!classSymbol) return false;
|
||||
|
||||
const iifeBody = classSymbol.valueDeclaration.parent;
|
||||
if (!iifeBody || !ts.isBlock(iifeBody)) return false;
|
||||
|
||||
const iife = iifeBody.parent;
|
||||
if (!iife || !ts.isFunctionExpression(iife)) return false;
|
||||
|
||||
return iife.parameters.length === 1 && isSuperIdentifier(iife.parameters[0].name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a symbol for a node that we think is a class.
|
||||
*
|
||||
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
|
||||
* So we might need to dig around inside to get hold of the "class" symbol.
|
||||
*
|
||||
* `node` might be one of:
|
||||
* - A class declaration (from a declaration file).
|
||||
* - The declaration of the outer variable, which is assigned the result of the IIFE.
|
||||
* - The function declaration inside the IIFE, which is eventually returned and assigned to the
|
||||
* outer variable.
|
||||
*
|
||||
* @param node the top level declaration that represents an exported class or the function
|
||||
* expression inside the IIFE.
|
||||
* @returns the symbol for the node or `undefined` if it is not a "class" or has no symbol.
|
||||
*/
|
||||
getClassSymbol(node: ts.Node): ts.Symbol|undefined {
|
||||
const symbol = super.getClassSymbol(node);
|
||||
if (symbol) return symbol;
|
||||
|
||||
if (ts.isVariableDeclaration(node)) {
|
||||
const iifeBody = getIifeBody(node);
|
||||
if (!iifeBody) return undefined;
|
||||
|
||||
const innerClassIdentifier = getReturnIdentifier(iifeBody);
|
||||
if (!innerClassIdentifier) return undefined;
|
||||
|
||||
return this.checker.getSymbolAtLocation(innerClassIdentifier);
|
||||
}
|
||||
|
||||
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(node);
|
||||
|
||||
return outerClassNode && this.getClassSymbol(outerClassNode);
|
||||
}
|
||||
|
||||
/**
|
||||
* Trace an identifier to its declaration, if possible.
|
||||
*
|
||||
* This method attempts to resolve the declaration of the given identifier, tracing back through
|
||||
* imports and re-exports until the original declaration statement is found. A `Declaration`
|
||||
* object is returned if the original declaration is found, or `null` is returned otherwise.
|
||||
*
|
||||
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE.
|
||||
* If we are looking for the declaration of the identifier of the inner function expression, we
|
||||
* will get hold of the outer "class" variable declaration and return its identifier instead. See
|
||||
* `getClassDeclarationFromInnerFunctionDeclaration()` for more info.
|
||||
*
|
||||
* @param id a TypeScript `ts.Identifier` to trace back to a declaration.
|
||||
*
|
||||
* @returns metadata about the `Declaration` if the original declaration is found, or `null`
|
||||
* otherwise.
|
||||
*/
|
||||
getDeclarationOfIdentifier(id: ts.Identifier): Declaration|null {
|
||||
// Get the identifier for the outer class node (if any).
|
||||
const outerClassNode = getClassDeclarationFromInnerFunctionDeclaration(id.parent);
|
||||
|
||||
if (outerClassNode && hasNameIdentifier(outerClassNode)) {
|
||||
id = outerClassNode.name;
|
||||
}
|
||||
|
||||
// Resolve the identifier to a Symbol, and return the declaration of that.
|
||||
return super.getDeclarationOfIdentifier(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a function declaration to find the relevant metadata about it.
|
||||
*
|
||||
* In ESM5 we need to do special work with optional arguments to the function, since they get
|
||||
* their own initializer statement that needs to be parsed and then not included in the "body"
|
||||
* statements of the function.
|
||||
*
|
||||
* @param node the function declaration to parse.
|
||||
* @returns an object containing the node, statements and parameters of the function.
|
||||
*/
|
||||
getDefinitionOfFunction<T extends ts.FunctionDeclaration|ts.MethodDeclaration|
|
||||
ts.FunctionExpression>(node: T): FunctionDefinition<T> {
|
||||
const parameters =
|
||||
node.parameters.map(p => ({name: getNameText(p.name), node: p, initializer: null}));
|
||||
let lookingForParamInitializers = true;
|
||||
|
||||
const statements = node.body && node.body.statements.filter(s => {
|
||||
lookingForParamInitializers =
|
||||
lookingForParamInitializers && reflectParamInitializer(s, parameters);
|
||||
// If we are no longer looking for parameter initializers then we include this statement
|
||||
return !lookingForParamInitializers;
|
||||
});
|
||||
|
||||
return {node, body: statements || null, parameters};
|
||||
}
|
||||
|
||||
|
||||
///////////// Protected Helpers /////////////
|
||||
|
||||
/**
|
||||
* Find the declarations of the constructor parameters of a class identified by its symbol.
|
||||
*
|
||||
* In ESM5 there is no "class" so the constructor that we want is actually the declaration
|
||||
* function itself.
|
||||
*
|
||||
* @param classSymbol the class whose parameters we want to find.
|
||||
* @returns an array of `ts.ParameterDeclaration` objects representing each of the parameters in
|
||||
* the class's constructor or null if there is no constructor.
|
||||
*/
|
||||
protected getConstructorParameterDeclarations(classSymbol: ts.Symbol):
|
||||
ts.ParameterDeclaration[]|null {
|
||||
const constructor = classSymbol.valueDeclaration as ts.FunctionDeclaration;
|
||||
if (constructor.parameters.length > 0) {
|
||||
return Array.from(constructor.parameters);
|
||||
}
|
||||
if (isSynthesizedConstructor(constructor)) {
|
||||
return null;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameter type and decorators for the constructor of a class,
|
||||
* where the information is stored on a static method of the class.
|
||||
*
|
||||
* In this case the decorators are stored in the body of a method
|
||||
* (`ctorParatemers`) attached to the constructor function.
|
||||
*
|
||||
* Note that unlike ESM2015 this is a function expression rather than an arrow
|
||||
* function:
|
||||
*
|
||||
* ```
|
||||
* SomeDirective.ctorParameters = function() { return [
|
||||
* { type: ViewContainerRef, },
|
||||
* { type: TemplateRef, },
|
||||
* { type: IterableDiffers, },
|
||||
* { type: undefined, decorators: [{ type: Inject, args: [INJECTED_TOKEN,] },] },
|
||||
* ]; };
|
||||
* ```
|
||||
*
|
||||
* @param paramDecoratorsProperty the property that holds the parameter info we want to get.
|
||||
* @returns an array of objects containing the type and decorators for each parameter.
|
||||
*/
|
||||
protected getParamInfoFromStaticProperty(paramDecoratorsProperty: ts.Symbol): ParamInfo[]|null {
|
||||
const paramDecorators = getPropertyValueFromSymbol(paramDecoratorsProperty);
|
||||
const returnStatement = getReturnStatement(paramDecorators);
|
||||
const expression = returnStatement && returnStatement.expression;
|
||||
if (expression && ts.isArrayLiteralExpression(expression)) {
|
||||
const elements = expression.elements;
|
||||
return elements.map(reflectArrayElement).map(paramInfo => {
|
||||
const typeExpression = paramInfo && paramInfo.get('type') || null;
|
||||
const decoratorInfo = paramInfo && paramInfo.get('decorators') || null;
|
||||
const decorators = decoratorInfo && this.reflectDecorators(decoratorInfo);
|
||||
return {typeExpression, decorators};
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reflect over a symbol and extract the member information, combining it with the
|
||||
* provided decorator information, and whether it is a static member.
|
||||
*
|
||||
* If a class member uses accessors (e.g getters and/or setters) then it gets downleveled
|
||||
* in ES5 to a single `Object.defineProperty()` call. In that case we must parse this
|
||||
* call to extract the one or two ClassMember objects that represent the accessors.
|
||||
*
|
||||
* @param symbol the symbol for the member to reflect over.
|
||||
* @param decorators an array of decorators associated with the member.
|
||||
* @param isStatic true if this member is static, false if it is an instance property.
|
||||
* @returns the reflected member information, or null if the symbol is not a member.
|
||||
*/
|
||||
protected reflectMembers(symbol: ts.Symbol, decorators?: Decorator[], isStatic?: boolean):
|
||||
ClassMember[]|null {
|
||||
const node = symbol.valueDeclaration || symbol.declarations && symbol.declarations[0];
|
||||
const propertyDefinition = node && getPropertyDefinition(node);
|
||||
if (propertyDefinition) {
|
||||
const members: ClassMember[] = [];
|
||||
if (propertyDefinition.setter) {
|
||||
members.push({
|
||||
node,
|
||||
implementation: propertyDefinition.setter,
|
||||
kind: ClassMemberKind.Setter,
|
||||
type: null,
|
||||
name: symbol.name,
|
||||
nameNode: null,
|
||||
value: null,
|
||||
isStatic: isStatic || false,
|
||||
decorators: decorators || [],
|
||||
});
|
||||
|
||||
// Prevent attaching the decorators to a potential getter. In ES5, we can't tell where the
|
||||
// decorators were originally attached to, however we only want to attach them to a single
|
||||
// `ClassMember` as otherwise ngtsc would handle the same decorators twice.
|
||||
decorators = undefined;
|
||||
}
|
||||
if (propertyDefinition.getter) {
|
||||
members.push({
|
||||
node,
|
||||
implementation: propertyDefinition.getter,
|
||||
kind: ClassMemberKind.Getter,
|
||||
type: null,
|
||||
name: symbol.name,
|
||||
nameNode: null,
|
||||
value: null,
|
||||
isStatic: isStatic || false,
|
||||
decorators: decorators || [],
|
||||
});
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
const members = super.reflectMembers(symbol, decorators, isStatic);
|
||||
members && members.forEach(member => {
|
||||
if (member && member.kind === ClassMemberKind.Method && member.isStatic && member.node &&
|
||||
ts.isPropertyAccessExpression(member.node) && member.node.parent &&
|
||||
ts.isBinaryExpression(member.node.parent) &&
|
||||
ts.isFunctionExpression(member.node.parent.right)) {
|
||||
// Recompute the implementation for this member:
|
||||
// ES5 static methods are variable declarations so the declaration is actually the
|
||||
// initializer of the variable assignment
|
||||
member.implementation = member.node.parent.right;
|
||||
}
|
||||
});
|
||||
return members;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find statements related to the given class that may contain calls to a helper.
|
||||
*
|
||||
* In ESM5 code the helper calls are hidden inside the class's IIFE.
|
||||
*
|
||||
* @param classSymbol the class whose helper calls we are interested in. We expect this symbol
|
||||
* to reference the inner identifier inside the IIFE.
|
||||
* @returns an array of statements that may contain helper calls.
|
||||
*/
|
||||
protected getStatementsForClass(classSymbol: ts.Symbol): ts.Statement[] {
|
||||
const classDeclaration = classSymbol.valueDeclaration;
|
||||
return ts.isBlock(classDeclaration.parent) ? Array.from(classDeclaration.parent.statements) :
|
||||
[];
|
||||
}
|
||||
}
|
||||
|
||||
///////////// Internal Helpers /////////////
|
||||
|
||||
/**
|
||||
* Represents the details about property definitions that were set using `Object.defineProperty`.
|
||||
*/
|
||||
interface PropertyDefinition {
|
||||
setter: ts.FunctionExpression|null;
|
||||
getter: ts.FunctionExpression|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* In ES5, getters and setters have been downleveled into call expressions of
|
||||
* `Object.defineProperty`, such as
|
||||
*
|
||||
* ```
|
||||
* Object.defineProperty(Clazz.prototype, "property", {
|
||||
* get: function () {
|
||||
* return 'value';
|
||||
* },
|
||||
* set: function (value) {
|
||||
* this.value = value;
|
||||
* },
|
||||
* enumerable: true,
|
||||
* configurable: true
|
||||
* });
|
||||
* ```
|
||||
*
|
||||
* This function inspects the given node to determine if it corresponds with such a call, and if so
|
||||
* extracts the `set` and `get` function expressions from the descriptor object, if they exist.
|
||||
*
|
||||
* @param node The node to obtain the property definition from.
|
||||
* @returns The property definition if the node corresponds with accessor, null otherwise.
|
||||
*/
|
||||
function getPropertyDefinition(node: ts.Node): PropertyDefinition|null {
|
||||
if (!ts.isCallExpression(node)) return null;
|
||||
|
||||
const fn = node.expression;
|
||||
if (!ts.isPropertyAccessExpression(fn) || !ts.isIdentifier(fn.expression) ||
|
||||
fn.expression.text !== 'Object' || fn.name.text !== 'defineProperty')
|
||||
return null;
|
||||
|
||||
const descriptor = node.arguments[2];
|
||||
if (!descriptor || !ts.isObjectLiteralExpression(descriptor)) return null;
|
||||
|
||||
return {
|
||||
setter: readPropertyFunctionExpression(descriptor, 'set'),
|
||||
getter: readPropertyFunctionExpression(descriptor, 'get'),
|
||||
};
|
||||
}
|
||||
|
||||
function readPropertyFunctionExpression(object: ts.ObjectLiteralExpression, name: string) {
|
||||
const property = object.properties.find(
|
||||
(p): p is ts.PropertyAssignment =>
|
||||
ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === name);
|
||||
|
||||
return property && ts.isFunctionExpression(property.initializer) && property.initializer || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual (outer) declaration of a class.
|
||||
*
|
||||
* In ES5, the implementation of a class is a function expression that is hidden inside an IIFE and
|
||||
* returned to be assigned to a variable outside the IIFE, which is what the rest of the program
|
||||
* interacts with.
|
||||
*
|
||||
* Given the inner function declaration, we want to get to the declaration of the outer variable
|
||||
* that represents the class.
|
||||
*
|
||||
* @param node a node that could be the function expression inside an ES5 class IIFE.
|
||||
* @returns the outer variable declaration or `undefined` if it is not a "class".
|
||||
*/
|
||||
function getClassDeclarationFromInnerFunctionDeclaration(node: ts.Node): ts.VariableDeclaration|
|
||||
undefined {
|
||||
if (ts.isFunctionDeclaration(node)) {
|
||||
// It might be the function expression inside the IIFE. We need to go 5 levels up...
|
||||
|
||||
// 1. IIFE body.
|
||||
let outerNode = node.parent;
|
||||
if (!outerNode || !ts.isBlock(outerNode)) return undefined;
|
||||
|
||||
// 2. IIFE function expression.
|
||||
outerNode = outerNode.parent;
|
||||
if (!outerNode || !ts.isFunctionExpression(outerNode)) return undefined;
|
||||
|
||||
// 3. IIFE call expression.
|
||||
outerNode = outerNode.parent;
|
||||
if (!outerNode || !ts.isCallExpression(outerNode)) return undefined;
|
||||
|
||||
// 4. Parenthesis around IIFE.
|
||||
outerNode = outerNode.parent;
|
||||
if (!outerNode || !ts.isParenthesizedExpression(outerNode)) return undefined;
|
||||
|
||||
// 5. Outer variable declaration.
|
||||
outerNode = outerNode.parent;
|
||||
if (!outerNode || !ts.isVariableDeclaration(outerNode)) return undefined;
|
||||
|
||||
return outerNode;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getIifeBody(declaration: ts.VariableDeclaration): ts.Block|undefined {
|
||||
if (!declaration.initializer || !ts.isParenthesizedExpression(declaration.initializer)) {
|
||||
return undefined;
|
||||
}
|
||||
const call = declaration.initializer;
|
||||
return ts.isCallExpression(call.expression) &&
|
||||
ts.isFunctionExpression(call.expression.expression) ?
|
||||
call.expression.expression.body :
|
||||
undefined;
|
||||
}
|
||||
|
||||
function getReturnIdentifier(body: ts.Block): ts.Identifier|undefined {
|
||||
const returnStatement = body.statements.find(ts.isReturnStatement);
|
||||
return returnStatement && returnStatement.expression &&
|
||||
ts.isIdentifier(returnStatement.expression) ?
|
||||
returnStatement.expression :
|
||||
undefined;
|
||||
}
|
||||
|
||||
function getReturnStatement(declaration: ts.Expression | undefined): ts.ReturnStatement|undefined {
|
||||
return declaration && ts.isFunctionExpression(declaration) ?
|
||||
declaration.body.statements.find(ts.isReturnStatement) :
|
||||
undefined;
|
||||
}
|
||||
|
||||
function reflectArrayElement(element: ts.Expression) {
|
||||
return ts.isObjectLiteralExpression(element) ? reflectObjectLiteral(element) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A constructor function may have been "synthesized" by TypeScript during JavaScript emit,
|
||||
* in the case no user-defined constructor exists and e.g. property initializers are used.
|
||||
* Those initializers need to be emitted into a constructor in JavaScript, so the TypeScript
|
||||
* compiler generates a synthetic constructor.
|
||||
*
|
||||
* We need to identify such constructors as ngcc needs to be able to tell if a class did
|
||||
* originally have a constructor in the TypeScript source. For ES5, we can not tell an
|
||||
* empty constructor apart from a synthesized constructor, but fortunately that does not
|
||||
* matter for the code generated by ngtsc.
|
||||
*
|
||||
* When a class has a superclass however, a synthesized constructor must not be considered
|
||||
* as a user-defined constructor as that prevents a base factory call from being created by
|
||||
* ngtsc, resulting in a factory function that does not inject the dependencies of the
|
||||
* superclass. Hence, we identify a default synthesized super call in the constructor body,
|
||||
* according to the structure that TypeScript's ES2015 to ES5 transformer generates in
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1082-L1098
|
||||
*
|
||||
* @param constructor a constructor function to test
|
||||
* @returns true if the constructor appears to have been synthesized
|
||||
*/
|
||||
function isSynthesizedConstructor(constructor: ts.FunctionDeclaration): boolean {
|
||||
if (!constructor.body) return false;
|
||||
|
||||
const firstStatement = constructor.body.statements[0];
|
||||
if (!firstStatement) return false;
|
||||
|
||||
return isSynthesizedSuperThisAssignment(firstStatement) ||
|
||||
isSynthesizedSuperReturnStatement(firstStatement);
|
||||
}
|
||||
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* var _this = _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperThisAssignment(statement: ts.Statement): boolean {
|
||||
if (!ts.isVariableStatement(statement)) return false;
|
||||
|
||||
const variableDeclarations = statement.declarationList.declarations;
|
||||
if (variableDeclarations.length !== 1) return false;
|
||||
|
||||
const variableDeclaration = variableDeclarations[0];
|
||||
if (!ts.isIdentifier(variableDeclaration.name) ||
|
||||
!variableDeclaration.name.text.startsWith('_this'))
|
||||
return false;
|
||||
|
||||
const initializer = variableDeclaration.initializer;
|
||||
if (!initializer) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(initializer);
|
||||
}
|
||||
/**
|
||||
* Identifies a synthesized super call of the form:
|
||||
*
|
||||
* ```
|
||||
* return _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be a synthesized super call
|
||||
* @returns true if the statement looks like a synthesized super call
|
||||
*/
|
||||
function isSynthesizedSuperReturnStatement(statement: ts.Statement): boolean {
|
||||
if (!ts.isReturnStatement(statement)) return false;
|
||||
|
||||
const expression = statement.expression;
|
||||
if (!expression) return false;
|
||||
|
||||
return isSynthesizedDefaultSuperCall(expression);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form:
|
||||
*
|
||||
* ```
|
||||
* _super !== null && _super.apply(this, arguments) || this;
|
||||
* ```
|
||||
*
|
||||
* This structure is generated by TypeScript when transforming ES2015 to ES5, see
|
||||
* https://github.com/Microsoft/TypeScript/blob/v3.2.2/src/compiler/transformers/es2015.ts#L1148-L1163
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSynthesizedDefaultSuperCall(expression: ts.Expression): boolean {
|
||||
if (!isBinaryExpr(expression, ts.SyntaxKind.BarBarToken)) return false;
|
||||
if (expression.right.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const left = expression.left;
|
||||
if (!isBinaryExpr(left, ts.SyntaxKind.AmpersandAmpersandToken)) return false;
|
||||
|
||||
return isSuperNotNull(left.left) && isSuperApplyCall(left.right);
|
||||
}
|
||||
|
||||
function isSuperNotNull(expression: ts.Expression): boolean {
|
||||
return isBinaryExpr(expression, ts.SyntaxKind.ExclamationEqualsEqualsToken) &&
|
||||
isSuperIdentifier(expression.left);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests whether the expression is of the form
|
||||
*
|
||||
* ```
|
||||
* _super.apply(this, arguments)
|
||||
* ```
|
||||
*
|
||||
* @param expression an expression that may represent a default super call
|
||||
* @returns true if the expression corresponds with the above form
|
||||
*/
|
||||
function isSuperApplyCall(expression: ts.Expression): boolean {
|
||||
if (!ts.isCallExpression(expression) || expression.arguments.length !== 2) return false;
|
||||
|
||||
const targetFn = expression.expression;
|
||||
if (!ts.isPropertyAccessExpression(targetFn)) return false;
|
||||
if (!isSuperIdentifier(targetFn.expression)) return false;
|
||||
if (targetFn.name.text !== 'apply') return false;
|
||||
|
||||
const thisArgument = expression.arguments[0];
|
||||
if (thisArgument.kind !== ts.SyntaxKind.ThisKeyword) return false;
|
||||
|
||||
const argumentsArgument = expression.arguments[1];
|
||||
return ts.isIdentifier(argumentsArgument) && argumentsArgument.text === 'arguments';
|
||||
}
|
||||
|
||||
function isBinaryExpr(
|
||||
expression: ts.Expression, operator: ts.BinaryOperator): expression is ts.BinaryExpression {
|
||||
return ts.isBinaryExpression(expression) && expression.operatorToken.kind === operator;
|
||||
}
|
||||
|
||||
function isSuperIdentifier(node: ts.Node): boolean {
|
||||
// Verify that the identifier is prefixed with `_super`. We don't test for equivalence
|
||||
// as TypeScript may have suffixed the name, e.g. `_super_1` to avoid name conflicts.
|
||||
// Requiring only a prefix should be sufficiently accurate.
|
||||
return ts.isIdentifier(node) && node.text.startsWith('_super');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the statement to extract the ESM5 parameter initializer if there is one.
|
||||
* If one is found, add it to the appropriate parameter in the `parameters` collection.
|
||||
*
|
||||
* The form we are looking for is:
|
||||
*
|
||||
* ```
|
||||
* if (arg === void 0) { arg = initializer; }
|
||||
* ```
|
||||
*
|
||||
* @param statement a statement that may be initializing an optional parameter
|
||||
* @param parameters the collection of parameters that were found in the function definition
|
||||
* @returns true if the statement was a parameter initializer
|
||||
*/
|
||||
function reflectParamInitializer(statement: ts.Statement, parameters: Parameter[]) {
|
||||
if (ts.isIfStatement(statement) && isUndefinedComparison(statement.expression) &&
|
||||
ts.isBlock(statement.thenStatement) && statement.thenStatement.statements.length === 1) {
|
||||
const ifStatementComparison = statement.expression; // (arg === void 0)
|
||||
const thenStatement = statement.thenStatement.statements[0]; // arg = initializer;
|
||||
if (isAssignmentStatement(thenStatement)) {
|
||||
const comparisonName = ifStatementComparison.left.text;
|
||||
const assignmentName = thenStatement.expression.left.text;
|
||||
if (comparisonName === assignmentName) {
|
||||
const parameter = parameters.find(p => p.name === comparisonName);
|
||||
if (parameter) {
|
||||
parameter.initializer = thenStatement.expression.right;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function isUndefinedComparison(expression: ts.Expression): expression is ts.Expression&
|
||||
{left: ts.Identifier, right: ts.Expression} {
|
||||
return ts.isBinaryExpression(expression) &&
|
||||
expression.operatorToken.kind === ts.SyntaxKind.EqualsEqualsEqualsToken &&
|
||||
ts.isVoidExpression(expression.right) && ts.isIdentifier(expression.left);
|
||||
}
|
72
packages/compiler-cli/ngcc/src/host/ngcc_host.ts
Normal file
72
packages/compiler-cli/ngcc/src/host/ngcc_host.ts
Normal file
@ -0,0 +1,72 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import {ReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {DecoratedClass} from './decorated_class';
|
||||
|
||||
export const PRE_R3_MARKER = '__PRE_R3__';
|
||||
export const POST_R3_MARKER = '__POST_R3__';
|
||||
|
||||
export type SwitchableVariableDeclaration = ts.VariableDeclaration & {initializer: ts.Identifier};
|
||||
export function isSwitchableVariableDeclaration(node: ts.Node):
|
||||
node is SwitchableVariableDeclaration {
|
||||
return ts.isVariableDeclaration(node) && !!node.initializer &&
|
||||
ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_R3_MARKER);
|
||||
}
|
||||
|
||||
/**
|
||||
* A structure returned from `getModuleWithProviderInfo` that describes functions
|
||||
* that return ModuleWithProviders objects.
|
||||
*/
|
||||
export interface ModuleWithProvidersFunction {
|
||||
/**
|
||||
* The declaration of the function that returns the `ModuleWithProviders` object.
|
||||
*/
|
||||
declaration: ts.SignatureDeclaration;
|
||||
/**
|
||||
* The identifier of the `ngModule` property on the `ModuleWithProviders` object.
|
||||
*/
|
||||
ngModule: ts.Identifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* A reflection host that has extra methods for looking at non-Typescript package formats
|
||||
*/
|
||||
export interface NgccReflectionHost extends ReflectionHost {
|
||||
/**
|
||||
* Find a symbol for a declaration that we think is a class.
|
||||
* @param declaration The declaration whose symbol we are finding
|
||||
* @returns the symbol for the declaration or `undefined` if it is not
|
||||
* a "class" or has no symbol.
|
||||
*/
|
||||
getClassSymbol(node: ts.Node): ts.Symbol|undefined;
|
||||
|
||||
/**
|
||||
* Search the given module for variable declarations in which the initializer
|
||||
* is an identifier marked with the `PRE_R3_MARKER`.
|
||||
* @param module The module in which to search for switchable declarations.
|
||||
* @returns An array of variable declarations that match.
|
||||
*/
|
||||
getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[];
|
||||
|
||||
/**
|
||||
* Find all the classes that contain decorations in a given file.
|
||||
* @param sourceFile The source file to search for decorated classes.
|
||||
* @returns An array of decorated classes.
|
||||
*/
|
||||
findDecoratedClasses(sourceFile: ts.SourceFile): DecoratedClass[];
|
||||
|
||||
/**
|
||||
* Search the given source file for exported functions and static class methods that return
|
||||
* ModuleWithProviders objects.
|
||||
* @param f The source file to search for these functions
|
||||
* @returns An array of info items about each of the functions that return ModuleWithProviders
|
||||
* objects.
|
||||
*/
|
||||
getModuleWithProvidersFunctions(f: ts.SourceFile): ModuleWithProvidersFunction[];
|
||||
}
|
87
packages/compiler-cli/ngcc/src/main.ts
Normal file
87
packages/compiler-cli/ngcc/src/main.ts
Normal file
@ -0,0 +1,87 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as path from 'canonical-path';
|
||||
import * as yargs from 'yargs';
|
||||
|
||||
import {checkMarkerFile, writeMarkerFile} from './packages/build_marker';
|
||||
import {DependencyHost} from './packages/dependency_host';
|
||||
import {DependencyResolver} from './packages/dependency_resolver';
|
||||
import {EntryPointFormat} from './packages/entry_point';
|
||||
import {makeEntryPointBundle} from './packages/entry_point_bundle';
|
||||
import {EntryPointFinder} from './packages/entry_point_finder';
|
||||
import {Transformer} from './packages/transformer';
|
||||
|
||||
export function mainNgcc(args: string[]): number {
|
||||
const options =
|
||||
yargs
|
||||
.option('s', {
|
||||
alias: 'source',
|
||||
describe: 'A path to the root folder to compile.',
|
||||
default: './node_modules'
|
||||
})
|
||||
.option('f', {
|
||||
alias: 'formats',
|
||||
array: true,
|
||||
describe: 'An array of formats to compile.',
|
||||
default: ['fesm2015', 'esm2015', 'fesm5', 'esm5']
|
||||
})
|
||||
.option('t', {
|
||||
alias: 'target',
|
||||
describe: 'A path to a root folder where the compiled files will be written.',
|
||||
defaultDescription: 'The `source` folder.'
|
||||
})
|
||||
.help()
|
||||
.parse(args);
|
||||
|
||||
const sourcePath: string = path.resolve(options['s']);
|
||||
const formats: EntryPointFormat[] = options['f'];
|
||||
const targetPath: string = options['t'] || sourcePath;
|
||||
|
||||
const transformer = new Transformer(sourcePath, targetPath);
|
||||
const host = new DependencyHost();
|
||||
const resolver = new DependencyResolver(host);
|
||||
const finder = new EntryPointFinder(resolver);
|
||||
|
||||
try {
|
||||
const {entryPoints} = finder.findEntryPoints(sourcePath);
|
||||
entryPoints.forEach(entryPoint => {
|
||||
|
||||
// Are we compiling the Angular core?
|
||||
const isCore = entryPoint.name === '@angular/core';
|
||||
|
||||
// We transform the d.ts typings files while transforming one of the formats.
|
||||
// This variable decides with which of the available formats to do this transform.
|
||||
// It is marginally faster to process via the flat file if available.
|
||||
const dtsTransformFormat: EntryPointFormat = entryPoint.fesm2015 ? 'fesm2015' : 'esm2015';
|
||||
|
||||
formats.forEach(format => {
|
||||
if (checkMarkerFile(entryPoint, format)) {
|
||||
console.warn(`Skipping ${entryPoint.name} : ${format} (already built).`);
|
||||
return;
|
||||
}
|
||||
|
||||
const bundle =
|
||||
makeEntryPointBundle(entryPoint, isCore, format, format === dtsTransformFormat);
|
||||
if (bundle === null) {
|
||||
console.warn(
|
||||
`Skipping ${entryPoint.name} : ${format} (no entry point file for this format).`);
|
||||
} else {
|
||||
transformer.transform(entryPoint, isCore, bundle);
|
||||
}
|
||||
|
||||
// Write the built-with-ngcc marker
|
||||
writeMarkerFile(entryPoint, format);
|
||||
});
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e.stack || e.message);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
41
packages/compiler-cli/ngcc/src/packages/build_marker.ts
Normal file
41
packages/compiler-cli/ngcc/src/packages/build_marker.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {resolve} from 'canonical-path';
|
||||
import {existsSync, readFileSync, writeFileSync} from 'fs';
|
||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||
|
||||
export const NGCC_VERSION = '0.0.0-PLACEHOLDER';
|
||||
|
||||
function getMarkerPath(entryPointPath: string, format: EntryPointFormat) {
|
||||
return resolve(entryPointPath, `__modified_by_ngcc_for_${format}__`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether there is a build marker for the given entry point and format.
|
||||
* @param entryPoint the entry point to check for a marker.
|
||||
* @param format the format for which we are checking for a marker.
|
||||
*/
|
||||
export function checkMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat): boolean {
|
||||
const markerPath = getMarkerPath(entryPoint.path, format);
|
||||
const markerExists = existsSync(markerPath);
|
||||
if (markerExists) {
|
||||
const previousVersion = readFileSync(markerPath, 'utf8');
|
||||
if (previousVersion !== NGCC_VERSION) {
|
||||
throw new Error(
|
||||
'The ngcc compiler has changed since the last ngcc build.\n' +
|
||||
'Please completely remove `node_modules` and try again.');
|
||||
}
|
||||
}
|
||||
return markerExists;
|
||||
}
|
||||
|
||||
export function writeMarkerFile(entryPoint: EntryPoint, format: EntryPointFormat) {
|
||||
const markerPath = getMarkerPath(entryPoint.path, format);
|
||||
writeFileSync(markerPath, NGCC_VERSION, 'utf8');
|
||||
}
|
74
packages/compiler-cli/ngcc/src/packages/bundle_program.ts
Normal file
74
packages/compiler-cli/ngcc/src/packages/bundle_program.ts
Normal file
@ -0,0 +1,74 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {dirname, resolve} from 'canonical-path';
|
||||
import {existsSync, lstatSync, readdirSync} from 'fs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* An entry point bundle contains one or two programs, e.g. `src` and `dts`,
|
||||
* that are compiled via TypeScript.
|
||||
*
|
||||
* To aid with processing the program, this interface exposes the program itself,
|
||||
* as well as path and TS file of the entry-point to the program and the r3Symbols
|
||||
* file, if appropriate.
|
||||
*/
|
||||
export interface BundleProgram {
|
||||
program: ts.Program;
|
||||
options: ts.CompilerOptions;
|
||||
host: ts.CompilerHost;
|
||||
path: string;
|
||||
file: ts.SourceFile;
|
||||
r3SymbolsPath: string|null;
|
||||
r3SymbolsFile: ts.SourceFile|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a bundle program.
|
||||
*/
|
||||
export function makeBundleProgram(
|
||||
isCore: boolean, path: string, r3FileName: string, options: ts.CompilerOptions,
|
||||
host: ts.CompilerHost): BundleProgram {
|
||||
const r3SymbolsPath = isCore ? findR3SymbolsPath(dirname(path), r3FileName) : null;
|
||||
const rootPaths = r3SymbolsPath ? [path, r3SymbolsPath] : [path];
|
||||
const program = ts.createProgram(rootPaths, options, host);
|
||||
const file = program.getSourceFile(path) !;
|
||||
const r3SymbolsFile = r3SymbolsPath && program.getSourceFile(r3SymbolsPath) || null;
|
||||
|
||||
return {program, options, host, path, file, r3SymbolsPath, r3SymbolsFile};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search the given directory hierarchy to find the path to the `r3_symbols` file.
|
||||
*/
|
||||
export function findR3SymbolsPath(directory: string, filename: string): string|null {
|
||||
const r3SymbolsFilePath = resolve(directory, filename);
|
||||
if (existsSync(r3SymbolsFilePath)) {
|
||||
return r3SymbolsFilePath;
|
||||
}
|
||||
|
||||
const subDirectories =
|
||||
readdirSync(directory)
|
||||
// Not interested in hidden files
|
||||
.filter(p => !p.startsWith('.'))
|
||||
// Ignore node_modules
|
||||
.filter(p => p !== 'node_modules')
|
||||
// Only interested in directories (and only those that are not symlinks)
|
||||
.filter(p => {
|
||||
const stat = lstatSync(resolve(directory, p));
|
||||
return stat.isDirectory() && !stat.isSymbolicLink();
|
||||
});
|
||||
|
||||
for (const subDirectory of subDirectories) {
|
||||
const r3SymbolsFilePath = findR3SymbolsPath(resolve(directory, subDirectory, ), filename);
|
||||
if (r3SymbolsFilePath) {
|
||||
return r3SymbolsFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
148
packages/compiler-cli/ngcc/src/packages/dependency_host.ts
Normal file
148
packages/compiler-cli/ngcc/src/packages/dependency_host.ts
Normal file
@ -0,0 +1,148 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
/**
|
||||
* Helper functions for computing dependencies.
|
||||
*/
|
||||
export class DependencyHost {
|
||||
/**
|
||||
* Get a list of the resolved paths to all the dependencies of this entry point.
|
||||
* @param from An absolute path to the file whose dependencies we want to get.
|
||||
* @param resolved A set that will have the absolute paths of resolved entry points added to it.
|
||||
* @param missing A set that will have the dependencies that could not be found added to it.
|
||||
* @param deepImports A set that will have the import paths that exist but cannot be mapped to
|
||||
* entry-points, i.e. deep-imports.
|
||||
* @param internal A set that is used to track internal dependencies to prevent getting stuck in a
|
||||
* circular dependency loop.
|
||||
*/
|
||||
computeDependencies(
|
||||
from: string, resolved: Set<string>, missing: Set<string>, deepImports: Set<string>,
|
||||
internal: Set<string> = new Set()): void {
|
||||
const fromContents = fs.readFileSync(from, 'utf8');
|
||||
if (!this.hasImportOrReexportStatements(fromContents)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the source into a TypeScript AST and then walk it looking for imports and re-exports.
|
||||
const sf =
|
||||
ts.createSourceFile(from, fromContents, ts.ScriptTarget.ES2015, false, ts.ScriptKind.JS);
|
||||
sf.statements
|
||||
// filter out statements that are not imports or reexports
|
||||
.filter(this.isStringImportOrReexport)
|
||||
// Grab the id of the module that is being imported
|
||||
.map(stmt => stmt.moduleSpecifier.text)
|
||||
// Resolve this module id into an absolute path
|
||||
.forEach(importPath => {
|
||||
if (importPath.startsWith('.')) {
|
||||
// This is an internal import so follow it
|
||||
const internalDependency = this.resolveInternal(from, importPath);
|
||||
// Avoid circular dependencies
|
||||
if (!internal.has(internalDependency)) {
|
||||
internal.add(internalDependency);
|
||||
this.computeDependencies(
|
||||
internalDependency, resolved, missing, deepImports, internal);
|
||||
}
|
||||
} else {
|
||||
const resolvedEntryPoint = this.tryResolveEntryPoint(from, importPath);
|
||||
if (resolvedEntryPoint !== null) {
|
||||
resolved.add(resolvedEntryPoint);
|
||||
} else {
|
||||
// If the import could not be resolved as entry point, it either does not exist
|
||||
// at all or is a deep import.
|
||||
const deeplyImportedFile = this.tryResolve(from, importPath);
|
||||
if (deeplyImportedFile !== null) {
|
||||
deepImports.add(importPath);
|
||||
} else {
|
||||
missing.add(importPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an internal module import.
|
||||
* @param from the absolute file path from where to start trying to resolve this module
|
||||
* @param to the module specifier of the internal dependency to resolve
|
||||
* @returns the resolved path to the import.
|
||||
*/
|
||||
resolveInternal(from: string, to: string): string {
|
||||
const fromDirectory = path.dirname(from);
|
||||
// `fromDirectory` is absolute so we don't need to worry about telling `require.resolve`
|
||||
// about it - unlike `tryResolve` below.
|
||||
return require.resolve(path.resolve(fromDirectory, to));
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't want to resolve external dependencies directly because if it is a path to a
|
||||
* sub-entry-point (e.g. @angular/animations/browser rather than @angular/animations)
|
||||
* then `require.resolve()` may return a path to a UMD bundle, which may actually live
|
||||
* in the folder containing the sub-entry-point
|
||||
* (e.g. @angular/animations/bundles/animations-browser.umd.js).
|
||||
*
|
||||
* Instead we try to resolve it as a package, which is what we would need anyway for it to be
|
||||
* compilable by ngcc.
|
||||
*
|
||||
* If `to` is actually a path to a file then this will fail, which is what we want.
|
||||
*
|
||||
* @param from the file path from where to start trying to resolve this module
|
||||
* @param to the module specifier of the dependency to resolve
|
||||
* @returns the resolved path to the entry point directory of the import or null
|
||||
* if it cannot be resolved.
|
||||
*/
|
||||
tryResolveEntryPoint(from: string, to: string): string|null {
|
||||
const entryPoint = this.tryResolve(from, `${to}/package.json`);
|
||||
return entryPoint && path.dirname(entryPoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the absolute path of a module from a particular starting point.
|
||||
*
|
||||
* @param from the file path from where to start trying to resolve this module
|
||||
* @param to the module specifier of the dependency to resolve
|
||||
* @returns an absolute path to the entry-point of the dependency or null if it could not be
|
||||
* resolved.
|
||||
*/
|
||||
tryResolve(from: string, to: string): string|null {
|
||||
try {
|
||||
return require.resolve(to, {paths: [from]});
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given statement is an import with a string literal module specifier.
|
||||
* @param stmt the statement node to check.
|
||||
* @returns true if the statement is an import with a string literal module specifier.
|
||||
*/
|
||||
isStringImportOrReexport(stmt: ts.Statement): stmt is ts.ImportDeclaration&
|
||||
{moduleSpecifier: ts.StringLiteral} {
|
||||
return ts.isImportDeclaration(stmt) ||
|
||||
ts.isExportDeclaration(stmt) && !!stmt.moduleSpecifier &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a source file needs to be parsed for imports.
|
||||
* This is a performance short-circuit, which saves us from creating
|
||||
* a TypeScript AST unnecessarily.
|
||||
*
|
||||
* @param source The content of the source file to check.
|
||||
*
|
||||
* @returns false if there are definitely no import or re-export statements
|
||||
* in this file, true otherwise.
|
||||
*/
|
||||
hasImportOrReexportStatements(source: string): boolean {
|
||||
return /(import|export)\s.+from/.test(source);
|
||||
}
|
||||
}
|
138
packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts
Normal file
138
packages/compiler-cli/ngcc/src/packages/dependency_resolver.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {DepGraph} from 'dependency-graph';
|
||||
import {DependencyHost} from './dependency_host';
|
||||
import {EntryPoint} from './entry_point';
|
||||
|
||||
|
||||
/**
|
||||
* Holds information about entry points that are removed because
|
||||
* they have dependencies that are missing (directly or transitively).
|
||||
*
|
||||
* This might not be an error, because such an entry point might not actually be used
|
||||
* in the application. If it is used then the `ngc` application compilation would
|
||||
* fail also, so we don't need ngcc to catch this.
|
||||
*
|
||||
* For example, consider an application that uses the `@angular/router` package.
|
||||
* This package includes an entry-point called `@angular/router/upgrade`, which has a dependency
|
||||
* on the `@angular/upgrade` package.
|
||||
* If the application never uses code from `@angular/router/upgrade` then there is no need for
|
||||
* `@angular/upgrade` to be installed.
|
||||
* In this case the ngcc tool should just ignore the `@angular/router/upgrade` end-point.
|
||||
*/
|
||||
export interface InvalidEntryPoint {
|
||||
entryPoint: EntryPoint;
|
||||
missingDependencies: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Holds information about dependencies of an entry-point that do not need to be processed
|
||||
* by the ngcc tool.
|
||||
*
|
||||
* For example, the `rxjs` package does not contain any Angular decorators that need to be
|
||||
* compiled and so this can be safely ignored by ngcc.
|
||||
*/
|
||||
export interface IgnoredDependency {
|
||||
entryPoint: EntryPoint;
|
||||
dependencyPath: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The result of sorting the entry-points by their dependencies.
|
||||
*
|
||||
* The `entryPoints` array will be ordered so that no entry point depends upon an entry point that
|
||||
* appears later in the array.
|
||||
*
|
||||
* Some entry points or their dependencies may be have been ignored. These are captured for
|
||||
* diagnostic purposes in `invalidEntryPoints` and `ignoredDependencies` respectively.
|
||||
*/
|
||||
export interface SortedEntryPointsInfo {
|
||||
entryPoints: EntryPoint[];
|
||||
invalidEntryPoints: InvalidEntryPoint[];
|
||||
ignoredDependencies: IgnoredDependency[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A class that resolves dependencies between entry-points.
|
||||
*/
|
||||
export class DependencyResolver {
|
||||
constructor(private host: DependencyHost) {}
|
||||
/**
|
||||
* Sort the array of entry points so that the dependant entry points always come later than
|
||||
* their dependencies in the array.
|
||||
* @param entryPoints An array entry points to sort.
|
||||
* @returns the result of sorting the entry points.
|
||||
*/
|
||||
sortEntryPointsByDependency(entryPoints: EntryPoint[]): SortedEntryPointsInfo {
|
||||
const invalidEntryPoints: InvalidEntryPoint[] = [];
|
||||
const ignoredDependencies: IgnoredDependency[] = [];
|
||||
const graph = new DepGraph<EntryPoint>();
|
||||
|
||||
// Add the entry points to the graph as nodes
|
||||
entryPoints.forEach(entryPoint => graph.addNode(entryPoint.path, entryPoint));
|
||||
|
||||
// Now add the dependencies between them
|
||||
entryPoints.forEach(entryPoint => {
|
||||
const entryPointPath = entryPoint.fesm2015 || entryPoint.esm2015;
|
||||
if (!entryPointPath) {
|
||||
throw new Error(
|
||||
`ESM2015 format (flat and non-flat) missing in '${entryPoint.path}' entry-point.`);
|
||||
}
|
||||
|
||||
const dependencies = new Set<string>();
|
||||
const missing = new Set<string>();
|
||||
const deepImports = new Set<string>();
|
||||
this.host.computeDependencies(entryPointPath, dependencies, missing, deepImports);
|
||||
|
||||
if (missing.size > 0) {
|
||||
// This entry point has dependencies that are missing
|
||||
// so remove it from the graph.
|
||||
removeNodes(entryPoint, Array.from(missing));
|
||||
} else {
|
||||
dependencies.forEach(dependencyPath => {
|
||||
if (graph.hasNode(dependencyPath)) {
|
||||
// The dependency path maps to an entry point that exists in the graph
|
||||
// so add the dependency.
|
||||
graph.addDependency(entryPoint.path, dependencyPath);
|
||||
} else if (invalidEntryPoints.some(i => i.entryPoint.path === dependencyPath)) {
|
||||
// The dependency path maps to an entry-point that was previously removed
|
||||
// from the graph, so remove this entry-point as well.
|
||||
removeNodes(entryPoint, [dependencyPath]);
|
||||
} else {
|
||||
// The dependency path points to a package that ngcc does not care about.
|
||||
ignoredDependencies.push({entryPoint, dependencyPath});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (deepImports.size) {
|
||||
const imports = Array.from(deepImports).map(i => `'${i}'`).join(', ');
|
||||
console.warn(
|
||||
`Entry point '${entryPoint.name}' contains deep imports into ${imports}. ` +
|
||||
`This is probably not a problem, but may cause the compilation of entry points to be out of order.`);
|
||||
}
|
||||
});
|
||||
|
||||
// The map now only holds entry-points that ngcc cares about and whose dependencies
|
||||
// (direct and transitive) all exist.
|
||||
return {
|
||||
entryPoints: graph.overallOrder().map(path => graph.getNodeData(path)),
|
||||
invalidEntryPoints,
|
||||
ignoredDependencies
|
||||
};
|
||||
|
||||
function removeNodes(entryPoint: EntryPoint, missingDependencies: string[]) {
|
||||
const nodesToRemove = [entryPoint.path, ...graph.dependantsOf(entryPoint.path)];
|
||||
nodesToRemove.forEach(node => {
|
||||
invalidEntryPoints.push({entryPoint: graph.getNodeData(node), missingDependencies});
|
||||
graph.removeNode(node);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
142
packages/compiler-cli/ngcc/src/packages/entry_point.ts
Normal file
142
packages/compiler-cli/ngcc/src/packages/entry_point.ts
Normal file
@ -0,0 +1,142 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
|
||||
/**
|
||||
* An object containing paths to the entry-points for each format.
|
||||
*/
|
||||
export interface EntryPointPaths {
|
||||
esm5?: string;
|
||||
fesm5?: string;
|
||||
esm2015?: string;
|
||||
fesm2015?: string;
|
||||
umd?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The possible values for the format of an entry-point.
|
||||
*/
|
||||
export type EntryPointFormat = keyof(EntryPointPaths);
|
||||
|
||||
/**
|
||||
* An object containing information about an entry-point, including paths
|
||||
* to each of the possible entry-point formats.
|
||||
*/
|
||||
export interface EntryPoint extends EntryPointPaths {
|
||||
/** The name of the package (e.g. `@angular/core`). */
|
||||
name: string;
|
||||
/** The path to the package that contains this entry-point. */
|
||||
package: string;
|
||||
/** The path to this entry point. */
|
||||
path: string;
|
||||
/** The path to a typings (.d.ts) file for this entry-point. */
|
||||
typings: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The properties that may be loaded from the `package.json` file.
|
||||
*/
|
||||
interface EntryPointPackageJson {
|
||||
name: string;
|
||||
fesm2015?: string;
|
||||
fesm5?: string;
|
||||
es2015?: string; // if exists then it is actually FESM2015
|
||||
esm2015?: string;
|
||||
esm5?: string;
|
||||
main?: string; // UMD
|
||||
module?: string; // if exists then it is actually FESM5
|
||||
types?: string; // Synonymous to `typings` property - see https://bit.ly/2OgWp2H
|
||||
typings?: string; // TypeScript .d.ts files
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the JSON from a package.json file.
|
||||
* @param packageJsonPath the absolute path to the package.json file.
|
||||
* @returns JSON from the package.json file if it is valid, `null` otherwise.
|
||||
*/
|
||||
function loadEntryPointPackage(packageJsonPath: string): EntryPointPackageJson|null {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
||||
} catch (e) {
|
||||
// We may have run into a package.json with unexpected symbols
|
||||
console.warn(`Failed to read entry point info from ${packageJsonPath} with error ${e}.`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to get an entry point from the given path.
|
||||
* @param packagePath the absolute path to the containing npm package
|
||||
* @param entryPointPath the absolute path to the potential entry point.
|
||||
* @returns Info about the entry point if it is valid, `null` otherwise.
|
||||
*/
|
||||
export function getEntryPointInfo(packagePath: string, entryPointPath: string): EntryPoint|null {
|
||||
const packageJsonPath = path.resolve(entryPointPath, 'package.json');
|
||||
if (!fs.existsSync(packageJsonPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entryPointPackageJson = loadEntryPointPackage(packageJsonPath);
|
||||
if (!entryPointPackageJson) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If there is `esm2015` then `es2015` will be FESM2015, otherwise ESM2015.
|
||||
// If there is `esm5` then `module` will be FESM5, otherwise it will be ESM5.
|
||||
const {
|
||||
name,
|
||||
module: modulePath,
|
||||
types,
|
||||
typings = types, // synonymous
|
||||
es2015,
|
||||
fesm2015 = es2015, // synonymous
|
||||
fesm5 = modulePath, // synonymous
|
||||
esm2015,
|
||||
esm5,
|
||||
main
|
||||
} = entryPointPackageJson;
|
||||
// Minimum requirement is that we have typings and one of esm2015 or fesm2015 formats.
|
||||
if (!typings || !(fesm2015 || esm2015)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Also there must exist a `metadata.json` file next to the typings entry-point.
|
||||
const metadataPath =
|
||||
path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
|
||||
if (!fs.existsSync(metadataPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const entryPointInfo: EntryPoint = {
|
||||
name,
|
||||
package: packagePath,
|
||||
path: entryPointPath,
|
||||
typings: path.resolve(entryPointPath, typings),
|
||||
};
|
||||
|
||||
if (esm2015) {
|
||||
entryPointInfo.esm2015 = path.resolve(entryPointPath, esm2015);
|
||||
}
|
||||
if (fesm2015) {
|
||||
entryPointInfo.fesm2015 = path.resolve(entryPointPath, fesm2015);
|
||||
}
|
||||
if (fesm5) {
|
||||
entryPointInfo.fesm5 = path.resolve(entryPointPath, fesm5);
|
||||
}
|
||||
if (esm5) {
|
||||
entryPointInfo.esm5 = path.resolve(entryPointPath, esm5);
|
||||
}
|
||||
if (main) {
|
||||
entryPointInfo.umd = path.resolve(entryPointPath, main);
|
||||
}
|
||||
|
||||
return entryPointInfo;
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||
|
||||
import {BundleProgram, makeBundleProgram} from './bundle_program';
|
||||
import {EntryPoint, EntryPointFormat} from './entry_point';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A bundle of files and paths (and TS programs) that correspond to a particular
|
||||
* format of a package entry-point.
|
||||
*/
|
||||
export interface EntryPointBundle {
|
||||
format: EntryPointFormat;
|
||||
isFlat: boolean;
|
||||
rootDirs: AbsoluteFsPath[];
|
||||
src: BundleProgram;
|
||||
dts: BundleProgram|null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an object that describes a formatted bundle for an entry-point.
|
||||
* @param entryPoint The entry-point that contains the bundle.
|
||||
* @param format The format of the bundle.
|
||||
* @param transformDts True if processing this bundle should also process its `.d.ts` files.
|
||||
*/
|
||||
export function makeEntryPointBundle(
|
||||
entryPoint: EntryPoint, isCore: boolean, format: EntryPointFormat,
|
||||
transformDts: boolean): EntryPointBundle|null {
|
||||
// Bail out if the entry-point does not have this format.
|
||||
const path = entryPoint[format];
|
||||
if (!path) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Create the TS program and necessary helpers.
|
||||
const options: ts.CompilerOptions = {
|
||||
allowJs: true,
|
||||
maxNodeModuleJsDepth: Infinity,
|
||||
rootDir: entryPoint.path,
|
||||
};
|
||||
const host = ts.createCompilerHost(options);
|
||||
const rootDirs = [AbsoluteFsPath.from(entryPoint.path)];
|
||||
|
||||
// Create the bundle programs, as necessary.
|
||||
const src = makeBundleProgram(isCore, path, 'r3_symbols.js', options, host);
|
||||
const dts = transformDts ?
|
||||
makeBundleProgram(isCore, entryPoint.typings, 'r3_symbols.d.ts', options, host) :
|
||||
null;
|
||||
const isFlat = src.r3SymbolsFile === null;
|
||||
|
||||
return {format, rootDirs, isFlat, src, dts};
|
||||
}
|
111
packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts
Normal file
111
packages/compiler-cli/ngcc/src/packages/entry_point_finder.ts
Normal file
@ -0,0 +1,111 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as path from 'canonical-path';
|
||||
import * as fs from 'fs';
|
||||
|
||||
import {DependencyResolver, SortedEntryPointsInfo} from './dependency_resolver';
|
||||
import {EntryPoint, getEntryPointInfo} from './entry_point';
|
||||
|
||||
|
||||
export class EntryPointFinder {
|
||||
constructor(private resolver: DependencyResolver) {}
|
||||
/**
|
||||
* Search the given directory, and sub-directories, for Angular package entry points.
|
||||
* @param sourceDirectory An absolute path to the directory to search for entry points.
|
||||
*/
|
||||
findEntryPoints(sourceDirectory: string): SortedEntryPointsInfo {
|
||||
const unsortedEntryPoints = walkDirectoryForEntryPoints(sourceDirectory);
|
||||
return this.resolver.sortEntryPointsByDependency(unsortedEntryPoints);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for entry points that need to be compiled, starting at the source directory.
|
||||
* The function will recurse into directories that start with `@...`, e.g. `@angular/...`.
|
||||
* @param sourceDirectory An absolute path to the root directory where searching begins.
|
||||
*/
|
||||
function walkDirectoryForEntryPoints(sourceDirectory: string): EntryPoint[] {
|
||||
const entryPoints: EntryPoint[] = [];
|
||||
fs.readdirSync(sourceDirectory)
|
||||
// Not interested in hidden files
|
||||
.filter(p => !p.startsWith('.'))
|
||||
// Ignore node_modules
|
||||
.filter(p => p !== 'node_modules')
|
||||
// Only interested in directories (and only those that are not symlinks)
|
||||
.filter(p => {
|
||||
const stat = fs.lstatSync(path.resolve(sourceDirectory, p));
|
||||
return stat.isDirectory() && !stat.isSymbolicLink();
|
||||
})
|
||||
.forEach(p => {
|
||||
// Either the directory is a potential package or a namespace containing packages (e.g
|
||||
// `@angular`).
|
||||
const packagePath = path.join(sourceDirectory, p);
|
||||
if (p.startsWith('@')) {
|
||||
entryPoints.push(...walkDirectoryForEntryPoints(packagePath));
|
||||
} else {
|
||||
entryPoints.push(...getEntryPointsForPackage(packagePath));
|
||||
|
||||
// Also check for any nested node_modules in this package
|
||||
const nestedNodeModulesPath = path.resolve(packagePath, 'node_modules');
|
||||
if (fs.existsSync(nestedNodeModulesPath)) {
|
||||
entryPoints.push(...walkDirectoryForEntryPoints(nestedNodeModulesPath));
|
||||
}
|
||||
}
|
||||
});
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recurse the folder structure looking for all the entry points
|
||||
* @param packagePath The absolute path to an npm package that may contain entry points
|
||||
* @returns An array of entry points that were discovered.
|
||||
*/
|
||||
function getEntryPointsForPackage(packagePath: string): EntryPoint[] {
|
||||
const entryPoints: EntryPoint[] = [];
|
||||
|
||||
// Try to get an entry point from the top level package directory
|
||||
const topLevelEntryPoint = getEntryPointInfo(packagePath, packagePath);
|
||||
if (topLevelEntryPoint !== null) {
|
||||
entryPoints.push(topLevelEntryPoint);
|
||||
}
|
||||
|
||||
// Now search all the directories of this package for possible entry points
|
||||
walkDirectory(packagePath, subdir => {
|
||||
const subEntryPoint = getEntryPointInfo(packagePath, subdir);
|
||||
if (subEntryPoint !== null) {
|
||||
entryPoints.push(subEntryPoint);
|
||||
}
|
||||
});
|
||||
|
||||
return entryPoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively walk a directory and its sub-directories, applying a given
|
||||
* function to each directory.
|
||||
* @param dir the directory to recursively walk.
|
||||
* @param fn the function to apply to each directory.
|
||||
*/
|
||||
function walkDirectory(dir: string, fn: (dir: string) => void) {
|
||||
return fs
|
||||
.readdirSync(dir)
|
||||
// Not interested in hidden files
|
||||
.filter(p => !p.startsWith('.'))
|
||||
// Ignore node_modules
|
||||
.filter(p => p !== 'node_modules')
|
||||
// Only interested in directories (and only those that are not symlinks)
|
||||
.filter(p => {
|
||||
const stat = fs.lstatSync(path.resolve(dir, p));
|
||||
return stat.isDirectory() && !stat.isSymbolicLink();
|
||||
})
|
||||
.forEach(subdir => {
|
||||
subdir = path.resolve(dir, subdir);
|
||||
fn(subdir);
|
||||
walkDirectory(subdir, fn);
|
||||
});
|
||||
}
|
147
packages/compiler-cli/ngcc/src/packages/transformer.ts
Normal file
147
packages/compiler-cli/ngcc/src/packages/transformer.ts
Normal file
@ -0,0 +1,147 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {dirname} from 'canonical-path';
|
||||
import {existsSync, writeFileSync} from 'fs';
|
||||
import {mkdir, mv} from 'shelljs';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {CompiledFile, DecorationAnalyzer} from '../analysis/decoration_analyzer';
|
||||
import {ModuleWithProvidersAnalyses, ModuleWithProvidersAnalyzer} from '../analysis/module_with_providers_analyzer';
|
||||
import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry';
|
||||
import {ExportInfo, PrivateDeclarationsAnalyzer} from '../analysis/private_declarations_analyzer';
|
||||
import {SwitchMarkerAnalyses, SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
|
||||
import {Esm2015ReflectionHost} from '../host/esm2015_host';
|
||||
import {Esm5ReflectionHost} from '../host/esm5_host';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {Esm5Renderer} from '../rendering/esm5_renderer';
|
||||
import {EsmRenderer} from '../rendering/esm_renderer';
|
||||
import {FileInfo, Renderer} from '../rendering/renderer';
|
||||
|
||||
import {EntryPoint} from './entry_point';
|
||||
import {EntryPointBundle} from './entry_point_bundle';
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A Package is stored in a directory on disk and that directory can contain one or more package
|
||||
* formats - e.g. fesm2015, UMD, etc. Additionally, each package provides typings (`.d.ts` files).
|
||||
*
|
||||
* Each of these formats exposes one or more entry points, which are source files that need to be
|
||||
* parsed to identify the decorated exported classes that need to be analyzed and compiled by one or
|
||||
* more `DecoratorHandler` objects.
|
||||
*
|
||||
* Each entry point to a package is identified by a `package.json` which contains properties that
|
||||
* indicate what formatted bundles are accessible via this end-point.
|
||||
*
|
||||
* Each bundle is identified by a root `SourceFile` that can be parsed and analyzed to
|
||||
* identify classes that need to be transformed; and then finally rendered and written to disk.
|
||||
*
|
||||
* Along with the source files, the corresponding source maps (either inline or external) and
|
||||
* `.d.ts` files are transformed accordingly.
|
||||
*
|
||||
* - Flat file packages have all the classes in a single file.
|
||||
* - Other packages may re-export classes from other non-entry point files.
|
||||
* - Some formats may contain multiple "modules" in a single file.
|
||||
*/
|
||||
export class Transformer {
|
||||
constructor(private sourcePath: string, private targetPath: string) {}
|
||||
|
||||
/**
|
||||
* Transform the source (and typings) files of a bundle.
|
||||
* @param bundle the bundle to transform.
|
||||
*/
|
||||
transform(entryPoint: EntryPoint, isCore: boolean, bundle: EntryPointBundle): void {
|
||||
console.warn(`Compiling ${entryPoint.name} - ${bundle.format}`);
|
||||
|
||||
const reflectionHost = this.getHost(isCore, bundle);
|
||||
|
||||
// Parse and analyze the files.
|
||||
const {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses} = this.analyzeProgram(reflectionHost, isCore, bundle);
|
||||
|
||||
// Transform the source files and source maps.
|
||||
const renderer = this.getRenderer(reflectionHost, isCore, bundle);
|
||||
const renderedFiles = renderer.renderProgram(
|
||||
decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses);
|
||||
|
||||
// Write out all the transformed files.
|
||||
renderedFiles.forEach(file => this.writeFile(file));
|
||||
}
|
||||
|
||||
getHost(isCore: boolean, bundle: EntryPointBundle): NgccReflectionHost {
|
||||
const typeChecker = bundle.src.program.getTypeChecker();
|
||||
switch (bundle.format) {
|
||||
case 'esm2015':
|
||||
case 'fesm2015':
|
||||
return new Esm2015ReflectionHost(isCore, typeChecker, bundle.dts);
|
||||
case 'esm5':
|
||||
case 'fesm5':
|
||||
return new Esm5ReflectionHost(isCore, typeChecker, bundle.dts);
|
||||
default:
|
||||
throw new Error(`Reflection host for "${bundle.format}" not yet implemented.`);
|
||||
}
|
||||
}
|
||||
|
||||
getRenderer(host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle): Renderer {
|
||||
switch (bundle.format) {
|
||||
case 'esm2015':
|
||||
case 'fesm2015':
|
||||
return new EsmRenderer(host, isCore, bundle, this.sourcePath, this.targetPath);
|
||||
case 'esm5':
|
||||
case 'fesm5':
|
||||
return new Esm5Renderer(host, isCore, bundle, this.sourcePath, this.targetPath);
|
||||
default:
|
||||
throw new Error(`Renderer for "${bundle.format}" not yet implemented.`);
|
||||
}
|
||||
}
|
||||
|
||||
analyzeProgram(reflectionHost: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle):
|
||||
ProgramAnalyses {
|
||||
const typeChecker = bundle.src.program.getTypeChecker();
|
||||
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
|
||||
|
||||
const switchMarkerAnalyzer = new SwitchMarkerAnalyzer(reflectionHost);
|
||||
const switchMarkerAnalyses = switchMarkerAnalyzer.analyzeProgram(bundle.src.program);
|
||||
|
||||
const decorationAnalyzer = new DecorationAnalyzer(
|
||||
bundle.src.program, bundle.src.options, bundle.src.host, typeChecker, reflectionHost,
|
||||
referencesRegistry, bundle.rootDirs, isCore);
|
||||
const decorationAnalyses = decorationAnalyzer.analyzeProgram();
|
||||
|
||||
const moduleWithProvidersAnalyzer =
|
||||
bundle.dts && new ModuleWithProvidersAnalyzer(reflectionHost, referencesRegistry);
|
||||
const moduleWithProvidersAnalyses = moduleWithProvidersAnalyzer &&
|
||||
moduleWithProvidersAnalyzer.analyzeProgram(bundle.src.program);
|
||||
|
||||
const privateDeclarationsAnalyzer =
|
||||
new PrivateDeclarationsAnalyzer(reflectionHost, referencesRegistry);
|
||||
const privateDeclarationsAnalyses =
|
||||
privateDeclarationsAnalyzer.analyzeProgram(bundle.src.program);
|
||||
|
||||
return {decorationAnalyses, switchMarkerAnalyses, privateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses};
|
||||
}
|
||||
|
||||
writeFile(file: FileInfo): void {
|
||||
mkdir('-p', dirname(file.path));
|
||||
const backPath = file.path + '.bak';
|
||||
if (existsSync(file.path) && !existsSync(backPath)) {
|
||||
mv(file.path, backPath);
|
||||
}
|
||||
writeFileSync(file.path, file.contents, 'utf8');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface ProgramAnalyses {
|
||||
decorationAnalyses: Map<ts.SourceFile, CompiledFile>;
|
||||
switchMarkerAnalyses: SwitchMarkerAnalyses;
|
||||
privateDeclarationsAnalyses: ExportInfo[];
|
||||
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null;
|
||||
}
|
44
packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts
Normal file
44
packages/compiler-cli/ngcc/src/rendering/esm5_renderer.ts
Normal file
@ -0,0 +1,44 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
import MagicString from 'magic-string';
|
||||
import {NgccReflectionHost} from '../host/ngcc_host';
|
||||
import {CompiledClass} from '../analysis/decoration_analyzer';
|
||||
import {EsmRenderer} from './esm_renderer';
|
||||
import {EntryPointBundle} from '../packages/entry_point_bundle';
|
||||
|
||||
export class Esm5Renderer extends EsmRenderer {
|
||||
constructor(
|
||||
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string,
|
||||
targetPath: string) {
|
||||
super(host, isCore, bundle, sourcePath, targetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the definitions to each decorated class
|
||||
*/
|
||||
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
|
||||
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
|
||||
if (!classSymbol) {
|
||||
throw new Error(
|
||||
`Compiled class does not have a valid symbol: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
|
||||
}
|
||||
const parent = classSymbol.valueDeclaration && classSymbol.valueDeclaration.parent;
|
||||
if (!parent || !ts.isBlock(parent)) {
|
||||
throw new Error(
|
||||
`Compiled class declaration is not inside an IIFE: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
|
||||
}
|
||||
const returnStatement = parent.statements.find(statement => ts.isReturnStatement(statement));
|
||||
if (!returnStatement) {
|
||||
throw new Error(
|
||||
`Compiled class wrapper IIFE does not have a return statement: ${compiledClass.name} in ${compiledClass.declaration.getSourceFile().fileName}`);
|
||||
}
|
||||
const insertionPoint = returnStatement.getFullStart();
|
||||
output.appendLeft(insertionPoint, '\n' + definitions);
|
||||
}
|
||||
}
|
125
packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts
Normal file
125
packages/compiler-cli/ngcc/src/rendering/esm_renderer.ts
Normal file
@ -0,0 +1,125 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {dirname, relative} from 'canonical-path';
|
||||
import MagicString from 'magic-string';
|
||||
import * as ts from 'typescript';
|
||||
import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
||||
import {CompiledClass} from '../analysis/decoration_analyzer';
|
||||
import {RedundantDecoratorMap, Renderer, stripExtension} from './renderer';
|
||||
import {EntryPointBundle} from '../packages/entry_point_bundle';
|
||||
import {ExportInfo} from '../analysis/private_declarations_analyzer';
|
||||
import {isDtsPath} from '../../../src/ngtsc/util/src/typescript';
|
||||
|
||||
export class EsmRenderer extends Renderer {
|
||||
constructor(
|
||||
host: NgccReflectionHost, isCore: boolean, bundle: EntryPointBundle, sourcePath: string,
|
||||
targetPath: string) {
|
||||
super(host, isCore, bundle, sourcePath, targetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the imports at the top of the file
|
||||
*/
|
||||
addImports(output: MagicString, imports: {specifier: string; qualifier: string;}[]): void {
|
||||
// The imports get inserted at the very top of the file.
|
||||
imports.forEach(
|
||||
i => { output.appendLeft(0, `import * as ${i.qualifier} from '${i.specifier}';\n`); });
|
||||
}
|
||||
|
||||
addExports(output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void {
|
||||
exports.forEach(e => {
|
||||
let exportFrom = '';
|
||||
const isDtsFile = isDtsPath(entryPointBasePath);
|
||||
const from = isDtsFile ? e.dtsFrom : e.from;
|
||||
|
||||
if (from) {
|
||||
const basePath = stripExtension(from);
|
||||
const relativePath = './' + relative(dirname(entryPointBasePath), basePath);
|
||||
exportFrom = entryPointBasePath !== basePath ? ` from '${relativePath}'` : '';
|
||||
}
|
||||
|
||||
// aliases should only be added in dts files as these are lost when rolling up dts file.
|
||||
const exportStatement = e.alias && isDtsFile ? `${e.alias} as ${e.identifier}` : e.identifier;
|
||||
const exportStr = `\nexport {${exportStatement}}${exportFrom};`;
|
||||
output.append(exportStr);
|
||||
});
|
||||
}
|
||||
|
||||
addConstants(output: MagicString, constants: string, file: ts.SourceFile): void {
|
||||
if (constants === '') {
|
||||
return;
|
||||
}
|
||||
const insertionPoint = file.statements.reduce((prev, stmt) => {
|
||||
if (ts.isImportDeclaration(stmt) || ts.isImportEqualsDeclaration(stmt) ||
|
||||
ts.isNamespaceImport(stmt)) {
|
||||
return stmt.getEnd();
|
||||
}
|
||||
return prev;
|
||||
}, 0);
|
||||
output.appendLeft(insertionPoint, '\n' + constants + '\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the definitions to each decorated class
|
||||
*/
|
||||
addDefinitions(output: MagicString, compiledClass: CompiledClass, definitions: string): void {
|
||||
const classSymbol = this.host.getClassSymbol(compiledClass.declaration);
|
||||
if (!classSymbol) {
|
||||
throw new Error(`Compiled class does not have a valid symbol: ${compiledClass.name}`);
|
||||
}
|
||||
const insertionPoint = classSymbol.valueDeclaration !.getEnd();
|
||||
output.appendLeft(insertionPoint, '\n' + definitions);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove static decorator properties from classes
|
||||
*/
|
||||
removeDecorators(output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void {
|
||||
decoratorsToRemove.forEach((nodesToRemove, containerNode) => {
|
||||
if (ts.isArrayLiteralExpression(containerNode)) {
|
||||
const items = containerNode.elements;
|
||||
if (items.length === nodesToRemove.length) {
|
||||
// Remove the entire statement
|
||||
const statement = findStatement(containerNode);
|
||||
if (statement) {
|
||||
output.remove(statement.getFullStart(), statement.getEnd());
|
||||
}
|
||||
} else {
|
||||
nodesToRemove.forEach(node => {
|
||||
// remove any trailing comma
|
||||
const end = (output.slice(node.getEnd(), node.getEnd() + 1) === ',') ?
|
||||
node.getEnd() + 1 :
|
||||
node.getEnd();
|
||||
output.remove(node.getFullStart(), end);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
rewriteSwitchableDeclarations(
|
||||
outputText: MagicString, sourceFile: ts.SourceFile,
|
||||
declarations: SwitchableVariableDeclaration[]): void {
|
||||
declarations.forEach(declaration => {
|
||||
const start = declaration.initializer.getStart();
|
||||
const end = declaration.initializer.getEnd();
|
||||
const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER);
|
||||
outputText.overwrite(start, end, replacement);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function findStatement(node: ts.Node) {
|
||||
while (node) {
|
||||
if (ts.isExpressionStatement(node)) {
|
||||
return node;
|
||||
}
|
||||
node = node.parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
|
||||
import {ImportRewriter, validateAndRewriteCoreSymbol} from '../../../src/ngtsc/imports';
|
||||
|
||||
export class NgccFlatImportRewriter implements ImportRewriter {
|
||||
shouldImportSymbol(symbol: string, specifier: string): boolean {
|
||||
if (specifier === '@angular/core') {
|
||||
// Don't use imports for @angular/core symbols in a flat bundle, as they'll be visible
|
||||
// directly.
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
rewriteSymbol(symbol: string, specifier: string): string {
|
||||
if (specifier === '@angular/core') {
|
||||
return validateAndRewriteCoreSymbol(symbol);
|
||||
} else {
|
||||
return symbol;
|
||||
}
|
||||
}
|
||||
|
||||
rewriteSpecifier(originalModulePath: string, inContextOfFile: string): string {
|
||||
return originalModulePath;
|
||||
}
|
||||
}
|
518
packages/compiler-cli/ngcc/src/rendering/renderer.ts
Normal file
518
packages/compiler-cli/ngcc/src/rendering/renderer.ts
Normal file
@ -0,0 +1,518 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import {ConstantPool, Expression, Statement, WrappedNodeExpr, WritePropExpr} from '@angular/compiler';
|
||||
import {SourceMapConverter, commentRegex, fromJSON, fromMapFileSource, fromObject, fromSource, generateMapFileComment, mapFileCommentRegex, removeComments, removeMapFileComments} from 'convert-source-map';
|
||||
import {readFileSync, statSync} from 'fs';
|
||||
import MagicString from 'magic-string';
|
||||
import {basename, dirname, relative, resolve} from 'canonical-path';
|
||||
import {SourceMapConsumer, SourceMapGenerator, RawSourceMap} from 'source-map';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {NoopImportRewriter, ImportRewriter, R3SymbolsImportRewriter, NOOP_DEFAULT_IMPORT_RECORDER} from '@angular/compiler-cli/src/ngtsc/imports';
|
||||
import {CompileResult} from '@angular/compiler-cli/src/ngtsc/transform';
|
||||
import {translateStatement, translateType, ImportManager} from '../../../src/ngtsc/translator';
|
||||
import {NgccFlatImportRewriter} from './ngcc_import_rewriter';
|
||||
import {CompiledClass, CompiledFile, DecorationAnalyses} from '../analysis/decoration_analyzer';
|
||||
import {ModuleWithProvidersInfo, ModuleWithProvidersAnalyses} from '../analysis/module_with_providers_analyzer';
|
||||
import {PrivateDeclarationsAnalyses, ExportInfo} from '../analysis/private_declarations_analyzer';
|
||||
import {SwitchMarkerAnalyses, SwitchMarkerAnalysis} from '../analysis/switch_marker_analyzer';
|
||||
import {IMPORT_PREFIX} from '../constants';
|
||||
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
|
||||
import {EntryPointBundle} from '../packages/entry_point_bundle';
|
||||
|
||||
interface SourceMapInfo {
|
||||
source: string;
|
||||
map: SourceMapConverter|null;
|
||||
isInline: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Information about a file that has been rendered.
|
||||
*/
|
||||
export interface FileInfo {
|
||||
/**
|
||||
* Path to where the file should be written.
|
||||
*/
|
||||
path: string;
|
||||
/**
|
||||
* The contents of the file to be be written.
|
||||
*/
|
||||
contents: string;
|
||||
}
|
||||
|
||||
interface DtsClassInfo {
|
||||
dtsDeclaration: ts.Declaration;
|
||||
compilation: CompileResult[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A structure that captures information about what needs to be rendered
|
||||
* in a typings file.
|
||||
*
|
||||
* It is created as a result of processing the analysis passed to the renderer.
|
||||
*
|
||||
* The `renderDtsFile()` method consumes it when rendering a typings file.
|
||||
*/
|
||||
class DtsRenderInfo {
|
||||
classInfo: DtsClassInfo[] = [];
|
||||
moduleWithProviders: ModuleWithProvidersInfo[] = [];
|
||||
privateExports: ExportInfo[] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* The collected decorators that have become redundant after the compilation
|
||||
* of Ivy static fields. The map is keyed by the container node, such that we
|
||||
* can tell if we should remove the entire decorator property
|
||||
*/
|
||||
export type RedundantDecoratorMap = Map<ts.Node, ts.Node[]>;
|
||||
export const RedundantDecoratorMap = Map;
|
||||
|
||||
/**
|
||||
* A base-class for rendering an `AnalyzedFile`.
|
||||
*
|
||||
* Package formats have output files that must be rendered differently. Concrete sub-classes must
|
||||
* implement the `addImports`, `addDefinitions` and `removeDecorators` abstract methods.
|
||||
*/
|
||||
export abstract class Renderer {
|
||||
constructor(
|
||||
protected host: NgccReflectionHost, protected isCore: boolean,
|
||||
protected bundle: EntryPointBundle, protected sourcePath: string,
|
||||
protected targetPath: string) {}
|
||||
|
||||
renderProgram(
|
||||
decorationAnalyses: DecorationAnalyses, switchMarkerAnalyses: SwitchMarkerAnalyses,
|
||||
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|null): FileInfo[] {
|
||||
const renderedFiles: FileInfo[] = [];
|
||||
|
||||
// Transform the source files.
|
||||
this.bundle.src.program.getSourceFiles().map(sourceFile => {
|
||||
const compiledFile = decorationAnalyses.get(sourceFile);
|
||||
const switchMarkerAnalysis = switchMarkerAnalyses.get(sourceFile);
|
||||
|
||||
if (compiledFile || switchMarkerAnalysis || sourceFile === this.bundle.src.file) {
|
||||
renderedFiles.push(...this.renderFile(
|
||||
sourceFile, compiledFile, switchMarkerAnalysis, privateDeclarationsAnalyses));
|
||||
}
|
||||
});
|
||||
|
||||
// Transform the .d.ts files
|
||||
if (this.bundle.dts) {
|
||||
const dtsFiles = this.getTypingsFilesToRender(
|
||||
decorationAnalyses, privateDeclarationsAnalyses, moduleWithProvidersAnalyses);
|
||||
|
||||
// If the dts entry-point is not already there (it did not have compiled classes)
|
||||
// then add it now, to ensure it gets its extra exports rendered.
|
||||
if (!dtsFiles.has(this.bundle.dts.file)) {
|
||||
dtsFiles.set(this.bundle.dts.file, new DtsRenderInfo());
|
||||
}
|
||||
dtsFiles.forEach(
|
||||
(renderInfo, file) => renderedFiles.push(...this.renderDtsFile(file, renderInfo)));
|
||||
}
|
||||
|
||||
return renderedFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the source code and source-map for an Analyzed file.
|
||||
* @param compiledFile The analyzed file to render.
|
||||
* @param targetPath The absolute path where the rendered file will be written.
|
||||
*/
|
||||
renderFile(
|
||||
sourceFile: ts.SourceFile, compiledFile: CompiledFile|undefined,
|
||||
switchMarkerAnalysis: SwitchMarkerAnalysis|undefined,
|
||||
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses): FileInfo[] {
|
||||
const input = this.extractSourceMap(sourceFile);
|
||||
const outputText = new MagicString(input.source);
|
||||
|
||||
if (switchMarkerAnalysis) {
|
||||
this.rewriteSwitchableDeclarations(
|
||||
outputText, switchMarkerAnalysis.sourceFile, switchMarkerAnalysis.declarations);
|
||||
}
|
||||
|
||||
if (compiledFile) {
|
||||
const importManager = new ImportManager(
|
||||
this.getImportRewriter(this.bundle.src.r3SymbolsFile, this.bundle.isFlat), IMPORT_PREFIX);
|
||||
|
||||
// TODO: remove constructor param metadata and property decorators (we need info from the
|
||||
// handlers to do this)
|
||||
const decoratorsToRemove = this.computeDecoratorsToRemove(compiledFile.compiledClasses);
|
||||
this.removeDecorators(outputText, decoratorsToRemove);
|
||||
|
||||
compiledFile.compiledClasses.forEach(clazz => {
|
||||
const renderedDefinition = renderDefinitions(compiledFile.sourceFile, clazz, importManager);
|
||||
this.addDefinitions(outputText, clazz, renderedDefinition);
|
||||
});
|
||||
|
||||
this.addConstants(
|
||||
outputText,
|
||||
renderConstantPool(compiledFile.sourceFile, compiledFile.constantPool, importManager),
|
||||
compiledFile.sourceFile);
|
||||
|
||||
this.addImports(outputText, importManager.getAllImports(compiledFile.sourceFile.fileName));
|
||||
}
|
||||
|
||||
// Add exports to the entry-point file
|
||||
if (sourceFile === this.bundle.src.file) {
|
||||
const entryPointBasePath = stripExtension(this.bundle.src.path);
|
||||
this.addExports(outputText, entryPointBasePath, privateDeclarationsAnalyses);
|
||||
}
|
||||
|
||||
return this.renderSourceAndMap(sourceFile, input, outputText);
|
||||
}
|
||||
|
||||
renderDtsFile(dtsFile: ts.SourceFile, renderInfo: DtsRenderInfo): FileInfo[] {
|
||||
const input = this.extractSourceMap(dtsFile);
|
||||
const outputText = new MagicString(input.source);
|
||||
const printer = ts.createPrinter();
|
||||
const importManager = new ImportManager(
|
||||
this.getImportRewriter(this.bundle.dts !.r3SymbolsFile, false), IMPORT_PREFIX);
|
||||
|
||||
renderInfo.classInfo.forEach(dtsClass => {
|
||||
const endOfClass = dtsClass.dtsDeclaration.getEnd();
|
||||
dtsClass.compilation.forEach(declaration => {
|
||||
const type = translateType(declaration.type, importManager);
|
||||
const typeStr = printer.printNode(ts.EmitHint.Unspecified, type, dtsFile);
|
||||
const newStatement = ` static ${declaration.name}: ${typeStr};\n`;
|
||||
outputText.appendRight(endOfClass - 1, newStatement);
|
||||
});
|
||||
});
|
||||
|
||||
this.addModuleWithProvidersParams(outputText, renderInfo.moduleWithProviders, importManager);
|
||||
this.addImports(outputText, importManager.getAllImports(dtsFile.fileName));
|
||||
|
||||
this.addExports(outputText, dtsFile.fileName, renderInfo.privateExports);
|
||||
|
||||
|
||||
return this.renderSourceAndMap(dtsFile, input, outputText);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the type parameters to the appropriate functions that return `ModuleWithProviders`
|
||||
* structures.
|
||||
*
|
||||
* This function only gets called on typings files, so it doesn't need different implementations
|
||||
* for each bundle format.
|
||||
*/
|
||||
protected addModuleWithProvidersParams(
|
||||
outputText: MagicString, moduleWithProviders: ModuleWithProvidersInfo[],
|
||||
importManager: ImportManager): void {
|
||||
moduleWithProviders.forEach(info => {
|
||||
const ngModuleName = (info.ngModule.node as ts.ClassDeclaration).name !.text;
|
||||
const declarationFile = info.declaration.getSourceFile().fileName;
|
||||
const ngModuleFile = info.ngModule.node.getSourceFile().fileName;
|
||||
const importPath = info.ngModule.viaModule ||
|
||||
(declarationFile !== ngModuleFile ?
|
||||
stripExtension(`./${relative(dirname(declarationFile), ngModuleFile)}`) :
|
||||
null);
|
||||
const ngModule = getImportString(importManager, importPath, ngModuleName);
|
||||
|
||||
if (info.declaration.type) {
|
||||
const typeName = info.declaration.type && ts.isTypeReferenceNode(info.declaration.type) ?
|
||||
info.declaration.type.typeName :
|
||||
null;
|
||||
if (this.isCoreModuleWithProvidersType(typeName)) {
|
||||
// The declaration already returns `ModuleWithProvider` but it needs the `NgModule` type
|
||||
// parameter adding.
|
||||
outputText.overwrite(
|
||||
info.declaration.type.getStart(), info.declaration.type.getEnd(),
|
||||
`ModuleWithProviders<${ngModule}>`);
|
||||
} else {
|
||||
// The declaration returns an unknown type so we need to convert it to a union that
|
||||
// includes the ngModule property.
|
||||
const originalTypeString = info.declaration.type.getText();
|
||||
outputText.overwrite(
|
||||
info.declaration.type.getStart(), info.declaration.type.getEnd(),
|
||||
`(${originalTypeString})&{ngModule:${ngModule}}`);
|
||||
}
|
||||
} else {
|
||||
// The declaration has no return type so provide one.
|
||||
const lastToken = info.declaration.getLastToken();
|
||||
const insertPoint = lastToken && lastToken.kind === ts.SyntaxKind.SemicolonToken ?
|
||||
lastToken.getStart() :
|
||||
info.declaration.getEnd();
|
||||
outputText.appendLeft(
|
||||
insertPoint,
|
||||
`: ${getImportString(importManager, '@angular/core', 'ModuleWithProviders')}<${ngModule}>`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
|
||||
void;
|
||||
protected abstract addImports(output: MagicString, imports: {specifier: string,
|
||||
qualifier: string}[]): void;
|
||||
protected abstract addExports(
|
||||
output: MagicString, entryPointBasePath: string, exports: ExportInfo[]): void;
|
||||
protected abstract addDefinitions(
|
||||
output: MagicString, compiledClass: CompiledClass, definitions: string): void;
|
||||
protected abstract removeDecorators(
|
||||
output: MagicString, decoratorsToRemove: RedundantDecoratorMap): void;
|
||||
protected abstract rewriteSwitchableDeclarations(
|
||||
outputText: MagicString, sourceFile: ts.SourceFile,
|
||||
declarations: SwitchableVariableDeclaration[]): void;
|
||||
|
||||
/**
|
||||
* From the given list of classes, computes a map of decorators that should be removed.
|
||||
* The decorators to remove are keyed by their container node, such that we can tell if
|
||||
* we should remove the entire decorator property.
|
||||
* @param classes The list of classes that may have decorators to remove.
|
||||
* @returns A map of decorators to remove, keyed by their container node.
|
||||
*/
|
||||
protected computeDecoratorsToRemove(classes: CompiledClass[]): RedundantDecoratorMap {
|
||||
const decoratorsToRemove = new RedundantDecoratorMap();
|
||||
classes.forEach(clazz => {
|
||||
clazz.decorators.forEach(dec => {
|
||||
const decoratorArray = dec.node.parent !;
|
||||
if (!decoratorsToRemove.has(decoratorArray)) {
|
||||
decoratorsToRemove.set(decoratorArray, [dec.node]);
|
||||
} else {
|
||||
decoratorsToRemove.get(decoratorArray) !.push(dec.node);
|
||||
}
|
||||
});
|
||||
});
|
||||
return decoratorsToRemove;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the map from the source (note whether it is inline or external)
|
||||
*/
|
||||
protected extractSourceMap(file: ts.SourceFile): SourceMapInfo {
|
||||
const inline = commentRegex.test(file.text);
|
||||
const external = mapFileCommentRegex.test(file.text);
|
||||
|
||||
if (inline) {
|
||||
const inlineSourceMap = fromSource(file.text);
|
||||
return {
|
||||
source: removeComments(file.text).replace(/\n\n$/, '\n'),
|
||||
map: inlineSourceMap,
|
||||
isInline: true,
|
||||
};
|
||||
} else if (external) {
|
||||
let externalSourceMap: SourceMapConverter|null = null;
|
||||
try {
|
||||
externalSourceMap = fromMapFileSource(file.text, dirname(file.fileName));
|
||||
} catch (e) {
|
||||
if (e.code === 'ENOENT') {
|
||||
console.warn(
|
||||
`The external map file specified in the source code comment "${e.path}" was not found on the file system.`);
|
||||
const mapPath = file.fileName + '.map';
|
||||
if (basename(e.path) !== basename(mapPath) && statSync(mapPath).isFile()) {
|
||||
console.warn(
|
||||
`Guessing the map file name from the source file name: "${basename(mapPath)}"`);
|
||||
try {
|
||||
externalSourceMap = fromObject(JSON.parse(readFileSync(mapPath, 'utf8')));
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
source: removeMapFileComments(file.text).replace(/\n\n$/, '\n'),
|
||||
map: externalSourceMap,
|
||||
isInline: false,
|
||||
};
|
||||
} else {
|
||||
return {source: file.text, map: null, isInline: false};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the input and output source-maps, replacing the source-map comment in the output file
|
||||
* with an appropriate source-map comment pointing to the merged source-map.
|
||||
*/
|
||||
protected renderSourceAndMap(
|
||||
sourceFile: ts.SourceFile, input: SourceMapInfo, output: MagicString): FileInfo[] {
|
||||
const outputPath = resolve(this.targetPath, relative(this.sourcePath, sourceFile.fileName));
|
||||
const outputMapPath = `${outputPath}.map`;
|
||||
const outputMap = output.generateMap({
|
||||
source: sourceFile.fileName,
|
||||
includeContent: true,
|
||||
// hires: true // TODO: This results in accurate but huge sourcemaps. Instead we should fix
|
||||
// the merge algorithm.
|
||||
});
|
||||
|
||||
// we must set this after generation as magic string does "manipulation" on the path
|
||||
outputMap.file = outputPath;
|
||||
|
||||
const mergedMap =
|
||||
mergeSourceMaps(input.map && input.map.toObject(), JSON.parse(outputMap.toString()));
|
||||
|
||||
const result: FileInfo[] = [];
|
||||
if (input.isInline) {
|
||||
result.push({path: outputPath, contents: `${output.toString()}\n${mergedMap.toComment()}`});
|
||||
} else {
|
||||
result.push({
|
||||
path: outputPath,
|
||||
contents: `${output.toString()}\n${generateMapFileComment(outputMapPath)}`
|
||||
});
|
||||
result.push({path: outputMapPath, contents: mergedMap.toJSON()});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
protected getTypingsFilesToRender(
|
||||
decorationAnalyses: DecorationAnalyses,
|
||||
privateDeclarationsAnalyses: PrivateDeclarationsAnalyses,
|
||||
moduleWithProvidersAnalyses: ModuleWithProvidersAnalyses|
|
||||
null): Map<ts.SourceFile, DtsRenderInfo> {
|
||||
const dtsMap = new Map<ts.SourceFile, DtsRenderInfo>();
|
||||
|
||||
// Capture the rendering info from the decoration analyses
|
||||
decorationAnalyses.forEach(compiledFile => {
|
||||
compiledFile.compiledClasses.forEach(compiledClass => {
|
||||
const dtsDeclaration = this.host.getDtsDeclaration(compiledClass.declaration);
|
||||
if (dtsDeclaration) {
|
||||
const dtsFile = dtsDeclaration.getSourceFile();
|
||||
const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
|
||||
renderInfo.classInfo.push({dtsDeclaration, compilation: compiledClass.compilation});
|
||||
dtsMap.set(dtsFile, renderInfo);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Capture the ModuleWithProviders functions/methods that need updating
|
||||
if (moduleWithProvidersAnalyses !== null) {
|
||||
moduleWithProvidersAnalyses.forEach((moduleWithProvidersToFix, dtsFile) => {
|
||||
const renderInfo = dtsMap.get(dtsFile) || new DtsRenderInfo();
|
||||
renderInfo.moduleWithProviders = moduleWithProvidersToFix;
|
||||
dtsMap.set(dtsFile, renderInfo);
|
||||
});
|
||||
}
|
||||
|
||||
// Capture the private declarations that need to be re-exported
|
||||
if (privateDeclarationsAnalyses.length) {
|
||||
privateDeclarationsAnalyses.forEach(e => {
|
||||
if (!e.dtsFrom && !e.alias) {
|
||||
throw new Error(
|
||||
`There is no typings path for ${e.identifier} in ${e.from}.\n` +
|
||||
`We need to add an export for this class to a .d.ts typings file because ` +
|
||||
`Angular compiler needs to be able to reference this class in compiled code, such as templates.\n` +
|
||||
`The simplest fix for this is to ensure that this class is exported from the package's entry-point.`);
|
||||
}
|
||||
});
|
||||
const dtsEntryPoint = this.bundle.dts !.file;
|
||||
const renderInfo = dtsMap.get(dtsEntryPoint) || new DtsRenderInfo();
|
||||
renderInfo.privateExports = privateDeclarationsAnalyses;
|
||||
dtsMap.set(dtsEntryPoint, renderInfo);
|
||||
}
|
||||
|
||||
return dtsMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the given type is the core Angular `ModuleWithProviders` interface.
|
||||
* @param typeName The type to check.
|
||||
* @returns true if the type is the core Angular `ModuleWithProviders` interface.
|
||||
*/
|
||||
private isCoreModuleWithProvidersType(typeName: ts.EntityName|null) {
|
||||
const id =
|
||||
typeName && ts.isIdentifier(typeName) ? this.host.getImportOfIdentifier(typeName) : null;
|
||||
return (
|
||||
id && id.name === 'ModuleWithProviders' && (this.isCore || id.from === '@angular/core'));
|
||||
}
|
||||
|
||||
private getImportRewriter(r3SymbolsFile: ts.SourceFile|null, isFlat: boolean): ImportRewriter {
|
||||
if (this.isCore && isFlat) {
|
||||
return new NgccFlatImportRewriter();
|
||||
} else if (this.isCore) {
|
||||
return new R3SymbolsImportRewriter(r3SymbolsFile !.fileName);
|
||||
} else {
|
||||
return new NoopImportRewriter();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the two specified source-maps into a single source-map that hides the intermediate
|
||||
* source-map.
|
||||
* E.g. Consider these mappings:
|
||||
*
|
||||
* ```
|
||||
* OLD_SRC -> OLD_MAP -> INTERMEDIATE_SRC -> NEW_MAP -> NEW_SRC
|
||||
* ```
|
||||
*
|
||||
* this will be replaced with:
|
||||
*
|
||||
* ```
|
||||
* OLD_SRC -> MERGED_MAP -> NEW_SRC
|
||||
* ```
|
||||
*/
|
||||
export function mergeSourceMaps(
|
||||
oldMap: RawSourceMap | null, newMap: RawSourceMap): SourceMapConverter {
|
||||
if (!oldMap) {
|
||||
return fromObject(newMap);
|
||||
}
|
||||
const oldMapConsumer = new SourceMapConsumer(oldMap);
|
||||
const newMapConsumer = new SourceMapConsumer(newMap);
|
||||
const mergedMapGenerator = SourceMapGenerator.fromSourceMap(newMapConsumer);
|
||||
mergedMapGenerator.applySourceMap(oldMapConsumer);
|
||||
const merged = fromJSON(mergedMapGenerator.toString());
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the constant pool as source code for the given class.
|
||||
*/
|
||||
export function renderConstantPool(
|
||||
sourceFile: ts.SourceFile, constantPool: ConstantPool, imports: ImportManager): string {
|
||||
const printer = ts.createPrinter();
|
||||
return constantPool.statements
|
||||
.map(stmt => translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER))
|
||||
.map(stmt => printer.printNode(ts.EmitHint.Unspecified, stmt, sourceFile))
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the definitions as source code for the given class.
|
||||
* @param sourceFile The file containing the class to process.
|
||||
* @param clazz The class whose definitions are to be rendered.
|
||||
* @param compilation The results of analyzing the class - this is used to generate the rendered
|
||||
* definitions.
|
||||
* @param imports An object that tracks the imports that are needed by the rendered definitions.
|
||||
*/
|
||||
export function renderDefinitions(
|
||||
sourceFile: ts.SourceFile, compiledClass: CompiledClass, imports: ImportManager): string {
|
||||
const printer = ts.createPrinter();
|
||||
const name = (compiledClass.declaration as ts.NamedDeclaration).name !;
|
||||
const translate = (stmt: Statement) =>
|
||||
translateStatement(stmt, imports, NOOP_DEFAULT_IMPORT_RECORDER);
|
||||
const definitions =
|
||||
compiledClass.compilation
|
||||
.map(
|
||||
c => c.statements.map(statement => translate(statement))
|
||||
.concat(translate(createAssignmentStatement(name, c.name, c.initializer)))
|
||||
.map(
|
||||
statement =>
|
||||
printer.printNode(ts.EmitHint.Unspecified, statement, sourceFile))
|
||||
.join('\n'))
|
||||
.join('\n');
|
||||
return definitions;
|
||||
}
|
||||
|
||||
export function stripExtension(filePath: string): string {
|
||||
return filePath.replace(/\.(js|d\.ts)$/, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Angular AST statement node that contains the assignment of the
|
||||
* compiled decorator to be applied to the class.
|
||||
* @param analyzedClass The info about the class whose statement we want to create.
|
||||
*/
|
||||
function createAssignmentStatement(
|
||||
receiverName: ts.DeclarationName, propName: string, initializer: Expression): Statement {
|
||||
const receiver = new WrappedNodeExpr(receiverName);
|
||||
return new WritePropExpr(receiver, propName, initializer).toStmt();
|
||||
}
|
||||
|
||||
function getImportString(
|
||||
importManager: ImportManager, importPath: string | null, importName: string) {
|
||||
const importAs = importPath ? importManager.generateNamedImport(importPath, importName) : null;
|
||||
return importAs ? `${importAs.moduleImport}.${importAs.symbol}` : `${importName}`;
|
||||
}
|
52
packages/compiler-cli/ngcc/src/utils.ts
Normal file
52
packages/compiler-cli/ngcc/src/utils.ts
Normal file
@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
export function getOriginalSymbol(checker: ts.TypeChecker): (symbol: ts.Symbol) => ts.Symbol {
|
||||
return function(symbol: ts.Symbol) {
|
||||
return ts.SymbolFlags.Alias & symbol.flags ? checker.getAliasedSymbol(symbol) : symbol;
|
||||
};
|
||||
}
|
||||
|
||||
export function isDefined<T>(value: T | undefined | null): value is T {
|
||||
return (value !== undefined) && (value !== null);
|
||||
}
|
||||
|
||||
export function getNameText(name: ts.PropertyName | ts.BindingName): string {
|
||||
return ts.isIdentifier(name) || ts.isLiteralExpression(name) ? name.text : name.getText();
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse down the AST and capture all the nodes that satisfy the test.
|
||||
* @param node The start node.
|
||||
* @param test The function that tests whether a node should be included.
|
||||
* @returns a collection of nodes that satisfy the test.
|
||||
*/
|
||||
export function findAll<T>(node: ts.Node, test: (node: ts.Node) => node is ts.Node & T): T[] {
|
||||
const nodes: T[] = [];
|
||||
findAllVisitor(node);
|
||||
return nodes;
|
||||
|
||||
function findAllVisitor(n: ts.Node) {
|
||||
if (test(n)) {
|
||||
nodes.push(n);
|
||||
} else {
|
||||
n.forEachChild(child => findAllVisitor(child));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Does the given declaration have a name which is an identifier?
|
||||
* @param declaration The declaration to test.
|
||||
* @returns true if the declaration has an identifer for a name.
|
||||
*/
|
||||
export function hasNameIdentifier(declaration: ts.Declaration): declaration is ts.Declaration&
|
||||
{name: ts.Identifier} {
|
||||
return ts.isIdentifier((declaration as any).name);
|
||||
}
|
69
packages/compiler-cli/ngcc/test/BUILD.bazel
Normal file
69
packages/compiler-cli/ngcc/test/BUILD.bazel
Normal file
@ -0,0 +1,69 @@
|
||||
load("//tools:defaults.bzl", "jasmine_node_test", "ts_library")
|
||||
|
||||
package(default_visibility = ["//visibility:public"])
|
||||
|
||||
ts_library(
|
||||
name = "test_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["**/*.ts"],
|
||||
exclude = ["integration/**/*.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//packages/compiler-cli/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",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"@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",
|
||||
],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "integration_lib",
|
||||
testonly = True,
|
||||
srcs = glob(
|
||||
["integration/**/*.ts"],
|
||||
),
|
||||
deps = [
|
||||
"//packages/compiler-cli/ngcc",
|
||||
"//packages/compiler-cli/test:test_utils",
|
||||
"@npm//@types/mock-fs",
|
||||
],
|
||||
)
|
||||
|
||||
jasmine_node_test(
|
||||
name = "integration",
|
||||
bootstrap = ["angular/tools/testing/init_node_no_angular_spec.js"],
|
||||
data = [
|
||||
"//packages/common:npm_package",
|
||||
"//packages/core:npm_package",
|
||||
"@npm//rxjs",
|
||||
],
|
||||
deps = [
|
||||
":integration_lib",
|
||||
"//packages/common",
|
||||
"//tools/testing:node_no_angular",
|
||||
"@npm//canonical-path",
|
||||
"@npm//convert-source-map",
|
||||
"@npm//shelljs",
|
||||
],
|
||||
)
|
@ -0,0 +1,231 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||
import {Decorator} from '../../../src/ngtsc/reflection';
|
||||
import {DecoratorHandler, DetectResult} from '../../../src/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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,401 @@
|
||||
/**
|
||||
* @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]) :
|
||||
[];
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,241 @@
|
||||
/**
|
||||
* @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 '../../../src/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));
|
||||
}
|
@ -0,0 +1,55 @@
|
||||
/**
|
||||
* @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 '../../../src/ngtsc/imports';
|
||||
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
|
||||
import {TypeScriptReflectionHost} from '../../../src/ngtsc/reflection';
|
||||
import {getDeclaration, makeProgram} from '../../../src/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);
|
||||
});
|
||||
});
|
@ -0,0 +1,76 @@
|
||||
/**
|
||||
* @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__',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
123
packages/compiler-cli/ngcc/test/helpers/utils.ts
Normal file
123
packages/compiler-cli/ngcc/test/helpers/utils.ts
Normal file
@ -0,0 +1,123 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright Google Inc. All Rights Reserved.
|
||||
*
|
||||
* Use of this source code is governed by an MIT-style license that can be
|
||||
* found in the LICENSE file at https://angular.io/license
|
||||
*/
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {AbsoluteFsPath} from '../../../src/ngtsc/path';
|
||||
import {makeProgram} from '../../../src/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 '../../../src/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};
|
||||
});
|
||||
}
|
@ -0,0 +1,387 @@
|
||||
/**
|
||||
* @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 '../../../src/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));
|
||||
}
|
||||
});
|
1563
packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts
Normal file
1563
packages/compiler-cli/ngcc/test/host/esm2015_host_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,424 @@
|
||||
/**
|
||||
* @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 '../../../src/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));
|
||||
}
|
||||
});
|
1747
packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts
Normal file
1747
packages/compiler-cli/ngcc/test/host/esm5_host_spec.ts
Normal file
File diff suppressed because it is too large
Load Diff
31
packages/compiler-cli/ngcc/test/host/util.ts
Normal file
31
packages/compiler-cli/ngcc/test/host/util.ts
Normal file
@ -0,0 +1,31 @@
|
||||
|
||||
/**
|
||||
* @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 '../../../src/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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
117
packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
Normal file
117
packages/compiler-cli/ngcc/test/integration/ngcc_spec.ts
Normal file
@ -0,0 +1,117 @@
|
||||
/**
|
||||
* @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, readdirSync, statSync} from 'fs';
|
||||
import * as mockFs from 'mock-fs';
|
||||
import {join} from 'path';
|
||||
const Module = require('module');
|
||||
|
||||
import {mainNgcc} from '../../src/main';
|
||||
import {getAngularPackagesFromRunfiles, resolveNpmTreeArtifact} from '../../../test/runfile_helpers';
|
||||
|
||||
describe('ngcc main()', () => {
|
||||
beforeEach(createMockFileSystem);
|
||||
afterEach(restoreRealFileSystem);
|
||||
|
||||
it('should run ngcc without errors for fesm2015', () => {
|
||||
const format = 'fesm2015';
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for fesm5', () => {
|
||||
const format = 'fesm5';
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for esm2015', () => {
|
||||
const format = 'esm2015';
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
|
||||
it('should run ngcc without errors for esm5', () => {
|
||||
const format = 'esm5';
|
||||
expect(mainNgcc(['-f', format, '-s', '/node_modules'])).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function createMockFileSystem() {
|
||||
mockFs({
|
||||
'/node_modules/@angular': loadAngularPackages(),
|
||||
'/node_modules/rxjs': loadDirectory(resolveNpmTreeArtifact('rxjs', 'index.js')),
|
||||
});
|
||||
spyOn(Module, '_resolveFilename').and.callFake(mockResolve);
|
||||
}
|
||||
|
||||
function restoreRealFileSystem() {
|
||||
mockFs.restore();
|
||||
}
|
||||
|
||||
|
||||
/** Load the built Angular packages into an in-memory structure. */
|
||||
function loadAngularPackages(): Directory {
|
||||
const packagesDirectory: Directory = {};
|
||||
|
||||
getAngularPackagesFromRunfiles().forEach(
|
||||
({name, pkgPath}) => { packagesDirectory[name] = loadDirectory(pkgPath); });
|
||||
|
||||
return packagesDirectory;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load real files from the filesystem into an "in-memory" structure,
|
||||
* which can be used with `mock-fs`.
|
||||
* @param directoryPath the path to the directory we want to load.
|
||||
*/
|
||||
function loadDirectory(directoryPath: string): Directory {
|
||||
const directory: Directory = {};
|
||||
|
||||
readdirSync(directoryPath).forEach(item => {
|
||||
const itemPath = join(directoryPath, item);
|
||||
if (statSync(itemPath).isDirectory()) {
|
||||
directory[item] = loadDirectory(itemPath);
|
||||
} else {
|
||||
directory[item] = readFileSync(itemPath, 'utf-8');
|
||||
}
|
||||
});
|
||||
|
||||
return directory;
|
||||
}
|
||||
|
||||
interface Directory {
|
||||
[pathSegment: string]: string|Directory;
|
||||
}
|
||||
|
||||
function mockResolve(request: string): string|null {
|
||||
if (existsSync(request)) {
|
||||
const stat = statSync(request);
|
||||
if (stat.isFile()) {
|
||||
return request;
|
||||
} else if (stat.isDirectory()) {
|
||||
const pIndex = mockResolve(request + '/index');
|
||||
if (pIndex && existsSync(pIndex)) {
|
||||
return pIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const ext of ['.js', '.d.ts']) {
|
||||
if (existsSync(request + ext)) {
|
||||
return request + ext;
|
||||
}
|
||||
}
|
||||
if (request.indexOf('/node_modules') === 0) {
|
||||
// We already tried adding node_modules so give up.
|
||||
return null;
|
||||
} else {
|
||||
return mockResolve(join('/node_modules', request));
|
||||
}
|
||||
}
|
||||
|
||||
function loadPackage(packageName: string) {
|
||||
return JSON.parse(readFileSync(`/node_modules/${packageName}/package.json`, 'utf8'));
|
||||
}
|
154
packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts
Normal file
154
packages/compiler-cli/ngcc/test/packages/build_marker_spec.ts
Normal file
@ -0,0 +1,154 @@
|
||||
/**
|
||||
* @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.');
|
||||
});
|
||||
});
|
||||
});
|
254
packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts
Normal file
254
packages/compiler-cli/ngcc/test/packages/dependency_host_spec.ts
Normal file
@ -0,0 +1,254 @@
|
||||
/**
|
||||
* @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(); }
|
||||
});
|
@ -0,0 +1,109 @@
|
||||
/**
|
||||
* @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));
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
@ -0,0 +1,165 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
156
packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts
Normal file
156
packages/compiler-cli/ngcc/test/packages/entry_point_spec.ts
Normal file
@ -0,0 +1,156 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
@ -0,0 +1,335 @@
|
||||
/**
|
||||
* @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 '../../../src/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 };`);
|
||||
});
|
||||
});
|
||||
});
|
405
packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts
Normal file
405
packages/compiler-cli/ngcc/test/rendering/esm5_renderer_spec.ts
Normal file
@ -0,0 +1,405 @@
|
||||
/**
|
||||
* @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 '../../../src/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;`);
|
||||
});
|
||||
});
|
||||
});
|
522
packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
Normal file
522
packages/compiler-cli/ngcc/test/rendering/renderer_spec.ts
Normal file
@ -0,0 +1,522 @@
|
||||
/**
|
||||
* @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};`);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Reference in New Issue
Block a user