From 1918f8d5b5f64d1a95e8e3322299bb91df3c04de Mon Sep 17 00:00:00 2001 From: Pete Bacon Darwin Date: Mon, 15 Oct 2018 11:48:03 +0100 Subject: [PATCH] feat(ivy): support separate .js and .d.ts trees when generating imports (#26403) The `NgModule` handler generates `R3References` for its declarations, imports, exports, and bootstrap components, based on the relative import path between the module and the classes it's referring to. This works fine for compilation of a .ts Program inside ngtsc, but in ngcc the import needed in the .d.ts file may be very different to the import needed between .js files (for example, if the .js files are flattened and the .d.ts is not). This commit introduces a new API in the `ReflectionHost` for extracting the .d.ts version of a declaration, and makes use of it in the `NgModuleDecorationHandler` to write a correct expression for the `NgModule` definition type. PR Close #26403 --- .../src/ngcc/src/host/fesm2015_host.ts | 6 ++-- .../src/ngcc/src/host/ngcc_host.ts | 8 ++--- .../ngcc/src/rendering/fesm2015_renderer.ts | 4 +-- .../src/ngtsc/annotations/src/ng_module.ts | 36 +++++++++++++++---- .../ngtsc/annotations/src/selector_scope.ts | 16 +++++++-- .../src/ngtsc/annotations/src/util.ts | 10 +++--- .../src/ngtsc/host/src/reflection.ts | 14 ++++++++ .../src/ngtsc/metadata/src/reflector.ts | 4 ++- 8 files changed, 74 insertions(+), 24 deletions(-) diff --git a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts index 87ea6c8f4d..1aa5cf196f 100644 --- a/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/fesm2015_host.ts @@ -15,7 +15,7 @@ import {findAll, getNameText, getOriginalSymbol, isDefined} from '../utils'; import {DecoratedClass} from './decorated_class'; import {DecoratedFile} from './decorated_file'; -import {NgccReflectionHost, PRE_NGCC_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; +import {NgccReflectionHost, PRE_R3_MARKER, SwitchableVariableDeclaration, isSwitchableVariableDeclaration} from './ngcc_host'; export const DECORATORS = 'decorators' as ts.__String; export const PROP_DECORATORS = 'propDecorators' as ts.__String; @@ -213,13 +213,13 @@ export class Fesm2015ReflectionHost extends TypeScriptReflectionHost implements /** * Search the given module for variable declarations in which the initializer - * is an identifier marked with the `PRE_NGCC_MARKER`. + * is an identifier marked with the `PRE_R3_MARKER`. * @param module the module in which to search for switchable declarations. * @returns an array of variable declarations that match. */ getSwitchableDeclarations(module: ts.Node): SwitchableVariableDeclaration[] { // Don't bother to walk the AST if the marker is not found in the text - return module.getText().indexOf(PRE_NGCC_MARKER) >= 0 ? + return module.getText().indexOf(PRE_R3_MARKER) >= 0 ? findAll(module, isSwitchableVariableDeclaration) : []; } diff --git a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts index 97c83b483a..483ade6be0 100644 --- a/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts +++ b/packages/compiler-cli/src/ngcc/src/host/ngcc_host.ts @@ -9,14 +9,14 @@ import * as ts from 'typescript'; import {ReflectionHost} from '../../../ngtsc/host'; import {DecoratedFile} from './decorated_file'; -export const PRE_NGCC_MARKER = '__PRE_R3__'; -export const POST_NGCC_MARKER = '__POST_R3__'; +export const PRE_R3_MARKER = '__PRE_R3__'; +export const POST_R3_MARKER = '__POST_R3__'; export type SwitchableVariableDeclaration = ts.VariableDeclaration & {initializer: ts.Identifier}; export function isSwitchableVariableDeclaration(node: ts.Node): node is SwitchableVariableDeclaration { return ts.isVariableDeclaration(node) && !!node.initializer && - ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_NGCC_MARKER); + ts.isIdentifier(node.initializer) && node.initializer.text.endsWith(PRE_R3_MARKER); } /** @@ -33,7 +33,7 @@ export interface NgccReflectionHost extends ReflectionHost { /** * Search the given module for variable declarations in which the initializer - * is an identifier marked with the `PRE_NGCC_MARKER`. + * is an identifier marked with the `PRE_R3_MARKER`. * @param module The module in which to search for switchable declarations. * @returns An array of variable declarations that match. */ diff --git a/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts b/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts index a07cfc43da..fe45f08a66 100644 --- a/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts +++ b/packages/compiler-cli/src/ngcc/src/rendering/fesm2015_renderer.ts @@ -7,7 +7,7 @@ */ import * as ts from 'typescript'; import MagicString from 'magic-string'; -import {NgccReflectionHost, POST_NGCC_MARKER, PRE_NGCC_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; +import {NgccReflectionHost, POST_R3_MARKER, PRE_R3_MARKER, SwitchableVariableDeclaration} from '../host/ngcc_host'; import {AnalyzedClass} from '../analysis/decoration_analyzer'; import {Renderer} from './renderer'; @@ -85,7 +85,7 @@ export class Fesm2015Renderer extends Renderer { declarations.forEach(declaration => { const start = declaration.initializer.getStart(); const end = declaration.initializer.getEnd(); - const replacement = declaration.initializer.text.replace(PRE_NGCC_MARKER, POST_NGCC_MARKER); + const replacement = declaration.initializer.text.replace(PRE_R3_MARKER, POST_R3_MARKER); outputText.overwrite(start, end, replacement); }); } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts index 5482a69e12..23bd83bf31 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/ng_module.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {ConstantPool, Expression, LiteralArrayExpr, R3DirectiveMetadata, R3InjectorMetadata, R3NgModuleMetadata, Statement, WrappedNodeExpr, compileInjector, compileNgModule, makeBindingParser, parseTemplate} from '@angular/compiler'; +import {Expression, LiteralArrayExpr, R3InjectorMetadata, R3NgModuleMetadata, R3Reference, Statement, WrappedNodeExpr, compileInjector, compileNgModule} from '@angular/compiler'; import * as ts from 'typescript'; import {ErrorCode, FatalDiagnosticError} from '../../diagnostics'; import {Decorator, ReflectionHost} from '../../host'; -import {Reference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; +import {Reference, ResolvedReference, ResolvedValue, reflectObjectLiteral, staticallyResolve} from '../../metadata'; import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform'; import {generateSetClassMetadataCall} from './metadata'; @@ -100,14 +100,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler toR3Reference(bootstrap, context)), - declarations: declarations.map(decl => toR3Reference(decl, context)), - exports: exports.map(exp => toR3Reference(exp, context)), - imports: imports.map(imp => toR3Reference(imp, context)), + bootstrap: + bootstrap.map(bootstrap => this._toR3Reference(bootstrap, valueContext, typeContext)), + declarations: declarations.map(decl => this._toR3Reference(decl, valueContext, typeContext)), + exports: exports.map(exp => this._toR3Reference(exp, valueContext, typeContext)), + imports: imports.map(imp => this._toR3Reference(imp, valueContext, typeContext)), emitInline: false, }; @@ -163,6 +170,21 @@ export class NgModuleDecoratorHandler implements DecoratorHandler, valueContext: ts.SourceFile, + typeContext: ts.SourceFile): R3Reference { + if (!(valueRef instanceof ResolvedReference)) { + return toR3Reference(valueRef, valueRef, valueContext, valueContext); + } else { + let typeRef = valueRef; + let typeNode = this.reflector.getDtsDeclarationOfClass(typeRef.node); + if (typeNode !== null) { + typeRef = new ResolvedReference(typeNode, typeNode.name !); + } + return toR3Reference(valueRef, typeRef, valueContext, typeContext); + } + } + /** * Given a `FunctionDeclaration` or `MethodDeclaration`, check if it is typed as a * `ModuleWithProviders` and return an expression referencing the module if available. diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts index 019f4e395d..3a669955cc 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/selector_scope.ts @@ -14,7 +14,7 @@ import {AbsoluteReference, Reference, ResolvedReference, reflectTypeEntityToDecl import {reflectIdentifierOfDeclaration, reflectNameOfDeclaration} from '../../metadata/src/reflector'; import {TypeCheckableDirectiveMeta} from '../../typecheck'; -import {extractDirectiveGuards, toR3Reference} from './util'; +import {extractDirectiveGuards} from './util'; /** @@ -423,7 +423,11 @@ function convertDirectiveReferenceMap( context: ts.SourceFile): Map> { const newMap = new Map>(); map.forEach((meta, selector) => { - newMap.set(selector, {...meta, directive: toR3Reference(meta.directive, context).value}); + const directive = meta.directive.toExpression(context); + if (directive === null) { + throw new Error(`Could not write expression to reference ${meta.directive.node}`); + } + newMap.set(selector, {...meta, directive}); }); return newMap; } @@ -431,7 +435,13 @@ function convertDirectiveReferenceMap( function convertPipeReferenceMap( map: Map, context: ts.SourceFile): Map { const newMap = new Map(); - map.forEach((meta, selector) => { newMap.set(selector, toR3Reference(meta, context).value); }); + map.forEach((meta, selector) => { + const pipe = meta.toExpression(context); + if (pipe === null) { + throw new Error(`Could not write expression to reference ${meta.node}`); + } + newMap.set(selector, pipe); + }); return newMap; } diff --git a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts index b64668b560..bcb7d79117 100644 --- a/packages/compiler-cli/src/ngtsc/annotations/src/util.ts +++ b/packages/compiler-cli/src/ngtsc/annotations/src/util.ts @@ -70,11 +70,13 @@ export function getConstructorDependencies( return useType; } -export function toR3Reference(ref: Reference, context: ts.SourceFile): R3Reference { - const value = ref.toExpression(context, ImportMode.UseExistingImport); - const type = ref.toExpression(context, ImportMode.ForceNewImport); +export function toR3Reference( + valueRef: Reference, typeRef: Reference, valueContext: ts.SourceFile, + typeContext: ts.SourceFile): R3Reference { + const value = valueRef.toExpression(valueContext, ImportMode.UseExistingImport); + const type = typeRef.toExpression(typeContext, ImportMode.ForceNewImport); if (value === null || type === null) { - throw new Error(`Could not refer to ${ts.SyntaxKind[ref.node.kind]}`); + throw new Error(`Could not refer to ${ts.SyntaxKind[valueRef.node.kind]}`); } return {value, type}; } diff --git a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts index 49722ed448..79f0d409a5 100644 --- a/packages/compiler-cli/src/ngtsc/host/src/reflection.ts +++ b/packages/compiler-cli/src/ngtsc/host/src/reflection.ts @@ -436,4 +436,18 @@ export interface ReflectionHost { * if the value cannot be computed. */ getVariableValue(declaration: ts.VariableDeclaration): ts.Expression|null; + + /** + * Take an exported declaration of a class (maybe downleveled to a variable) and look up the + * declaration of its type in a separate .d.ts tree. + * + * This function is allowed to return `null` if the current compilation unit does not have a + * separate .d.ts tree. When compiling TypeScript code this is always the case, since .d.ts files + * are produced only during the emit of such a compilation. When compiling .js code, however, + * there is frequently a parallel .d.ts tree which this method exposes. + * + * Note that the `ts.ClassDeclaration` returned from this function may not be from the same + * `ts.Program` as the input declaration. + */ + getDtsDeclarationOfClass(declaration: ts.Declaration): ts.ClassDeclaration|null; } diff --git a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts index 48472794ec..69ea1c8d2d 100644 --- a/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts +++ b/packages/compiler-cli/src/ngtsc/metadata/src/reflector.ts @@ -170,6 +170,8 @@ export class TypeScriptReflectionHost implements ReflectionHost { return declaration.initializer || null; } + getDtsDeclarationOfClass(_: ts.Declaration): ts.ClassDeclaration|null { return null; } + /** * Resolve a `ts.Symbol` to its declaration, keeping track of the `viaModule` along the way. * @@ -312,7 +314,7 @@ export function reflectTypeEntityToDeclaration( type: ts.EntityName, checker: ts.TypeChecker): {node: ts.Declaration, from: string | null} { let realSymbol = checker.getSymbolAtLocation(type); if (realSymbol === undefined) { - throw new Error(`Cannot resolve type entity to symbol`); + throw new Error(`Cannot resolve type entity ${type.getText()} to symbol`); } while (realSymbol.flags & ts.SymbolFlags.Alias) { realSymbol = checker.getAliasedSymbol(realSymbol);