fix(ivy): generate type references to a default import (#29146)

This commit refactors and expands ngtsc's support for generating imports of
values from imports of types (this is used for example when importing a
class referenced in a type annotation in a constructor).

Previously, this logic handled "import {Foo} from" and "import * as foo
from" style imports, but failed on imports of default values ("import
Foo from"). This commit moves the type-to-value logic to a separate file and
expands it to cover the default import case. Doing this also required
augmenting the ImportManager to track default as well as non-default import
generation. The APIs were made a little cleaner at the same time.

PR Close #29146
This commit is contained in:
Alex Rickabaugh 2019-03-06 16:35:08 -08:00 committed by Kara Erickson
parent 37c5a26421
commit b6f6b1178f
14 changed files with 349 additions and 156 deletions

View File

@ -23,9 +23,17 @@ export class EsmRenderer extends Renderer {
/** /**
* Add the imports at the top of the file * Add the imports at the top of the file
*/ */
addImports(output: MagicString, imports: {name: string; as: string;}[]): void { addImports(output: MagicString, imports: {
specifier: string; qualifier: string; isDefault: boolean
}[]): void {
// The imports get inserted at the very top of the file. // The imports get inserted at the very top of the file.
imports.forEach(i => { output.appendLeft(0, `import * as ${i.as} from '${i.name}';\n`); }); imports.forEach(i => {
if (!i.isDefault) {
output.appendLeft(0, `import * as ${i.qualifier} from '${i.specifier}';\n`);
} else {
output.appendLeft(0, `import ${i.qualifier} from '${i.specifier}';\n`);
}
});
} }
addExports(output: MagicString, entryPointBasePath: string, exports: { addExports(output: MagicString, entryPointBasePath: string, exports: {

View File

@ -245,7 +245,9 @@ export abstract class Renderer {
protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile): protected abstract addConstants(output: MagicString, constants: string, file: ts.SourceFile):
void; void;
protected abstract addImports(output: MagicString, imports: {name: string, as: string}[]): void; protected abstract addImports(
output: MagicString,
imports: {specifier: string, qualifier: string, isDefault: boolean}[]): void;
protected abstract addExports(output: MagicString, entryPointBasePath: string, exports: { protected abstract addExports(output: MagicString, entryPointBasePath: string, exports: {
identifier: string, identifier: string,
from: string from: string

View File

@ -115,13 +115,24 @@ describe('Esm2015Renderer', () => {
it('should insert the given imports at the start of the source file', () => { it('should insert the given imports at the start of the source file', () => {
const {renderer} = setup(PROGRAM); const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(output, [
output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]); {specifier: '@angular/core', qualifier: 'i0', isDefault: false},
{specifier: '@angular/common', qualifier: 'i1', isDefault: false}
]);
expect(output.toString()).toContain(`import * as i0 from '@angular/core'; expect(output.toString()).toContain(`import * as i0 from '@angular/core';
import * as i1 from '@angular/common'; import * as i1 from '@angular/common';
/* A copyright notice */`); /* A copyright notice */`);
}); });
it('should insert a default import at the start of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addImports(output, [
{specifier: 'test', qualifier: 'i0', isDefault: true},
]);
expect(output.toString()).toContain(`import i0 from 'test';`);
});
}); });
describe('addExports', () => { describe('addExports', () => {

View File

@ -152,13 +152,24 @@ describe('Esm5Renderer', () => {
it('should insert the given imports at the start of the source file', () => { it('should insert the given imports at the start of the source file', () => {
const {renderer} = setup(PROGRAM); const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents); const output = new MagicString(PROGRAM.contents);
renderer.addImports( renderer.addImports(output, [
output, [{name: '@angular/core', as: 'i0'}, {name: '@angular/common', as: 'i1'}]); {specifier: '@angular/core', qualifier: 'i0', isDefault: false},
{specifier: '@angular/common', qualifier: 'i1', isDefault: false}
]);
expect(output.toString()).toContain(`import * as i0 from '@angular/core'; expect(output.toString()).toContain(`import * as i0 from '@angular/core';
import * as i1 from '@angular/common'; import * as i1 from '@angular/common';
/* A copyright notice */`); /* A copyright notice */`);
}); });
it('should insert a default import at the start of the source file', () => {
const {renderer} = setup(PROGRAM);
const output = new MagicString(PROGRAM.contents);
renderer.addImports(output, [
{specifier: 'test', qualifier: 'i0', isDefault: true},
]);
expect(output.toString()).toContain(`import i0 from 'test';`);
});
}); });
describe('addExports', () => { describe('addExports', () => {

View File

@ -23,7 +23,8 @@ class TestRenderer extends Renderer {
constructor(host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) { constructor(host: Esm2015ReflectionHost, isCore: boolean, bundle: EntryPointBundle) {
super(host, isCore, bundle, '/src', '/dist'); super(host, isCore, bundle, '/src', '/dist');
} }
addImports(output: MagicString, imports: {name: string, as: string}[]) { addImports(
output: MagicString, imports: {specifier: string, qualifier: string, isDefault: boolean}[]) {
output.prepend('\n// ADD IMPORTS\n'); output.prepend('\n// ADD IMPORTS\n');
} }
addExports(output: MagicString, baseEntryPointPath: string, exports: { addExports(output: MagicString, baseEntryPointPath: string, exports: {
@ -171,7 +172,7 @@ A.ngComponentDef = ɵngcc0.ɵdefineComponent({ type: A, selectors: [["a"]], fact
const addImportsSpy = renderer.addImports as jasmine.Spy; const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS); expect(addImportsSpy.calls.first().args[0].toString()).toEqual(RENDERED_CONTENTS);
expect(addImportsSpy.calls.first().args[1]).toEqual([ expect(addImportsSpy.calls.first().args[1]).toEqual([
{name: '@angular/core', as: 'ɵngcc0'} {specifier: '@angular/core', qualifier: 'ɵngcc0', isDefault: false}
]); ]);
}); });
@ -287,7 +288,9 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
expect(addDefinitionsSpy.calls.first().args[2]) expect(addDefinitionsSpy.calls.first().args[2])
.toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`); .toContain(`/*@__PURE__*/ ɵngcc0.setClassMetadata(`);
const addImportsSpy = renderer.addImports as jasmine.Spy; const addImportsSpy = renderer.addImports as jasmine.Spy;
expect(addImportsSpy.calls.first().args[1]).toEqual([{name: './r3_symbols', as: 'ɵngcc0'}]); expect(addImportsSpy.calls.first().args[1]).toEqual([
{specifier: './r3_symbols', qualifier: 'ɵngcc0', isDefault: false}
]);
}); });
it('should render no imports in FESM bundles', () => { it('should render no imports in FESM bundles', () => {
@ -502,9 +505,9 @@ A.ngDirectiveDef = ɵngcc0.ɵdefineDirective({ type: A, selectors: [["", "a", ""
export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`); export declare function withProviders8(): (MyModuleWithProviders)&{ngModule:SomeModule};`);
expect(renderer.addImports).toHaveBeenCalledWith(jasmine.any(MagicString), [ expect(renderer.addImports).toHaveBeenCalledWith(jasmine.any(MagicString), [
{name: './module', as: 'ɵngcc0'}, {specifier: './module', qualifier: 'ɵngcc0', isDefault: false},
{name: '@angular/core', as: 'ɵngcc1'}, {specifier: '@angular/core', qualifier: 'ɵngcc1', isDefault: false},
{name: 'some-library', as: 'ɵngcc2'}, {specifier: 'some-library', qualifier: 'ɵngcc2', isDefault: false},
]); ]);

View File

@ -7,4 +7,5 @@
*/ */
export * from './src/host'; export * from './src/host';
export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration, typeNodeToValueExpr} from './src/typescript'; export {DEFAULT_EXPORT_NAME, typeNodeToValueExpr} from './src/type_to_value';
export {TypeScriptReflectionHost, filterToMembersWithDecorator, reflectIdentifierOfDeclaration, reflectNameOfDeclaration, reflectObjectLiteral, reflectTypeEntityToDeclaration} from './src/typescript';

View File

@ -0,0 +1,185 @@
/**
* @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 {TypeValueReference} from './host';
export const DEFAULT_EXPORT_NAME = '*';
/**
* Potentially convert a `ts.TypeNode` to a `TypeValueReference`, which indicates how to use the
* type given in the `ts.TypeNode` in a value position.
*
* This can return `null` if the `typeNode` is `null`, if it does not refer to a symbol with a value
* declaration, or if it is not possible to statically understand.
*/
export function typeToValue(
typeNode: ts.TypeNode | null, checker: ts.TypeChecker): TypeValueReference|null {
// It's not possible to get a value expression if the parameter doesn't even have a type.
if (typeNode === null || !ts.isTypeReferenceNode(typeNode)) {
return null;
}
const symbols = resolveTypeSymbols(typeNode, checker);
if (symbols === null) {
return null;
}
const {local, decl} = symbols;
// It's only valid to convert a type reference to a value reference if the type actually
// has a value declaration associated with it.
if (decl.valueDeclaration === undefined) {
return null;
}
// The type points to a valid value declaration. Rewrite the TypeReference into an
// Expression which references the value pointed to by the TypeReference, if possible.
// Look at the local `ts.Symbol`'s declarations and see if it comes from an import
// statement. If so, extract the module specifier and the name of the imported type.
const firstDecl = local.declarations && local.declarations[0];
if (firstDecl && isImportSource(firstDecl)) {
const origin = extractModuleAndNameFromImport(firstDecl, symbols.importName);
return {local: false, valueDeclaration: decl.valueDeclaration, ...origin};
} else {
const expression = typeNodeToValueExpr(typeNode);
if (expression !== null) {
return {
local: true,
expression,
};
} else {
return null;
}
}
}
/**
* Attempt to extract a `ts.Expression` that's equivalent to a `ts.TypeNode`, as the two have
* different AST shapes but can reference the same symbols.
*
* This will return `null` if an equivalent expression cannot be constructed.
*/
export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
/**
* Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source.
*
* In the event that the `TypeReference` refers to a locally declared symbol, these will be the
* same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved
* `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifer` which
* points to the import statement by which the symbol was imported.
*
* In the event `typeRef` refers to a default import, an `importName` will also be returned to
* give the identifier name within the current file by which the import is known.
*/
function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker):
{local: ts.Symbol, decl: ts.Symbol, importName: string | null}|null {
const typeName = typeRef.typeName;
// typeRefSymbol is the ts.Symbol of the entire type reference.
const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName);
if (typeRefSymbol === undefined) {
return null;
}
// local is the ts.Symbol for the local ts.Identifier for the type.
// If the type is actually locally declared or is imported by name, for example:
// import {Foo} from './foo';
// then it'll be the same as top. If the type is imported via a namespace import, for example:
// import * as foo from './foo';
// and then referenced as:
// constructor(f: foo.Foo)
// then local will be the ts.Symbol of `foo`, whereas top will be the ts.Symbol of `foo.Foo`.
// This allows tracking of the import behind whatever type reference exists.
let local = typeRefSymbol;
let importName: string|null = null;
// TODO(alxhub): this is technically not correct. The user could have any import type with any
// amount of qualification following the imported type:
//
// import * as foo from 'foo'
// constructor(inject: foo.X.Y.Z)
//
// What we really want is the ability to express the arbitrary operation of `.X.Y.Z` on top of
// whatever import we generate for 'foo'. This logic is sufficient for now, though.
if (ts.isQualifiedName(typeName) && ts.isIdentifier(typeName.left) &&
ts.isIdentifier(typeName.right)) {
const localTmp = checker.getSymbolAtLocation(typeName.left);
if (localTmp !== undefined) {
local = localTmp;
importName = typeName.right.text;
}
}
// De-alias the top-level type reference symbol to get the symbol of the actual declaration.
let decl = typeRefSymbol;
if (typeRefSymbol.flags & ts.SymbolFlags.Alias) {
decl = checker.getAliasedSymbol(typeRefSymbol);
}
return {local, decl, importName};
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
function isImportSource(node: ts.Declaration): node is(
ts.ImportSpecifier | ts.NamespaceImport | ts.ImportClause) {
return ts.isImportSpecifier(node) || ts.isNamespaceImport(node) || ts.isImportClause(node);
}
function extractModuleAndNameFromImport(
node: ts.ImportSpecifier | ts.NamespaceImport | ts.ImportClause,
localName: string | null): {name: string, moduleName: string} {
let name: string;
let moduleSpecifier: ts.Expression;
switch (node.kind) {
case ts.SyntaxKind.ImportSpecifier:
// The symbol was imported by name, in a ts.ImportSpecifier.
name = (node.propertyName || node.name).text;
moduleSpecifier = node.parent.parent.parent.moduleSpecifier;
break;
case ts.SyntaxKind.NamespaceImport:
// The symbol was imported via a namespace import. In this case, the name to use when
// importing it was extracted by resolveTypeSymbols.
if (localName === null) {
// resolveTypeSymbols() should have extracted the correct local name for the import.
throw new Error(`Debug failure: no local name provided for NamespaceImport`);
}
name = localName;
moduleSpecifier = node.parent.parent.moduleSpecifier;
break;
case ts.SyntaxKind.ImportClause:
name = DEFAULT_EXPORT_NAME;
moduleSpecifier = node.parent.moduleSpecifier;
break;
default:
throw new Error(`Unreachable: ${ts.SyntaxKind[(node as ts.Node).kind]}`);
}
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
return {moduleName, name};
}

View File

@ -9,6 +9,7 @@
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost, TypeValueReference} from './host'; import {ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, FunctionDefinition, Import, ReflectionHost, TypeValueReference} from './host';
import {typeToValue} from './type_to_value';
/** /**
* reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`. * reflector.ts implements static reflection of declarations using the TypeScript `ts.TypeChecker`.
@ -48,7 +49,7 @@ export class TypeScriptReflectionHost implements ReflectionHost {
// It may or may not be possible to write an expression that refers to the value side of the // It may or may not be possible to write an expression that refers to the value side of the
// type named for the parameter. // type named for the parameter.
let typeValueExpr: TypeValueReference|null = null;
let originalTypeNode = node.type || null; let originalTypeNode = node.type || null;
let typeNode = originalTypeNode; let typeNode = originalTypeNode;
@ -67,68 +68,11 @@ export class TypeScriptReflectionHost implements ReflectionHost {
} }
} }
// It's not possible to get a value expression if the parameter doesn't even have a type. const typeValueReference = typeToValue(typeNode, this.checker);
if (typeNode && ts.isTypeReferenceNode(typeNode)) {
const symbols = resolveTypeSymbols(typeNode, this.checker);
if (symbols !== null) {
const {local, decl} = symbols;
// It's only valid to convert a type reference to a value reference if the type actually
// has a value declaration associated with it.
if (decl.valueDeclaration !== undefined) {
// The type points to a valid value declaration. Rewrite the TypeReference into an
// Expression which references the value pointed to by the TypeReference, if possible.
// Look at the local `ts.Symbol`'s declarations and see if it comes from an import
// statement. If so, extract the module specifier and the name of the imported type.
const firstDecl = local.declarations && local.declarations[0];
if (firstDecl && ts.isImportSpecifier(firstDecl)) {
// The symbol was imported by name, in a ts.ImportSpecifier.
const name = (firstDecl.propertyName || firstDecl.name).text;
const moduleSpecifier = firstDecl.parent.parent.parent.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
typeValueExpr = {
local: false,
name,
moduleName,
valueDeclaration: decl.valueDeclaration,
};
} else if (
firstDecl && ts.isNamespaceImport(firstDecl) && symbols.importName !== null) {
// The symbol was imported via a namespace import. In this case, the name to use when
// importing it was extracted by resolveTypeSymbols.
const name = symbols.importName;
const moduleSpecifier = firstDecl.parent.parent.moduleSpecifier;
if (!ts.isStringLiteral(moduleSpecifier)) {
throw new Error('not a module specifier');
}
const moduleName = moduleSpecifier.text;
typeValueExpr = {
local: false,
name,
moduleName,
valueDeclaration: decl.valueDeclaration,
};
} else {
const expression = typeNodeToValueExpr(typeNode);
if (expression !== null) {
typeValueExpr = {
local: true,
expression,
};
}
}
}
}
}
return { return {
name, name,
nameNode: node.name, nameNode: node.name, typeValueReference,
typeValueReference: typeValueExpr,
typeNode: originalTypeNode, decorators, typeNode: originalTypeNode, decorators,
}; };
}); });
@ -490,25 +434,6 @@ function parameterName(name: ts.BindingName): string|null {
} }
} }
export function typeNodeToValueExpr(node: ts.TypeNode): ts.Expression|null {
if (ts.isTypeReferenceNode(node)) {
return entityNameToValue(node.typeName);
} else {
return null;
}
}
function entityNameToValue(node: ts.EntityName): ts.Expression|null {
if (ts.isQualifiedName(node)) {
const left = entityNameToValue(node.left);
return left !== null ? ts.createPropertyAccess(left, node.right) : null;
} else if (ts.isIdentifier(node)) {
return ts.getMutableClone(node);
} else {
return null;
}
}
function propertyNameToString(node: ts.PropertyName): string|null { function propertyNameToString(node: ts.PropertyName): string|null {
if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) { if (ts.isIdentifier(node) || ts.isStringLiteral(node) || ts.isNumericLiteral(node)) {
return node.text; return node.text;
@ -516,48 +441,3 @@ function propertyNameToString(node: ts.PropertyName): string|null {
return null; return null;
} }
} }
/**
* Resolve a `TypeReference` node to the `ts.Symbol`s for both its declaration and its local source.
*
* In the event that the `TypeReference` refers to a locally declared symbol, these will be the
* same. If the `TypeReference` refers to an imported symbol, then `decl` will be the fully resolved
* `ts.Symbol` of the referenced symbol. `local` will be the `ts.Symbol` of the `ts.Identifer` which
* points to the import statement by which the symbol was imported.
*/
function resolveTypeSymbols(typeRef: ts.TypeReferenceNode, checker: ts.TypeChecker):
{local: ts.Symbol, decl: ts.Symbol, importName: string | null}|null {
const typeName = typeRef.typeName;
// typeRefSymbol is the ts.Symbol of the entire type reference.
const typeRefSymbol: ts.Symbol|undefined = checker.getSymbolAtLocation(typeName);
if (typeRefSymbol === undefined) {
return null;
}
// local is the ts.Symbol for the local ts.Identifier for the type.
// If the type is actually locally declared or is imported by name, for example:
// import {Foo} from './foo';
// then it'll be the same as top. If the type is imported via a namespace import, for example:
// import * as foo from './foo';
// and then referenced as:
// constructor(f: foo.Foo)
// then local will be the ts.Symbol of `foo`, whereas top will be the ts.Symbol of `foo.Foo`.
// This allows tracking of the import behind whatever type reference exists.
let local = typeRefSymbol;
let importName: string|null = null;
if (ts.isQualifiedName(typeName) && ts.isIdentifier(typeName.left) &&
ts.isIdentifier(typeName.right)) {
const localTmp = checker.getSymbolAtLocation(typeName.left);
if (localTmp !== undefined) {
local = localTmp;
importName = typeName.right.text;
}
}
// De-alias the top-level type reference symbol to get the symbol of the actual declaration.
let decl = typeRefSymbol;
if (typeRefSymbol.flags & ts.SymbolFlags.Alias) {
decl = checker.getAliasedSymbol(typeRefSymbol);
}
return {local, decl, importName};
}

View File

@ -147,6 +147,33 @@ describe('reflector', () => {
expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'}); expectParameter(args[0], 'bar', {moduleName: './bar', name: 'Bar'});
}); });
it('should reflect an argument from a default import', () => {
const {program} = makeProgram([
{
name: 'bar.ts',
contents: `
export default class Bar {}
`
},
{
name: 'entry.ts',
contents: `
import Bar from './bar';
class Foo {
constructor(bar: Bar) {}
}
`
}
]);
const clazz = getDeclaration(program, 'entry.ts', 'Foo', ts.isClassDeclaration);
const checker = program.getTypeChecker();
const host = new TypeScriptReflectionHost(checker);
const args = host.getConstructorParameters(clazz) !;
expect(args.length).toBe(1);
expectParameter(args[0], 'bar', {moduleName: './bar', name: '*'});
});
it('should reflect a nullable argument', () => { it('should reflect a nullable argument', () => {
const {program} = makeProgram([ const {program} = makeProgram([
{ {

View File

@ -19,10 +19,22 @@ export function addImports(
extraStatements: ts.Statement[] = []): ts.SourceFile { extraStatements: ts.Statement[] = []): ts.SourceFile {
// Generate the import statements to prepend. // Generate the import statements to prepend.
const addedImports = importManager.getAllImports(sf.fileName).map(i => { const addedImports = importManager.getAllImports(sf.fileName).map(i => {
const qualifier = ts.createIdentifier(i.qualifier);
let importClause: ts.ImportClause;
if (!i.isDefault) {
importClause = ts.createImportClause(
/* name */ undefined,
/* namedBindings */ ts.createNamespaceImport(qualifier));
} else {
importClause = ts.createImportClause(
/* name */ qualifier,
/* namedBindings */ undefined);
}
return ts.createImportDeclaration( return ts.createImportDeclaration(
undefined, undefined, /* decorators */ undefined,
ts.createImportClause(undefined, ts.createNamespaceImport(ts.createIdentifier(i.as))), /* modifiers */ undefined,
ts.createLiteral(i.name)); /* importClause */ importClause,
/* moduleSpecifier */ ts.createLiteral(i.specifier));
}); });
// Filter out the existing imports and the source file body. All new statements // Filter out the existing imports and the source file body. All new statements

View File

@ -9,6 +9,7 @@ ts_library(
"//packages:types", "//packages:types",
"//packages/compiler", "//packages/compiler",
"//packages/compiler-cli/src/ngtsc/imports", "//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/util", "//packages/compiler-cli/src/ngtsc/util",
"@npm//typescript", "@npm//typescript",
], ],

View File

@ -10,6 +10,7 @@ import {ArrayType, AssertNotNull, BinaryOperator, BinaryOperatorExpr, BuiltinTyp
import * as ts from 'typescript'; import * as ts from 'typescript';
import {ImportRewriter, NoopImportRewriter} from '../../imports'; import {ImportRewriter, NoopImportRewriter} from '../../imports';
import {DEFAULT_EXPORT_NAME} from '../../reflection';
export class Context { export class Context {
constructor(readonly isStatement: boolean) {} constructor(readonly isStatement: boolean) {}
@ -38,11 +39,9 @@ const BINARY_OPERATORS = new Map<BinaryOperator, ts.BinaryOperator>([
[BinaryOperator.Plus, ts.SyntaxKind.PlusToken], [BinaryOperator.Plus, ts.SyntaxKind.PlusToken],
]); ]);
export class ImportManager { export class ImportManager {
private moduleToIndex = new Map<string, string>(); private nonDefaultImports = new Map<string, string>();
private importedModules = new Set<string>(); private defaultImports = new Map<string, string>();
private nextIndex = 0; private nextIndex = 0;
constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') { constructor(protected rewriter: ImportRewriter = new NoopImportRewriter(), private prefix = 'i') {
@ -61,20 +60,39 @@ export class ImportManager {
} }
// If not, this symbol will be imported. Allocate a prefix for the imported module if needed. // If not, this symbol will be imported. Allocate a prefix for the imported module if needed.
if (!this.moduleToIndex.has(moduleName)) {
this.moduleToIndex.set(moduleName, `${this.prefix}${this.nextIndex++}`);
}
const moduleImport = this.moduleToIndex.get(moduleName) !;
return {moduleImport, symbol}; const isDefault = symbol === DEFAULT_EXPORT_NAME;
// Use a different map for non-default vs default imports. This allows the same module to be
// imported in both ways simultaneously.
const trackingMap = !isDefault ? this.nonDefaultImports : this.defaultImports;
if (!trackingMap.has(moduleName)) {
trackingMap.set(moduleName, `${this.prefix}${this.nextIndex++}`);
}
const moduleImport = trackingMap.get(moduleName) !;
if (isDefault) {
// For an import of a module's default symbol, the moduleImport *is* the name to use to refer
// to the import.
return {moduleImport: null, symbol: moduleImport};
} else {
// Non-default imports have a qualifier and the symbol name to import.
return {moduleImport, symbol};
}
} }
getAllImports(contextPath: string): {name: string, as: string}[] { getAllImports(contextPath: string): {specifier: string, qualifier: string, isDefault: boolean}[] {
return Array.from(this.moduleToIndex.keys()).map(name => { const imports: {specifier: string, qualifier: string, isDefault: boolean}[] = [];
const as = this.moduleToIndex.get(name) !; this.nonDefaultImports.forEach((qualifier, specifier) => {
name = this.rewriter.rewriteSpecifier(name, contextPath); specifier = this.rewriter.rewriteSpecifier(specifier, contextPath);
return {name, as}; imports.push({specifier, qualifier, isDefault: false});
}); });
this.defaultImports.forEach((qualifier, specifier) => {
specifier = this.rewriter.rewriteSpecifier(specifier, contextPath);
imports.push({specifier, qualifier, isDefault: true});
});
return imports;
} }
} }

View File

@ -135,7 +135,13 @@ export class TypeCheckContext {
// Write out the imports that need to be added to the beginning of the file. // Write out the imports that need to be added to the beginning of the file.
let imports = importManager.getAllImports(sf.fileName) let imports = importManager.getAllImports(sf.fileName)
.map(i => `import * as ${i.as} from '${i.name}';`) .map(i => {
if (!i.isDefault) {
return `import * as ${i.qualifier} from '${i.specifier}';`;
} else {
return `import ${i.qualifier} from '${i.specifier}';`;
}
})
.join('\n'); .join('\n');
code = imports + '\n' + code; code = imports + '\n' + code;

View File

@ -1970,6 +1970,34 @@ describe('ngtsc behavioral tests', () => {
expect(jsContents).toMatch(setClassMetadataRegExp('type: i\\d\\.MyTypeB')); expect(jsContents).toMatch(setClassMetadataRegExp('type: i\\d\\.MyTypeB'));
}); });
it('should use default-imported types if they can be represented as values', () => {
env.tsconfig({});
env.write(`types.ts`, `
export default class Default {}
export class Other {}
`);
env.write(`test.ts`, `
import {Component} from '@angular/core';
import {Other} from './types';
import Default from './types';
@Component({selector: 'test', template: 'test'})
export class SomeCmp {
constructor(arg: Default, other: Other) {}
}
`);
env.driveMain();
const jsContents = trim(env.getContents('test.js'));
expect(jsContents).toContain(`import i1 from "./types";`);
expect(jsContents).toContain(`import * as i2 from "./types";`);
expect(jsContents).toContain('i0.ɵdirectiveInject(i1)');
expect(jsContents).toContain('i0.ɵdirectiveInject(i2.Other)');
expect(jsContents).toMatch(setClassMetadataRegExp('type: i1'));
expect(jsContents).toMatch(setClassMetadataRegExp('type: i2.Other'));
});
it('should use `undefined` in setClassMetadata if types can\'t be represented as values', () => { it('should use `undefined` in setClassMetadata if types can\'t be represented as values', () => {
env.tsconfig({}); env.tsconfig({});