/** * @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 {ExternalExpr, ExternalReference} from '@angular/compiler'; import * as ts from 'typescript'; import {UnifiedModulesHost} from '../../core/api'; import {absoluteFrom} from '../../file_system'; import {runInEachFileSystem} from '../../file_system/testing'; import {AliasingHost, Reference, UnifiedModulesAliasingHost} from '../../imports'; import {DtsMetadataReader} from '../../metadata'; import {ClassDeclaration, TypeScriptReflectionHost} from '../../reflection'; import {makeProgram} from '../../testing'; import {ExportScope} from '../src/api'; import {MetadataDtsModuleScopeResolver} from '../src/dependency'; const MODULE_FROM_NODE_MODULES_PATH = /.*node_modules\/(\w+)\/index\.d\.ts$/; const testHost: UnifiedModulesHost = { fileNameToModuleName: function(imported: string): string { const res = MODULE_FROM_NODE_MODULES_PATH.exec(imported) !; return 'root/' + res[1]; } }; /** * 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}, aliasGenerator: AliasingHost | null = null): { 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: absoluteFrom(`/node_modules/${moduleName}/index.d.ts`), contents: PROLOG + (modules as any)[moduleName], }; }); const {program} = makeProgram(files); const checker = program.getTypeChecker(); const reflector = new TypeScriptReflectionHost(checker); const resolver = new MetadataDtsModuleScopeResolver(new DtsMetadataReader(checker, reflector), aliasGenerator); // 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}), }; } runInEachFileSystem(() => { describe('MetadataDtsModuleScopeResolver', () => { it('should produce an accurate scope for a basic NgModule', () => { const {resolver, refs} = makeTestEnv({ 'test': ` export declare class Dir { static ɵdir: DirectiveMeta; } export declare class Module { static ɵmod: 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 ɵdir: DirectiveMeta; } export declare class ModuleA { static ɵmod: ModuleMeta; } export declare class ModuleB { static ɵmod: 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 ɵdir: DirectiveMeta; } export declare class ModuleA { static ɵmod: ModuleMeta; } `, 'exported': ` import * as d from 'declaration'; export declare class ModuleB { static ɵmod: 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'); }); it('should write correct aliases for deep dependencies', () => { const {resolver, refs} = makeTestEnv( { 'deep': ` export declare class DeepDir { static ɵdir: DirectiveMeta; } export declare class DeepModule { static ɵmod: ModuleMeta; } `, 'middle': ` import * as deep from 'deep'; export declare class MiddleDir { static ɵdir: DirectiveMeta; } export declare class MiddleModule { static ɵmod: ModuleMeta; } `, 'shallow': ` import * as middle from 'middle'; export declare class ShallowDir { static ɵdir: DirectiveMeta; } export declare class ShallowModule { static ɵmod: ModuleMeta; } `, }, new UnifiedModulesAliasingHost(testHost)); const {ShallowModule} = refs; const scope = resolver.resolve(ShallowModule) !; const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); expect(getAlias(DeepDir)).toEqual({ moduleName: 'root/shallow', name: 'ɵng$root$deep$$DeepDir', }); expect(getAlias(MiddleDir)).toEqual({ moduleName: 'root/shallow', name: 'ɵng$root$middle$$MiddleDir', }); expect(getAlias(ShallowDir)).toBeNull(); }); it('should write correct aliases for bare directives in exports', () => { const {resolver, refs} = makeTestEnv( { 'deep': ` export declare class DeepDir { static ɵdir: DirectiveMeta; } export declare class DeepModule { static ɵmod: ModuleMeta; } `, 'middle': ` import * as deep from 'deep'; export declare class MiddleDir { static ɵdir: DirectiveMeta; } export declare class MiddleModule { static ɵmod: ModuleMeta; } `, 'shallow': ` import * as middle from 'middle'; export declare class ShallowDir { static ɵdir: DirectiveMeta; } export declare class ShallowModule { static ɵmod: ModuleMeta; } `, }, new UnifiedModulesAliasingHost(testHost)); const {ShallowModule} = refs; const scope = resolver.resolve(ShallowModule) !; const [DeepDir, MiddleDir, ShallowDir] = scopeToRefs(scope); expect(getAlias(DeepDir)).toEqual({ moduleName: 'root/shallow', name: 'ɵng$root$deep$$DeepDir', }); expect(getAlias(MiddleDir)).toEqual({ moduleName: 'root/shallow', name: 'ɵng$root$middle$$MiddleDir', }); expect(getAlias(ShallowDir)).toBeNull(); }); it('should not use an alias if a directive is declared in the same file as the re-exporting module', () => { const {resolver, refs} = makeTestEnv( { 'module': ` export declare class DeepDir { static ɵdir: DirectiveMeta; } export declare class DeepModule { static ɵmod: ModuleMeta; } export declare class DeepExportModule { static ɵmod: ModuleMeta; } `, }, new UnifiedModulesAliasingHost(testHost)); const {DeepExportModule} = refs; const scope = resolver.resolve(DeepExportModule) !; const [DeepDir] = scopeToRefs(scope); expect(getAlias(DeepDir)).toBeNull(); }); }); 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 !)); } function getAlias(ref: Reference): ExternalReference|null { if (ref.alias === null) { return null; } else { return (ref.alias as ExternalExpr).value; } } });