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:
Alex Rickabaugh 2018-10-16 14:47:08 -07:00 committed by Alex Rickabaugh
parent 0b885ecaf7
commit ce8053103e
7 changed files with 202 additions and 161 deletions

View File

@ -51,13 +51,17 @@ export class NgtscProgram implements api.Program {
this.resourceLoader = host.readResource !== undefined ? this.resourceLoader = host.readResource !== undefined ?
new HostResourceLoader(host.readResource.bind(host)) : new HostResourceLoader(host.readResource.bind(host)) :
new FileResourceLoader(); new FileResourceLoader();
const shouldGenerateFactories = options.allowEmptyCodegenFiles || false; const shouldGenerateShims = options.allowEmptyCodegenFiles || false;
this.host = host; this.host = host;
let rootFiles = [...rootNames]; let rootFiles = [...rootNames];
if (shouldGenerateFactories) { if (shouldGenerateShims) {
const generator = new FactoryGenerator(); // Summary generation.
const factoryFileMap = generator.computeFactoryFileMap(rootNames);
rootFiles.push(...Array.from(factoryFileMap.keys())); // 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.factoryToSourceInfo = new Map<string, FactoryInfo>();
this.sourceToFactorySymbols = new Map<string, Set<string>>(); this.sourceToFactorySymbols = new Map<string, Set<string>>();
factoryFileMap.forEach((sourceFilePath, factoryPath) => { factoryFileMap.forEach((sourceFilePath, factoryPath) => {
@ -65,7 +69,7 @@ export class NgtscProgram implements api.Program {
this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames); this.sourceToFactorySymbols !.set(sourceFilePath, moduleSymbolNames);
this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames}); this.factoryToSourceInfo !.set(factoryPath, {sourceFilePath, moduleSymbolNames});
}); });
this.host = new GeneratedShimsHostWrapper(host, generator, factoryFileMap); this.host = new GeneratedShimsHostWrapper(host, [factoryGenerator]);
} }
this.tsProgram = this.tsProgram =

View File

@ -8,6 +8,5 @@
/// <reference types="node" /> /// <reference types="node" />
export {FactoryGenerator} from './src/generator'; export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator';
export {GeneratedShimsHostWrapper} from './src/host'; export {GeneratedShimsHostWrapper} from './src/host';
export {FactoryInfo, generatedFactoryTransform} from './src/transform';

View File

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

View File

@ -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);
}

View File

@ -9,15 +9,26 @@
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; 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. * A wrapper around a `ts.CompilerHost` which supports generated files.
*/ */
export class GeneratedShimsHostWrapper implements ts.CompilerHost { export class GeneratedShimsHostWrapper implements ts.CompilerHost {
constructor( constructor(private delegate: ts.CompilerHost, private shimGenerators: ShimGenerator[]) {
private delegate: ts.CompilerHost, private generator: FactoryGenerator,
private factoryToSourceMap: Map<string, string>) {
if (delegate.resolveTypeReferenceDirectives) { if (delegate.resolveTypeReferenceDirectives) {
// Backward compatibility with TypeScript 2.9 and older since return // Backward compatibility with TypeScript 2.9 and older since return
// type has changed from (ts.ResolvedTypeReferenceDirective | undefined)[] // type has changed from (ts.ResolvedTypeReferenceDirective | undefined)[]
@ -38,14 +49,20 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost {
onError?: ((message: string) => void)|undefined, onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined { shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
const canonical = this.getCanonicalFileName(fileName); const canonical = this.getCanonicalFileName(fileName);
if (this.factoryToSourceMap.has(canonical)) { for (let i = 0; i < this.shimGenerators.length; i++) {
const sourceFileName = this.getCanonicalFileName(this.factoryToSourceMap.get(canonical) !); const generator = this.shimGenerators[i];
const sourceFile = this.delegate.getSourceFile( const originalFile = generator.getOriginalSourceOfShim(canonical);
sourceFileName, languageVersion, onError, shouldCreateNewSourceFile); if (originalFile !== null) {
if (sourceFile === undefined) { // This shim generator has recognized the filename being requested, and is now responsible
return undefined; // 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( return this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile); fileName, languageVersion, onError, shouldCreateNewSourceFile);
@ -75,7 +92,11 @@ export class GeneratedShimsHostWrapper implements ts.CompilerHost {
getNewLine(): string { return this.delegate.getNewLine(); } getNewLine(): string { return this.delegate.getNewLine(); }
fileExists(fileName: string): boolean { 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); } readFile(fileName: string): string|undefined { return this.delegate.readFile(fileName); }

View File

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

View File

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