Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 0 additions & 9 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion skill/example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const allModules = [CoreLogging, AlertDispatch] as const;
type AllModules = typeof allModules;

class PolisAlert {
private ctx?: Context<AllModules, 'options'>;
private ctx?: Context<AllModules, 'options' | 'root'>;

dispatchAlert: (message: string) => Promise<boolean>;
log: (level: Level, message: string) => void;
Expand Down
198 changes: 88 additions & 110 deletions src/types.ts → src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ type ModuleConstructor<C extends object> = new (context: C) => General;

type GeneralModuleInput = ReadonlyArray<GeneralConstructor> | ReadonlyArray<GeneralObject>;

type Instances<T extends GeneralModuleInput> =
T extends ReadonlyArray<GeneralConstructor> ? { [K in keyof T]: InstanceType<T[K]> } : T;

export type ModuleInput<T extends GeneralConstructor> =
| ReadonlyArray<T>
| ReadonlyArray<InstanceType<T>>;
Expand All @@ -20,112 +17,115 @@ type IsPlainObject<T> = T extends object
: true
: false;

type ShallowMergeValue<A, B> =
type ShallowMerge<A, B> =
IsPlainObject<A> extends true ? (IsPlainObject<B> extends true ? Omit<A, keyof B> & B : B) : B;

type ShallowMergeObjects<A extends object, B extends object> = {
[K in keyof A | keyof B]: K extends keyof B
? K extends keyof A
? ShallowMergeValue<A[K], B[K]>
: B[K]
: K extends keyof A
? A[K]
: never;
type Keys<T> = T extends any ? keyof T : never;

type InstanceEach<T extends GeneralModuleInput> =
T extends ReadonlyArray<GeneralConstructor> ? { [K in keyof T]: InstanceType<T[K]> } : T;

type PickEach<T extends ReadonlyArray<object>, K extends PropertyKey> = {
[I in keyof T]: Pick<T[I], Extract<K, keyof T[I]>>;
};

type RootValue<T extends object, R extends PropertyKey> = R extends keyof T
? Extract<T[R], object>
: {};
type RootValue<T extends object> = RootKey extends keyof T ? Extract<T[RootKey], object> : {};
type MergeRootEach<T extends ReadonlyArray<object>> = {
[I in keyof T]: ShallowMerge<Omit<T[I], RootKey>, RootValue<T[I]>>;
};

type PickMerged<T extends object, K extends keyof T, R extends PropertyKey> = ShallowMergeObjects<
Pick<T, Exclude<K, R>>,
RootValue<T, R>
>;
type ExtractKeyEach<T extends ReadonlyArray<object>, K extends Keys<T>> = {
[I in keyof T]: K extends keyof T[I] ? T[I][K] : never;
};

type PickEach<T extends ReadonlyArray<object>, K extends keyof T[number], R extends PropertyKey> = {
[I in keyof T]: PickMerged<T[I], Extract<K, keyof T[I]>, R>;
type MergeObjects<
T extends ReadonlyArray<object>,
O extends ReadonlyArray<object> = MergeRootEach<T>,
> = {
[P in Keys<O[number]>]: MergeValues<ExtractKeyEach<O, P>>;
};

type MergeObjects<T extends ReadonlyArray<object>> = T extends readonly [
infer First extends object,
...infer Rest extends ReadonlyArray<object>,
]
? Rest['length'] extends 0
? First
: ShallowMergeObjects<First, MergeObjects<Rest>>
: {};
export type MergeSingleKey<
M extends GeneralModuleInput,
K extends Keys<InstanceEach<M>[number]>,
> = MergeValues<ExtractKeyEach<InstanceEach<M>, K>>;

type MergePair<A, B> = [A] extends [never] ? B : [B] extends [never] ? A : ShallowMerge<A, B>;
type MergeValues<T extends ReadonlyArray<unknown>> = T extends readonly [infer First, ...infer Rest]
? Rest extends ReadonlyArray<unknown>
? Rest['length'] extends 0
? First
: ShallowMergeValue<First, MergeValues<Rest>>
: never
: {};
? [First] extends [never]
? MergeValues<Rest>
: Rest extends ReadonlyArray<unknown>
? Rest['length'] extends 0
? First
: MergePair<First, MergeValues<Rest>>
: never
: never;

type MergeResult<
M extends GeneralModuleInput,
K extends keyof Instances<M>[number],
K extends Keys<InstanceEach<M>[number]>,
Pr extends object,
Po extends object,
R extends string,
> = MergeObjects<[Pr, ...PickEach<Instances<M>, K, R>, Po]>;
> = MergeObjects<[Pr, ...PickEach<InstanceEach<M>, K>, Po]>;

export type Context<
M extends ReadonlyArray<ModuleConstructor<General>>,
K extends keyof Instances<M>[number],
K extends Keys<InstanceEach<M>[number]>,
Pr extends object = {},
Po extends object = {},
R extends string = 'root',
> = MergeResult<M, K, Pr, Po, R> & {
> = MergeResult<M, K, Pr, Po> & {
__modules__: WeakMap<M[number], InstanceType<M[number]>>;
__getModule__: <C extends M[number]>(ctor: C) => InstanceType<C>;
__addModule__: <N extends ModuleConstructor<MergeResult<[...M, N], K, Pr, Po, R>>>(
__addModule__: <N extends ModuleConstructor<MergeResult<[...M, N], K, Pr, Po>>>(
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<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value);
}

function assignShallow(target: Record<string, unknown>, 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<string, unknown>, 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;
}

/**
* 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.
* @typeParam K Keys copied from modules into context.
* @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.
Expand All @@ -137,67 +137,64 @@ function mergeShallow(target: Record<string, unknown>, source: unknown) {
export default function createContext<
Po extends object,
Pr extends object,
M extends ReadonlyArray<ModuleConstructor<Context<M, K, Pr, Po, R>>>,
K extends keyof Instances<M>[number],
R extends string = 'root',
M extends ReadonlyArray<ModuleConstructor<Context<M, K, Pr, Po>>>,
K extends Keys<InstanceEach<M>[number]>,
I extends K = K,
>(
classes: M,
options: {
rootKey?: R;
preMerge?: Pr;
postMerge?: Po;
assign?: Partial<MergeResult<M, K, Pr, Po, R>>;
assign?: Partial<MergeResult<M, K, Pr, Po>>;
mergeKeys: ReadonlyArray<K>;
injectKeys?: ReadonlyArray<I>;
},
): Context<M, K, Pr, Po, R> {
): Context<M, K, Pr, Po> {
const context: Record<string, unknown> = {};
const rootKey = (options.rootKey ?? 'root') as R;
const mergeKeys = options.mergeKeys as ReadonlyArray<string>;
const mergeKeys = options.mergeKeys;
const injectKeys = (options.injectKeys ?? options.mergeKeys) as ReadonlyArray<string>;
const instances: Array<Record<string, unknown>> = [];
const modules = new WeakMap<ModuleConstructor<General>>();

const mergeInstance = (instance: Record<string, unknown>) => {
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 = <C extends ModuleConstructor<General>>(Class: C) => {
const instance = new Class(context as Context<M, K, Pr, Po>);
const record = instance as Record<string, unknown>;
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: <N extends ModuleConstructor<MergeResult<[...M, N], K, Pr, Po, R>>>(
value: <N extends ModuleConstructor<MergeResult<[...M, N], K, Pr, Po>>>(
newModule: N,
) => {
const instance = new newModule(context as Context<[...M, N], K, Pr, Po, R>);
const record = instance as Record<string, unknown>;
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__: {
Expand All @@ -215,26 +212,7 @@ export default function createContext<
});

mergeShallow(context, options.preMerge);

for (const Class of classes) {
const instance = new Class(context as Context<M, K, Pr, Po, R>);
const record = instance as Record<string, unknown>;
modules.set(Class, instance);
instances.push(record);
mergeInstance(record);
}

mergeShallow(context, options.postMerge);
mergeShallow(context, options.assign);
injectContext();

return context as Context<M, K, Pr, Po, R>;
for (const Class of classes) registerModule(Class);
finalizeContext();
return context as Context<M, K, Pr, Po>;
}

type ExtractKeyEach<T extends ReadonlyArray<object>, 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<M>[number],
> = MergeValues<ExtractKeyEach<Instances<M>, K>>;
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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';