feat(ivy): able to compile @angular/core with ngtsc (#24677)

@angular/core is unique in that it defines the Angular decorators
(@Component, @Directive, etc). Ordinarily ngtsc looks for imports
from @angular/core in order to identify these decorators. Clearly
within core itself, this strategy doesn't work.

Instead, a special constant ITS_JUST_ANGULAR is declared within a
known file in @angular/core. If ngtsc sees this constant it knows
core is being compiled and can ignore the imports when evaluating
decorators.

Additionally, when compiling decorators ngtsc will often write an
import to @angular/core for needed symbols. However @angular/core
cannot import itself. This change creates a module within core to
export all the symbols needed to compile it and adds intelligence
within ngtsc to write relative imports to that module, instead of
absolute imports to @angular/core.

PR Close #24677
This commit is contained in:
Alex Rickabaugh 2018-06-20 15:54:16 -07:00 committed by Miško Hevery
parent c57b491778
commit 104d30507a
14 changed files with 208 additions and 49 deletions

View File

@ -25,10 +25,11 @@ const EMPTY_MAP = new Map<string, Expression>();
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry) {}
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(decorator => decorator.name === 'Component' && isAngularCore(decorator));
return decorators.find(
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
@ -43,7 +44,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
// on it.
const directiveMetadata =
extractDirectiveMetadata(node, decorator, this.checker, this.reflector);
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
if (directiveMetadata === undefined) {
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
// case, compilation of the decorator is skipped. Returning an empty object signifies

View File

@ -22,14 +22,16 @@ const EMPTY_OBJECT: {[key: string]: string} = {};
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry) {}
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(decorator => decorator.name === 'Directive' && isAngularCore(decorator));
return decorators.find(
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
const analysis = extractDirectiveMetadata(node, decorator, this.checker, this.reflector);
const analysis =
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
// when this directive appears in an `@NgModule` scope, its selector can be determined.
@ -57,7 +59,7 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
*/
export function extractDirectiveMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker,
reflector: ReflectionHost): R3DirectiveMetadata|undefined {
reflector: ReflectionHost, isCore: boolean): R3DirectiveMetadata|undefined {
if (decorator.args === null || decorator.args.length !== 1) {
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
}
@ -108,7 +110,7 @@ export function extractDirectiveMetadata(
return {
name: clazz.name !.text,
deps: getConstructorDependencies(clazz, reflector),
deps: getConstructorDependencies(clazz, reflector, isCore),
host: {
attributes: {},
listeners: {},

View File

@ -20,15 +20,16 @@ import {getConstructorDependencies, isAngularCore} from './util';
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
*/
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
constructor(private reflector: ReflectionHost) {}
constructor(private reflector: ReflectionHost, private isCore: boolean) {}
detect(decorator: Decorator[]): Decorator|undefined {
return decorator.find(decorator => decorator.name === 'Injectable' && isAngularCore(decorator));
return decorator.find(
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3InjectableMetadata> {
return {
analysis: extractInjectableMetadata(node, decorator, this.reflector),
analysis: extractInjectableMetadata(node, decorator, this.reflector, this.isCore),
};
}
@ -48,8 +49,8 @@ export class InjectableDecoratorHandler implements DecoratorHandler<R3Injectable
* metadata needed to run `compileIvyInjectable`.
*/
function extractInjectableMetadata(
clazz: ts.ClassDeclaration, decorator: Decorator,
reflector: ReflectionHost): R3InjectableMetadata {
clazz: ts.ClassDeclaration, decorator: Decorator, reflector: ReflectionHost,
isCore: boolean): R3InjectableMetadata {
if (clazz.name === undefined) {
throw new Error(`@Injectables must have names`);
}
@ -63,7 +64,7 @@ function extractInjectableMetadata(
name,
type,
providedIn: new LiteralExpr(null),
deps: getConstructorDependencies(clazz, reflector),
deps: getConstructorDependencies(clazz, reflector, isCore),
};
} else if (decorator.args.length === 1) {
const metaNode = decorator.args[0];
@ -102,7 +103,7 @@ function extractInjectableMetadata(
}
return {name, type, providedIn, useFactory: factory, deps};
} else {
const deps = getConstructorDependencies(clazz, reflector);
const deps = getConstructorDependencies(clazz, reflector, isCore);
return {name, type, providedIn, deps};
}
} else {

View File

@ -29,10 +29,11 @@ export interface NgModuleAnalysis {
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry) {}
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
detect(decorators: Decorator[]): Decorator|undefined {
return decorators.find(decorator => decorator.name === 'NgModule' && isAngularCore(decorator));
return decorators.find(
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
}
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<NgModuleAnalysis> {
@ -89,7 +90,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
const ngInjectorDef: R3InjectorMetadata = {
name: node.name !.text,
type: new WrappedNodeExpr(node.name !),
deps: getConstructorDependencies(node, this.reflector), providers,
deps: getConstructorDependencies(node, this.reflector, this.isCore), providers,
imports: new LiteralArrayExpr(
[...imports, ...exports].map(imp => referenceToExpression(imp, context))),
};

View File

@ -13,14 +13,15 @@ import {Decorator, ReflectionHost} from '../../host';
import {Reference} from '../../metadata';
export function getConstructorDependencies(
clazz: ts.ClassDeclaration, reflector: ReflectionHost): R3DependencyMetadata[] {
clazz: ts.ClassDeclaration, reflector: ReflectionHost,
isCore: boolean): R3DependencyMetadata[] {
const useType: R3DependencyMetadata[] = [];
const ctorParams = reflector.getConstructorParameters(clazz) || [];
ctorParams.forEach((param, idx) => {
let tokenExpr = param.type;
let optional = false, self = false, skipSelf = false, host = false;
let resolved = R3ResolvedDependencyType.Token;
(param.decorators || []).filter(isAngularCore).forEach(dec => {
(param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => {
if (dec.name === 'Inject') {
if (dec.args === null || dec.args.length !== 1) {
throw new Error(`Unexpected number of arguments to @Inject().`);

View File

@ -13,5 +13,6 @@ ts_library(
"//packages:types",
"//packages/compiler",
"//packages/compiler-cli/src/ngtsc/host",
"//packages/compiler-cli/src/ngtsc/util",
],
)

View File

@ -91,17 +91,21 @@ export class NgtscProgram implements api.Program {
const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults;
const checker = this.tsProgram.getTypeChecker();
const isCore = isAngularCorePackage(this.tsProgram);
const reflector = new TypeScriptReflectionHost(checker);
const scopeRegistry = new SelectorScopeRegistry(checker, reflector);
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers = [
new ComponentDecoratorHandler(checker, reflector, scopeRegistry),
new DirectiveDecoratorHandler(checker, reflector, scopeRegistry),
new InjectableDecoratorHandler(reflector),
new NgModuleDecoratorHandler(checker, reflector, scopeRegistry),
new ComponentDecoratorHandler(checker, reflector, scopeRegistry, isCore),
new DirectiveDecoratorHandler(checker, reflector, scopeRegistry, isCore),
new InjectableDecoratorHandler(reflector, isCore),
new NgModuleDecoratorHandler(checker, reflector, scopeRegistry, isCore),
];
const compilation = new IvyCompilation(handlers, checker, reflector);
const coreImportsFrom = isCore && getR3SymbolsFile(this.tsProgram) || null;
const compilation = new IvyCompilation(handlers, checker, reflector, coreImportsFrom);
// Analyze every source file in the program.
this.tsProgram.getSourceFiles()
@ -115,7 +119,7 @@ export class NgtscProgram implements api.Program {
sourceFiles: ReadonlyArray<ts.SourceFile>) => {
if (fileName.endsWith('.d.ts')) {
data = sourceFiles.reduce(
(data, sf) => compilation.transformedDtsFor(sf.fileName, data), data);
(data, sf) => compilation.transformedDtsFor(sf.fileName, data, fileName), data);
}
this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
};
@ -128,7 +132,7 @@ export class NgtscProgram implements api.Program {
options: this.options,
emitOnlyDtsFiles: false, writeFile,
customTransformers: {
before: [ivyTransformFactory(compilation)],
before: [ivyTransformFactory(compilation, coreImportsFrom)],
},
});
return emitResult;
@ -152,3 +156,47 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
}
return {diagnostics, emitSkipped, emittedFiles};
}
/**
* Find the 'r3_symbols.ts' file in the given `Program`, or return `null` if it wasn't there.
*/
function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null {
return program.getSourceFiles().find(file => file.fileName.indexOf('r3_symbols.ts') >= 0) || null;
}
/**
* Determine if the given `Program` is @angular/core.
*/
function isAngularCorePackage(program: ts.Program): boolean {
// Look for its_just_angular.ts somewhere in the program.
const r3Symbols = getR3SymbolsFile(program);
if (r3Symbols === null) {
return false;
}
// Look for the constant ITS_JUST_ANGULAR in that file.
return r3Symbols.statements.some(stmt => {
// The statement must be a variable declaration statement.
if (!ts.isVariableStatement(stmt)) {
return false;
}
// It must be exported.
if (stmt.modifiers === undefined ||
!stmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) {
return false;
}
// It must declare ITS_JUST_ANGULAR.
return stmt.declarationList.declarations.some(decl => {
// The declaration must match the name.
if (!ts.isIdentifier(decl.name) || decl.name.text !== 'ITS_JUST_ANGULAR') {
return false;
}
// It must initialize the variable to true.
if (decl.initializer === undefined || decl.initializer.kind !== ts.SyntaxKind.TrueKeyword) {
return false;
}
// This definition matches.
return true;
});
});
}

View File

@ -46,9 +46,18 @@ export class IvyCompilation {
*/
private dtsMap = new Map<string, DtsFileTransformer>();
/**
* @param handlers array of `DecoratorHandler`s which will be executed against each class in the
* program
* @param checker TypeScript `TypeChecker` instance for the program
* @param reflector `ReflectionHost` through which all reflection operations will be performed
* @param coreImportsFrom a TypeScript `SourceFile` which exports symbols needed for Ivy imports
* when compiling @angular/core, or `null` if the current program is not @angular/core. This is
* `null` in most cases.
*/
constructor(
private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker,
private reflector: ReflectionHost) {}
private reflector: ReflectionHost, private coreImportsFrom: ts.SourceFile|null) {}
/**
* Analyze a source file and produce diagnostics for it (if any).
@ -147,19 +156,19 @@ export class IvyCompilation {
* Process a .d.ts source string and return a transformed version that incorporates the changes
* made to the source file.
*/
transformedDtsFor(tsFileName: string, dtsOriginalSource: string): string {
transformedDtsFor(tsFileName: string, dtsOriginalSource: string, dtsPath: string): string {
// No need to transform if no changes have been requested to the input file.
if (!this.dtsMap.has(tsFileName)) {
return dtsOriginalSource;
}
// Return the transformed .d.ts source.
return this.dtsMap.get(tsFileName) !.transform(dtsOriginalSource);
return this.dtsMap.get(tsFileName) !.transform(dtsOriginalSource, tsFileName);
}
private getDtsTransformer(tsFileName: string): DtsFileTransformer {
if (!this.dtsMap.has(tsFileName)) {
this.dtsMap.set(tsFileName, new DtsFileTransformer());
this.dtsMap.set(tsFileName, new DtsFileTransformer(this.coreImportsFrom));
}
return this.dtsMap.get(tsFileName) !;
}

View File

@ -8,6 +8,8 @@
import * as ts from 'typescript';
import {relativePathBetween} from '../../util/src/path';
import {CompileResult} from './api';
import {ImportManager, translateType} from './translator';
@ -18,7 +20,11 @@ import {ImportManager, translateType} from './translator';
*/
export class DtsFileTransformer {
private ivyFields = new Map<string, CompileResult[]>();
private imports = new ImportManager();
private imports: ImportManager;
constructor(private coreImportsFrom: ts.SourceFile|null) {
this.imports = new ImportManager(coreImportsFrom !== null);
}
/**
* Track that a static field was added to the code for a class.
@ -28,7 +34,7 @@ export class DtsFileTransformer {
/**
* Process the .d.ts text for a file and add any declarations which were recorded.
*/
transform(dts: string): string {
transform(dts: string, tsPath: string): string {
const dtsFile =
ts.createSourceFile('out.d.ts', dts, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
@ -51,7 +57,7 @@ export class DtsFileTransformer {
}
}
const imports = this.imports.getAllImports();
const imports = this.imports.getAllImports(tsPath, this.coreImportsFrom);
if (imports.length !== 0) {
dts = imports.map(i => `import * as ${i.as} from '${i.name}';\n`).join() + dts;
}

View File

@ -15,11 +15,12 @@ import {CompileResult} from './api';
import {IvyCompilation} from './compilation';
import {ImportManager, translateExpression, translateStatement} from './translator';
export function ivyTransformFactory(compilation: IvyCompilation):
ts.TransformerFactory<ts.SourceFile> {
export function ivyTransformFactory(
compilation: IvyCompilation,
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
return (file: ts.SourceFile): ts.SourceFile => {
return transformIvySourceFile(compilation, context, file);
return transformIvySourceFile(compilation, context, coreImportsFrom, file);
};
};
}
@ -74,18 +75,19 @@ class IvyVisitor extends Visitor {
*/
function transformIvySourceFile(
compilation: IvyCompilation, context: ts.TransformationContext,
file: ts.SourceFile): ts.SourceFile {
const importManager = new ImportManager();
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
const importManager = new ImportManager(coreImportsFrom !== null);
// Recursively scan through the AST and perform any updates requested by the IvyCompilation.
const sf = visit(file, new IvyVisitor(compilation, importManager), context);
// Generate the import statements to prepend.
const imports = importManager.getAllImports().map(
i => ts.createImportDeclaration(
const imports = importManager.getAllImports(file.fileName, coreImportsFrom).map(i => {
return ts.createImportDeclaration(
undefined, undefined,
ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))),
ts.createLiteral(i.name)));
ts.createLiteral(i.name));
});
// Prepend imports if needed.
if (imports.length > 0) {

View File

@ -8,6 +8,7 @@
import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinType, BuiltinTypeName, CastExpr, ClassStmt, CommaExpr, CommentStmt, ConditionalExpr, DeclareFunctionStmt, DeclareVarStmt, Expression, ExpressionStatement, ExpressionType, ExpressionVisitor, ExternalExpr, ExternalReference, FunctionExpr, IfStmt, InstantiateExpr, InvokeFunctionExpr, InvokeMethodExpr, JSDocCommentStmt, LiteralArrayExpr, LiteralExpr, LiteralMapExpr, MapType, NotExpr, ReadKeyExpr, ReadPropExpr, ReadVarExpr, ReturnStatement, Statement, StatementVisitor, ThrowStmt, TryCatchStmt, Type, TypeVisitor, WrappedNodeExpr, WriteKeyExpr, WritePropExpr, WriteVarExpr} from '@angular/compiler';
import * as ts from 'typescript';
import {relativePathBetween} from '../../util/src/path';
const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.And, ts.SyntaxKind.AmpersandAmpersandToken],
@ -28,20 +29,44 @@ const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken],
]);
const CORE_SUPPORTED_SYMBOLS = new Set<string>([
'defineInjectable',
'defineInjector',
'ɵdefineNgModule',
'inject',
'InjectableDef',
'InjectorDef',
'NgModuleDef',
]);
export class ImportManager {
private moduleToIndex = new Map<string, string>();
private nextIndex = 0;
generateNamedImport(moduleName: string): string {
constructor(private isCore: boolean) {}
generateNamedImport(moduleName: string, symbol: string): string {
if (!this.moduleToIndex.has(moduleName)) {
this.moduleToIndex.set(moduleName, `i${this.nextIndex++}`);
}
if (this.isCore && moduleName === '@angular/core' && !CORE_SUPPORTED_SYMBOLS.has(symbol)) {
throw new Error(`Importing unexpected symbol ${symbol} while compiling core`);
}
return this.moduleToIndex.get(moduleName) !;
}
getAllImports(): {name: string, as: string}[] {
getAllImports(contextPath: string, rewriteCoreImportsTo: ts.SourceFile|null):
{name: string, as: string}[] {
return Array.from(this.moduleToIndex.keys()).map(name => {
const as = this.moduleToIndex.get(name) !;
const as: string|null = this.moduleToIndex.get(name) !;
if (rewriteCoreImportsTo !== null && name === '@angular/core') {
const relative = relativePathBetween(contextPath, rewriteCoreImportsTo.fileName);
if (relative === null) {
throw new Error(
`Failed to rewrite import inside core: ${contextPath} -> ${rewriteCoreImportsTo.fileName}`);
}
name = relative;
}
return {name, as};
});
}
@ -166,7 +191,7 @@ class ExpressionTranslatorVisitor implements ExpressionVisitor, StatementVisitor
throw new Error(`Import unknown module or symbol ${ast.value}`);
}
return ts.createPropertyAccess(
ts.createIdentifier(this.imports.generateNamedImport(ast.value.moduleName)),
ts.createIdentifier(this.imports.generateNamedImport(ast.value.moduleName, ast.value.name)),
ts.createIdentifier(ast.value.name));
}
@ -314,7 +339,8 @@ export class TypeTranslatorVisitor implements ExpressionVisitor, TypeVisitor {
if (ast.value.moduleName === null || ast.value.name === null) {
throw new Error(`Import unknown module or symbol`);
}
const base = `${this.imports.generateNamedImport(ast.value.moduleName)}.${ast.value.name}`;
const moduleSymbol = this.imports.generateNamedImport(ast.value.moduleName, ast.value.name);
const base = `${moduleSymbol}.${ast.value.name}`;
if (ast.typeParams !== null) {
const generics = ast.typeParams.map(type => type.visitType(this, context)).join(', ');
return `${base}<${generics}>`;

View File

@ -9,4 +9,7 @@ ts_library(
"src/**/*.ts",
]),
module_name = "@angular/compiler-cli/src/ngtsc/util",
deps = [
"//packages:types",
],
)

View File

@ -0,0 +1,26 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'path';
const TS_DTS_EXTENSION = /(\.d)?\.ts$/;
export function relativePathBetween(from: string, to: string): string|null {
let relative = path.posix.relative(path.dirname(from), to).replace(TS_DTS_EXTENSION, '');
if (relative === '') {
return null;
}
// path.relative() does not include the leading './'.
if (!relative.startsWith('.')) {
relative = `./${relative}`;
}
return relative;
}

View File

@ -0,0 +1,32 @@
/**
* @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
*/
/*
* This file exists to support compilation of @angular/core in Ivy mode.
*
* When the Angular compiler processes a compilation unit, it normally writes imports to
* @angular/core. When compiling the core package itself this strategy isn't usable. Instead, the
* compiler writes imports to this file.
*
* Only a subset of such imports are supported - core is not allowed to declare components or pipes.
* A check in ngtsc's translator.ts validates this condition.
*
* The below symbols are used for @Injectable and @NgModule compilation.
*/
export {InjectableDef, InjectorDef, defineInjectable, defineInjector} from './di/defs';
export {inject} from './di/injector';
export {NgModuleDef} from './metadata/ng_module';
export {defineNgModule as ɵdefineNgModule} from './render3/definition';
/**
* The existence of this constant (in this particular file) informs the Angular compiler that the
* current program is actually @angular/core, which needs to be compiled specially.
*/
export const ITS_JUST_ANGULAR = true;