diff --git a/package.json b/package.json index ae6a6c8..c4b7a50 100644 --- a/package.json +++ b/package.json @@ -41,9 +41,6 @@ "lint": "oxlint --fix && oxfmt", "check": "tsc && oxlint && oxfmt --check" }, - "dependencies": { - "@needle-di/core": "^1.1.2" - }, "devDependencies": { "@types/node": "^25.9.1", "oxfmt": "^0.52.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 51f1e20..5da2774 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -206,10 +206,6 @@ settings: importers: .: - dependencies: - '@needle-di/core': - specifier: ^1.1.2 - version: 1.1.2 devDependencies: '@types/node': specifier: ^25.9.1 @@ -287,9 +283,6 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 - '@needle-di/core@1.1.2': - resolution: {integrity: sha512-9CVnXjK33JeqUwAz5k/D+ti4ZfJ17m5AXsUcOwErbQ8baTNDPt1erVFGjipxxwcUj86Oxap3dALz6U70xsUsFg==} - '@oxc-project/types@0.132.0': resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} @@ -1464,8 +1457,6 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true - '@needle-di/core@1.1.2': {} - '@oxc-project/types@0.132.0': {} '@oxc-project/types@0.133.0': {} diff --git a/skill/example/index.ts b/skill/example/index.ts index 08f85e1..e837ccb 100644 --- a/skill/example/index.ts +++ b/skill/example/index.ts @@ -13,7 +13,7 @@ const allModules = [CoreLogging, AlertDispatch] as const; type AllModules = typeof allModules; class PolisAlert { - private ctx?: Context; + private ctx?: Context; dispatchAlert: (message: string) => Promise; log: (level: Level, message: string) => void; diff --git a/src/types.ts b/src/context.ts similarity index 53% rename from src/types.ts rename to src/context.ts index c85c4ca..ab4958f 100644 --- a/src/types.ts +++ b/src/context.ts @@ -7,9 +7,6 @@ type ModuleConstructor = new (context: C) => General; type GeneralModuleInput = ReadonlyArray | ReadonlyArray; -type Instances = - T extends ReadonlyArray ? { [K in keyof T]: InstanceType } : T; - export type ModuleInput = | ReadonlyArray | ReadonlyArray>; @@ -20,87 +17,92 @@ type IsPlainObject = T extends object : true : false; -type ShallowMergeValue = +type ShallowMerge = IsPlainObject extends true ? (IsPlainObject extends true ? Omit & B : B) : B; -type ShallowMergeObjects = { - [K in keyof A | keyof B]: K extends keyof B - ? K extends keyof A - ? ShallowMergeValue - : B[K] - : K extends keyof A - ? A[K] - : never; +type Keys = T extends any ? keyof T : never; + +type InstanceEach = + T extends ReadonlyArray ? { [K in keyof T]: InstanceType } : T; + +type PickEach, K extends PropertyKey> = { + [I in keyof T]: Pick>; }; -type RootValue = R extends keyof T - ? Extract - : {}; +type RootValue = RootKey extends keyof T ? Extract : {}; +type MergeRootEach> = { + [I in keyof T]: ShallowMerge, RootValue>; +}; -type PickMerged = ShallowMergeObjects< - Pick>, - RootValue ->; +type ExtractKeyEach, K extends Keys> = { + [I in keyof T]: K extends keyof T[I] ? T[I][K] : never; +}; -type PickEach, K extends keyof T[number], R extends PropertyKey> = { - [I in keyof T]: PickMerged, R>; +type MergeObjects< + T extends ReadonlyArray, + O extends ReadonlyArray = MergeRootEach, +> = { + [P in Keys]: MergeValues>; }; -type MergeObjects> = T extends readonly [ - infer First extends object, - ...infer Rest extends ReadonlyArray, -] - ? Rest['length'] extends 0 - ? First - : ShallowMergeObjects> - : {}; +export type MergeSingleKey< + M extends GeneralModuleInput, + K extends Keys[number]>, +> = MergeValues, K>>; +type MergePair = [A] extends [never] ? B : [B] extends [never] ? A : ShallowMerge; type MergeValues> = T extends readonly [infer First, ...infer Rest] - ? Rest extends ReadonlyArray - ? Rest['length'] extends 0 - ? First - : ShallowMergeValue> - : never - : {}; + ? [First] extends [never] + ? MergeValues + : Rest extends ReadonlyArray + ? Rest['length'] extends 0 + ? First + : MergePair> + : never + : never; type MergeResult< M extends GeneralModuleInput, - K extends keyof Instances[number], + K extends Keys[number]>, Pr extends object, Po extends object, - R extends string, -> = MergeObjects<[Pr, ...PickEach, K, R>, Po]>; +> = MergeObjects<[Pr, ...PickEach, K>, Po]>; export type Context< M extends ReadonlyArray>, - K extends keyof Instances[number], + K extends Keys[number]>, Pr extends object = {}, Po extends object = {}, - R extends string = 'root', -> = MergeResult & { +> = MergeResult & { __modules__: WeakMap>; __getModule__: (ctor: C) => InstanceType; - __addModule__: >>( + __addModule__: >>( newModule: N, - ) => Context<[...M, N], K, Pr, Po, R>; + ) => Context<[...M, N], K, Pr, Po>; }; +const ROOT_KEY = 'root'; +type RootKey = typeof ROOT_KEY; + function isPlainObject(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } +function assignShallow(target: Record, key: string, value: unknown) { + if (value === undefined) return target; + const current = target[key]; + target[key] = + isPlainObject(current) && isPlainObject(value) + ? { ...current, ...value } + : isPlainObject(value) + ? { ...value } + : value; + return target; +} + function mergeShallow(target: Record, source: unknown) { if (!isPlainObject(source)) return target; - for (const key of Object.keys(source)) { - const sourceValue = source[key]; - if (sourceValue === undefined) continue; - const targetValue = target[key]; - if (isPlainObject(targetValue) && isPlainObject(sourceValue)) { - target[key] = { ...targetValue, ...sourceValue }; - continue; - } - target[key] = isPlainObject(sourceValue) ? { ...sourceValue } : sourceValue; - } + for (const key of Object.keys(source)) assignShallow(target, key, source[key]); return target; } @@ -108,16 +110,15 @@ function mergeShallow(target: Record, source: unknown) { * Creates shared context from ordered modules. * * Merge order: `preMerge` -> module keys -> `postMerge` -> `assign`. - * Object values merge shallowly. `rootKey` flattens matching module value into - * context root instead of assigning `context[rootKey]`. + * Object values merge shallowly. Module key `root` flattens into context root + * instead of assigning `context.root`. * * After final merge, `injectKeys` are written back to every instance. When - * omitted, `mergeKeys` are reused. Injecting `rootKey` writes whole context. + * omitted, `mergeKeys` are reused. Injecting `root` writes whole context. * * Returned context also exposes `__modules__`, `__getModule__`, and * `__addModule__` as non-enumerable helpers. * - * @typeParam R Root merge key. Defaults to `root`. * @typeParam Po Object merged after module output. * @typeParam Pr Object merged before module construction. * @typeParam M Ordered module constructor list. @@ -125,7 +126,6 @@ function mergeShallow(target: Record, source: unknown) { * @typeParam I Keys injected back into module instances. * @param classes Module constructors instantiated in order. * @param options Context build options. - * @param options.rootKey Key flattened into context root. Defaults to `root`. * @param options.preMerge Values merged before module construction. * @param options.postMerge Values merged after module merges. * @param options.assign Final values assigned after all other merges. @@ -137,67 +137,64 @@ function mergeShallow(target: Record, source: unknown) { export default function createContext< Po extends object, Pr extends object, - M extends ReadonlyArray>>, - K extends keyof Instances[number], - R extends string = 'root', + M extends ReadonlyArray>>, + K extends Keys[number]>, I extends K = K, >( classes: M, options: { - rootKey?: R; preMerge?: Pr; postMerge?: Po; - assign?: Partial>; + assign?: Partial>; mergeKeys: ReadonlyArray; injectKeys?: ReadonlyArray; }, -): Context { +): Context { const context: Record = {}; - const rootKey = (options.rootKey ?? 'root') as R; - const mergeKeys = options.mergeKeys as ReadonlyArray; + const mergeKeys = options.mergeKeys; const injectKeys = (options.injectKeys ?? options.mergeKeys) as ReadonlyArray; const instances: Array> = []; const modules = new WeakMap>(); const mergeInstance = (instance: Record) => { for (const key of mergeKeys) { - const value = instance[key]; - if (value === undefined) continue; - if (key === rootKey) { + const value = instance[key as string]; + if (key === ROOT_KEY) { if (isPlainObject(value)) mergeShallow(context, value); continue; } - - const current = context[key]; - context[key] = - isPlainObject(current) && isPlainObject(value) - ? { ...current, ...value } - : isPlainObject(value) - ? { ...value } - : value; + assignShallow(context, key as string, value); } }; + const finalizeContext = () => { + mergeShallow(context, options.postMerge); + mergeShallow(context, options.assign); + injectContext(); + }; + + const registerModule = >(Class: C) => { + const instance = new Class(context as Context); + const record = instance as Record; + modules.set(Class, instance); + instances.push(record); + mergeInstance(record); + }; + const injectContext = () => { for (const instance of instances) - for (const key of injectKeys) instance[key] = key === rootKey ? context : context[key]; + for (const key of injectKeys) instance[key] = key === ROOT_KEY ? context : context[key]; }; Object.defineProperties(context, { __addModule__: { enumerable: false, - value: >>( + value: >>( newModule: N, ) => { - const instance = new newModule(context as Context<[...M, N], K, Pr, Po, R>); - const record = instance as Record; - modules.set(newModule, instance); - instances.push(record); - mergeInstance(record); - mergeShallow(context, options.postMerge); - mergeShallow(context, options.assign); - injectContext(); - return context as Context<[...M, N], K, Pr, Po, R>; + registerModule(newModule); + finalizeContext(); + return context as Context<[...M, N], K, Pr, Po>; }, }, __getModule__: { @@ -215,26 +212,7 @@ export default function createContext< }); mergeShallow(context, options.preMerge); - - for (const Class of classes) { - const instance = new Class(context as Context); - const record = instance as Record; - modules.set(Class, instance); - instances.push(record); - mergeInstance(record); - } - - mergeShallow(context, options.postMerge); - mergeShallow(context, options.assign); - injectContext(); - - return context as Context; + for (const Class of classes) registerModule(Class); + finalizeContext(); + return context as Context; } - -type ExtractKeyEach, K extends keyof T[number]> = { - [I in keyof T]: K extends keyof T[I] ? T[I][K] : never; -}; -export type MergeSingleKey< - M extends GeneralModuleInput, - K extends keyof Instances[number], -> = MergeValues, K>>; diff --git a/src/index.ts b/src/index.ts index bc93908..e5706bb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,3 @@ -export { default as createContext } from './types'; -export type * from './types'; +export { default as createContext } from './context'; +export type * from './context'; export * from './reactive';