feat(ivy): register references from NgModule annotations (#26906)

The `NgModuleDecoratorHandler` can now register all the references that
it finds in the `NgModule` metadata, such as `declarations`, `imports`,
`exports` etc.

This information can then be used by ngcc to work out if any of these
references are internal only and need to be manually exported from a
library's entry-point.

PR Close #26906
This commit is contained in:
Pete Bacon Darwin 2018-11-13 14:40:54 +00:00 committed by Igor Minar
parent b93c1dffa1
commit 4a70b669be
12 changed files with 191 additions and 20 deletions

View File

@ -9,9 +9,8 @@ import {ConstantPool} from '@angular/compiler';
import * as fs from 'fs';
import * as ts from 'typescript';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations';
import {BaseDefDecoratorHandler, ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader, SelectorScopeRegistry} from '../../../ngtsc/annotations';
import {CompileResult, DecoratorHandler} from '../../../ngtsc/transform';
import {DecoratedClass} from '../host/decorated_class';
import {NgccReflectionHost} from '../host/ngcc_host';
import {isDefined} from '../utils';
@ -63,13 +62,15 @@ export class DecorationAnalyzer {
this.rootDirs, /* defaultPreserveWhitespaces */ false, /* i18nUseExternalIds */ true),
new DirectiveDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, this.isCore),
new InjectableDecoratorHandler(this.host, this.isCore),
new NgModuleDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, this.isCore),
new NgModuleDecoratorHandler(
this.typeChecker, this.host, this.scopeRegistry, this.referencesRegistry, this.isCore),
new PipeDecoratorHandler(this.typeChecker, this.host, this.scopeRegistry, this.isCore),
];
constructor(
private typeChecker: ts.TypeChecker, private host: NgccReflectionHost,
private rootDirs: string[], private isCore: boolean) {}
private referencesRegistry: ReferencesRegistry, private rootDirs: string[],
private isCore: boolean) {}
/**
* Analyze a program to find all the decorated files should be transformed.

View File

@ -0,0 +1,49 @@
/**
* @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 {ReferencesRegistry} from '../../../ngtsc/annotations';
import {Declaration, ReflectionHost} from '../../../ngtsc/host';
import {Reference, ResolvedReference} from '../../../ngtsc/metadata';
import {hasNameIdentifier} from '../utils';
/**
* This is a place for DecoratorHandlers to register references that they
* find in their analysis of the code.
*
* This registry is used to ensure that these references are publicly exported
* from libraries that are compiled by ngcc.
*/
export class NgccReferencesRegistry implements ReferencesRegistry {
private map = new Map<ts.Identifier, Declaration>();
constructor(private host: ReflectionHost) {}
/**
* Register one or more references in the registry.
* Only `ResolveReference` references are stored. Other types are ignored.
* @param references A collection of references to register.
*/
add(...references: Reference<ts.Declaration>[]): void {
references.forEach(ref => {
// Only store resolved references. We are not interested in literals.
if (ref instanceof ResolvedReference && hasNameIdentifier(ref.node)) {
const declaration = this.host.getDeclarationOfIdentifier(ref.node.name);
if (declaration && hasNameIdentifier(declaration.node)) {
this.map.set(declaration.node.name, declaration);
}
}
});
}
/**
* Create and return a mapping for the registered resolved references.
* @returns A map of reference identifiers to reference declarations.
*/
getDeclarationMap(): Map<ts.Identifier, Declaration> { return this.map; }
}

View File

@ -62,7 +62,7 @@ interface EntryPointPackageJson {
* @param packageJsonPath the absolute path to the package.json file.
* @returns JSON from the package.json file if it is valid, `null` otherwise.
*/
function loadEntryPointPackage(packageJsonPath: string): {[key: string]: any}|null {
function loadEntryPointPackage(packageJsonPath: string): EntryPointPackageJson|null {
try {
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
} catch (e) {
@ -109,7 +109,8 @@ export function getEntryPointInfo(packagePath: string, entryPointPath: string):
}
// Also there must exist a `metadata.json` file next to the typings entry-point.
const metadataPath = path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
const metadataPath =
path.resolve(entryPointPath, typings.replace(/\.d\.ts$/, '') + '.metadata.json');
if (!fs.existsSync(metadataPath)) {
return null;
}

View File

@ -11,6 +11,7 @@ import {mkdir, mv} from 'shelljs';
import * as ts from 'typescript';
import {DecorationAnalyzer} from '../analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../analysis/switch_marker_analyzer';
import {Esm2015ReflectionHost} from '../host/esm2015_host';
import {Esm5ReflectionHost} from '../host/esm5_host';
@ -156,8 +157,10 @@ export class Transformer {
analyzeProgram(
program: ts.Program, reflectionHost: NgccReflectionHost, rootDirs: string[],
isCore: boolean) {
const typeChecker = bundle.program.getTypeChecker();
const referencesRegistry = new NgccReferencesRegistry(reflectionHost);
const decorationAnalyzer =
new DecorationAnalyzer(program.getTypeChecker(), reflectionHost, rootDirs, isCore);
new DecorationAnalyzer(typeChecker, reflectionHost, referencesRegistry, rootDirs, isCore);
const switchMarkerAnalyzer = new SwitchMarkerAnalyzer(reflectionHost);
return {
decorationAnalyses: decorationAnalyzer.analyzeProgram(program),

View File

@ -40,3 +40,13 @@ export function findAll<T>(node: ts.Node, test: (node: ts.Node) => node is ts.No
}
}
}
/**
* Does the given declaration have a name which is an identifier?
* @param declaration The declaration to test.
* @returns true if the declaration has an identifer for a name.
*/
export function hasNameIdentifier(declaration: ts.Declaration): declaration is ts.Declaration&
{name: ts.Identifier} {
return ts.isIdentifier((declaration as any).name);
}

View File

@ -10,6 +10,7 @@ import * as ts from 'typescript';
import {Decorator} from '../../../ngtsc/host';
import {DecoratorHandler} from '../../../ngtsc/transform';
import {DecorationAnalyses, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
import {makeProgram} from '../helpers/utils';
@ -84,9 +85,10 @@ describe('DecorationAnalyzer', () => {
beforeEach(() => {
program = makeProgram(TEST_PROGRAM);
const analyzer = new DecorationAnalyzer(
program.getTypeChecker(), new Esm2015ReflectionHost(false, program.getTypeChecker()),
[''], false);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer =
new DecorationAnalyzer(program.getTypeChecker(), host, referencesRegistry, [''], false);
testHandler = createTestHandler();
analyzer.handlers = [testHandler];
result = analyzer.analyzeProgram(program);
@ -126,9 +128,10 @@ describe('DecorationAnalyzer', () => {
it('should analyze an internally imported component, which is not publicly exported from the entry-point',
() => {
const program = makeProgram(...INTERNAL_COMPONENT_PROGRAM);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer = new DecorationAnalyzer(
program.getTypeChecker(), new Esm2015ReflectionHost(false, program.getTypeChecker()),
[''], false);
program.getTypeChecker(), host, referencesRegistry, [''], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram(program);
@ -142,9 +145,10 @@ describe('DecorationAnalyzer', () => {
it('should analyze an internally defined component, which is not exported at all', () => {
const program = makeProgram(...INTERNAL_COMPONENT_PROGRAM);
const analyzer = new DecorationAnalyzer(
program.getTypeChecker(), new Esm2015ReflectionHost(false, program.getTypeChecker()),
[''], false);
const host = new Esm2015ReflectionHost(false, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(host);
const analyzer =
new DecorationAnalyzer(program.getTypeChecker(), host, referencesRegistry, [''], false);
const testHandler = createTestHandler();
analyzer.handlers = [testHandler];
const result = analyzer.analyzeProgram(program);

View File

@ -0,0 +1,52 @@
/**
* @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 {Reference, TypeScriptReflectionHost, staticallyResolve} from '../../../ngtsc/metadata';
import {getDeclaration, makeProgram} from '../../../ngtsc/testing/in_memory_typescript';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
describe('NgccReferencesRegistry', () => {
it('should return a mapping from resolved reference identifiers to their declarations', () => {
const {program} = makeProgram([{
name: 'index.ts',
contents: `
export class SomeClass {}
export function someFunction() {}
export const someVariable = 42;
export const testArray = [SomeClass, someFunction, someVariable];
`
}]);
const checker = program.getTypeChecker();
const testArrayDeclaration =
getDeclaration(program, 'index.ts', 'testArray', ts.isVariableDeclaration);
const someClassDecl = getDeclaration(program, 'index.ts', 'SomeClass', ts.isClassDeclaration);
const someFunctionDecl =
getDeclaration(program, 'index.ts', 'someFunction', ts.isFunctionDeclaration);
const someVariableDecl =
getDeclaration(program, 'index.ts', 'someVariable', ts.isVariableDeclaration);
const testArrayExpression = testArrayDeclaration.initializer !;
const host = new TypeScriptReflectionHost(checker);
const registry = new NgccReferencesRegistry(host);
const references =
staticallyResolve(testArrayExpression, host, checker) as Reference<ts.Declaration>[];
registry.add(...references);
const map = registry.getDeclarationMap();
expect(map.size).toEqual(2);
expect(map.get(someClassDecl.name !) !.node).toBe(someClassDecl);
expect(map.get(someFunctionDecl.name !) !.node).toBe(someFunctionDecl);
expect(map.has(someVariableDecl.name as ts.Identifier)).toBe(false);
});
});

View File

@ -12,6 +12,7 @@ import MagicString from 'magic-string';
import {fromObject, generateMapFileComment} from 'convert-source-map';
import {makeProgram} from '../helpers/utils';
import {CompiledClass, DecorationAnalyzer} from '../../src/analysis/decoration_analyzer';
import {NgccReferencesRegistry} from '../../src/analysis/ngcc_references_registry';
import {SwitchMarkerAnalyzer} from '../../src/analysis/switch_marker_analyzer';
import {BundleInfo, createBundleInfo} from '../../src/packages/bundle';
import {Esm2015ReflectionHost} from '../../src/host/esm2015_host';
@ -46,8 +47,10 @@ function createTestRenderer(
options.rewriteCoreImportsTo ? program.getSourceFile(options.rewriteCoreImportsTo) ! : null;
const bundle = createBundleInfo(options.isCore || false, rewriteCoreImportsTo, null);
const host = new Esm2015ReflectionHost(bundle.isCore, program.getTypeChecker());
const referencesRegistry = new NgccReferencesRegistry(host);
const decorationAnalyses =
new DecorationAnalyzer(program.getTypeChecker(), host, [''], bundle.isCore)
new DecorationAnalyzer(
program.getTypeChecker(), host, referencesRegistry, [''], bundle.isCore)
.analyzeProgram(program);
const switchMarkerAnalyses = new SwitchMarkerAnalyzer(host).analyzeProgram(program);
const renderer = new TestRenderer(host, bundle);

View File

@ -15,4 +15,5 @@ export {DirectiveDecoratorHandler} from './src/directive';
export {InjectableDecoratorHandler} from './src/injectable';
export {NgModuleDecoratorHandler} from './src/ng_module';
export {PipeDecoratorHandler} from './src/pipe';
export {NoopReferencesRegistry, ReferencesRegistry} from './src/references_registry';
export {CompilationScope, SelectorScopeRegistry} from './src/selector_scope';

View File

@ -15,6 +15,7 @@ import {Reference, ResolvedReference, ResolvedValue, reflectObjectLiteral, stati
import {AnalysisOutput, CompileResult, DecoratorHandler} from '../../transform';
import {generateSetClassMetadataCall} from './metadata';
import {ReferencesRegistry} from './references_registry';
import {SelectorScopeRegistry} from './selector_scope';
import {getConstructorDependencies, isAngularCore, toR3Reference, unwrapExpression} from './util';
@ -32,7 +33,8 @@ export interface NgModuleAnalysis {
export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalysis, Decorator> {
constructor(
private checker: ts.TypeChecker, private reflector: ReflectionHost,
private scopeRegistry: SelectorScopeRegistry, private isCore: boolean) {}
private scopeRegistry: SelectorScopeRegistry, private referencesRegistry: ReferencesRegistry,
private isCore: boolean) {}
detect(node: ts.Declaration, decorators: Decorator[]|null): Decorator|undefined {
if (!decorators) {
@ -72,6 +74,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
const expr = ngModule.get('declarations') !;
const declarationMeta = staticallyResolve(expr, this.reflector, this.checker);
declarations = this.resolveTypeList(expr, declarationMeta, 'declarations');
this.referencesRegistry.add(...declarations);
}
let imports: Reference<ts.Declaration>[] = [];
if (ngModule.has('imports')) {
@ -80,6 +83,7 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
expr, this.reflector, this.checker,
ref => this._extractModuleFromModuleWithProvidersFn(ref.node));
imports = this.resolveTypeList(expr, importsMeta, 'imports');
this.referencesRegistry.add(...imports);
}
let exports: Reference<ts.Declaration>[] = [];
if (ngModule.has('exports')) {
@ -88,12 +92,14 @@ export class NgModuleDecoratorHandler implements DecoratorHandler<NgModuleAnalys
expr, this.reflector, this.checker,
ref => this._extractModuleFromModuleWithProvidersFn(ref.node));
exports = this.resolveTypeList(expr, exportsMeta, 'exports');
this.referencesRegistry.add(...exports);
}
let bootstrap: Reference<ts.Declaration>[] = [];
if (ngModule.has('bootstrap')) {
const expr = ngModule.get('bootstrap') !;
const bootstrapMeta = staticallyResolve(expr, this.reflector, this.checker);
bootstrap = this.resolveTypeList(expr, bootstrapMeta, 'bootstrap');
this.referencesRegistry.add(...bootstrap);
}
// Register this module's information with the SelectorScopeRegistry. This ensures that during

View File

@ -0,0 +1,39 @@
/**
* @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 {Declaration} from '../../host';
import {Reference} from '../../metadata';
/**
* Implement this interface if you want DecoratorHandlers to register
* references that they find in their analysis of the code.
*/
export interface ReferencesRegistry {
/**
* Register one or more references in the registry.
* Only `ResolveReference` references are stored. Other types are ignored.
* @param references A collection of references to register.
*/
add(...references: Reference<ts.Declaration>[]): void;
/**
* Create and return a mapping for the registered resolved references.
* @returns A map of reference identifiers to reference declarations.
*/
getDeclarationMap(): Map<ts.Identifier, Declaration>;
}
/**
* This registry does nothing, since ngtsc does not currently need
* this functionality.
* The ngcc tool implements a working version for its purposes.
*/
export class NoopReferencesRegistry implements ReferencesRegistry {
add(...references: Reference<ts.Declaration>[]): void {}
getDeclarationMap(): Map<ts.Identifier, Declaration> { return new Map(); }
}

View File

@ -13,7 +13,7 @@ import * as ts from 'typescript';
import * as api from '../transformers/api';
import {nocollapseHack} from '../transformers/nocollapse_hack';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, NoopReferencesRegistry, PipeDecoratorHandler, ResourceLoader, SelectorScopeRegistry} from './annotations';
import {BaseDefDecoratorHandler} from './annotations/src/base_def';
import {TypeScriptReflectionHost} from './metadata';
import {FileResourceLoader, HostResourceLoader} from './resource_loader';
@ -214,6 +214,7 @@ export class NgtscProgram implements api.Program {
private makeCompilation(): IvyCompilation {
const checker = this.tsProgram.getTypeChecker();
const scopeRegistry = new SelectorScopeRegistry(checker, this.reflector);
const referencesRegistry = new NoopReferencesRegistry();
// Set up the IvyCompilation, which manages state for the Ivy transformer.
const handlers = [
@ -223,7 +224,8 @@ export class NgtscProgram implements api.Program {
this.options.preserveWhitespaces || false, this.options.i18nUseExternalIds !== false),
new DirectiveDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),
new InjectableDecoratorHandler(this.reflector, this.isCore),
new NgModuleDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),
new NgModuleDecoratorHandler(
checker, this.reflector, scopeRegistry, referencesRegistry, this.isCore),
new PipeDecoratorHandler(checker, this.reflector, scopeRegistry, this.isCore),
];