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

@ -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(
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) {

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}>`;