refactor(ivy): make shim generation generic in ngtsc (#26495)
This commit refactors the shim host to be agnostic to the shims being generated, and provides an API for generating additional shims besides the .ngfactory.js. This will be used in a following commit to generate .ngsummary.js shims. Testing strategy: this refactor introduces no new behavior, so it's sufficient that the existing tests for factory shim generation continue to pass. PR Close #26495
This commit is contained in:
parent
0b885ecaf7
commit
ce8053103e
|
@ -51,13 +51,17 @@ export class NgtscProgram implements api.Program {
|
|||
this.resourceLoader = host.readResource !== undefined ?
|
||||
new HostResourceLoader(host.readResource.bind(host)) :
|
||||
new FileResourceLoader();
|
||||
const shouldGenerateFactories = options.allowEmptyCodegenFiles || false;
|
||||
const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
|
||||
this.host = host;
|
||||
let rootFiles = [...rootNames];
|
||||
if (shouldGenerateFactories) {
|
||||
const generator = new FactoryGenerator();
|
||||
const factoryFileMap = generator.computeFactoryFileMap(rootNames);
|
||||
rootFiles.push(...Array.from(factoryFileMap.keys()));
|
||||
if (shouldGenerateShims) {
|
||||
// Summary generation.
|
||||
|
||||
// Factory generation.
|
||||
const factoryGenerator = FactoryGenerator.forRootFiles(rootNames);
|
||||
const factoryFileMap = factoryGenerator.factoryFileMap;
|
||||
const factoryFileNames = Array.from(factoryFileMap.keys());
|
||||
rootFiles.push(...factoryFileNames);
|
||||
this.factoryToSourceInfo = new Map<string, FactoryInfo>();
|
||||
this.sourceToFactorySymbols = new Map<string, Set<string>>();
|
||||
factoryFileMap.forEach((sourceFilePath, factoryPath) => {
|
||||
|
@ -65,7 +69,7 @@ export class NgtscProgram implements api.Program {
|
|||
this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames);
|
||||
this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames});
|
||||
});
|
||||
this.host = new GeneratedShimsHostWrapper(host, generator, factoryFileMap);
|
||||
this.host = new GeneratedShimsHostWrapper(host, [factoryGenerator]);
|
||||
}
|
||||
|
||||
this.tsProgram =
|
||||
|
|
|
@ -8,6 +8,5 @@
|
|||
|
||||
/// <reference types="node" />
|
||||
|
||||
export {FactoryGenerator} from './src/generator';
|
||||
export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
|
||||
export {GeneratedShimsHostWrapper} from './src/host';
|
||||
export {FactoryInfo, generatedFactoryTransform} from './src/transform';
|
||||
|
|
|
@ -0,0 +1,144 @@
|
|||
/**
|
||||
* @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';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {relativePathBetween} from '../../util/src/path';
|
||||
|
||||
import {ShimGenerator} from './host';
|
||||
import {isNonDeclarationTsFile} from './util';
|
||||
|
||||
const TS_DTS_SUFFIX = /(\.d)?\.ts$/;
|
||||
const STRIP_NG_FACTORY = /(.*)NgFactory$/;
|
||||
|
||||
/**
|
||||
* Generates ts.SourceFiles which contain variable declarations for NgFactories for every exported
|
||||
* class of an input ts.SourceFile.
|
||||
*/
|
||||
export class FactoryGenerator implements ShimGenerator {
|
||||
private constructor(private map: Map<string, string>) {}
|
||||
|
||||
get factoryFileMap(): Map<string, string> { return this.map; }
|
||||
|
||||
getOriginalSourceOfShim(fileName: string): string|null { return this.map.get(fileName) || null; }
|
||||
|
||||
generate(original: ts.SourceFile, genFilePath: string): ts.SourceFile {
|
||||
const relativePathToSource =
|
||||
'./' + path.posix.basename(original.fileName).replace(TS_DTS_SUFFIX, '');
|
||||
// Collect a list of classes that need to have factory types emitted for them. This list is
|
||||
// overly broad as at this point the ts.TypeChecker hasn't been created, and can't be used to
|
||||
// semantically understand which decorated types are actually decorated with Angular decorators.
|
||||
//
|
||||
// The exports generated here are pruned in the factory transform during emit.
|
||||
const symbolNames = original
|
||||
.statements
|
||||
// Pick out top level class declarations...
|
||||
.filter(ts.isClassDeclaration)
|
||||
// which are named, exported, and have decorators.
|
||||
.filter(
|
||||
decl => isExported(decl) && decl.decorators !== undefined &&
|
||||
decl.name !== undefined)
|
||||
// Grab the symbol name.
|
||||
.map(decl => decl.name !.text);
|
||||
|
||||
// For each symbol name, generate a constant export of the corresponding NgFactory.
|
||||
// This will encompass a lot of symbols which don't need factories, but that's okay
|
||||
// because it won't miss any that do.
|
||||
const varLines = symbolNames.map(
|
||||
name => `export const ${name}NgFactory = new i0.ɵNgModuleFactory(${name});`);
|
||||
const sourceText = [
|
||||
// This might be incorrect if the current package being compiled is Angular core, but it's
|
||||
// okay to leave in at type checking time. TypeScript can handle this reference via its path
|
||||
// mapping, but downstream bundlers can't. If the current package is core itself, this will be
|
||||
// replaced in the factory transformer before emit.
|
||||
`import * as i0 from '@angular/core';`,
|
||||
`import {${symbolNames.join(', ')}} from '${relativePathToSource}';`,
|
||||
...varLines,
|
||||
].join('\n');
|
||||
return ts.createSourceFile(
|
||||
genFilePath, sourceText, original.languageVersion, true, ts.ScriptKind.TS);
|
||||
}
|
||||
|
||||
static forRootFiles(files: ReadonlyArray<string>): FactoryGenerator {
|
||||
const map = new Map<string, string>();
|
||||
files.filter(sourceFile => isNonDeclarationTsFile(sourceFile))
|
||||
.forEach(sourceFile => map.set(sourceFile.replace(/\.ts$/, '.ngfactory.ts'), sourceFile));
|
||||
return new FactoryGenerator(map);
|
||||
}
|
||||
}
|
||||
|
||||
function isExported(decl: ts.Declaration): boolean {
|
||||
return decl.modifiers !== undefined &&
|
||||
decl.modifiers.some(mod => mod.kind == ts.SyntaxKind.ExportKeyword);
|
||||
}
|
||||
|
||||
export interface FactoryInfo {
|
||||
sourceFilePath: string;
|
||||
moduleSymbolNames: Set<string>;
|
||||
}
|
||||
|
||||
export function generatedFactoryTransform(
|
||||
factoryMap: Map<string, FactoryInfo>,
|
||||
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
|
||||
return (file: ts.SourceFile): ts.SourceFile => {
|
||||
return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function transformFactorySourceFile(
|
||||
factoryMap: Map<string, FactoryInfo>, context: ts.TransformationContext,
|
||||
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
|
||||
// If this is not a generated file, it won't have factory info associated with it.
|
||||
if (!factoryMap.has(file.fileName)) {
|
||||
// Don't transform non-generated code.
|
||||
return file;
|
||||
}
|
||||
|
||||
const {moduleSymbolNames, sourceFilePath} = factoryMap.get(file.fileName) !;
|
||||
|
||||
const clone = ts.getMutableClone(file);
|
||||
|
||||
const transformedStatements = file.statements.map(stmt => {
|
||||
if (coreImportsFrom !== null && ts.isImportDeclaration(stmt) &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === '@angular/core') {
|
||||
const path = relativePathBetween(sourceFilePath, coreImportsFrom.fileName);
|
||||
if (path !== null) {
|
||||
return ts.updateImportDeclaration(
|
||||
stmt, stmt.decorators, stmt.modifiers, stmt.importClause, ts.createStringLiteral(path));
|
||||
} else {
|
||||
return ts.createNotEmittedStatement(stmt);
|
||||
}
|
||||
} else if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) {
|
||||
const decl = stmt.declarationList.declarations[0];
|
||||
if (ts.isIdentifier(decl.name)) {
|
||||
const match = STRIP_NG_FACTORY.exec(decl.name.text);
|
||||
if (match === null || !moduleSymbolNames.has(match[1])) {
|
||||
// Remove the given factory as it wasn't actually for an NgModule.
|
||||
return ts.createNotEmittedStatement(stmt);
|
||||
}
|
||||
}
|
||||
return stmt;
|
||||
} else {
|
||||
return stmt;
|
||||
}
|
||||
});
|
||||
if (!transformedStatements.some(ts.isVariableStatement)) {
|
||||
// If the resulting file has no factories, include an empty export to
|
||||
// satisfy closure compiler.
|
||||
transformedStatements.push(ts.createVariableStatement(
|
||||
[ts.createModifier(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration('ɵNonEmptyModule', undefined, ts.createTrue())],
|
||||
ts.NodeFlags.Const)));
|
||||
}
|
||||
clone.statements = ts.createNodeArray(transformedStatements);
|
||||
return clone;
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
/**
|
||||
* @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';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
const TS_DTS_SUFFIX = /(\.d)?\.ts$/;
|
||||
|
||||
/**
|
||||
* Generates ts.SourceFiles which contain variable declarations for NgFactories for every exported
|
||||
* class of an input ts.SourceFile.
|
||||
*/
|
||||
export class FactoryGenerator {
|
||||
factoryFor(original: ts.SourceFile, genFilePath: string): ts.SourceFile {
|
||||
const relativePathToSource =
|
||||
'./' + path.posix.basename(original.fileName).replace(TS_DTS_SUFFIX, '');
|
||||
// Collect a list of classes that need to have factory types emitted for them.
|
||||
const symbolNames = original
|
||||
.statements
|
||||
// Pick out top level class declarations...
|
||||
.filter(ts.isClassDeclaration)
|
||||
// which are named, exported, and have decorators.
|
||||
.filter(
|
||||
decl => isExported(decl) && decl.decorators !== undefined &&
|
||||
decl.name !== undefined)
|
||||
// Grab the symbol name.
|
||||
.map(decl => decl.name !.text);
|
||||
|
||||
// For each symbol name, generate a constant export of the corresponding NgFactory.
|
||||
// This will encompass a lot of symbols which don't need factories, but that's okay
|
||||
// because it won't miss any that do.
|
||||
const varLines = symbolNames.map(
|
||||
name => `export const ${name}NgFactory = new i0.ɵNgModuleFactory(${name});`);
|
||||
const sourceText = [
|
||||
// This might be incorrect if the current package being compiled is Angular core, but it's
|
||||
// okay to leave in at type checking time. TypeScript can handle this reference via its path
|
||||
// mapping, but downstream bundlers can't. If the current package is core itself, this will be
|
||||
// replaced in the factory transformer before emit.
|
||||
`import * as i0 from '@angular/core';`,
|
||||
`import {${symbolNames.join(', ')}} from '${relativePathToSource}';`,
|
||||
...varLines,
|
||||
].join('\n');
|
||||
return ts.createSourceFile(
|
||||
genFilePath, sourceText, original.languageVersion, true, ts.ScriptKind.TS);
|
||||
}
|
||||
|
||||
computeFactoryFileMap(files: ReadonlyArray<string>): Map<string, string> {
|
||||
const map = new Map<string, string>();
|
||||
files.filter(sourceFile => !sourceFile.endsWith('.d.ts'))
|
||||
.forEach(sourceFile => map.set(sourceFile.replace(/\.ts$/, '.ngfactory.ts'), sourceFile));
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
function isExported(decl: ts.Declaration): boolean {
|
||||
return decl.modifiers !== undefined &&
|
||||
decl.modifiers.some(mod => mod.kind == ts.SyntaxKind.ExportKeyword);
|
||||
}
|
|
@ -9,15 +9,26 @@
|
|||
import * as path from 'path';
|
||||
import * as ts from 'typescript';
|
||||
|
||||
import {FactoryGenerator} from './generator';
|
||||
export interface ShimGenerator {
|
||||
/**
|
||||
* Get the original source file for the given shim path, the contents of which determine the
|
||||
* contents of the shim file.
|
||||
*
|
||||
* If this returns `null` then the given file was not a shim file handled by this generator.
|
||||
*/
|
||||
getOriginalSourceOfShim(fileName: string): string|null;
|
||||
|
||||
/**
|
||||
* Generate a shim's `ts.SourceFile` for the given original file.
|
||||
*/
|
||||
generate(original: ts.SourceFile, genFileName: string): ts.SourceFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* A wrapper around a `ts.CompilerHost` which supports generated files.
|
||||
*/
|
||||
export class GeneratedShimsHostWrapper implements ts.CompilerHost {
|
||||
constructor(
|
||||
private delegate: ts.CompilerHost, private generator: FactoryGenerator,
|
||||
private factoryToSourceMap: Map<string, string>) {
|
||||
constructor(private delegate: ts.CompilerHost, private shimGenerators: ShimGenerator[]) {
|
||||
if (delegate.resolveTypeReferenceDirectives) {
|
||||
// Backward compatibility with TypeScript 2.9 and older since return
|
||||
// type has changed from (ts.ResolvedTypeReferenceDirective | undefined)[]
|
||||
|
@ -38,14 +49,20 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost {
|
|||
onError?: ((message: string) => void)|undefined,
|
||||
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
|
||||
const canonical = this.getCanonicalFileName(fileName);
|
||||
if (this.factoryToSourceMap.has(canonical)) {
|
||||
const sourceFileName = this.getCanonicalFileName(this.factoryToSourceMap.get(canonical) !);
|
||||
const sourceFile = this.delegate.getSourceFile(
|
||||
sourceFileName, languageVersion, onError, shouldCreateNewSourceFile);
|
||||
if (sourceFile === undefined) {
|
||||
return undefined;
|
||||
for (let i = 0; i < this.shimGenerators.length; i++) {
|
||||
const generator = this.shimGenerators[i];
|
||||
const originalFile = generator.getOriginalSourceOfShim(canonical);
|
||||
if (originalFile !== null) {
|
||||
// This shim generator has recognized the filename being requested, and is now responsible
|
||||
// for generating its contents, based on the contents of the original file it has requested.
|
||||
const originalSource = this.delegate.getSourceFile(
|
||||
originalFile, languageVersion, onError, shouldCreateNewSourceFile);
|
||||
if (originalSource === undefined) {
|
||||
// The original requested file doesn't exist, so the shim cannot exist either.
|
||||
return undefined;
|
||||
}
|
||||
return generator.generate(originalSource, fileName);
|
||||
}
|
||||
return this.generator.factoryFor(sourceFile, fileName);
|
||||
}
|
||||
return this.delegate.getSourceFile(
|
||||
fileName, languageVersion, onError, shouldCreateNewSourceFile);
|
||||
|
@ -75,7 +92,11 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost {
|
|||
getNewLine(): string { return this.delegate.getNewLine(); }
|
||||
|
||||
fileExists(fileName: string): boolean {
|
||||
return this.factoryToSourceMap.has(fileName) || this.delegate.fileExists(fileName);
|
||||
const canonical = this.getCanonicalFileName(fileName);
|
||||
// Consider the file as existing whenever 1) it really does exist in the delegate host, or
|
||||
// 2) at least one of the shim generators recognizes it.
|
||||
return this.delegate.fileExists(fileName) ||
|
||||
this.shimGenerators.some(gen => gen.getOriginalSourceOfShim(canonical) !== null);
|
||||
}
|
||||
|
||||
readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
/**
|
||||
* @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 ts from 'typescript';
|
||||
|
||||
import {relativePathBetween} from '../../util/src/path';
|
||||
|
||||
const STRIP_NG_FACTORY = /(.*)NgFactory$/;
|
||||
|
||||
export interface FactoryInfo {
|
||||
sourceFilePath: string;
|
||||
moduleSymbolNames: Set<string>;
|
||||
}
|
||||
|
||||
export function generatedFactoryTransform(
|
||||
factoryMap: Map<string, FactoryInfo>,
|
||||
coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory<ts.SourceFile> {
|
||||
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
|
||||
return (file: ts.SourceFile): ts.SourceFile => {
|
||||
return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file);
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function transformFactorySourceFile(
|
||||
factoryMap: Map<string, FactoryInfo>, context: ts.TransformationContext,
|
||||
coreImportsFrom: ts.SourceFile | null, file: ts.SourceFile): ts.SourceFile {
|
||||
// If this is not a generated file, it won't have factory info associated with it.
|
||||
if (!factoryMap.has(file.fileName)) {
|
||||
// Don't transform non-generated code.
|
||||
return file;
|
||||
}
|
||||
|
||||
const {moduleSymbolNames, sourceFilePath} = factoryMap.get(file.fileName) !;
|
||||
|
||||
const clone = ts.getMutableClone(file);
|
||||
|
||||
const transformedStatements = file.statements.map(stmt => {
|
||||
if (coreImportsFrom !== null && ts.isImportDeclaration(stmt) &&
|
||||
ts.isStringLiteral(stmt.moduleSpecifier) && stmt.moduleSpecifier.text === '@angular/core') {
|
||||
const path = relativePathBetween(sourceFilePath, coreImportsFrom.fileName);
|
||||
if (path !== null) {
|
||||
return ts.updateImportDeclaration(
|
||||
stmt, stmt.decorators, stmt.modifiers, stmt.importClause, ts.createStringLiteral(path));
|
||||
} else {
|
||||
return ts.createNotEmittedStatement(stmt);
|
||||
}
|
||||
} else if (ts.isVariableStatement(stmt) && stmt.declarationList.declarations.length === 1) {
|
||||
const decl = stmt.declarationList.declarations[0];
|
||||
if (ts.isIdentifier(decl.name)) {
|
||||
const match = STRIP_NG_FACTORY.exec(decl.name.text);
|
||||
if (match === null || !moduleSymbolNames.has(match[1])) {
|
||||
// Remove the given factory as it wasn't actually for an NgModule.
|
||||
return ts.createNotEmittedStatement(stmt);
|
||||
}
|
||||
}
|
||||
return stmt;
|
||||
} else {
|
||||
return stmt;
|
||||
}
|
||||
});
|
||||
if (!transformedStatements.some(ts.isVariableStatement)) {
|
||||
// If the resulting file has no factories, include an empty export to
|
||||
// satisfy closure compiler.
|
||||
transformedStatements.push(ts.createVariableStatement(
|
||||
[ts.createModifier(ts.SyntaxKind.ExportKeyword)],
|
||||
ts.createVariableDeclarationList(
|
||||
[ts.createVariableDeclaration('ɵNonEmptyModule', undefined, ts.createTrue())],
|
||||
ts.NodeFlags.Const)));
|
||||
}
|
||||
clone.statements = ts.createNodeArray(transformedStatements);
|
||||
return clone;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
const TS_FILE = /\.tsx?$/;
|
||||
const D_TS_FILE = /\.d\.ts$/;
|
||||
|
||||
export function isNonDeclarationTsFile(file: string): boolean {
|
||||
return TS_FILE.exec(file) !== null && D_TS_FILE.exec(file) === null;
|
||||
}
|
Loading…
Reference in New Issue