Skip to content

Commit 104d305

Browse files
alxhubmhevery
authored andcommitted
feat(ivy): able to compile @angular/core with ngtsc (angular#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 angular#24677
1 parent c57b491 commit 104d305

File tree

14 files changed

+208
-49
lines changed

14 files changed

+208
-49
lines changed

packages/compiler-cli/src/ngtsc/annotations/src/component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ const EMPTY_MAP = new Map<string, Expression>();
2525
export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMetadata> {
2626
constructor(
2727
private checker: ts.TypeChecker, private reflector: ReflectionHost,
28-
private scopeRegistry: SelectorScopeRegistry) {}
28+
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
2929

3030
detect(decorators: Decorator[]): Decorator|undefined {
31-
return decorators.find(decorator => decorator.name === 'Component' && isAngularCore(decorator));
31+
return decorators.find(
32+
decorator => decorator.name === 'Component' && (this.isCore || isAngularCore(decorator)));
3233
}
3334

3435
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3ComponentMetadata> {
@@ -43,7 +44,7 @@ export class ComponentDecoratorHandler implements DecoratorHandler<R3ComponentMe
4344
// @Component inherits @Directive, so begin by extracting the @Directive metadata and building
4445
// on it.
4546
const directiveMetadata =
46-
extractDirectiveMetadata(node, decorator, this.checker, this.reflector);
47+
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
4748
if (directiveMetadata === undefined) {
4849
// `extractDirectiveMetadata` returns undefined when the @Directive has `jit: true`. In this
4950
// case, compilation of the decorator is skipped. Returning an empty object signifies

packages/compiler-cli/src/ngtsc/annotations/src/directive.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,16 @@ const EMPTY_OBJECT: {[key: string]: string} = {};
2222
export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMetadata> {
2323
constructor(
2424
private checker: ts.TypeChecker, private reflector: ReflectionHost,
25-
private scopeRegistry: SelectorScopeRegistry) {}
25+
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
2626

2727
detect(decorators: Decorator[]): Decorator|undefined {
28-
return decorators.find(decorator => decorator.name === 'Directive' && isAngularCore(decorator));
28+
return decorators.find(
29+
decorator => decorator.name === 'Directive' && (this.isCore || isAngularCore(decorator)));
2930
}
3031

3132
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3DirectiveMetadata> {
32-
const analysis = extractDirectiveMetadata(node, decorator, this.checker, this.reflector);
33+
const analysis =
34+
extractDirectiveMetadata(node, decorator, this.checker, this.reflector, this.isCore);
3335

3436
// If the directive has a selector, it should be registered with the `SelectorScopeRegistry` so
3537
// when this directive appears in an `@NgModule` scope, its selector can be determined.
@@ -57,7 +59,7 @@ export class DirectiveDecoratorHandler implements DecoratorHandler<R3DirectiveMe
5759
*/
5860
export function extractDirectiveMetadata(
5961
clazz: ts.ClassDeclaration, decorator: Decorator, checker: ts.TypeChecker,
60-
reflector: ReflectionHost): R3DirectiveMetadata|undefined {
62+
reflector: ReflectionHost, isCore: boolean): R3DirectiveMetadata|undefined {
6163
if (decorator.args === null || decorator.args.length !== 1) {
6264
throw new Error(`Incorrect number of arguments to @${decorator.name} decorator`);
6365
}
@@ -108,7 +110,7 @@ export function extractDirectiveMetadata(
108110

109111
return {
110112
name: clazz.name !.text,
111-
deps: getConstructorDependencies(clazz, reflector),
113+
deps: getConstructorDependencies(clazz, reflector, isCore),
112114
host: {
113115
attributes: {},
114116
listeners: {},

packages/compiler-cli/src/ngtsc/annotations/src/injectable.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,16 @@ import {getConstructorDependencies, isAngularCore} from './util';
2020
* Adapts the `compileIvyInjectable` compiler for `@Injectable` decorators to the Ivy compiler.
2121
*/
2222
export class InjectableDecoratorHandler implements DecoratorHandler<R3InjectableMetadata> {
23-
constructor(private reflector: ReflectionHost) {}
23+
constructor(private reflector: ReflectionHost, private isCore: boolean) {}
2424

2525
detect(decorator: Decorator[]): Decorator|undefined {
26-
return decorator.find(decorator => decorator.name === 'Injectable' && isAngularCore(decorator));
26+
return decorator.find(
27+
decorator => decorator.name === 'Injectable' && (this.isCore || isAngularCore(decorator)));
2728
}
2829

2930
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<R3InjectableMetadata> {
3031
return {
31-
analysis: extractInjectableMetadata(node, decorator, this.reflector),
32+
analysis: extractInjectableMetadata(node, decorator, this.reflector, this.isCore),
3233
};
3334
}
3435

@@ -48,8 +49,8 @@ export class InjectableDecoratorHandler implements DecoratorHandler<R3Injectable
4849
* metadata needed to run `compileIvyInjectable`.
4950
*/
5051
function extractInjectableMetadata(
51-
clazz: ts.ClassDeclaration, decorator: Decorator,
52-
reflector: ReflectionHost): R3InjectableMetadata {
52+
clazz: ts.ClassDeclaration, decorator: Decorator, reflector: ReflectionHost,
53+
isCore: boolean): R3InjectableMetadata {
5354
if (clazz.name === undefined) {
5455
throw new Error(`@Injectables must have names`);
5556
}
@@ -63,7 +64,7 @@ function extractInjectableMetadata(
6364
name,
6465
type,
6566
providedIn: new LiteralExpr(null),
66-
deps: getConstructorDependencies(clazz, reflector),
67+
deps: getConstructorDependencies(clazz, reflector, isCore),
6768
};
6869
} else if (decorator.args.length === 1) {
6970
const metaNode = decorator.args[0];
@@ -102,7 +103,7 @@ function extractInjectableMetadata(
102103
}
103104
return {name, type, providedIn, useFactory: factory, deps};
104105
} else {
105-
const deps = getConstructorDependencies(clazz, reflector);
106+
const deps = getConstructorDependencies(clazz, reflector, isCore);
106107
return {name, type, providedIn, deps};
107108
}
108109
} else {

packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,11 @@ export interface NgModuleAnalysis {
2929
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis> {
3030
constructor(
3131
private checker: ts.TypeChecker, private reflector: ReflectionHost,
32-
private scopeRegistry: SelectorScopeRegistry) {}
32+
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
3333

3434
detect(decorators: Decorator[]): Decorator|undefined {
35-
return decorators.find(decorator => decorator.name === 'NgModule' && isAngularCore(decorator));
35+
return decorators.find(
36+
decorator => decorator.name === 'NgModule' && (this.isCore || isAngularCore(decorator)));
3637
}
3738

3839
analyze(node: ts.ClassDeclaration, decorator: Decorator): AnalysisOutput<NgModuleAnalysis> {
@@ -89,7 +90,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
8990
const ngInjectorDef: R3InjectorMetadata = {
9091
name: node.name !.text,
9192
type: new WrappedNodeExpr(node.name !),
92-
deps: getConstructorDependencies(node, this.reflector), providers,
93+
deps: getConstructorDependencies(node, this.reflector, this.isCore), providers,
9394
imports: new LiteralArrayExpr(
9495
[...imports, ...exports].map(imp => referenceToExpression(imp, context))),
9596
};

packages/compiler-cli/src/ngtsc/annotations/src/util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,15 @@ import {Decorator, ReflectionHost} from '../../host';
1313
import {Reference} from '../../metadata';
1414

1515
export function getConstructorDependencies(
16-
clazz: ts.ClassDeclaration, reflector: ReflectionHost): R3DependencyMetadata[] {
16+
clazz: ts.ClassDeclaration, reflector: ReflectionHost,
17+
isCore: boolean): R3DependencyMetadata[] {
1718
const useType: R3DependencyMetadata[] = [];
1819
const ctorParams = reflector.getConstructorParameters(clazz) || [];
1920
ctorParams.forEach((param, idx) => {
2021
let tokenExpr = param.type;
2122
let optional = false, self = false, skipSelf = false, host = false;
2223
let resolved = R3ResolvedDependencyType.Token;
23-
(param.decorators || []).filter(isAngularCore).forEach(dec => {
24+
(param.decorators || []).filter(dec => isCore || isAngularCore(dec)).forEach(dec => {
2425
if (dec.name === 'Inject') {
2526
if (dec.args === null || dec.args.length !== 1) {
2627
throw new Error(`Unexpected number of arguments to @Inject().`);

packages/compiler-cli/src/ngtsc/metadata/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,6 @@ ts_library(
1313
"//packages:types",
1414
"//packages/compiler",
1515
"//packages/compiler-cli/src/ngtsc/host",
16+
"//packages/compiler-cli/src/ngtsc/util",
1617
],
1718
)

packages/compiler-cli/src/ngtsc/program.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -91,17 +91,21 @@ export class NgtscProgram implements api.Program {
9191
const mergeEmitResultsCallback = opts && opts.mergeEmitResultsCallback || mergeEmitResults;
9292

9393
const checker = this.tsProgram.getTypeChecker();
94+
const isCore = isAngularCorePackage(this.tsProgram);
9495
const reflector = new TypeScriptReflectionHost(checker);
9596
const scopeRegistry = new SelectorScopeRegistry(checker, reflector);
9697

9798
// Set up the IvyCompilation, which manages state for the Ivy transformer.
9899
const handlers = [
99-
new ComponentDecoratorHandler(checker, reflector, scopeRegistry),
100-
new DirectiveDecoratorHandler(checker, reflector, scopeRegistry),
101-
new InjectableDecoratorHandler(reflector),
102-
new NgModuleDecoratorHandler(checker, reflector, scopeRegistry),
100+
new ComponentDecoratorHandler(checker, reflector, scopeRegistry, isCore),
101+
new DirectiveDecoratorHandler(checker, reflector, scopeRegistry, isCore),
102+
new InjectableDecoratorHandler(reflector, isCore),
103+
new NgModuleDecoratorHandler(checker, reflector, scopeRegistry, isCore),
103104
];
104-
const compilation = new IvyCompilation(handlers, checker, reflector);
105+
106+
const coreImportsFrom = isCore && getR3SymbolsFile(this.tsProgram) || null;
107+
108+
const compilation = new IvyCompilation(handlers, checker, reflector, coreImportsFrom);
105109

106110
// Analyze every source file in the program.
107111
this.tsProgram.getSourceFiles()
@@ -115,7 +119,7 @@ export class NgtscProgram implements api.Program {
115119
sourceFiles: ReadonlyArray<ts.SourceFile>) => {
116120
if (fileName.endsWith('.d.ts')) {
117121
data = sourceFiles.reduce(
118-
(data, sf) => compilation.transformedDtsFor(sf.fileName, data), data);
122+
(data, sf) => compilation.transformedDtsFor(sf.fileName, data, fileName), data);
119123
}
120124
this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
121125
};
@@ -128,7 +132,7 @@ export class NgtscProgram implements api.Program {
128132
options: this.options,
129133
emitOnlyDtsFiles: false, writeFile,
130134
customTransformers: {
131-
before: [ivyTransformFactory(compilation)],
135+
before: [ivyTransformFactory(compilation, coreImportsFrom)],
132136
},
133137
});
134138
return emitResult;
@@ -152,3 +156,47 @@ function mergeEmitResults(emitResults: ts.EmitResult[]): ts.EmitResult {
152156
}
153157
return {diagnostics, emitSkipped, emittedFiles};
154158
}
159+
160+
/**
161+
* Find the 'r3_symbols.ts' file in the given `Program`, or return `null` if it wasn't there.
162+
*/
163+
function getR3SymbolsFile(program: ts.Program): ts.SourceFile|null {
164+
return program.getSourceFiles().find(file => file.fileName.indexOf('r3_symbols.ts') >= 0) || null;
165+
}
166+
167+
/**
168+
* Determine if the given `Program` is @angular/core.
169+
*/
170+
function isAngularCorePackage(program: ts.Program): boolean {
171+
// Look for its_just_angular.ts somewhere in the program.
172+
const r3Symbols = getR3SymbolsFile(program);
173+
if (r3Symbols === null) {
174+
return false;
175+
}
176+
177+
// Look for the constant ITS_JUST_ANGULAR in that file.
178+
return r3Symbols.statements.some(stmt => {
179+
// The statement must be a variable declaration statement.
180+
if (!ts.isVariableStatement(stmt)) {
181+
return false;
182+
}
183+
// It must be exported.
184+
if (stmt.modifiers === undefined ||
185+
!stmt.modifiers.some(mod => mod.kind === ts.SyntaxKind.ExportKeyword)) {
186+
return false;
187+
}
188+
// It must declare ITS_JUST_ANGULAR.
189+
return stmt.declarationList.declarations.some(decl => {
190+
// The declaration must match the name.
191+
if (!ts.isIdentifier(decl.name) || decl.name.text !== 'ITS_JUST_ANGULAR') {
192+
return false;
193+
}
194+
// It must initialize the variable to true.
195+
if (decl.initializer === undefined || decl.initializer.kind !== ts.SyntaxKind.TrueKeyword) {
196+
return false;
197+
}
198+
// This definition matches.
199+
return true;
200+
});
201+
});
202+
}

packages/compiler-cli/src/ngtsc/transform/src/compilation.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,18 @@ export class IvyCompilation {
4646
*/
4747
private dtsMap = new Map<string, DtsFileTransformer>();
4848

49+
/**
50+
* @param handlers array of `DecoratorHandler`s which will be executed against each class in the
51+
* program
52+
* @param checker TypeScript `TypeChecker` instance for the program
53+
* @param reflector `ReflectionHost` through which all reflection operations will be performed
54+
* @param coreImportsFrom a TypeScript `SourceFile` which exports symbols needed for Ivy imports
55+
* when compiling @angular/core, or `null` if the current program is not @angular/core. This is
56+
* `null` in most cases.
57+
*/
4958
constructor(
5059
private handlers: DecoratorHandler<any>[], private checker: ts.TypeChecker,
51-
private reflector: ReflectionHost) {}
60+
private reflector: ReflectionHost, private coreImportsFrom: ts.SourceFile|null) {}
5261

5362
/**
5463
* Analyze a source file and produce diagnostics for it (if any).
@@ -147,19 +156,19 @@ export class IvyCompilation {
147156
* Process a .d.ts source string and return a transformed version that incorporates the changes
148157
* made to the source file.
149158
*/
150-
transformedDtsFor(tsFileName: string, dtsOriginalSource: string): string {
159+
transformedDtsFor(tsFileName: string, dtsOriginalSource: string, dtsPath: string): string {
151160
// No need to transform if no changes have been requested to the input file.
152161
if (!this.dtsMap.has(tsFileName)) {
153162
return dtsOriginalSource;
154163
}
155164

156165
// Return the transformed .d.ts source.
157-
return this.dtsMap.get(tsFileName) !.transform(dtsOriginalSource);
166+
return this.dtsMap.get(tsFileName) !.transform(dtsOriginalSource, tsFileName);
158167
}
159168

160169
private getDtsTransformer(tsFileName: string): DtsFileTransformer {
161170
if (!this.dtsMap.has(tsFileName)) {
162-
this.dtsMap.set(tsFileName, new DtsFileTransformer());
171+
this.dtsMap.set(tsFileName, new DtsFileTransformer(this.coreImportsFrom));
163172
}
164173
return this.dtsMap.get(tsFileName) !;
165174
}

packages/compiler-cli/src/ngtsc/transform/src/declaration.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import * as ts from 'typescript';
1010

11+
import {relativePathBetween} from '../../util/src/path';
12+
1113
import {CompileResult} from './api';
1214
import {ImportManager, translateType} from './translator';
1315

@@ -18,7 +20,11 @@ import {ImportManager, translateType} from './translator';
1820
*/
1921
export class DtsFileTransformer {
2022
private ivyFields = new Map<string, CompileResult[]>();
21-
private imports = new ImportManager();
23+
private imports: ImportManager;
24+
25+
constructor(private coreImportsFrom: ts.SourceFile|null) {
26+
this.imports = new ImportManager(coreImportsFrom !== null);
27+
}
2228

2329
/**
2430
* Track that a static field was added to the code for a class.
@@ -28,7 +34,7 @@ export class DtsFileTransformer {
2834
/**
2935
* Process the .d.ts text for a file and add any declarations which were recorded.
3036
*/
31-
transform(dts: string): string {
37+
transform(dts: string, tsPath: string): string {
3238
const dtsFile =
3339
ts.createSourceFile('out.d.ts', dts, ts.ScriptTarget.Latest, false, ts.ScriptKind.TS);
3440

@@ -51,7 +57,7 @@ export class DtsFileTransformer {
5157
}
5258
}
5359

54-
const imports = this.imports.getAllImports();
60+
const imports = this.imports.getAllImports(tsPath, this.coreImportsFrom);
5561
if (imports.length !== 0) {
5662
dts = imports.map(i => `import * as ${i.as} from '${i.name}';\n`).join() + dts;
5763
}

packages/compiler-cli/src/ngtsc/transform/src/transform.ts

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@ import {CompileResult} from './api';
1515
import {IvyCompilation} from './compilation';
1616
import {ImportManager, translateExpression, translateStatement} from './translator';
1717

18-
export function ivyTransformFactory(compilation: IvyCompilation):
19-
ts.TransformerFactory<ts.SourceFile> {
18+
export function ivyTransformFactory(
19+
compilation: IvyCompilation,
20+
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
2021
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
2122
return (file: ts.SourceFile): ts.SourceFile => {
22-
return transformIvySourceFile(compilation, context, file);
23+
return transformIvySourceFile(compilation, context, coreImportsFrom, file);
2324
};
2425
};
2526
}
@@ -74,18 +75,19 @@ class IvyVisitor extends Visitor {
7475
*/
7576
function transformIvySourceFile(
7677
compilation: IvyCompilation, context: ts.TransformationContext,
77-
file: ts.SourceFile): ts.SourceFile {
78-
const importManager = new ImportManager();
78+
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
79+
const importManager = new ImportManager(coreImportsFrom !== null);
7980

8081
// Recursively scan through the AST and perform any updates requested by the IvyCompilation.
8182
const sf = visit(file, new IvyVisitor(compilation, importManager), context);
8283

8384
// Generate the import statements to prepend.
84-
const imports = importManager.getAllImports().map(
85-
i => ts.createImportDeclaration(
86-
undefined, undefined,
87-
ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))),
88-
ts.createLiteral(i.name)));
85+
const imports = importManager.getAllImports(file.fileName, coreImportsFrom).map(i => {
86+
return ts.createImportDeclaration(
87+
undefined, undefined,
88+
ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))),
89+
ts.createLiteral(i.name));
90+
});
8991

9092
// Prepend imports if needed.
9193
if (imports.length > 0) {

0 commit comments

Comments
 (0)