/** * @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} from '../../imports'; import {TypeScriptReflectionHost} from '../../reflection'; import {makeProgram} from '../../testing/in_memory_typescript'; import {ExportScope} from '../src/api'; import {MetadataDtsModuleScopeResolver} from '../src/dependency'; const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/; /** * Simple metadata types are added to the top of each testing file, for convenience. */ const PROLOG = ` export declare type ModuleMeta = never; export declare type ComponentMeta = never; export declare type DirectiveMeta = never; export declare type PipeMeta = never; `; /** * Construct the testing environment with a given set of absolute modules and their contents. * * This returns both the `MetadataDtsModuleScopeResolver` and a `refs` object which can be * destructured to retrieve references to specific declared classes. */ function makeTestEnv(modules: {[module: string]: string}): { refs: {[name: string]: Reference}, resolver: MetadataDtsModuleScopeResolver, } { // Map the modules object to an array of files for `makeProgram`. const files = Object.keys(modules).map(moduleName => { return { name: `node_modules/${moduleName}/index.d.ts`, contents: PROLOG + (modules as any)[moduleName], }; }); const {program} = makeProgram(files); const checker = program.getTypeChecker(); const resolver = new MetadataDtsModuleScopeResolver(checker, new TypeScriptReflectionHost(checker)); // Resolver for the refs object. const get = (target: {}, name: string): Reference => { for (const sf of program.getSourceFiles()) { const symbol = checker.getSymbolAtLocation(sf) !; const exportedSymbol = symbol.exports !.get(name as ts.__String); if (exportedSymbol !== undefined) { const decl = exportedSymbol.valueDeclaration as ts.ClassDeclaration; const specifier = MODULE_FROM_NODE_MODULES_PATH.exec(sf.fileName) ![1]; return new Reference(decl, {specifier, resolutionContext: sf.fileName}); } } throw new Error('Class not found: ' + name); }; return { resolver, refs: new Proxy({}, {get}), }; } describe('MetadataDtsModuleScopeResolver', () => { it('should produce an accurate scope for a basic NgModule', () => { const {resolver, refs} = makeTestEnv({ 'test': ` export declare class Dir { static ngDirectiveDef: DirectiveMeta; } export declare class Module { static ngModuleDef: ModuleMeta; } ` }); const {Dir, Module} = refs; const scope = resolver.resolve(Module) !; expect(scopeToRefs(scope)).toEqual([Dir]); }); it('should produce an accurate scope when a module is exported', () => { const {resolver, refs} = makeTestEnv({ 'test': ` export declare class Dir { static ngDirectiveDef: DirectiveMeta; } export declare class ModuleA { static ngModuleDef: ModuleMeta; } export declare class ModuleB { static ngModuleDef: ModuleMeta; } ` }); const {Dir, ModuleB} = refs; const scope = resolver.resolve(ModuleB) !; expect(scopeToRefs(scope)).toEqual([Dir]); }); it('should resolve correctly across modules', () => { const {resolver, refs} = makeTestEnv({ 'declaration': ` export declare class Dir { static ngDirectiveDef: DirectiveMeta; } export declare class ModuleA { static ngModuleDef: ModuleMeta; } `, 'exported': ` import * as d from 'declaration'; export declare class ModuleB { static ngModuleDef: ModuleMeta; } ` }); const {Dir, ModuleB} = refs; const scope = resolver.resolve(ModuleB) !; expect(scopeToRefs(scope)).toEqual([Dir]); // Explicitly verify that the directive has the correct owning module. expect(scope.exported.directives[0].ref.ownedByModuleGuess).toBe('declaration'); }); }); function scopeToRefs(scope: ExportScope): Reference[] { const directives = scope.exported.directives.map(dir => dir.ref); const pipes = scope.exported.pipes.map(pipe => pipe.ref); return [...directives, ...pipes].sort((a, b) => a.debugName !.localeCompare(b.debugName !)); }