/** * @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, ReferenceEmitter} from '../../imports'; import {ScopeData, ScopeDirective, ScopePipe} from '../src/api'; import {DtsModuleScopeResolver} from '../src/dependency'; import {LocalModuleScopeRegistry} from '../src/local'; function registerFakeRefs(registry: LocalModuleScopeRegistry): {[name: string]: Reference<ts.ClassDeclaration>} { const get = (target: {}, name: string): Reference<ts.ClassDeclaration> => { const sf = ts.createSourceFile( name + '.ts', `export class ${name} {}`, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS); const clazz = sf.statements[0] as ts.ClassDeclaration; const ref = new Reference(clazz); if (name.startsWith('Dir') || name.startsWith('Cmp')) { registry.registerDirective(fakeDirective(ref)); } else if (name.startsWith('Pipe')) { registry.registerPipe(fakePipe(ref)); } return ref; }; return new Proxy({}, {get}); } describe('LocalModuleScopeRegistry', () => { const refEmitter = new ReferenceEmitter([]); let registry !: LocalModuleScopeRegistry; beforeEach(() => { registry = new LocalModuleScopeRegistry(new MockDtsModuleScopeResolver(), refEmitter, null); }); it('should produce an accurate LocalModuleScope for a basic NgModule', () => { const {Dir1, Dir2, Pipe1, Module} = registerFakeRefs(registry); registry.registerNgModule(Module.node, { imports: [], declarations: [Dir1, Dir2, Pipe1], exports: [Dir1, Pipe1], }); const scope = registry.getScopeOfModule(Module.node) !; expect(scopeToRefs(scope.compilation)).toEqual([Dir1, Dir2, Pipe1]); expect(scopeToRefs(scope.exported)).toEqual([Dir1, Pipe1]); }); it('should produce accurate LocalModuleScopes for a complex module chain', () => { const {DirA, DirB, DirCI, DirCE, ModuleA, ModuleB, ModuleC} = registerFakeRefs(registry); registry.registerNgModule( ModuleA.node, {imports: [ModuleB], declarations: [DirA], exports: []}); registry.registerNgModule( ModuleB.node, {exports: [ModuleC, DirB], declarations: [DirB], imports: []}); registry.registerNgModule( ModuleC.node, {declarations: [DirCI, DirCE], exports: [DirCE], imports: []}); const scopeA = registry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scopeA.compilation)).toEqual([DirA, DirB, DirCE]); expect(scopeToRefs(scopeA.exported)).toEqual([]); }); it('should not treat exported modules as imported', () => { const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry); registry.registerNgModule(ModuleA.node, {exports: [ModuleB], imports: [], declarations: []}); registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); const scopeA = registry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scopeA.compilation)).toEqual([]); expect(scopeToRefs(scopeA.exported)).toEqual([Dir]); }); it('should deduplicate declarations and exports', () => { const {DirA, ModuleA, DirB, ModuleB, ModuleC} = registerFakeRefs(registry); registry.registerNgModule(ModuleA.node, { declarations: [DirA, DirA], imports: [ModuleB, ModuleC], exports: [DirA, DirA, DirB, ModuleB], }); registry.registerNgModule(ModuleB.node, {declarations: [DirB], imports: [], exports: [DirB]}); registry.registerNgModule(ModuleC.node, {declarations: [], imports: [], exports: [ModuleB]}); const scope = registry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scope.compilation)).toEqual([DirA, DirB]); expect(scopeToRefs(scope.exported)).toEqual([DirA, DirB]); }); it('should preserve reference identities in module metadata', () => { const {Dir, Module} = registerFakeRefs(registry); const idSf = ts.createSourceFile('id.ts', 'var id;', ts.ScriptTarget.Latest, true); // Create a new Reference to Dir, with a special `ts.Identifier`, and register the directive // using it. This emulates what happens when an NgModule declares a Directive. const idVar = idSf.statements[0] as ts.VariableStatement; const id = idVar.declarationList.declarations[0].name as ts.Identifier; const DirInModule = new Reference(Dir.node); DirInModule.addIdentifier(id); registry.registerNgModule(Module.node, {exports: [], imports: [], declarations: [DirInModule]}); const scope = registry.getScopeOfModule(Module.node) !; expect(scope.compilation.directives[0].ref.getIdentityIn(idSf)).toBe(id); }); it('should allow directly exporting a directive that\'s not imported', () => { const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry); registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [ModuleB], declarations: []}); registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); const scopeA = registry.getScopeOfModule(ModuleA.node) !; expect(scopeToRefs(scopeA.exported)).toEqual([Dir]); }); it('should not allow directly exporting a directive that\'s not imported', () => { const {Dir, ModuleA, ModuleB} = registerFakeRefs(registry); registry.registerNgModule(ModuleA.node, {exports: [Dir], imports: [], declarations: []}); registry.registerNgModule(ModuleB.node, {declarations: [Dir], exports: [Dir], imports: []}); expect(registry.getScopeOfModule(ModuleA.node)).toBe(null); // ModuleA should have associated diagnostics as it exports `Dir` without declaring it. expect(registry.getDiagnosticsOfModule(ModuleA.node)).not.toBeNull(); // ModuleB should have no diagnostics as it correctly declares `Dir`. expect(registry.getDiagnosticsOfModule(ModuleB.node)).toBeNull(); }); }); function fakeDirective(ref: Reference<ts.ClassDeclaration>): ScopeDirective { const name = ref.debugName !; return { ref, name, selector: `[${ref.debugName}]`, isComponent: name.startsWith('Cmp'), inputs: {}, outputs: {}, exportAs: null, queries: [], hasNgTemplateContextGuard: false, ngTemplateGuards: [], }; } function fakePipe(ref: Reference<ts.ClassDeclaration>): ScopePipe { const name = ref.debugName !; return {ref, name}; } class MockDtsModuleScopeResolver implements DtsModuleScopeResolver { resolve(ref: Reference<ts.ClassDeclaration>): null { return null; } } function scopeToRefs(scopeData: ScopeData): Reference<ts.Declaration>[] { const directives = scopeData.directives.map(dir => dir.ref); const pipes = scopeData.pipes.map(pipe => pipe.ref); return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !)); }