From b438495d974a5a719eacac5d8f87a5c8dcb0afa1 Mon Sep 17 00:00:00 2001 From: Neko Ayaka Date: Sat, 18 Oct 2025 17:20:05 +0800 Subject: [PATCH] feat(injecta): supported typed use and auto name use --- apps/stage-tamagotchi/src/main/index.ts | 23 ++- package.json | 1 + packages/injecta/package.json | 6 +- packages/injecta/src/global.test.ts | 187 ++++++++++++++++++++++++ packages/injecta/src/global.ts | 28 +++- packages/injecta/src/index.ts | 7 + packages/injecta/src/logger.ts | 6 +- packages/injecta/src/scoped.test.ts | 169 ++++++++++++++++++++- packages/injecta/src/scoped.ts | 113 ++++++++++++-- pnpm-lock.yaml | 20 +-- vitest.config.ts | 9 ++ 11 files changed, 517 insertions(+), 52 deletions(-) create mode 100644 packages/injecta/src/global.test.ts create mode 100644 vitest.config.ts diff --git a/apps/stage-tamagotchi/src/main/index.ts b/apps/stage-tamagotchi/src/main/index.ts index dfa7ee3f6..e6f910920 100644 --- a/apps/stage-tamagotchi/src/main/index.ts +++ b/apps/stage-tamagotchi/src/main/index.ts @@ -108,11 +108,24 @@ async function setupProjectAIRIServerRuntime() { app.whenReady().then(async () => { await setupProjectAIRIServerRuntime() - injecta.setLogger(createLoggLogger()) - injecta.provide('windows:settings', () => setupSettingsWindowReusableFunc()) - injecta.provide<{ settingsWindow: () => Promise }>('windows:main', { dependsOn: { settingsWindow: 'windows:settings' }, build: async ({ dependsOn }) => setupMainWindow(dependsOn) }) - injecta.provide<{ mainWindow: BrowserWindow, settingsWindow: () => Promise }>('tray', { dependsOn: { mainWindow: 'windows:main', settingsWindow: 'windows:settings' }, build: async ({ dependsOn }) => setupTray(dependsOn) }) - injecta.invoke({ dependsOn: { mainWindow: 'windows:main', tray: 'tray' }, callback: noop }) + injecta.setLogger(createLoggLogger(useLogg('injecta').useGlobalConfig())) + + const settingsWindow = injecta.provide('windows:settings', { + build: () => setupSettingsWindowReusableFunc(), + }) + const mainWindow = injecta.provide('windows:main', { + dependsOn: { settingsWindow }, + build: async ({ dependsOn }) => setupMainWindow(dependsOn), + }) + const tray = injecta.provide('app:tray', { + dependsOn: { mainWindow, settingsWindow }, + build: async ({ dependsOn }) => setupTray(dependsOn), + }) + injecta.invoke({ + dependsOn: { mainWindow, tray }, + callback: noop, + }) + injecta.start() // Lifecycle diff --git a/package.json b/package.json index f8ca214cc..27410e028 100644 --- a/package.json +++ b/package.json @@ -99,6 +99,7 @@ "@parcel/watcher", "bufferutil", "electron", + "electron-click-drag-plugin", "es5-ext", "esbuild", "ffmpeg-static", diff --git a/packages/injecta/package.json b/packages/injecta/package.json index 15bb848b8..8dff28727 100644 --- a/packages/injecta/package.json +++ b/packages/injecta/package.json @@ -24,7 +24,9 @@ "typecheck": "tsc --noEmit", "build": "tsdown" }, - "dependencies": { - "@guiiai/logg": "catalog:" + "devDependencies": { + "@guiiai/logg": "catalog:", + "error-stack-parser": "^2.1.4", + "nanoid": "^5.1.6" } } diff --git a/packages/injecta/src/global.test.ts b/packages/injecta/src/global.test.ts new file mode 100644 index 000000000..9b4ccdd5e --- /dev/null +++ b/packages/injecta/src/global.test.ts @@ -0,0 +1,187 @@ +import type { Lifecycle } from './builtin' + +import { beforeEach, describe, expect, it, vi } from 'vitest' + +import { invoke, provide, resetContainer, start, stop } from './global' +import { lifecycle } from './scoped' + +beforeEach(() => { + resetContainer() +}) + +describe('workflow with named', () => { + it('should work with named pattern', async () => { + interface Database { + connect: () => Promise + close: () => Promise + } + + const databaseConnectSpy = vi.fn() + const databaseCloseSpy = vi.fn() + + function createDatabase(params: { lifecycle?: Lifecycle }): Database { + const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy } + params.lifecycle?.appHooks.onStop(async () => await database.close()) + return database + } + + interface WebSocketServer { + start: () => Promise + stop: () => Promise + } + + const webSocketServerStartSpy = vi.fn() + const webSocketServerStopSpy = vi.fn() + + async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { + await params.database.connect() + const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy } + params.lifecycle?.appHooks.onStop(async () => await server.stop()) + return server + } + + provide<{ lifecycle: Lifecycle }>('db', { + dependsOn: { lifecycle: 'lifecycle' }, + build: async ({ dependsOn }) => createDatabase({ lifecycle: dependsOn.lifecycle }), + }) + + provide<{ database: Database, lifecycle: Lifecycle }>('ws', { + dependsOn: { database: 'db', lifecycle: 'lifecycle' }, + build: async ({ dependsOn }) => createWebSocketServer({ database: dependsOn.database, lifecycle: dependsOn.lifecycle }), + }) + + invoke<{ webSocketServer: WebSocketServer }>({ + dependsOn: { webSocketServer: 'ws' }, + callback: async ({ webSocketServer }) => await webSocketServer.start(), + }) + + await start() + await stop() + + // eslint-disable-next-line no-lone-blocks + { + expect(databaseConnectSpy).toHaveBeenCalledTimes(1) + expect(databaseCloseSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1) + } + }) +}) + +describe('workflow with typed', () => { + it('should work with typed named pattern', async () => { + interface Database { + connect: () => Promise + close: () => Promise + } + + const databaseConnectSpy = vi.fn() + const databaseCloseSpy = vi.fn() + + function createDatabase(params: { lifecycle?: Lifecycle }): Database { + const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy } + params.lifecycle?.appHooks.onStop(async () => await database.close()) + return database + } + + interface WebSocketServer { + start: () => Promise + stop: () => Promise + } + + const webSocketServerStartSpy = vi.fn() + const webSocketServerStopSpy = vi.fn() + + async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { + await params.database.connect() + const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy } + params.lifecycle?.appHooks.onStop(async () => await server.stop()) + return server + } + + const database = provide('db', { + dependsOn: { lifecycle }, + build: async ({ dependsOn }) => createDatabase(dependsOn), + }) + + const webSocketServer = provide('ws', { + dependsOn: { database, lifecycle }, + build: async ({ dependsOn }) => createWebSocketServer(dependsOn), + }) + + invoke({ + dependsOn: { webSocketServer }, + callback: async ({ webSocketServer }) => await webSocketServer.start(), + }) + + await start() + await stop() + + // eslint-disable-next-line no-lone-blocks + { + expect(databaseConnectSpy).toHaveBeenCalledTimes(1) + expect(databaseCloseSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1) + } + }) +}) + +describe('workflow with auto name', () => { + it('should work with auto name pattern', async () => { + interface Database { + connect: () => Promise + close: () => Promise + } + + const databaseConnectSpy = vi.fn() + const databaseCloseSpy = vi.fn() + + function createDatabase(params: { lifecycle?: Lifecycle }): Database { + const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy } + params.lifecycle?.appHooks.onStop(async () => await database.close()) + return database + } + + interface WebSocketServer { + start: () => Promise + stop: () => Promise + } + + const webSocketServerStartSpy = vi.fn() + const webSocketServerStopSpy = vi.fn() + + async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { + await params.database.connect() + const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy } + params.lifecycle?.appHooks.onStop(async () => await server.stop()) + return server + } + + const database = provide({ + dependsOn: { lifecycle }, + build: async ({ dependsOn }) => createDatabase(dependsOn), + }) + + const webSocketServer = provide({ + dependsOn: { database, lifecycle }, + build: async ({ dependsOn }) => createWebSocketServer(dependsOn), + }) + + invoke({ + dependsOn: { webSocketServer }, + callback: async ({ webSocketServer }) => await webSocketServer.start(), + }) + + await start() + await stop() + + // eslint-disable-next-line no-lone-blocks + { + expect(databaseConnectSpy).toHaveBeenCalledTimes(1) + expect(databaseCloseSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1) + } + }) +}) diff --git a/packages/injecta/src/global.ts b/packages/injecta/src/global.ts index 1658993ac..4ceded22b 100644 --- a/packages/injecta/src/global.ts +++ b/packages/injecta/src/global.ts @@ -1,21 +1,33 @@ -import type { DependencyMap, InvokeOption, Logger, ProvideOption } from '.' +import type { DependencyMap, InvokeOption, InvokeOptionWithKeys, Logger, ProvideOption, ProvideOptionWithKeys, ResolveDependencyDeclaration } from '.' +import type { ProvidedKey } from './scoped' import { createContainer, provide as indexProvide, start as indexStart, stop as indexStop } from '.' -const globalContainer = createContainer() +let globalContainer = createContainer() export function setLogger(logger: Logger) { globalContainer.logger = logger } -export function provide( - name: string, - option: ProvideOption, -): void { - indexProvide(globalContainer, name, option) +export function resetContainer() { + globalContainer = createContainer() } -export function invoke(option: InvokeOption): void { +export function provide>>(name: Key, option: ProvideOptionWithKeys,): ProvidedKey> +export function provide(name: Key, option: ProvideOption,): ProvidedKey +export function provide>>(option: ProvideOptionWithKeys,): ProvidedKey> +export function provide(option: ProvideOption,): ProvidedKey +export function provide(nameOrOption: Key | ProvideOption | ProvideOptionWithKeys, option?: ProvideOption | ProvideOptionWithKeys): ProvidedKey { + if (option != null && typeof option === 'function') { + return indexProvide(globalContainer, nameOrOption as any, { build: option, autoNameStackIndex: 2 } as any) + } + + return indexProvide(globalContainer, nameOrOption as any, { ...option, autoNameStackIndex: 2 } as any) +} + +export function invoke>>(option: InvokeOptionWithKeys): void +export function invoke(option: InvokeOption): void +export function invoke(option: InvokeOption | InvokeOptionWithKeys): void { if (typeof option === 'function') { globalContainer.invocations.push({ callback: option } as any) } diff --git a/packages/injecta/src/index.ts b/packages/injecta/src/index.ts index 534a50e1d..b0e0aab75 100644 --- a/packages/injecta/src/index.ts +++ b/packages/injecta/src/index.ts @@ -19,6 +19,7 @@ export { export { createContainer, invoke, + lifecycle, provide, start, stop, @@ -30,9 +31,15 @@ export type { InvokeOption, InvokeOptionFunc, InvokeOptionObject, + InvokeOptionObjectWithKeys, + InvokeOptionWithKeys, + ProvidedKey, ProvideOption, ProvideOptionFunc, ProvideOptionObject, + ProvideOptionObjectWithKeys, + ProvideOptionWithKeys, + ResolveDependencyDeclaration, } from './scoped' export const injecta = { diff --git a/packages/injecta/src/logger.ts b/packages/injecta/src/logger.ts index 66cad8702..de961a7d9 100644 --- a/packages/injecta/src/logger.ts +++ b/packages/injecta/src/logger.ts @@ -1,4 +1,4 @@ -import { useLogg } from '@guiiai/logg' +import type { Logg as LoggLogger } from '@guiiai/logg' import { name } from '../package.json' @@ -80,9 +80,7 @@ export function createDefaultLogger(): Logger { } } -export function createLoggLogger(): Logger { - const logg = useLogg(name).useGlobalConfig() - +export function createLoggLogger(logg: LoggLogger): Logger { return { provide: (name: string, dependencies: string[]) => { const depsStr = dependencies.length > 0 ? ` (depends on: ${dependencies.join(', ')})` : '' diff --git a/packages/injecta/src/scoped.test.ts b/packages/injecta/src/scoped.test.ts index 06d8a1b68..3ff408eef 100644 --- a/packages/injecta/src/scoped.test.ts +++ b/packages/injecta/src/scoped.test.ts @@ -2,10 +2,39 @@ import type { Lifecycle } from './builtin' import { describe, expect, it, vi } from 'vitest' -import { invoke, provide, start, stop } from './global' +import { createContainer, invoke, lifecycle, normalizeName, normalizeProvideOption, provide, start, stop } from './scoped' -describe('di', () => { - it('should work with individual lifecycle injection', async () => { +describe('normalizeName', () => { + it('should normalize names correctly', () => { + expect(normalizeName('simpleName')).toBe('simpleName') + expect(normalizeName({ key: 'objectName' })).toBe('objectName') + }) +}) + +describe('normalizeProvideOption', () => { + it('should normalize provide options correctly', () => { + expect(() => normalizeProvideOption('simpleName')).toThrowError('When using provide(...) as named callback, the second argument must be either a valid ProvideOptionObject or a ProvideOptionFunc.') + expect(() => normalizeProvideOption({ key: 'objectName' })).toThrowError('When using provide(...) as typed ProvideOptionWithKeys callback, the second argument must be either a valid ProvideOptionObject or a ProvideOptionFunc.') + + const namedOption = normalizeProvideOption('name', () => {}) + expect(namedOption).toBeTypeOf('object') + expect(namedOption).toHaveProperty('build') + expect(() => namedOption.build({} as any)).not.toThrow() + + const typedOption = normalizeProvideOption({ key: 'name' }, () => {}) + expect(typedOption).toBeTypeOf('object') + expect(typedOption).toHaveProperty('build') + expect(() => typedOption.build({} as any)).not.toThrow() + + const autoNameOption = normalizeProvideOption(() => {}) + expect(autoNameOption).toBeTypeOf('object') + expect(autoNameOption).toHaveProperty('build') + expect(() => autoNameOption.build({} as any)).not.toThrow() + }) +}) + +describe('workflow with named', () => { + it('should work with named pattern', async () => { interface Database { connect: () => Promise close: () => Promise @@ -35,23 +64,147 @@ describe('di', () => { return server } - provide<{ lifecycle: Lifecycle }>('db', { + const app = createContainer() + + provide<{ lifecycle: Lifecycle }>(app, 'db', { dependsOn: { lifecycle: 'lifecycle' }, build: async ({ dependsOn }) => createDatabase({ lifecycle: dependsOn.lifecycle }), }) - provide<{ database: Database, lifecycle: Lifecycle }>('ws', { + provide<{ database: Database, lifecycle: Lifecycle }>(app, 'ws', { dependsOn: { database: 'db', lifecycle: 'lifecycle' }, build: async ({ dependsOn }) => createWebSocketServer({ database: dependsOn.database, lifecycle: dependsOn.lifecycle }), }) - invoke<{ webSocketServer: WebSocketServer }>({ + invoke<{ webSocketServer: WebSocketServer }>(app, { dependsOn: { webSocketServer: 'ws' }, callback: async ({ webSocketServer }) => await webSocketServer.start(), }) - await start() - await stop() + await start(app) + await stop(app) + + // eslint-disable-next-line no-lone-blocks + { + expect(databaseConnectSpy).toHaveBeenCalledTimes(1) + expect(databaseCloseSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1) + } + }) +}) + +describe('workflow with typed', () => { + it('should work with typed named pattern', async () => { + interface Database { + connect: () => Promise + close: () => Promise + } + + const databaseConnectSpy = vi.fn() + const databaseCloseSpy = vi.fn() + + function createDatabase(params: { lifecycle?: Lifecycle }): Database { + const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy } + params.lifecycle?.appHooks.onStop(async () => await database.close()) + return database + } + + interface WebSocketServer { + start: () => Promise + stop: () => Promise + } + + const webSocketServerStartSpy = vi.fn() + const webSocketServerStopSpy = vi.fn() + + async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { + await params.database.connect() + const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy } + params.lifecycle?.appHooks.onStop(async () => await server.stop()) + return server + } + + const app = createContainer() + + const database = provide(app, 'db', { + dependsOn: { lifecycle }, + build: async ({ dependsOn }) => createDatabase(dependsOn), + }) + + const webSocketServer = provide(app, 'ws', { + dependsOn: { database, lifecycle }, + build: async ({ dependsOn }) => createWebSocketServer(dependsOn), + }) + + invoke(app, { + dependsOn: { webSocketServer }, + callback: async ({ webSocketServer }) => await webSocketServer.start(), + }) + + await start(app) + await stop(app) + + // eslint-disable-next-line no-lone-blocks + { + expect(databaseConnectSpy).toHaveBeenCalledTimes(1) + expect(databaseCloseSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStartSpy).toHaveBeenCalledTimes(1) + expect(webSocketServerStopSpy).toHaveBeenCalledTimes(1) + } + }) +}) + +describe('workflow with auto name', () => { + it('should work with auto name pattern', async () => { + interface Database { + connect: () => Promise + close: () => Promise + } + + const databaseConnectSpy = vi.fn() + const databaseCloseSpy = vi.fn() + + function createDatabase(params: { lifecycle?: Lifecycle }): Database { + const database: Database = { connect: databaseConnectSpy, close: databaseCloseSpy } + params.lifecycle?.appHooks.onStop(async () => await database.close()) + return database + } + + interface WebSocketServer { + start: () => Promise + stop: () => Promise + } + + const webSocketServerStartSpy = vi.fn() + const webSocketServerStopSpy = vi.fn() + + async function createWebSocketServer(params: { database: Database, lifecycle?: Lifecycle }): Promise { + await params.database.connect() + const server: WebSocketServer = { start: webSocketServerStartSpy, stop: webSocketServerStopSpy } + params.lifecycle?.appHooks.onStop(async () => await server.stop()) + return server + } + + const app = createContainer() + + const database = provide(app, { + dependsOn: { lifecycle }, + build: async ({ dependsOn }) => createDatabase(dependsOn), + }) + + const webSocketServer = provide(app, { + dependsOn: { database, lifecycle }, + build: async ({ dependsOn }) => createWebSocketServer(dependsOn), + }) + + invoke(app, { + dependsOn: { webSocketServer }, + callback: async ({ webSocketServer }) => await webSocketServer.start(), + }) + + await start(app) + await stop(app) // eslint-disable-next-line no-lone-blocks { diff --git a/packages/injecta/src/scoped.ts b/packages/injecta/src/scoped.ts index 6e7263423..15ea2392d 100644 --- a/packages/injecta/src/scoped.ts +++ b/packages/injecta/src/scoped.ts @@ -1,6 +1,10 @@ -import type { LifecycleTriggerable } from './builtin' +import type { Lifecycle, LifecycleTriggerable } from './builtin' import type { Logger, LoggerOptions } from './logger' +import ErrorStackParser from 'error-stack-parser' + +import { nanoid } from 'nanoid' + import { createDefaultLogger, createNoopLogger } from './logger' export type DependencyMap = Record @@ -19,14 +23,32 @@ export type BuildContext = { name: string } & (D extends undefined ? { dependsOn?: unknown } : { dependsOn: D }) +export type ResolveDependencyDeclaration>> = { + [K in keyof Deps]: Deps[K] extends ProvidedKey ? T : any +} + export type ProvideOptionObject = { build: (context: BuildContext) => T | Promise } & (D extends undefined ? { dependsOn?: Record } : { dependsOn: { [K in keyof D]: string } }) export type ProvideOptionFunc = (context: BuildContext) => T | Promise export type ProvideOption = ProvideOptionObject | ProvideOptionFunc +export interface ProvideOptionObjectWithKeys>> { + build: (context: BuildContext>) => T | Promise + dependsOn: Deps +} +export type ProvideOptionWithKeys>> = ProvideOptionObjectWithKeys + export type InvokeOptionObject = { callback: (dependencies: D) => void | Promise } & (D extends undefined ? { dependsOn?: Record } : { dependsOn: { [K in keyof D]: string } }) export type InvokeOptionFunc = (dependencies: D) => void | Promise export type InvokeOption = InvokeOptionObject | InvokeOptionFunc +export interface InvokeOptionObjectWithKeys>> { + callback: (dependencies: ResolveDependencyDeclaration) => void | Promise + dependsOn: Deps +} +export type InvokeOptionWithKeys>> = InvokeOptionObjectWithKeys + +export const lifecycle: ProvidedKey<'lifecycle', Lifecycle, undefined> = { key: 'lifecycle' } + export function createContainer(options?: LoggerOptions): Container { const logger = options?.enabled === false ? createNoopLogger() @@ -42,29 +64,93 @@ export function createContainer(options?: LoggerOptions): Container { } } -export function provide( - container: Container, - name: string, - option: ProvideOption, -): void { - const providerObject = typeof option === 'function' - ? { build: option } as ProvideOptionObject - : option as ProvideOptionObject +// eslint-disable-next-line unused-imports/no-unused-vars +export interface ProvidedKey { + key: Key +} + +export function normalizeName(nameOrProvidedKey: string | ProvidedKey): string { + if (typeof nameOrProvidedKey === 'object' && nameOrProvidedKey !== null && 'key' in nameOrProvidedKey) { + return (nameOrProvidedKey as ProvidedKey).key as string + } + + return nameOrProvidedKey as string +} + +export function normalizeProvideOption(nameOrOption: Key | ProvidedKey | ProvideOption | ProvideOptionWithKeys, option?: (ProvideOption | ProvideOptionWithKeys) & { autoNameStackIndex?: number }): ProvideOptionObject { + if (typeof nameOrOption === 'string') { + if (option == null || (!('build' in option) && typeof option !== 'function')) { + throw new Error('When using provide(...) as named callback, the second argument must be either a valid ProvideOptionObject or a ProvideOptionFunc.') + } + if (typeof option === 'function') { + return { build: option } as ProvideOptionObject + } + + return option as ProvideOptionObject + } + if (typeof nameOrOption === 'object' && 'key' in nameOrOption) { + if (option == null || (!('build' in option) && typeof option !== 'function')) { + throw new Error('When using provide(...) as typed ProvideOptionWithKeys callback, the second argument must be either a valid ProvideOptionObject or a ProvideOptionFunc.') + } + if (typeof option === 'function') { + return { build: option } as ProvideOptionObject + } + + return option as ProvideOptionObject + } + + if (typeof nameOrOption === 'function') { + return { build: nameOrOption } as ProvideOptionObject + } + + return nameOrOption as ProvideOptionObject +} + +export function provide>>(container: Container, name: Key, option: ProvideOptionWithKeys,): ProvidedKey> +export function provide>>(container: Container, option: ProvideOptionWithKeys & { autoNameStackIndex?: number }): ProvidedKey> +export function provide(container: Container, name: Key, option: ProvideOption,): ProvidedKey +export function provide(container: Container, option: ProvideOption & { autoNameStackIndex?: number }): ProvidedKey +export function provide(container: Container, nameOrOption: Key | ProvideOption | ProvideOptionWithKeys, option?: (ProvideOption | ProvideOptionWithKeys) & { autoNameStackIndex?: number }): ProvidedKey { + const parentFile = ErrorStackParser.parse(new Error('providing'))[option?.autoNameStackIndex ?? 1] + const name = typeof nameOrOption === 'string' + ? nameOrOption + : `${parentFile.fileName ?? `unknown-${nanoid()}`}:${parentFile.lineNumber ?? 0}:${parentFile.columnNumber ?? 0}` + + const providerObject = normalizeProvideOption(nameOrOption, option) as unknown as ProvideOptionObject + if (providerObject.dependsOn) { + const resolvedDependsOn: Record = {} + for (const [key, value] of Object.entries(providerObject.dependsOn)) { + resolvedDependsOn[key] = normalizeName(value) + } + + providerObject.dependsOn = resolvedDependsOn + } container.providers.set(name, providerObject) - // Track dependencies for lifecycle ordering const dependencies = providerObject.dependsOn ? Object.values(providerObject.dependsOn) : [] - container.logger.provide(name, dependencies) container.dependencyGraph.set(name, dependencies) + + return { key: name } as ProvidedKey } -export function invoke(container: Container, option: InvokeOption): void { +export function invoke>>(container: Container, option: InvokeOptionWithKeys): void +export function invoke(container: Container, option: InvokeOption): void +export function invoke(container: Container, option: InvokeOption | InvokeOptionWithKeys): void { const invocationObject = typeof option === 'function' ? { callback: option } as InvokeOptionObject : option as InvokeOptionObject + if (invocationObject.dependsOn) { + const resolvedDependsOn: Record = {} + for (const [key, value] of Object.entries(invocationObject.dependsOn)) { + resolvedDependsOn[key] = normalizeName(value) + } + + invocationObject.dependsOn = resolvedDependsOn + } + const dependencies = invocationObject.dependsOn ? Object.values(invocationObject.dependsOn) : [] container.logger.invoke(dependencies) @@ -91,6 +177,7 @@ async function resolveInstance(container: Container, name: string): Promise { if (invocation.dependsOn) { for (const [key, depName] of Object.entries(invocation.dependsOn)) { - resolvedDependencies[key] = await resolveInstance(container, depName) + resolvedDependencies[key] = await resolveInstance(container, normalizeName(depName)) } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index db01884a3..cd426d8c4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1148,10 +1148,16 @@ importers: version: 3.0.6(@nuxt/kit@4.0.3(magicast@0.3.5))(@nuxt/schema@4.0.3)(astro@5.10.1(@netlify/blobs@9.1.2)(@types/node@24.7.2)(db0@0.3.2(drizzle-orm@0.44.6(@opentelemetry/api@1.9.0)(@types/pg@8.15.5)(gel@2.0.0)(pg@8.16.3)(postgres@3.4.7)))(ioredis@5.7.0)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(rollup@4.52.4)(terser@5.43.1)(tsx@4.20.6)(typescript@5.9.3)(yaml@2.8.1))(esbuild@0.25.9)(rolldown@1.0.0-beta.43)(rollup@4.52.4)(vite@7.1.9(@types/node@24.7.2)(jiti@2.5.1)(less@4.4.2)(lightningcss@1.30.2)(terser@5.43.1)(tsx@4.20.6)(yaml@2.8.1)) packages/injecta: - dependencies: + devDependencies: '@guiiai/logg': specifier: 'catalog:' version: 1.1.0 + error-stack-parser: + specifier: ^2.1.4 + version: 2.1.4 + nanoid: + specifier: ^5.1.6 + version: 5.1.6 packages/memory-pgvector: dependencies: @@ -4619,9 +4625,6 @@ packages: '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} - '@napi-rs/wasm-runtime@1.0.6': - resolution: {integrity: sha512-DXj75ewm11LIWUk198QSKUTxjyRjsBwk09MuMk5DGK+GDUtyPhhEHOGP/Xwwj3DjQXXkivoBirmOnKrLfc0+9g==} - '@napi-rs/wasm-runtime@1.0.7': resolution: {integrity: sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw==} @@ -18743,13 +18746,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@napi-rs/wasm-runtime@1.0.6': - dependencies: - '@emnapi/core': 1.5.0 - '@emnapi/runtime': 1.5.0 - '@tybys/wasm-util': 0.10.1 - optional: true - '@napi-rs/wasm-runtime@1.0.7': dependencies: '@emnapi/core': 1.5.0 @@ -20782,7 +20778,7 @@ snapshots: '@rolldown/binding-wasm32-wasi@1.0.0-beta.42': dependencies: - '@napi-rs/wasm-runtime': 1.0.6 + '@napi-rs/wasm-runtime': 1.0.7 optional: true '@rolldown/binding-wasm32-wasi@1.0.0-beta.43': diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 000000000..7f40d85e6 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: [ + 'packages/injecta', + ], + }, +})