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:
parent
37c5a26421
commit
b6f6b1178f
|
@ -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: {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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';
|
|
@ -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};
|
||||||
|
}
|
|
@ -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};
|
|
||||||
}
|
|
||||||
|
|
|
@ -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([
|
||||||
{
|
{
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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",
|
||||||
],
|
],
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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({});
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue