UNPKG

@notjustcoders/ioc-arise

Version:

Arise type-safe IoC containers from your code. Zero overhead, zero coupling.

222 lines (217 loc) 11.1 kB
"use strict"; /** * Tests for DI token resolution when types originate in external packages. * * Covers three shapes: * 1. Direct external import: import type { IFoo } from 'some-package' * 2. Local re-export stub: export type { IFoo } from 'some-package' (chain) * 3. Mixed: local + external deps in the same factory / class */ Object.defineProperty(exports, "__esModule", { value: true }); const vitest_1 = require("vitest"); const fs_1 = require("fs"); const path_1 = require("path"); const os_1 = require("os"); const class_analyzer_1 = require("./class-analyzer"); const factory_analyzer_1 = require("./factory-analyzer"); const ast_parser_1 = require("./ast-parser"); // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function tmpDir(prefix) { const dir = (0, path_1.join)((0, os_1.tmpdir)(), `ioc-arise-test-${prefix}-${Date.now()}`); (0, fs_1.mkdirSync)(dir, { recursive: true }); return dir; } function write(dir, relativePath, content) { const full = (0, path_1.join)(dir, relativePath); (0, fs_1.mkdirSync)((0, path_1.join)(full, '..'), { recursive: true }); (0, fs_1.writeFileSync)(full, content, 'utf-8'); return full; } // --------------------------------------------------------------------------- // ASTParser.extractExternalReExports // --------------------------------------------------------------------------- (0, vitest_1.describe)('ASTParser.extractExternalReExports', () => { let parser; let dir; (0, vitest_1.beforeEach)(() => { parser = new ast_parser_1.ASTParser(); dir = tmpDir('ast-parser'); }); (0, vitest_1.afterEach)(() => (0, fs_1.rmSync)(dir, { recursive: true, force: true })); (0, vitest_1.it)('extracts names from export type { X } from external package', () => { const file = write(dir, 'stub.ts', `export type { ITombstoneRepository } from '@word-tracker/sync';`); const root = parser.parseFile(file); const names = parser.extractExternalReExports(root); (0, vitest_1.expect)(names).toContain('ITombstoneRepository'); }); (0, vitest_1.it)('extracts multiple names from a single export statement', () => { const file = write(dir, 'stub.ts', `export type { IFoo, IBar } from 'some-package';`); const root = parser.parseFile(file); const names = parser.extractExternalReExports(root); (0, vitest_1.expect)(names).toContain('IFoo'); (0, vitest_1.expect)(names).toContain('IBar'); }); (0, vitest_1.it)('handles export with alias: export type { Foo as IFoo } from pkg', () => { const file = write(dir, 'stub.ts', `export type { InternalFoo as IFoo } from 'some-package';`); const root = parser.parseFile(file); const names = parser.extractExternalReExports(root); (0, vitest_1.expect)(names).toContain('IFoo'); (0, vitest_1.expect)(names).not.toContain('InternalFoo'); }); (0, vitest_1.it)('ignores re-exports from local (relative) paths', () => { const file = write(dir, 'stub.ts', `export type { IFoo } from './local-file';`); const root = parser.parseFile(file); const names = parser.extractExternalReExports(root); (0, vitest_1.expect)(names).toHaveLength(0); }); (0, vitest_1.it)('returns empty array for a file with no re-exports', () => { const file = write(dir, 'stub.ts', `export interface IFoo { doSomething(): void; }`); const root = parser.parseFile(file); const names = parser.extractExternalReExports(root); (0, vitest_1.expect)(names).toHaveLength(0); }); }); // --------------------------------------------------------------------------- // FactoryAnalyzer — direct external import // --------------------------------------------------------------------------- (0, vitest_1.describe)('FactoryAnalyzer — external package types as DI tokens', () => { let dir; (0, vitest_1.beforeEach)(() => { dir = tmpDir('factory-analyzer'); }); (0, vitest_1.afterEach)(() => (0, fs_1.rmSync)(dir, { recursive: true, force: true })); (0, vitest_1.it)('resolves a dep typed as an interface imported directly from node_modules', async () => { // Local interface that resolves fine write(dir, 'ILocalRepo.ts', `export interface ILocalRepo { find(): void; }`); // Factory that depends on both a local and an external interface write(dir, 'MyUseCase.ts', ` import type { ILocalRepo } from './ILocalRepo'; import type { IExternalService } from 'some-external-package'; /** @factory */ export function createMyUseCase(localRepo: ILocalRepo, externalService: IExternalService) { return { localRepo, externalService }; } `); const analyzer = new factory_analyzer_1.FactoryAnalyzer(dir); const filePaths = [(0, path_1.join)(dir, 'ILocalRepo.ts'), (0, path_1.join)(dir, 'MyUseCase.ts')]; const factories = await analyzer.analyzeFiles(filePaths); const uc = factories.find(f => f.name === 'createMyUseCase'); (0, vitest_1.expect)(uc).toBeDefined(); const depNames = uc.dependencies.map(d => d.name); (0, vitest_1.expect)(depNames).toContain('ILocalRepo'); (0, vitest_1.expect)(depNames).toContain('IExternalService'); }); (0, vitest_1.it)('resolves a dep typed via a local re-export stub for an external package', async () => { // Re-export stub: local file that only re-exports from external write(dir, 'ports/ITombstoneRepository.ts', `export type { ITombstoneRepository } from '@word-tracker/sync';`); // Local interface write(dir, 'IWordEntryRepository.ts', `export interface IWordEntryRepository { find(): void; }`); // Factory using both write(dir, 'PermanentlyDeleteWordUseCase.ts', ` import type { IWordEntryRepository } from './IWordEntryRepository'; import type { ITombstoneRepository } from './ports/ITombstoneRepository'; /** @factory */ export function createPermanentlyDeleteWordUseCase(deps: { wordEntryRepository: IWordEntryRepository; tombstoneRepository: ITombstoneRepository; }) { return deps; } `); const analyzer = new factory_analyzer_1.FactoryAnalyzer(dir); const filePaths = [ (0, path_1.join)(dir, 'ports/ITombstoneRepository.ts'), (0, path_1.join)(dir, 'IWordEntryRepository.ts'), (0, path_1.join)(dir, 'PermanentlyDeleteWordUseCase.ts'), ]; const factories = await analyzer.analyzeFiles(filePaths); const uc = factories.find(f => f.name === 'createPermanentlyDeleteWordUseCase'); (0, vitest_1.expect)(uc).toBeDefined(); const depNames = uc.dependencies.map(d => d.name); (0, vitest_1.expect)(depNames).toContain('IWordEntryRepository'); (0, vitest_1.expect)(depNames).toContain('ITombstoneRepository'); }); (0, vitest_1.it)('mixed: local deps and external deps both appear in dependencies[]', async () => { write(dir, 'ILocalA.ts', `export interface ILocalA { a(): void; }`); write(dir, 'ILocalB.ts', `export interface ILocalB { b(): void; }`); write(dir, 'MyService.ts', ` import type { ILocalA } from './ILocalA'; import type { ILocalB } from './ILocalB'; import type { IExternal } from 'external-pkg'; /** @factory */ export function createMyService(a: ILocalA, b: ILocalB, ext: IExternal) { return { a, b, ext }; } `); const analyzer = new factory_analyzer_1.FactoryAnalyzer(dir); const filePaths = [ (0, path_1.join)(dir, 'ILocalA.ts'), (0, path_1.join)(dir, 'ILocalB.ts'), (0, path_1.join)(dir, 'MyService.ts'), ]; const factories = await analyzer.analyzeFiles(filePaths); const svc = factories.find(f => f.name === 'createMyService'); (0, vitest_1.expect)(svc).toBeDefined(); const depNames = svc.dependencies.map(d => d.name); (0, vitest_1.expect)(depNames).toContain('ILocalA'); (0, vitest_1.expect)(depNames).toContain('ILocalB'); (0, vitest_1.expect)(depNames).toContain('IExternal'); }); }); // --------------------------------------------------------------------------- // ClassAnalyzer — external package types as DI tokens // --------------------------------------------------------------------------- (0, vitest_1.describe)('ClassAnalyzer — external package types as DI tokens', () => { let dir; (0, vitest_1.beforeEach)(() => { dir = tmpDir('class-analyzer'); }); (0, vitest_1.afterEach)(() => (0, fs_1.rmSync)(dir, { recursive: true, force: true })); (0, vitest_1.it)('resolves a constructor dep imported directly from an external package', async () => { write(dir, 'ILocalDep.ts', `export interface ILocalDep { doLocal(): void; }`); write(dir, 'MyService.ts', ` import type { ILocalDep } from './ILocalDep'; import type { IExternalDep } from 'some-external-lib'; import type { IMyService } from './IMyService'; export class MyService implements IMyService { constructor(private local: ILocalDep, private ext: IExternalDep) {} } `); write(dir, 'IMyService.ts', `export interface IMyService { run(): void; }`); const analyzer = new class_analyzer_1.ClassAnalyzer(dir); const filePaths = [ (0, path_1.join)(dir, 'ILocalDep.ts'), (0, path_1.join)(dir, 'IMyService.ts'), (0, path_1.join)(dir, 'MyService.ts'), ]; const classes = await analyzer.analyzeFiles(filePaths); const svc = classes.find(c => c.name === 'MyService'); (0, vitest_1.expect)(svc).toBeDefined(); const depNames = svc.dependencies.map(d => d.name); (0, vitest_1.expect)(depNames).toContain('ILocalDep'); (0, vitest_1.expect)(depNames).toContain('IExternalDep'); }); (0, vitest_1.it)('resolves a constructor dep coming via a local re-export stub', async () => { // Re-export stub write(dir, 'ports/IExternalPort.ts', `export type { IExternalPort } from 'monorepo-pkg';`); write(dir, 'IMyService.ts', `export interface IMyService { run(): void; }`); write(dir, 'MyService.ts', ` import type { IExternalPort } from './ports/IExternalPort'; import type { IMyService } from './IMyService'; export class MyService implements IMyService { constructor(private port: IExternalPort) {} run() {} } `); const analyzer = new class_analyzer_1.ClassAnalyzer(dir); const filePaths = [ (0, path_1.join)(dir, 'ports/IExternalPort.ts'), (0, path_1.join)(dir, 'IMyService.ts'), (0, path_1.join)(dir, 'MyService.ts'), ]; const classes = await analyzer.analyzeFiles(filePaths); const svc = classes.find(c => c.name === 'MyService'); (0, vitest_1.expect)(svc).toBeDefined(); const depNames = svc.dependencies.map(d => d.name); (0, vitest_1.expect)(depNames).toContain('IExternalPort'); }); }); //# sourceMappingURL=external-types.test.js.map