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
This commit is contained in:
Pete Bacon Darwin 2018-10-15 11:48:03 +01:00 committed by Kara Erickson
parent eb5d3088a4
commit 1918f8d5b5
8 changed files with 74 additions and 24 deletions

View File

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

View File

@ -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.
*/

View File

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

View File

@ -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<NgModuleAnalys
// the compile() phase, the module's metadata is available for selector scope computation.
this.scopeRegistry.registerModule(node, {declarations, imports, exports});
const context = node.getSourceFile();
const valueContext = node.getSourceFile();
let typeContext = valueContext;
const typeNode = this.reflector.getDtsDeclarationOfClass(node);
if (typeNode !== null) {
typeContext = typeNode.getSourceFile();
}
const ngModuleDef: R3NgModuleMetadata = {
type: new WrappedNodeExpr(node.name !),
bootstrap: bootstrap.map(bootstrap => 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<NgModuleAnalys
];
}
private _toR3Reference(
valueRef: Reference<ts.Declaration>, 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.

View File

@ -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<string, ScopeDirective<Expression>> {
const newMap = new Map<string, ScopeDirective<Expression>>();
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<string, Reference>, context: ts.SourceFile): Map<string, Expression> {
const newMap = new Map<string, Expression>();
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;
}

View File

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

View File

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

View File

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