From ce8053103efa7498318a8c8c15216ea35a08a20f Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Tue, 16 Oct 2018 14:47:08 -0700 Subject: [PATCH] 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 --- packages/compiler-cli/src/ngtsc/program.ts | 16 +- .../compiler-cli/src/ngtsc/shims/index.ts | 3 +- .../src/ngtsc/shims/src/factory_generator.ts | 144 ++++++++++++++++++ .../src/ngtsc/shims/src/generator.ts | 63 -------- .../compiler-cli/src/ngtsc/shims/src/host.ts | 45 ++++-- .../src/ngtsc/shims/src/transform.ts | 78 ---------- .../compiler-cli/src/ngtsc/shims/src/util.ts | 14 ++ 7 files changed, 202 insertions(+), 161 deletions(-) create mode 100644 packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts delete mode 100644 packages/compiler-cli/src/ngtsc/shims/src/generator.ts delete mode 100644 packages/compiler-cli/src/ngtsc/shims/src/transform.ts create mode 100644 packages/compiler-cli/src/ngtsc/shims/src/util.ts diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 2acd5e8eb4..12cc5684b5 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -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(); this.sourceToFactorySymbols = new Map>(); 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 = diff --git a/packages/compiler-cli/src/ngtsc/shims/index.ts b/packages/compiler-cli/src/ngtsc/shims/index.ts index 11875063a4..5323905db3 100644 --- a/packages/compiler-cli/src/ngtsc/shims/index.ts +++ b/packages/compiler-cli/src/ngtsc/shims/index.ts @@ -8,6 +8,5 @@ /// -export {FactoryGenerator} from './src/generator'; +export {FactoryGenerator, FactoryInfo, generatedFactoryTransform} from './src/factory_generator'; export {GeneratedShimsHostWrapper} from './src/host'; -export {FactoryInfo, generatedFactoryTransform} from './src/transform'; diff --git a/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts new file mode 100644 index 0000000000..de102329b1 --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts @@ -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) {} + + get factoryFileMap(): Map { 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): FactoryGenerator { + const map = new Map(); + 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; +} + +export function generatedFactoryTransform( + factoryMap: Map, + coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory { + return (context: ts.TransformationContext): ts.Transformer => { + return (file: ts.SourceFile): ts.SourceFile => { + return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file); + }; + }; +} + +function transformFactorySourceFile( + factoryMap: Map, 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; +} diff --git a/packages/compiler-cli/src/ngtsc/shims/src/generator.ts b/packages/compiler-cli/src/ngtsc/shims/src/generator.ts deleted file mode 100644 index ec1a8a3cb3..0000000000 --- a/packages/compiler-cli/src/ngtsc/shims/src/generator.ts +++ /dev/null @@ -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): Map { - const map = new Map(); - 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); -} diff --git a/packages/compiler-cli/src/ngtsc/shims/src/host.ts b/packages/compiler-cli/src/ngtsc/shims/src/host.ts index 3a41022d2b..58a9d551b3 100644 --- a/packages/compiler-cli/src/ngtsc/shims/src/host.ts +++ b/packages/compiler-cli/src/ngtsc/shims/src/host.ts @@ -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) { + 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); } diff --git a/packages/compiler-cli/src/ngtsc/shims/src/transform.ts b/packages/compiler-cli/src/ngtsc/shims/src/transform.ts deleted file mode 100644 index acd0d9b747..0000000000 --- a/packages/compiler-cli/src/ngtsc/shims/src/transform.ts +++ /dev/null @@ -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; -} - -export function generatedFactoryTransform( - factoryMap: Map, - coreImportsFrom: ts.SourceFile | null): ts.TransformerFactory { - return (context: ts.TransformationContext): ts.Transformer => { - return (file: ts.SourceFile): ts.SourceFile => { - return transformFactorySourceFile(factoryMap, context, coreImportsFrom, file); - }; - }; -} - -function transformFactorySourceFile( - factoryMap: Map, 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; -} diff --git a/packages/compiler-cli/src/ngtsc/shims/src/util.ts b/packages/compiler-cli/src/ngtsc/shims/src/util.ts new file mode 100644 index 0000000000..e7285663db --- /dev/null +++ b/packages/compiler-cli/src/ngtsc/shims/src/util.ts @@ -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; +}