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'; | ||||
|  | ||||
							
								
								
									
										144
									
								
								packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								packages/compiler-cli/src/ngtsc/shims/src/factory_generator.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
| @ -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; | ||||
| } | ||||
							
								
								
									
										14
									
								
								packages/compiler-cli/src/ngtsc/shims/src/util.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								packages/compiler-cli/src/ngtsc/shims/src/util.ts
									
									
									
									
									
										Normal 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; | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user