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:

committed by
Miško Hevery

parent
c57b491778
commit
104d30507a
@ -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) !;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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}>`;
|
||||
|
Reference in New Issue
Block a user