feat(ivy): generate .ngfactory stubs if requested (#25176) · angular/angular@0822dc7 · GitHub
Skip to content

Commit 0822dc7

Browse files
alxhubkara
authored andcommitted
feat(ivy): generate .ngfactory stubs if requested (#25176)
Existing bootstrap code in the wild depends on the existence of .ngfactory files, which Ivy does not need. This commit adds the capability in ngtsc to generate .ngfactory files which bridge existing bootstrap code with Ivy. This is an initial step. Remaining work includes complying with the compiler option to specify a generated file directory, as well as presumably testing in g3. PR Close #25176
1 parent 728d98d commit 0822dc7

13 files changed

Lines changed: 338 additions & 10 deletions

File tree

‎packages/compiler-cli/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
116116
analysis: {
117117
ngModuleDef, ngInjectorDef,
118118
},
119+
factorySymbolName: node.name !== undefined ? node.name.text : undefined,
119120
};
120121
}
121122

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package(default_visibility = ["//visibility:public"])
2+
3+
load("//tools:defaults.bzl", "ts_library")
4+
5+
ts_library(
6+
name = "factories",
7+
srcs = glob([
8+
"index.ts",
9+
"src/**/*.ts",
10+
]),
11+
module_name = "@angular/compiler-cli/src/ngtsc/factories",
12+
deps = [
13+
"//packages/compiler",
14+
"//packages/compiler-cli/src/ngtsc/host",
15+
"//packages/compiler-cli/src/ngtsc/metadata",
16+
"//packages/compiler-cli/src/ngtsc/util",
17+
],
18+
)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
Deals with the creation of generated factory files.
2+
3+
Generated factory files create a catch-22 in ngtsc. Their contents depends on static analysis of the current program, yet they're also importable from the current program. This importability gives rise to the requirement that the contents of the generated file must be known before program creation, so that imports of it are valid. However, until the program is created, the analysis to determine the contents of the generated file cannot take place.
4+
5+
ngc used to get away with this because the analysis phase did not depend on program creation but on the metadata collection / global analysis process.
6+
7+
ngtsc is forced to take a different approach. A lightweight analysis pipeline which does not rely on the ts.TypeChecker (and thus can run before the program is created) is used to estimate the contents of a generated file, in a way that allows the program to be created. A transformer then operates on this estimated file during emit and replaces the estimated contents with accurate information.
8+
9+
It is important that this estimate be an overestimate, as type-checking will always be run against the estimated file, and must succeed in every case where it would have succeeded with accurate info.
10+
11+
This directory contains the utilities for generating, updating, and incorporating these factory files into a ts.Program.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
export {FactoryGenerator} from './src/generator';
10+
export {GeneratedFactoryHostWrapper} from './src/host';
11+
export {FactoryInfo, generatedFactoryTransform} from './src/transform';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as path from 'path';
10+
import * as ts from 'typescript';
11+
12+
const TS_DTS_SUFFIX = /(\.d)?\.ts$/;
13+
14+
/**
15+
* Generates ts.SourceFiles which contain variable declarations for NgFactories for every exported
16+
* class of an input ts.SourceFile.
17+
*/
18+
export class FactoryGenerator {
19+
factoryFor(original: ts.SourceFile, genFilePath: string): ts.SourceFile {
20+
const relativePathToSource =
21+
'./' + path.posix.basename(original.fileName).replace(TS_DTS_SUFFIX, '');
22+
// Collect a list of classes that need to have factory types emitted for them.
23+
const symbolNames = original
24+
.statements
25+
// Pick out top level class declarations...
26+
.filter(ts.isClassDeclaration)
27+
// which are named, exported, and have decorators.
28+
.filter(
29+
decl => isExported(decl) && decl.decorators !== undefined &&
30+
decl.name !== undefined)
31+
// Grab the symbol name.
32+
.map(decl => decl.name !.text);
33+
34+
// For each symbol name, generate a constant export of the corresponding NgFactory.
35+
// This will encompass a lot of symbols which don't need factories, but that's okay
36+
// because it won't miss any that do.
37+
const varLines = symbolNames.map(
38+
name => `export const ${name}NgFactory = new i0.ɵNgModuleFactory(${name});`);
39+
const sourceText = [
40+
// This might be incorrect if the current package being compiled is Angular core, but it's
41+
// okay to leave in at type checking time. TypeScript can handle this reference via its path
42+
// mapping, but downstream bundlers can't. If the current package is core itself, this will be
43+
// replaced in the factory transformer before emit.
44+
`import * as i0 from '@angular/core';`,
45+
`import {${symbolNames.join(', ')}} from '${relativePathToSource}';`,
46+
...varLines,
47+
].join('\n');
48+
return ts.createSourceFile(
49+
genFilePath, sourceText, original.languageVersion, true, ts.ScriptKind.TS);
50+
}
51+
52+
computeFactoryFileMap(files: ReadonlyArray<string>): Map<string, string> {
53+
const map = new Map<string, string>();
54+
files.filter(sourceFile => !sourceFile.endsWith('.d.ts'))
55+
.forEach(sourceFile => map.set(sourceFile.replace(/\.ts$/, '.ngfactory.ts'), sourceFile));
56+
return map;
57+
}
58+
}
59+
60+
function isExported(decl: ts.Declaration): boolean {
61+
return decl.modifiers !== undefined &&
62+
decl.modifiers.some(mod => mod.kind == ts.SyntaxKind.ExportKeyword);
63+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as path from 'path';
10+
import * as ts from 'typescript';
11+
12+
import {FactoryGenerator} from './generator';
13+
14+
/**
15+
* A wrapper around a `ts.CompilerHost` which supports generated files.
16+
*/
17+
export class GeneratedFactoryHostWrapper implements ts.CompilerHost {
18+
constructor(
19+
private delegate: ts.CompilerHost, private generator: FactoryGenerator,
20+
private factoryToSourceMap: Map<string, string>) {}
21+
22+
getSourceFile(
23+
fileName: string, languageVersion: ts.ScriptTarget,
24+
onError?: ((message: string) => void)|undefined,
25+
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
26+
const canonical = this.getCanonicalFileName(fileName);
27+
if (this.factoryToSourceMap.has(canonical)) {
28+
const sourceFileName = this.getCanonicalFileName(this.factoryToSourceMap.get(canonical) !);
29+
const sourceFile = this.delegate.getSourceFile(
30+
sourceFileName, languageVersion, onError, shouldCreateNewSourceFile);
31+
if (sourceFile === undefined) {
32+
return undefined;
33+
}
34+
return this.generator.factoryFor(sourceFile, fileName);
35+
}
36+
return this.delegate.getSourceFile(
37+
fileName, languageVersion, onError, shouldCreateNewSourceFile);
38+
}
39+
40+
getDefaultLibFileName(options: ts.CompilerOptions): string {
41+
return this.delegate.getDefaultLibFileName(options);
42+
}
43+
44+
writeFile(
45+
fileName: string, data: string, writeByteOrderMark: boolean,
46+
onError: ((message: string) => void)|undefined,
47+
sourceFiles: ReadonlyArray<ts.SourceFile>): void {
48+
return this.delegate.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
49+
}
50+
51+
getCurrentDirectory(): string { return this.delegate.getCurrentDirectory(); }
52+
53+
getDirectories(path: string): string[] { return this.delegate.getDirectories(path); }
54+
55+
getCanonicalFileName(fileName: string): string {
56+
return this.delegate.getCanonicalFileName(fileName);
57+
}
58+
59+
useCaseSensitiveFileNames(): boolean { return this.delegate.useCaseSensitiveFileNames(); }
60+
61+
getNewLine(): string { return this.delegate.getNewLine(); }
62+
63+
fileExists(fileName: string): boolean {
64+
return this.factoryToSourceMap.has(fileName) || this.delegate.fileExists(fileName);
65+
}
66+
67+
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
68+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {relativePathBetween} from '../../util/src/path';
12+
13+
const STRIP_NG_FACTORY = /(.*)NgFactory$/;
14+
15+
export interface FactoryInfo {
16+
sourceFilePath: string;
17+
moduleSymbolNames: Set<string>;
18+
}
19+
20+
export function generatedFactoryTransform(
21+
factoryMap: Map<string, FactoryInfo>,
22+
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
23+
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
24+
return (file: ts.SourceFile): ts.SourceFile => {
25+
return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file);
26+
};
27+
};
28+
}
29+
30+
function transformFactorySourceFile(
31+
factoryMap: Map<string, FactoryInfo>, context: ts.TransformationContext,
32+
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
33+
// If this is not a generated file, it won't have factory info associated with it.
34+
if (!factoryMap.has(file.fileName)) {
35+
// Don't transform non-generated code.
36+
return file;
37+
}
38+
39+
const {moduleSymbolNames, sourceFilePath} = factoryMap.get(file.fileName) !;
40+
41+
const clone = ts.getMutableClone(file);
42+
43+
const transformedStatements = file.statements.map(stmt => {
44+
if (coreImportsFrom !== null && ts.isImportDeclaration(stmt) &&
45+
ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === '@angular/core') {
46+
const path = relativePathBetween(sourceFilePath, coreImportsFrom.fileName);
47+
if (path !== null) {
48+
return ts.updateImportDeclaration(
49+
stmt, stmt.decorators, stmt.modifiers, stmt.importClause, ts.createStringLiteral(path));
50+
} else {
51+
return ts.createNotEmittedStatement(stmt);
52+
}
53+
} else if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) {
54+
const decl = stmt.declarationList.declarations[0];
55+
if (ts.isIdentifier(decl.name)) {
56+
const match = STRIP_NG_FACTORY.exec(decl.name.text);
57+
if (match === null || !moduleSymbolNames.has(match[1])) {
58+
// Remove the given factory as it wasn't actually for an NgModule.
59+
return ts.createNotEmittedStatement(stmt);
60+
}
61+
}
62+
return stmt;
63+
} else {
64+
return stmt;
65+
}
66+
});
67+
if (!transformedStatements.some(ts.isVariableStatement)) {
68+
// If the resulting file has no factories, include an empty export to
69+
// satisfy closure compiler.
70+
transformedStatements.push(ts.createVariableStatement(
71+
[ts.createModifier(ts.SyntaxKind.ExportKeyword)],
72+
ts.createVariableDeclarationList(
73+
[ts.createVariableDeclaration('ɵNonEmptyModule', undefined, ts.createTrue())],
74+
ts.NodeFlags.Const)));
75+
}
76+
clone.statements = ts.createNodeArray(transformedStatements);
77+
return clone;
78+
}

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

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import * as ts from 'typescript';
1313
import * as api from '../transformers/api';
1414

1515
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations';
16+
import {FactoryGenerator, FactoryInfo, GeneratedFactoryHostWrapper, generatedFactoryTransform} from './factories';
1617
import {TypeScriptReflectionHost} from './metadata';
1718
import {FileResourceLoader, HostResourceLoader} from './resource_loader';
1819
import {IvyCompilation, ivyTransformFactory} from './transform';
@@ -21,20 +22,39 @@ export class NgtscProgram implements api.Program {
2122
private tsProgram: ts.Program;
2223
private resourceLoader: ResourceLoader;
2324
private compilation: IvyCompilation|undefined = undefined;
24-
25+
private factoryToSourceInfo: Map<string, FactoryInfo>|null = null;
26+
private sourceToFactorySymbols: Map<string, Set<string>>|null = null;
27+
private host: ts.CompilerHost;
2528
private _coreImportsFrom: ts.SourceFile|null|undefined = undefined;
2629
private _reflector: TypeScriptReflectionHost|undefined = undefined;
2730
private _isCore: boolean|undefined = undefined;
2831

32+
2933
constructor(
3034
rootNames: ReadonlyArray<string>, private options: api.CompilerOptions,
31-
private host: api.CompilerHost, oldProgram?: api.Program) {
35+
host: api.CompilerHost, oldProgram?: api.Program) {
3236
this.resourceLoader = host.readResource !== undefined ?
3337
new HostResourceLoader(host.readResource.bind(host)) :
3438
new FileResourceLoader();
39+
const shouldGenerateFactories = options.allowEmptyCodegenFiles || false;
40+
this.host = host;
41+
let rootFiles = [...rootNames];
42+
if (shouldGenerateFactories) {
43+
const generator = new FactoryGenerator();
44+
const factoryFileMap = generator.computeFactoryFileMap(rootNames);
45+
rootFiles.push(...Array.from(factoryFileMap.keys()));
46+
this.factoryToSourceInfo = new Map<string, FactoryInfo>();
47+
this.sourceToFactorySymbols = new Map<string, Set<string>>();
48+
factoryFileMap.forEach((sourceFilePath, factoryPath) => {
49+
const moduleSymbolNames = new Set<string>();
50+
this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames);
51+
this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames});
52+
});
53+
this.host = new GeneratedFactoryHostWrapper(host, generator, factoryFileMap);
54+
}
3555

3656
this.tsProgram =
37-
ts.createProgram(rootNames, options, host, oldProgram && oldProgram.getTsProgram());
57+
ts.createProgram(rootFiles, options, this.host, oldProgram && oldProgram.getTsProgram());
3858
}
3959

4060
getTsProgram(): ts.Program { return this.tsProgram; }
@@ -125,15 +145,19 @@ export class NgtscProgram implements api.Program {
125145
this.host.writeFile(fileName, data, writeByteOrderMark, onError, sourceFiles);
126146
};
127147

128-
148+
const transforms =
149+
[ivyTransformFactory(this.compilation !, this.reflector, this.coreImportsFrom)];
150+
if (this.factoryToSourceInfo !== null) {
151+
transforms.push(generatedFactoryTransform(this.factoryToSourceInfo, this.coreImportsFrom));
152+
}
129153
// Run the emit, including a custom transformer that will downlevel the Ivy decorators in code.
130154
const emitResult = emitCallback({
131155
program: this.tsProgram,
132156
host: this.host,
133157
options: this.options,
134158
emitOnlyDtsFiles: false, writeFile,
135159
customTransformers: {
136-
before: [ivyTransformFactory(this.compilation !, this.reflector, this.coreImportsFrom)],
160+
before: transforms,
137161
},
138162
});
139163
return emitResult;
@@ -153,7 +177,8 @@ export class NgtscProgram implements api.Program {
153177
new PipeDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),
154178
];
155179

156-
return new IvyCompilation(handlers, checker, this.reflector, this.coreImportsFrom);
180+
return new IvyCompilation(
181+
handlers, checker, this.reflector, this.coreImportsFrom, this.sourceToFactorySymbols);
157182
}
158183

159184
private get reflector(): TypeScriptReflectionHost {

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

Lines changed: 1 addition & 0 deletions

0 commit comments

Comments
 (0)