diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 7378985406..00b4211e99 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -32,6 +32,7 @@ import {ComponentScopeReader, CompoundComponentScopeReader, LocalModuleScopeRegi import {FactoryGenerator, FactoryInfo, GeneratedShimsHostWrapper, ShimGenerator, SummaryGenerator, TypeCheckShimGenerator, generatedFactoryTransform} from './shims'; import {ivySwitchTransform} from './switch'; import {IvyCompilation, declarationTransformFactory, ivyTransformFactory} from './transform'; +import {DtsTransformRegistry} from './transform'; import {aliasTransformFactory} from './transform/src/alias'; import {TypeCheckContext, TypeCheckingConfig, typeCheckFilePath} from './typecheck'; import {normalizeSeparators} from './util/src/path'; @@ -68,8 +69,8 @@ export class NgtscProgram implements api.Program { private perfTracker: PerfTracker|null = null; private incrementalDriver: IncrementalDriver; private typeCheckFilePath: AbsoluteFsPath; - private modifiedResourceFiles: Set|null; + private dtsTransforms: DtsTransformRegistry|null = null; constructor( rootNames: ReadonlyArray, private options: api.CompilerOptions, @@ -369,9 +370,12 @@ export class NgtscProgram implements api.Program { aliasTransformFactory(compilation.exportStatements) as ts.TransformerFactory, this.defaultImportTracker.importPreservingTransformer(), ]; - const afterDeclarationsTransforms = [ - declarationTransformFactory(compilation), - ]; + + const afterDeclarationsTransforms: ts.TransformerFactory[] = []; + if (this.dtsTransforms !== null) { + afterDeclarationsTransforms.push( + declarationTransformFactory(this.dtsTransforms, this.importRewriter)); + } // Only add aliasing re-exports to the .d.ts output if the `AliasingHost` requests it. if (this.aliasingHost !== null && this.aliasingHost.aliasExportsInDts) { @@ -617,6 +621,8 @@ export class NgtscProgram implements api.Program { this.routeAnalyzer = new NgModuleRouteAnalyzer(this.moduleResolver, evaluator); + this.dtsTransforms = new DtsTransformRegistry(); + // Set up the IvyCompilation, which manages state for the Ivy transformer. const handlers = [ new ComponentDecoratorHandler( @@ -645,7 +651,7 @@ export class NgtscProgram implements api.Program { return new IvyCompilation( handlers, this.reflector, this.importRewriter, this.incrementalDriver, this.perfRecorder, this.sourceToFactorySymbols, scopeRegistry, - this.options.compileNonExportedClasses !== false); + this.options.compileNonExportedClasses !== false, this.dtsTransforms); } private get reflector(): TypeScriptReflectionHost { diff --git a/packages/compiler-cli/src/ngtsc/transform/index.ts b/packages/compiler-cli/src/ngtsc/transform/index.ts index e7a8a0f74e..7cbceebaf0 100644 --- a/packages/compiler-cli/src/ngtsc/transform/index.ts +++ b/packages/compiler-cli/src/ngtsc/transform/index.ts @@ -8,5 +8,5 @@ export * from './src/api'; export {IvyCompilation} from './src/compilation'; -export {DtsFileTransformer, declarationTransformFactory} from './src/declaration'; +export {declarationTransformFactory, DtsTransformRegistry, IvyDeclarationDtsTransform} from './src/declaration'; export {ivyTransformFactory} from './src/transform'; diff --git a/packages/compiler-cli/src/ngtsc/transform/src/api.ts b/packages/compiler-cli/src/ngtsc/transform/src/api.ts index 6c01f812b4..1aeb07b3a8 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/api.ts @@ -12,6 +12,7 @@ import * as ts from 'typescript'; import {Reexport} from '../../imports'; import {IndexingContext} from '../../indexer'; import {ClassDeclaration, Decorator} from '../../reflection'; +import {ImportManager} from '../../translator'; import {TypeCheckContext} from '../../typecheck'; export enum HandlerPrecedence { @@ -156,3 +157,12 @@ export interface ResolveResult { reexports?: Reexport[]; diagnostics?: ts.Diagnostic[]; } + +export interface DtsTransform { + transformClassElement?(element: ts.ClassElement, imports: ImportManager): ts.ClassElement; + transformFunctionDeclaration? + (element: ts.FunctionDeclaration, imports: ImportManager): ts.FunctionDeclaration; + transformClass? + (clazz: ts.ClassDeclaration, elements: ReadonlyArray, + imports: ImportManager): ts.ClassDeclaration; +} diff --git a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts index c7762386cb..52f26cf6d1 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/compilation.ts @@ -14,13 +14,13 @@ import {ImportRewriter} from '../../imports'; import {IncrementalDriver} from '../../incremental'; import {IndexingContext} from '../../indexer'; import {PerfRecorder} from '../../perf'; -import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration, reflectNameOfDeclaration} from '../../reflection'; +import {ClassDeclaration, ReflectionHost, isNamedClassDeclaration} from '../../reflection'; import {LocalModuleScopeRegistry} from '../../scope'; import {TypeCheckContext} from '../../typecheck'; import {getSourceFile, isExported} from '../../util/src/typescript'; import {AnalysisOutput, CompileResult, DecoratorHandler, DetectResult, HandlerPrecedence} from './api'; -import {DtsFileTransformer} from './declaration'; +import {DtsTransformRegistry} from './declaration'; const EMPTY_ARRAY: any = []; @@ -54,19 +54,9 @@ export class IvyCompilation { */ private ivyClasses = new Map(); - /** - * Tracks factory information which needs to be generated. - */ - - /** - * Tracks the `DtsFileTransformer`s for each TS file that needs .d.ts transformations. - */ - private dtsMap = new Map(); - private reexportMap = new Map>(); private _diagnostics: ts.Diagnostic[] = []; - /** * @param handlers array of `DecoratorHandler`s which will be executed against each class in the * program @@ -80,9 +70,8 @@ export class IvyCompilation { private handlers: DecoratorHandler[], private reflector: ReflectionHost, private importRewriter: ImportRewriter, private incrementalDriver: IncrementalDriver, private perf: PerfRecorder, private sourceToFactorySymbols: Map>|null, - private scopeRegistry: LocalModuleScopeRegistry, private compileNonExportedClasses: boolean) { - } - + private scopeRegistry: LocalModuleScopeRegistry, private compileNonExportedClasses: boolean, + private dtsTransforms: DtsTransformRegistry) {} get exportStatements(): Map> { return this.reexportMap; } @@ -392,9 +381,8 @@ export class IvyCompilation { // Look up the .d.ts transformer for the input file and record that at least one field was // generated, which will allow the .d.ts to be transformed later. - const fileName = original.getSourceFile().fileName; - const dtsTransformer = this.getDtsTransformer(fileName); - dtsTransformer.recordStaticField(reflectNameOfDeclaration(node) !, res); + this.dtsTransforms.getIvyDeclarationTransform(original.getSourceFile()) + .addFields(original, res); // Return the instruction to the transformer so the fields will be added. return res.length > 0 ? res : undefined; @@ -424,30 +412,5 @@ export class IvyCompilation { return decorators; } - /** - * Process a declaration file and return a transformed version that incorporates the changes - * made to the source file. - */ - transformedDtsFor(file: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile { - // No need to transform if it's not a declarations file, or if no changes have been requested - // to the input file. - // Due to the way TypeScript afterDeclarations transformers work, the SourceFile path is the - // same as the original .ts. - // The only way we know it's actually a declaration file is via the isDeclarationFile property. - if (!file.isDeclarationFile || !this.dtsMap.has(file.fileName)) { - return file; - } - - // Return the transformed source. - return this.dtsMap.get(file.fileName) !.transform(file, context); - } - get diagnostics(): ReadonlyArray { return this._diagnostics; } - - private getDtsTransformer(tsFileName: string): DtsFileTransformer { - if (!this.dtsMap.has(tsFileName)) { - this.dtsMap.set(tsFileName, new DtsFileTransformer(this.importRewriter)); - } - return this.dtsMap.get(tsFileName) !; - } } diff --git a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts index 0873dea3e2..d33f094b6d 100644 --- a/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts +++ b/packages/compiler-cli/src/ngtsc/transform/src/declaration.ts @@ -6,26 +6,67 @@ * found in the LICENSE file at https://angular.io/license */ +import {Type} from '@angular/compiler'; import * as ts from 'typescript'; import {ImportRewriter} from '../../imports'; +import {ClassDeclaration} from '../../reflection'; import {ImportManager, translateType} from '../../translator'; -import {CompileResult} from './api'; -import {IvyCompilation} from './compilation'; +import {DtsTransform} from './api'; import {addImports} from './utils'; +/** + * Keeps track of `DtsTransform`s per source file, so that it is known which source files need to + * have their declaration file transformed. + */ +export class DtsTransformRegistry { + private ivyDeclarationTransforms = new Map(); + getIvyDeclarationTransform(sf: ts.SourceFile): IvyDeclarationDtsTransform { + if (!this.ivyDeclarationTransforms.has(sf.fileName)) { + this.ivyDeclarationTransforms.set(sf.fileName, new IvyDeclarationDtsTransform()); + } + return this.ivyDeclarationTransforms.get(sf.fileName) !; + } -export function declarationTransformFactory(compilation: IvyCompilation): - ts.TransformerFactory { + /** + * Gets the dts transforms to be applied for the given source file, or `null` if no transform is + * necessary. + */ + getAllTransforms(sf: ts.SourceFile): DtsTransform[]|null { + // No need to transform if it's not a declarations file, or if no changes have been requested + // to the input file. Due to the way TypeScript afterDeclarations transformers work, the + // `ts.SourceFile` path is the same as the original .ts. The only way we know it's actually a + // declaration file is via the `isDeclarationFile` property. + if (!sf.isDeclarationFile) { + return null; + } + + let transforms: DtsTransform[]|null = null; + if (this.ivyDeclarationTransforms.has(sf.fileName)) { + transforms = []; + transforms.push(this.ivyDeclarationTransforms.get(sf.fileName) !); + } + return transforms; + } +} + +export function declarationTransformFactory( + transformRegistry: DtsTransformRegistry, importRewriter: ImportRewriter, + importPrefix?: string): ts.TransformerFactory { return (context: ts.TransformationContext) => { + const transformer = new DtsTransformer(context, importRewriter, importPrefix); return (fileOrBundle) => { if (ts.isBundle(fileOrBundle)) { // Only attempt to transform source files. return fileOrBundle; } - return compilation.transformedDtsFor(fileOrBundle, context); + const transforms = transformRegistry.getAllTransforms(fileOrBundle); + if (transforms === null) { + return fileOrBundle; + } + return transformer.transform(fileOrBundle, transforms); }; }; } @@ -33,47 +74,140 @@ export function declarationTransformFactory(compilation: IvyCompilation): /** * Processes .d.ts file text and adds static field declarations, with types. */ -export class DtsFileTransformer { - private ivyFields = new Map(); - private imports: ImportManager; - - constructor(private importRewriter: ImportRewriter, importPrefix?: string) { - this.imports = new ImportManager(importRewriter, importPrefix); - } - - /** - * Track that a static field was added to the code for a class. - */ - recordStaticField(name: string, decls: CompileResult[]): void { this.ivyFields.set(name, decls); } +class DtsTransformer { + constructor( + private ctx: ts.TransformationContext, private importRewriter: ImportRewriter, + private importPrefix?: string) {} /** * Transform the declaration file and add any declarations which were recorded. */ - transform(file: ts.SourceFile, context: ts.TransformationContext): ts.SourceFile { + transform(sf: ts.SourceFile, transforms: DtsTransform[]): ts.SourceFile { + const imports = new ImportManager(this.importRewriter, this.importPrefix); + const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult => { - // This class declaration needs to have fields added to it. - if (ts.isClassDeclaration(node) && node.name !== undefined && - this.ivyFields.has(node.name.text)) { - const decls = this.ivyFields.get(node.name.text) !; - const newMembers = decls.map(decl => { - const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)]; - const typeRef = translateType(decl.type, this.imports); - return ts.createProperty(undefined, modifiers, decl.name, undefined, typeRef, undefined); - }); - - return ts.updateClassDeclaration( - node, node.decorators, node.modifiers, node.name, node.typeParameters, - node.heritageClauses, [...node.members, ...newMembers]); + if (ts.isClassDeclaration(node)) { + return this.transformClassDeclaration(node, transforms, imports); + } else if (ts.isFunctionDeclaration(node)) { + return this.transformFunctionDeclaration(node, transforms, imports); + } else { + // Otherwise return node as is. + return ts.visitEachChild(node, visitor, this.ctx); } - - // Otherwise return node as is. - return ts.visitEachChild(node, visitor, context); }; - // Recursively scan through the AST and add all class members needed. - const sf = ts.visitNode(file, visitor); + // Recursively scan through the AST and process all nodes as desired. + sf = ts.visitNode(sf, visitor); // Add new imports for this file. - return addImports(this.imports, sf); + return addImports(imports, sf); + } + + private transformClassDeclaration( + clazz: ts.ClassDeclaration, transforms: DtsTransform[], + imports: ImportManager): ts.ClassDeclaration { + let elements: ts.ClassElement[]|ReadonlyArray = clazz.members; + let elementsChanged = false; + + for (const transform of transforms) { + if (transform.transformClassElement !== undefined) { + for (let i = 0; i < elements.length; i++) { + const res = transform.transformClassElement(elements[i], imports); + if (res !== elements[i]) { + if (!elementsChanged) { + elements = [...elements]; + elementsChanged = true; + } + (elements as ts.ClassElement[])[i] = res; + } + } + } + } + + let newClazz: ts.ClassDeclaration = clazz; + + for (const transform of transforms) { + if (transform.transformClass !== undefined) { + // If no DtsTransform has changed the class yet, then the (possibly mutated) elements have + // not yet been incorporated. Otherwise, `newClazz.members` holds the latest class members. + const inputMembers = (clazz === newClazz ? elements : newClazz.members); + + newClazz = transform.transformClass(newClazz, inputMembers, imports); + } + } + + // If some elements have been transformed but the class itself has not been transformed, create + // an updated class declaration with the updated elements. + if (elementsChanged && clazz === newClazz) { + newClazz = ts.updateClassDeclaration( + /* node */ clazz, + /* decorators */ clazz.decorators, + /* modifiers */ clazz.modifiers, + /* name */ clazz.name, + /* typeParameters */ clazz.typeParameters, + /* heritageClauses */ clazz.heritageClauses, + /* members */ elements); + } + + return newClazz; + } + + private transformFunctionDeclaration( + declaration: ts.FunctionDeclaration, transforms: DtsTransform[], + imports: ImportManager): ts.FunctionDeclaration { + let newDecl = declaration; + + for (const transform of transforms) { + if (transform.transformFunctionDeclaration !== undefined) { + newDecl = transform.transformFunctionDeclaration(newDecl, imports); + } + } + + return newDecl; + } +} + +export interface IvyDeclarationField { + name: string; + type: Type; +} + +export class IvyDeclarationDtsTransform implements DtsTransform { + private declarationFields = new Map(); + + addFields(decl: ClassDeclaration, fields: IvyDeclarationField[]): void { + this.declarationFields.set(decl, fields); + } + + transformClass( + clazz: ts.ClassDeclaration, members: ReadonlyArray, + imports: ImportManager): ts.ClassDeclaration { + const original = ts.getOriginalNode(clazz) as ClassDeclaration; + + if (!this.declarationFields.has(original)) { + return clazz; + } + const fields = this.declarationFields.get(original) !; + + const newMembers = fields.map(decl => { + const modifiers = [ts.createModifier(ts.SyntaxKind.StaticKeyword)]; + const typeRef = translateType(decl.type, imports); + return ts.createProperty( + /* decorators */ undefined, + /* modifiers */ modifiers, + /* name */ decl.name, + /* questionOrExclamationToken */ undefined, + /* type */ typeRef, + /* initializer */ undefined); + }); + + return ts.updateClassDeclaration( + /* node */ clazz, + /* decorators */ clazz.decorators, + /* modifiers */ clazz.modifiers, + /* name */ clazz.name, + /* typeParameters */ clazz.typeParameters, + /* heritageClauses */ clazz.heritageClauses, + /* members */[...members, ...newMembers]); } }