@notjustcoders/ioc-arise
Version:
Arise type-safe IoC containers from your code. Zero overhead, zero coupling.
222 lines (217 loc) • 11.1 kB
JavaScript
"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