From 104d30507a2bea9d9347ca896288b27a735f9862 Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Wed, 20 Jun 2018 15:54:16 -0700 Subject: [PATCH] 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 --- .../src/ngtsc/annotations/src/component.ts | 7 ++- .../src/ngtsc/annotations/src/directive.ts | 12 ++-- .../src/ngtsc/annotations/src/injectable.ts | 15 ++--- .../src/ngtsc/annotations/src/ng_module.ts | 7 ++- .../src/ngtsc/annotations/src/util.ts | 5 +- .../src/ngtsc/metadata/BUILD.bazel | 1 + packages/compiler-cli/src/ngtsc/program.ts | 62 ++++++++++++++++--- .../src/ngtsc/transform/src/compilation.ts | 17 +++-- .../src/ngtsc/transform/src/declaration.ts | 12 +++- .../src/ngtsc/transform/src/transform.ts | 22 ++++--- .../src/ngtsc/transform/src/translator.ts | 36 +++++++++-- .../compiler-cli/src/ngtsc/util/BUILD.bazel | 3 + .../compiler-cli/src/ngtsc/util/src/path.ts | 26 ++++++++ packages/core/src/r3_symbols.ts | 32 ++++++++++ 14 files changed, 208 insertions(+), 49 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/util/src/path.ts create mode 100644 packages/core/src/r3_symbols.ts diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts index ca4bdb788e..2de6d830d9 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/component.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/component.ts @@ -25,10 +25,11 @@ const EMPTY_MAP = new Map(); export class ComponentDecoratorHandler implements DecoratorHandler { 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 { @@ -43,7 +44,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler { 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 { - 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 { - 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 { return { - analysis: extractInjectableMetadata(node, decorator, this.reflector), + analysis: extractInjectableMetadata(node, decorator, this.reflector, this.isCore), }; } @@ -48,8 +49,8 @@ export class InjectableDecoratorHandler implements DecoratorHandler { 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 { @@ -89,7 +90,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler referenceToExpression(imp, context))), }; diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index 4a17e086ea..ec63dd357b 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -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().`); diff --git a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel index 6780665ad1..d782bc3878 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel @@ -13,5 +13,6 @@ ts_library( "//packages:types", "//packages/compiler", "//packages/compiler-cli/src/ngtsc/host", + "//packages/compiler-cli/src/ngtsc/util", ], ) diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 7d1bff4e35..1b9793ee90 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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) => { 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; + }); + }); +} diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index d58a0db402..7129efa8f2 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -46,9 +46,18 @@ export class IvyCompilation { */ private dtsMap = new Map(); + /** + * @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[], 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) !; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index 499e6406e7..3c24a6be95 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -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(); - 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; } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts index 47fc8c05b2..f5a2ee9604 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/transform.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/transform.ts @@ -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 { +export function ivyTransformFactory( + compilation: IvyCompilation, + coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory { return (context: ts.TransformationContext): ts.Transformer => { 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( - undefined, undefined, - ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))), - ts.createLiteral(i.name))); + 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)); + }); // Prepend imports if needed. if (imports.length > 0) { diff --git a/packages/compiler-cli/src/ngtsc/transform/src/translator.ts b/packages/compiler-cli/src/ngtsc/transform/src/translator.ts index 69ad563a71..b6b3122bad 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/translator.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/translator.ts @@ -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.And, ts.SyntaxKind.AmpersandAmpersandToken], @@ -28,20 +29,44 @@ const BINARY_OPERATORS = new Map([ [BinaryOperator.Plus, ts.SyntaxKind.PlusToken], ]); +const CORE_SUPPORTED_SYMBOLS = new Set([ + 'defineInjectable', + 'defineInjector', + 'ɵdefineNgModule', + 'inject', + 'InjectableDef', + 'InjectorDef', + 'NgModuleDef', +]); + export class ImportManager { private moduleToIndex = new Map(); 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}>`; diff --git a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel index c8d3c82949..b8c65905d3 100644 --- a/packages/compiler-cli/src/ngtsc/util/BUILD.bazel +++ b/packages/compiler-cli/src/ngtsc/util/BUILD.bazel @@ -9,4 +9,7 @@ ts_library( "src/**/*.ts", ]), module_name = "@angular/compiler-cli/src/ngtsc/util", + deps = [ + "//packages:types", + ], ) diff --git a/packages/compiler-cli/src/ngtsc/util/src/path.ts b/packages/compiler-cli/src/ngtsc/util/src/path.ts new file mode 100644 index 0000000000..8410083396 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/util/src/path.ts @@ -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; +} \ No newline at end of file diff --git a/packages/core/src/r3_symbols.ts b/packages/core/src/r3_symbols.ts new file mode 100644 index 0000000000..e76e2d02e9 --- /dev/null +++ b/packages/core/src/r3_symbols.ts @@ -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;