alloy-di 1.4.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/container.d.ts +77 -3
- package/dist/lib/container.js +125 -2
- package/dist/lib/providers.d.ts +26 -2
- package/dist/lib/providers.js +23 -1
- package/dist/lib/scope.d.ts +20 -1
- package/dist/plugins/core/codegen.js +2 -2
- package/dist/plugins/core/container-loader.js +3 -2
- package/dist/plugins/core/discovery-runtime.js +19 -4
- package/dist/plugins/core/discovery-store.js +20 -6
- package/dist/plugins/core/scanner.js +71 -7
- package/dist/plugins/core/visualizer.js +40 -10
- package/dist/plugins/vite-plugin/index.js +12 -4
- package/dist/runtime.d.ts +3 -3
- package/dist/runtime.js +2 -2
- package/dist/scopes.d.ts +8 -1
- package/dist/scopes.js +26 -0
- package/dist/test.d.ts +37 -3
- package/dist/test.js +20 -2
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
package/dist/lib/container.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { Constructor, Newable, Token } from "./types.js";
|
|
2
|
-
import { ResolutionContext, ServiceScope } from "./scope.js";
|
|
2
|
+
import { FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope } from "./scope.js";
|
|
3
3
|
import { ServiceIdentifier } from "./service-identifiers.js";
|
|
4
4
|
|
|
5
5
|
//#region src/lib/container.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* The container-like surface a factory receives. Both the root `Container` and
|
|
8
|
+
* a `Scope` satisfy it, so a factory can resolve its own dependencies against
|
|
9
|
+
* whichever context owns its lifecycle (root for `singleton`, the scope for a
|
|
10
|
+
* custom-scoped factory).
|
|
11
|
+
*/
|
|
12
|
+
interface FactoryContext {
|
|
13
|
+
get<T>(target: Newable<T> | ServiceIdentifier<T>): Promise<T>;
|
|
14
|
+
getToken<T>(token: Token<T>): T;
|
|
15
|
+
}
|
|
16
|
+
/** A token-bound factory function executed at resolution time. */
|
|
17
|
+
type FactoryFn<T = unknown> = (context: FactoryContext) => T | Promise<T>;
|
|
6
18
|
/**
|
|
7
19
|
* Runtime dependency injection container used by generated modules and tests.
|
|
8
20
|
*
|
|
@@ -15,6 +27,10 @@ declare class Container implements ResolutionContext {
|
|
|
15
27
|
private readonly instanceOverrides;
|
|
16
28
|
private readonly metadataCache;
|
|
17
29
|
private readonly valueProviders;
|
|
30
|
+
private readonly factoryRegistry;
|
|
31
|
+
private readonly factoryValues;
|
|
32
|
+
private readonly factoryPending;
|
|
33
|
+
private factoryGeneration;
|
|
18
34
|
private readonly factoryWarningCache;
|
|
19
35
|
private readonly scopeHierarchy;
|
|
20
36
|
readonly scopeName: ServiceScope;
|
|
@@ -26,6 +42,11 @@ declare class Container implements ResolutionContext {
|
|
|
26
42
|
deletePending(target: Constructor): void;
|
|
27
43
|
getProvider(tokenId: symbol): unknown;
|
|
28
44
|
hasProvider(tokenId: symbol): boolean;
|
|
45
|
+
getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
|
|
46
|
+
setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
|
|
47
|
+
getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
|
|
48
|
+
setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
|
|
49
|
+
deleteFactoryPending(tokenId: symbol, generation: number): void;
|
|
29
50
|
/**
|
|
30
51
|
* Resolve (and construct) the requested service.
|
|
31
52
|
*
|
|
@@ -64,6 +85,23 @@ declare class Container implements ResolutionContext {
|
|
|
64
85
|
* @param value - The value that should be injected when the token is requested.
|
|
65
86
|
*/
|
|
66
87
|
provideValue<T>(token: Token<T>, value: T): void;
|
|
88
|
+
/**
|
|
89
|
+
* Register a factory function for an injection token. The factory runs at
|
|
90
|
+
* resolution time and receives the container so it can resolve its own
|
|
91
|
+
* dependencies.
|
|
92
|
+
*
|
|
93
|
+
* This is the imperative escape hatch backing the declarative `asFactory`
|
|
94
|
+
* provider; prefer `asFactory` + `applyProviders` for static registration.
|
|
95
|
+
*
|
|
96
|
+
* @param token - The token created via `createToken`.
|
|
97
|
+
* @param fn - Factory invoked with the resolving context; may be async.
|
|
98
|
+
* @param options.lifecycle - `singleton` (default, cached on the root),
|
|
99
|
+
* `transient` (re-run on every resolution), or a custom scope name (cached
|
|
100
|
+
* once per matching scope instance and disposed with it).
|
|
101
|
+
*/
|
|
102
|
+
provideFactory<T>(token: Token<T>, fn: FactoryFn<T>, options?: {
|
|
103
|
+
lifecycle?: ServiceScope;
|
|
104
|
+
}): void;
|
|
67
105
|
/**
|
|
68
106
|
* Retrieve a provided value for a token from this container.
|
|
69
107
|
* Throws if no provider is registered for the token.
|
|
@@ -125,9 +163,45 @@ declare class Container implements ResolutionContext {
|
|
|
125
163
|
*/
|
|
126
164
|
private runImporterWithRetry;
|
|
127
165
|
/**
|
|
128
|
-
* Resolve a token dependency via registered value providers.
|
|
166
|
+
* Resolve a token dependency via registered value providers or factories.
|
|
129
167
|
*/
|
|
130
168
|
private resolveTokenLike;
|
|
169
|
+
/**
|
|
170
|
+
* Execute a token-bound factory according to its lifecycle, caching its
|
|
171
|
+
* result on the context that owns that lifecycle:
|
|
172
|
+
*
|
|
173
|
+
* - `transient`: run `fn` on every call against the current context; nothing
|
|
174
|
+
* is cached.
|
|
175
|
+
* - `singleton`: cache on the root container.
|
|
176
|
+
* - custom scope: cache on the nearest ancestor context whose `scopeName`
|
|
177
|
+
* matches; if none is active, fall back to transient behaviour (mirrors how
|
|
178
|
+
* class resolution treats a custom scope with no matching context).
|
|
179
|
+
*/
|
|
180
|
+
private resolveFactory;
|
|
181
|
+
/**
|
|
182
|
+
* Resolve a factory whose result is cached on `targetCtx`, keyed by token id.
|
|
183
|
+
* Mirrors `resolveCached` for classes: return the cached value, else coalesce
|
|
184
|
+
* concurrent first-resolutions onto a single `pending` promise, else run the
|
|
185
|
+
* factory and cache. `pending` is cleared in `finally` so a rejected factory
|
|
186
|
+
* can be retried rather than caching the failure.
|
|
187
|
+
*/
|
|
188
|
+
private resolveFactoryCached;
|
|
189
|
+
/**
|
|
190
|
+
* Invoke a factory's function against the context that owns its lifecycle,
|
|
191
|
+
* with a best-effort synchronous re-entrancy guard.
|
|
192
|
+
*
|
|
193
|
+
* `executing` is set only for the synchronous duration of `fn(...)` — long
|
|
194
|
+
* enough to catch a factory that resolves its own token before returning (a
|
|
195
|
+
* synchronous self-cycle, or a synchronous mutual cycle between factories),
|
|
196
|
+
* but cleared the moment `fn` returns so that concurrent resolutions which
|
|
197
|
+
* legitimately coalesce on an in-flight result are never mistaken for a cycle.
|
|
198
|
+
*
|
|
199
|
+
* Known limitation: a factory that re-enters its own token *after* awaiting
|
|
200
|
+
* (an async cross-await self-reference) is indistinguishable from valid
|
|
201
|
+
* coalescing without async-context tracking, so that case is not caught here
|
|
202
|
+
* — it is documented in the factory-providers guide.
|
|
203
|
+
*/
|
|
204
|
+
private runFactory;
|
|
131
205
|
/**
|
|
132
206
|
* Format a readable representation of the resolution stack for error messages.
|
|
133
207
|
*/
|
|
@@ -138,4 +212,4 @@ declare class Container implements ResolutionContext {
|
|
|
138
212
|
private getServiceMetadata;
|
|
139
213
|
}
|
|
140
214
|
//#endregion
|
|
141
|
-
export { Container };
|
|
215
|
+
export { Container, FactoryFn };
|
package/dist/lib/container.js
CHANGED
|
@@ -42,6 +42,10 @@ var Container = class {
|
|
|
42
42
|
instanceOverrides = /* @__PURE__ */ new Map();
|
|
43
43
|
metadataCache = /* @__PURE__ */ new Map();
|
|
44
44
|
valueProviders = /* @__PURE__ */ new Map();
|
|
45
|
+
factoryRegistry = /* @__PURE__ */ new Map();
|
|
46
|
+
factoryValues = /* @__PURE__ */ new Map();
|
|
47
|
+
factoryPending = /* @__PURE__ */ new Map();
|
|
48
|
+
factoryGeneration = 0;
|
|
45
49
|
factoryWarningCache = /* @__PURE__ */ new WeakSet();
|
|
46
50
|
scopeHierarchy = /* @__PURE__ */ new Map();
|
|
47
51
|
scopeName = ServiceScope.SINGLETON;
|
|
@@ -67,6 +71,27 @@ var Container = class {
|
|
|
67
71
|
hasProvider(tokenId) {
|
|
68
72
|
return this.valueProviders.has(tokenId);
|
|
69
73
|
}
|
|
74
|
+
getFactoryValue(tokenId) {
|
|
75
|
+
return this.factoryValues.get(tokenId);
|
|
76
|
+
}
|
|
77
|
+
setFactoryValue(tokenId, generation, value) {
|
|
78
|
+
this.factoryValues.set(tokenId, {
|
|
79
|
+
generation,
|
|
80
|
+
value
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
getFactoryPending(tokenId) {
|
|
84
|
+
return this.factoryPending.get(tokenId);
|
|
85
|
+
}
|
|
86
|
+
setFactoryPending(tokenId, generation, promise) {
|
|
87
|
+
this.factoryPending.set(tokenId, {
|
|
88
|
+
generation,
|
|
89
|
+
promise
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
deleteFactoryPending(tokenId, generation) {
|
|
93
|
+
if (this.factoryPending.get(tokenId)?.generation === generation) this.factoryPending.delete(tokenId);
|
|
94
|
+
}
|
|
70
95
|
async get(targetOrIdentifier) {
|
|
71
96
|
if (typeof targetOrIdentifier === "symbol") return this.getByIdentifier(targetOrIdentifier);
|
|
72
97
|
return this.getByConstructor(targetOrIdentifier, this);
|
|
@@ -123,11 +148,38 @@ var Container = class {
|
|
|
123
148
|
this.valueProviders.set(token.id, value);
|
|
124
149
|
}
|
|
125
150
|
/**
|
|
151
|
+
* Register a factory function for an injection token. The factory runs at
|
|
152
|
+
* resolution time and receives the container so it can resolve its own
|
|
153
|
+
* dependencies.
|
|
154
|
+
*
|
|
155
|
+
* This is the imperative escape hatch backing the declarative `asFactory`
|
|
156
|
+
* provider; prefer `asFactory` + `applyProviders` for static registration.
|
|
157
|
+
*
|
|
158
|
+
* @param token - The token created via `createToken`.
|
|
159
|
+
* @param fn - Factory invoked with the resolving context; may be async.
|
|
160
|
+
* @param options.lifecycle - `singleton` (default, cached on the root),
|
|
161
|
+
* `transient` (re-run on every resolution), or a custom scope name (cached
|
|
162
|
+
* once per matching scope instance and disposed with it).
|
|
163
|
+
*/
|
|
164
|
+
provideFactory(token, fn, options) {
|
|
165
|
+
this.factoryRegistry.set(token.id, {
|
|
166
|
+
fn,
|
|
167
|
+
lifecycle: options?.lifecycle ?? ServiceScope.SINGLETON,
|
|
168
|
+
description: token.description,
|
|
169
|
+
generation: ++this.factoryGeneration,
|
|
170
|
+
executing: false
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
126
174
|
* Retrieve a provided value for a token from this container.
|
|
127
175
|
* Throws if no provider is registered for the token.
|
|
128
176
|
*/
|
|
129
177
|
getToken(token) {
|
|
130
|
-
if (!this.valueProviders.has(token.id))
|
|
178
|
+
if (!this.valueProviders.has(token.id)) {
|
|
179
|
+
const label = token.description ?? String(token.id);
|
|
180
|
+
if (this.factoryRegistry.has(token.id)) throw new Error(`Token ${label} is registered as a factory provider, which cannot be retrieved synchronously via getToken(). Declare it as a dependency so it resolves through the async resolution path instead.`);
|
|
181
|
+
throw new Error(`No provider registered for token ${label}`);
|
|
182
|
+
}
|
|
131
183
|
return this.valueProviders.get(token.id);
|
|
132
184
|
}
|
|
133
185
|
async getByConstructor(target, context, options) {
|
|
@@ -292,7 +344,7 @@ var Container = class {
|
|
|
292
344
|
}
|
|
293
345
|
}
|
|
294
346
|
/**
|
|
295
|
-
* Resolve a token dependency via registered value providers.
|
|
347
|
+
* Resolve a token dependency via registered value providers or factories.
|
|
296
348
|
*/
|
|
297
349
|
resolveTokenLike(tok, target, resolutionStack, context) {
|
|
298
350
|
let current = context;
|
|
@@ -300,6 +352,8 @@ var Container = class {
|
|
|
300
352
|
if (current.hasProvider(tok.id)) return current.getProvider(tok.id);
|
|
301
353
|
current = current.parent;
|
|
302
354
|
}
|
|
355
|
+
const factory = this.factoryRegistry.get(tok.id);
|
|
356
|
+
if (factory) return this.resolveFactory(tok.id, factory, context);
|
|
303
357
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
304
358
|
throw new DependencyResolutionError(`No provider registered for token ${tok.description ?? String(tok.id)} while resolving ${target.name}. Resolution stack: ${stackPath}`, {
|
|
305
359
|
target,
|
|
@@ -308,6 +362,75 @@ var Container = class {
|
|
|
308
362
|
});
|
|
309
363
|
}
|
|
310
364
|
/**
|
|
365
|
+
* Execute a token-bound factory according to its lifecycle, caching its
|
|
366
|
+
* result on the context that owns that lifecycle:
|
|
367
|
+
*
|
|
368
|
+
* - `transient`: run `fn` on every call against the current context; nothing
|
|
369
|
+
* is cached.
|
|
370
|
+
* - `singleton`: cache on the root container.
|
|
371
|
+
* - custom scope: cache on the nearest ancestor context whose `scopeName`
|
|
372
|
+
* matches; if none is active, fall back to transient behaviour (mirrors how
|
|
373
|
+
* class resolution treats a custom scope with no matching context).
|
|
374
|
+
*/
|
|
375
|
+
resolveFactory(tokenId, descriptor, context) {
|
|
376
|
+
if (descriptor.lifecycle === ServiceScope.TRANSIENT) return this.runFactory(descriptor, context);
|
|
377
|
+
const targetCtx = this.findContextForScope(descriptor.lifecycle, context);
|
|
378
|
+
if (descriptor.lifecycle !== ServiceScope.SINGLETON && targetCtx.scopeName !== descriptor.lifecycle) return this.runFactory(descriptor, context);
|
|
379
|
+
return this.resolveFactoryCached(tokenId, descriptor, targetCtx);
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Resolve a factory whose result is cached on `targetCtx`, keyed by token id.
|
|
383
|
+
* Mirrors `resolveCached` for classes: return the cached value, else coalesce
|
|
384
|
+
* concurrent first-resolutions onto a single `pending` promise, else run the
|
|
385
|
+
* factory and cache. `pending` is cleared in `finally` so a rejected factory
|
|
386
|
+
* can be retried rather than caching the failure.
|
|
387
|
+
*/
|
|
388
|
+
resolveFactoryCached(tokenId, descriptor, targetCtx) {
|
|
389
|
+
const generation = descriptor.generation;
|
|
390
|
+
const cached = targetCtx.getFactoryValue(tokenId);
|
|
391
|
+
if (cached?.generation === generation) return cached.value;
|
|
392
|
+
const pending = targetCtx.getFactoryPending(tokenId);
|
|
393
|
+
if (pending?.generation === generation) return pending.promise;
|
|
394
|
+
const creation = (async () => {
|
|
395
|
+
try {
|
|
396
|
+
const value = await this.runFactory(descriptor, targetCtx);
|
|
397
|
+
targetCtx.setFactoryValue(tokenId, generation, value);
|
|
398
|
+
return value;
|
|
399
|
+
} finally {
|
|
400
|
+
targetCtx.deleteFactoryPending(tokenId, generation);
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
targetCtx.setFactoryPending(tokenId, generation, creation);
|
|
404
|
+
return creation;
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Invoke a factory's function against the context that owns its lifecycle,
|
|
408
|
+
* with a best-effort synchronous re-entrancy guard.
|
|
409
|
+
*
|
|
410
|
+
* `executing` is set only for the synchronous duration of `fn(...)` — long
|
|
411
|
+
* enough to catch a factory that resolves its own token before returning (a
|
|
412
|
+
* synchronous self-cycle, or a synchronous mutual cycle between factories),
|
|
413
|
+
* but cleared the moment `fn` returns so that concurrent resolutions which
|
|
414
|
+
* legitimately coalesce on an in-flight result are never mistaken for a cycle.
|
|
415
|
+
*
|
|
416
|
+
* Known limitation: a factory that re-enters its own token *after* awaiting
|
|
417
|
+
* (an async cross-await self-reference) is indistinguishable from valid
|
|
418
|
+
* coalescing without async-context tracking, so that case is not caught here
|
|
419
|
+
* — it is documented in the factory-providers guide.
|
|
420
|
+
*/
|
|
421
|
+
runFactory(descriptor, context) {
|
|
422
|
+
if (descriptor.executing) {
|
|
423
|
+
const label = descriptor.description ?? "<token>";
|
|
424
|
+
throw new Error(`Circular factory dependency detected: the factory for token ${label} resolved its own token while it was still being constructed. Break the cycle by restructuring the factory or resolving the dependency lazily.`);
|
|
425
|
+
}
|
|
426
|
+
descriptor.executing = true;
|
|
427
|
+
try {
|
|
428
|
+
return descriptor.fn(context);
|
|
429
|
+
} finally {
|
|
430
|
+
descriptor.executing = false;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
311
434
|
* Format a readable representation of the resolution stack for error messages.
|
|
312
435
|
*/
|
|
313
436
|
formatStackPath(target, resolutionStack) {
|
package/dist/lib/providers.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Newable, Token } from "./types.js";
|
|
2
2
|
import { ServiceScope } from "./scope.js";
|
|
3
|
-
import { Container } from "./container.js";
|
|
3
|
+
import { Container, FactoryFn } from "./container.js";
|
|
4
4
|
import { Lazy } from "./lazy.js";
|
|
5
5
|
|
|
6
6
|
//#region src/lib/providers.d.ts
|
|
@@ -56,11 +56,19 @@ interface LazyServiceProviderDescriptor<T = unknown> {
|
|
|
56
56
|
lifecycle: ProviderLifecycle;
|
|
57
57
|
deps?: DependenciesOption;
|
|
58
58
|
}
|
|
59
|
+
/** Descriptor for a token-bound factory provider with an explicit lifecycle. */
|
|
60
|
+
interface FactoryProviderDescriptor<T = unknown> {
|
|
61
|
+
kind: "factory";
|
|
62
|
+
token: Token<T>;
|
|
63
|
+
factory: FactoryFn<T>;
|
|
64
|
+
lifecycle: ProviderLifecycle;
|
|
65
|
+
}
|
|
59
66
|
/** Aggregate provider definitions for batch application. */
|
|
60
67
|
interface ProviderDefinitions {
|
|
61
68
|
values?: ValueProviderDescriptor[];
|
|
62
69
|
services?: ServiceProviderDescriptor[];
|
|
63
70
|
lazyServices?: LazyPlaceholder[];
|
|
71
|
+
factories?: FactoryProviderDescriptor[];
|
|
64
72
|
}
|
|
65
73
|
/**
|
|
66
74
|
* Identity helper for provider definition blocks.
|
|
@@ -76,6 +84,22 @@ declare function asClass<T extends Newable<unknown>>(useClass: T, options: {
|
|
|
76
84
|
lifecycle: ProviderLifecycle;
|
|
77
85
|
deps?: DependenciesOption;
|
|
78
86
|
}): ServiceProviderDescriptor<T>;
|
|
87
|
+
/**
|
|
88
|
+
* Create a factory provider: bind a token to a function executed at resolution
|
|
89
|
+
* time. Mirrors `asClass` — a token, a factory function, and a lifecycle.
|
|
90
|
+
*
|
|
91
|
+
* The factory receives the resolving context and may be async, so it can
|
|
92
|
+
* resolve its own dependencies. `singleton` caches the result on the root;
|
|
93
|
+
* `transient` re-runs on every resolution; a custom scope caches once per
|
|
94
|
+
* matching scope instance and disposes the result with it.
|
|
95
|
+
*
|
|
96
|
+
* @param token Token the produced value is bound to (its type checks `factory`'s return).
|
|
97
|
+
* @param factory Function invoked with the resolving context; may be async.
|
|
98
|
+
* @param options.lifecycle Lifecycle scope (singleton, transient, or a custom scope).
|
|
99
|
+
*/
|
|
100
|
+
declare function asFactory<T>(token: Token<T>, factory: FactoryFn<T>, options: {
|
|
101
|
+
lifecycle: ProviderLifecycle;
|
|
102
|
+
}): FactoryProviderDescriptor<T>;
|
|
79
103
|
/**
|
|
80
104
|
* Convenience lifecycle helpers for fluent provider creation.
|
|
81
105
|
*/
|
|
@@ -108,4 +132,4 @@ declare function asLazyClass<T, const TDeps extends DependenciesOption>(importer
|
|
|
108
132
|
*/
|
|
109
133
|
declare function applyProviders(container: Container, definitions: ProviderDefinitions | ProviderDefinitions[]): void;
|
|
110
134
|
//#endregion
|
|
111
|
-
export { ProviderDefinitions, applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle };
|
|
135
|
+
export { FactoryProviderDescriptor, ProviderDefinitions, applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle };
|
package/dist/lib/providers.js
CHANGED
|
@@ -34,6 +34,27 @@ function asClass(useClass, options) {
|
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
+
* Create a factory provider: bind a token to a function executed at resolution
|
|
38
|
+
* time. Mirrors `asClass` — a token, a factory function, and a lifecycle.
|
|
39
|
+
*
|
|
40
|
+
* The factory receives the resolving context and may be async, so it can
|
|
41
|
+
* resolve its own dependencies. `singleton` caches the result on the root;
|
|
42
|
+
* `transient` re-runs on every resolution; a custom scope caches once per
|
|
43
|
+
* matching scope instance and disposes the result with it.
|
|
44
|
+
*
|
|
45
|
+
* @param token Token the produced value is bound to (its type checks `factory`'s return).
|
|
46
|
+
* @param factory Function invoked with the resolving context; may be async.
|
|
47
|
+
* @param options.lifecycle Lifecycle scope (singleton, transient, or a custom scope).
|
|
48
|
+
*/
|
|
49
|
+
function asFactory(token, factory, options) {
|
|
50
|
+
return {
|
|
51
|
+
kind: "factory",
|
|
52
|
+
token,
|
|
53
|
+
factory,
|
|
54
|
+
lifecycle: options.lifecycle
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
37
58
|
* Convenience lifecycle helpers for fluent provider creation.
|
|
38
59
|
*/
|
|
39
60
|
const lifecycle = {
|
|
@@ -145,6 +166,7 @@ function applyProviders(container, definitions) {
|
|
|
145
166
|
detectProviderCycles(planEntries);
|
|
146
167
|
for (const definition of list) {
|
|
147
168
|
for (const valueProvider of definition.values ?? []) container.provideValue(valueProvider.token, valueProvider.value);
|
|
169
|
+
for (const factoryProvider of definition.factories ?? []) container.provideFactory(factoryProvider.token, factoryProvider.factory, { lifecycle: factoryProvider.lifecycle });
|
|
148
170
|
const registerService = (ctor, lifecycle, deps, factory) => {
|
|
149
171
|
dependenciesRegistry.set(ctor, {
|
|
150
172
|
scope: lifecycle,
|
|
@@ -161,4 +183,4 @@ function applyProviders(container, definitions) {
|
|
|
161
183
|
}
|
|
162
184
|
}
|
|
163
185
|
//#endregion
|
|
164
|
-
export { applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle };
|
|
186
|
+
export { applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle };
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -10,6 +10,20 @@ interface AlloyScopes {
|
|
|
10
10
|
transient: true;
|
|
11
11
|
}
|
|
12
12
|
type ServiceScope = keyof AlloyScopes;
|
|
13
|
+
/**
|
|
14
|
+
* A cached factory result tagged with the `generation` of the descriptor that
|
|
15
|
+
* produced it. A mismatch against the current descriptor's generation (after a
|
|
16
|
+
* factory is re-registered) makes the entry stale, so it is recomputed.
|
|
17
|
+
*/
|
|
18
|
+
interface FactoryCacheEntry {
|
|
19
|
+
generation: number;
|
|
20
|
+
value: unknown;
|
|
21
|
+
}
|
|
22
|
+
/** An in-flight factory resolution tagged with its descriptor generation. */
|
|
23
|
+
interface FactoryPendingEntry {
|
|
24
|
+
generation: number;
|
|
25
|
+
promise: Promise<unknown>;
|
|
26
|
+
}
|
|
13
27
|
interface ResolutionContext {
|
|
14
28
|
readonly scopeName: ServiceScope;
|
|
15
29
|
getCached(target: Constructor): unknown;
|
|
@@ -19,7 +33,12 @@ interface ResolutionContext {
|
|
|
19
33
|
deletePending(target: Constructor): void;
|
|
20
34
|
getProvider(tokenId: symbol): unknown;
|
|
21
35
|
hasProvider(tokenId: symbol): boolean;
|
|
36
|
+
getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
|
|
37
|
+
setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
|
|
38
|
+
getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
|
|
39
|
+
setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
|
|
40
|
+
deleteFactoryPending(tokenId: symbol, generation: number): void;
|
|
22
41
|
readonly parent: ResolutionContext | null;
|
|
23
42
|
}
|
|
24
43
|
//#endregion
|
|
25
|
-
export { AlloyScopes, ResolutionContext, ServiceScope };
|
|
44
|
+
export { AlloyScopes, FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope };
|
|
@@ -381,8 +381,8 @@ function generateContainerTypeDefinition(metas, pathResolver) {
|
|
|
381
381
|
for (const meta of metas) {
|
|
382
382
|
const importName = pool.claim(resolver.resolve(meta.className, meta.filePath));
|
|
383
383
|
const importPath = pathResolver(meta.filePath);
|
|
384
|
-
if (importName === meta.className) imports.push(`import { ${meta.className} } from '${importPath}';`);
|
|
385
|
-
else imports.push(`import { ${meta.className} as ${importName} } from '${importPath}';`);
|
|
384
|
+
if (importName === meta.className) imports.push(` import { ${meta.className} } from '${importPath}';`);
|
|
385
|
+
else imports.push(` import { ${meta.className} as ${importName} } from '${importPath}';`);
|
|
386
386
|
const exportKey = createIdentifierExportKey(meta, resolver);
|
|
387
387
|
interfaceMembers.push(`${exportKey}: ServiceIdentifier<${importName}>;`);
|
|
388
388
|
}
|
|
@@ -42,7 +42,7 @@ async function loadVirtualContainerModule(options) {
|
|
|
42
42
|
scopes: options.scopes
|
|
43
43
|
});
|
|
44
44
|
writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir, options.scopes);
|
|
45
|
-
writeVisualizationArtifact(metas, lazyClassKeys, options.resolvedVisualization, options.scopes);
|
|
45
|
+
writeVisualizationArtifact(metas, lazyClassKeys, options.factoryProviders, options.resolvedVisualization, options.scopes);
|
|
46
46
|
return {
|
|
47
47
|
code,
|
|
48
48
|
moduleType: "js"
|
|
@@ -93,11 +93,12 @@ function resolveDeclarationImportPath(dtsDir, filePath) {
|
|
|
93
93
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
94
94
|
return rel;
|
|
95
95
|
}
|
|
96
|
-
function writeVisualizationArtifact(metas, lazyReferencedClassKeys, resolvedVisualization, scopes) {
|
|
96
|
+
function writeVisualizationArtifact(metas, lazyReferencedClassKeys, factoryProviders, resolvedVisualization, scopes) {
|
|
97
97
|
if (!resolvedVisualization) return;
|
|
98
98
|
const artifact = generateMermaidDiagram({
|
|
99
99
|
metas,
|
|
100
100
|
lazyClassKeys: new Set(lazyReferencedClassKeys),
|
|
101
|
+
factoryProviders,
|
|
101
102
|
options: resolvedVisualization.mermaidOptions,
|
|
102
103
|
scopes
|
|
103
104
|
});
|
|
@@ -34,31 +34,46 @@ function lazyKeysSignature(keys) {
|
|
|
34
34
|
if (!keys || keys.size === 0) return "";
|
|
35
35
|
return Array.from(keys).toSorted().join("|");
|
|
36
36
|
}
|
|
37
|
+
function factoryProvidersSignature(providers) {
|
|
38
|
+
return JSON.stringify((providers ?? []).map((provider) => ({
|
|
39
|
+
filePath: provider.filePath,
|
|
40
|
+
tokenExpression: provider.tokenExpression,
|
|
41
|
+
tokenLabel: provider.tokenLabel,
|
|
42
|
+
lifecycle: provider.lifecycle
|
|
43
|
+
})));
|
|
44
|
+
}
|
|
37
45
|
function createDiscoveryRuntime() {
|
|
38
46
|
const discovery = createDiscoveryStore();
|
|
39
47
|
const discoveredClasses = /* @__PURE__ */ new Map();
|
|
40
48
|
const lazyReferencedClassKeys = /* @__PURE__ */ new Set();
|
|
49
|
+
const factoryProvidersByFile = /* @__PURE__ */ new Map();
|
|
41
50
|
return {
|
|
42
51
|
discoveredClasses,
|
|
43
52
|
lazyReferencedClassKeys,
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
factoryProvidersByFile,
|
|
54
|
+
processUpdate(id, code, options) {
|
|
55
|
+
const trackFactoryProviders = options?.factoryProviders ?? true;
|
|
56
|
+
const { metas, lazyClassKeys, factoryProviders, previousMetas, previousLazyClassKeys, previousFactoryProviders } = discovery.updateFile(id, code, { factoryProviders: trackFactoryProviders });
|
|
46
57
|
if (previousMetas) for (const meta of previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
|
|
47
58
|
for (const meta of metas) discoveredClasses.set(createClassKey(meta.filePath, meta.className), meta);
|
|
48
59
|
if (previousLazyClassKeys) for (const key of previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
|
|
49
60
|
if (lazyClassKeys.size) for (const key of lazyClassKeys) lazyReferencedClassKeys.add(key);
|
|
50
|
-
|
|
61
|
+
if (trackFactoryProviders && factoryProviders.length) factoryProvidersByFile.set(id, factoryProviders);
|
|
62
|
+
else factoryProvidersByFile.delete(id);
|
|
63
|
+
return metasSignature(previousMetas ?? []) !== metasSignature(metas) || lazyKeysSignature(previousLazyClassKeys) !== lazyKeysSignature(lazyClassKeys) || trackFactoryProviders && factoryProvidersSignature(previousFactoryProviders) !== factoryProvidersSignature(factoryProviders);
|
|
51
64
|
},
|
|
52
65
|
removeDiscoveredFile(file) {
|
|
53
66
|
const removed = discovery.removeFile(file);
|
|
54
67
|
if (removed.previousMetas) for (const meta of removed.previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
|
|
55
68
|
if (removed.previousLazyClassKeys) for (const key of removed.previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
|
|
56
|
-
|
|
69
|
+
factoryProvidersByFile.delete(file);
|
|
70
|
+
return Boolean(removed.previousMetas?.length || removed.previousLazyClassKeys?.size || removed.previousFactoryProviders?.length);
|
|
57
71
|
},
|
|
58
72
|
clear() {
|
|
59
73
|
discovery.clear();
|
|
60
74
|
discoveredClasses.clear();
|
|
61
75
|
lazyReferencedClassKeys.clear();
|
|
76
|
+
factoryProvidersByFile.clear();
|
|
62
77
|
}
|
|
63
78
|
};
|
|
64
79
|
}
|
|
@@ -19,6 +19,7 @@ function hashContent(code) {
|
|
|
19
19
|
function createDiscoveryStore(options = {}) {
|
|
20
20
|
const fileMetas = /* @__PURE__ */ new Map();
|
|
21
21
|
const fileLazyRefs = /* @__PURE__ */ new Map();
|
|
22
|
+
const fileFactoryProviders = /* @__PURE__ */ new Map();
|
|
22
23
|
const fileContentHashes = /* @__PURE__ */ new Map();
|
|
23
24
|
const fileSources = options.trackSources ? /* @__PURE__ */ new Map() : void 0;
|
|
24
25
|
/**
|
|
@@ -28,28 +29,36 @@ function createDiscoveryStore(options = {}) {
|
|
|
28
29
|
* @param id - Module identifier or path.
|
|
29
30
|
* @param code - Current file contents to analyze.
|
|
30
31
|
*/
|
|
31
|
-
function updateFile(id, code) {
|
|
32
|
+
function updateFile(id, code, options) {
|
|
33
|
+
const scanFactoryProviders = options?.factoryProviders ?? true;
|
|
32
34
|
const previousMetas = fileMetas.get(id);
|
|
33
35
|
const previousLazyClassKeys = fileLazyRefs.get(id);
|
|
34
|
-
const
|
|
36
|
+
const previousFactoryProviders = fileFactoryProviders.get(id);
|
|
37
|
+
const contentHash = hashContent(`${scanFactoryProviders ? "factory:1" : "factory:0"}\0${code}`);
|
|
35
38
|
if (fileContentHashes.get(id) === contentHash) return {
|
|
36
39
|
metas: previousMetas ?? [],
|
|
37
40
|
lazyClassKeys: new Set(previousLazyClassKeys),
|
|
41
|
+
factoryProviders: previousFactoryProviders ?? [],
|
|
38
42
|
previousMetas,
|
|
39
|
-
previousLazyClassKeys
|
|
43
|
+
previousLazyClassKeys,
|
|
44
|
+
previousFactoryProviders
|
|
40
45
|
};
|
|
41
46
|
fileContentHashes.set(id, contentHash);
|
|
42
47
|
if (fileSources) fileSources.set(id, code);
|
|
43
|
-
const { metas, lazyClassKeys } = scanSource(code, id);
|
|
48
|
+
const { metas, lazyClassKeys, factoryProviders } = scanSource(code, id, { factoryProviders: scanFactoryProviders });
|
|
44
49
|
if (metas.length) fileMetas.set(id, metas);
|
|
45
50
|
else fileMetas.delete(id);
|
|
46
51
|
if (lazyClassKeys.size) fileLazyRefs.set(id, lazyClassKeys);
|
|
47
52
|
else fileLazyRefs.delete(id);
|
|
53
|
+
if (factoryProviders.length) fileFactoryProviders.set(id, factoryProviders);
|
|
54
|
+
else fileFactoryProviders.delete(id);
|
|
48
55
|
return {
|
|
49
56
|
metas,
|
|
50
57
|
lazyClassKeys,
|
|
58
|
+
factoryProviders,
|
|
51
59
|
previousMetas,
|
|
52
|
-
previousLazyClassKeys
|
|
60
|
+
previousLazyClassKeys,
|
|
61
|
+
previousFactoryProviders
|
|
53
62
|
};
|
|
54
63
|
}
|
|
55
64
|
/**
|
|
@@ -60,13 +69,16 @@ function createDiscoveryStore(options = {}) {
|
|
|
60
69
|
function removeFile(id) {
|
|
61
70
|
const previousMetas = fileMetas.get(id);
|
|
62
71
|
const previousLazyClassKeys = fileLazyRefs.get(id);
|
|
72
|
+
const previousFactoryProviders = fileFactoryProviders.get(id);
|
|
63
73
|
fileMetas.delete(id);
|
|
64
74
|
fileLazyRefs.delete(id);
|
|
75
|
+
fileFactoryProviders.delete(id);
|
|
65
76
|
fileContentHashes.delete(id);
|
|
66
77
|
if (fileSources) fileSources.delete(id);
|
|
67
78
|
return {
|
|
68
79
|
previousMetas,
|
|
69
|
-
previousLazyClassKeys
|
|
80
|
+
previousLazyClassKeys,
|
|
81
|
+
previousFactoryProviders
|
|
70
82
|
};
|
|
71
83
|
}
|
|
72
84
|
/**
|
|
@@ -75,12 +87,14 @@ function createDiscoveryStore(options = {}) {
|
|
|
75
87
|
function clear() {
|
|
76
88
|
fileMetas.clear();
|
|
77
89
|
fileLazyRefs.clear();
|
|
90
|
+
fileFactoryProviders.clear();
|
|
78
91
|
fileContentHashes.clear();
|
|
79
92
|
fileSources?.clear();
|
|
80
93
|
}
|
|
81
94
|
return {
|
|
82
95
|
fileMetas,
|
|
83
96
|
fileLazyRefs,
|
|
97
|
+
fileFactoryProviders,
|
|
84
98
|
fileSources,
|
|
85
99
|
updateFile,
|
|
86
100
|
removeFile,
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createClassKey, createSymbolKey } from "./utils.js";
|
|
2
|
+
import { ServiceScope } from "../../lib/scope.js";
|
|
2
3
|
import { extractServiceMetadata } from "./decorators.js";
|
|
3
4
|
import { processLazyCall, resolveModuleSpecifierCandidates } from "./lazy.js";
|
|
4
5
|
import fs from "node:fs";
|
|
@@ -41,17 +42,23 @@ function collectFileImports(sourceFile) {
|
|
|
41
42
|
* discovery results: decorators require an `@` and lazy references require
|
|
42
43
|
* the `Lazy` identifier.
|
|
43
44
|
*/
|
|
44
|
-
function
|
|
45
|
-
return
|
|
45
|
+
function shouldScanFactoryProviders(options) {
|
|
46
|
+
return options?.factoryProviders ?? true;
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
48
|
+
function mayContainDiscoverableSyntax(code, options) {
|
|
49
|
+
return code.includes("@") || code.includes("Lazy") || shouldScanFactoryProviders(options) && code.includes("asFactory");
|
|
50
|
+
}
|
|
51
|
+
function scanSource(code, id, options) {
|
|
52
|
+
const scanFactoryProviders = shouldScanFactoryProviders(options);
|
|
53
|
+
if (!mayContainDiscoverableSyntax(code, options)) return {
|
|
49
54
|
metas: [],
|
|
50
|
-
lazyClassKeys: /* @__PURE__ */ new Set()
|
|
55
|
+
lazyClassKeys: /* @__PURE__ */ new Set(),
|
|
56
|
+
factoryProviders: []
|
|
51
57
|
};
|
|
52
58
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.ESNext, true);
|
|
53
59
|
const discovered = /* @__PURE__ */ new Map();
|
|
54
60
|
const lazyRefs = /* @__PURE__ */ new Set();
|
|
61
|
+
const factoryProviders = [];
|
|
55
62
|
const fileImports = collectFileImports(sourceFile);
|
|
56
63
|
const decoratorResolutionCache = /* @__PURE__ */ new Map();
|
|
57
64
|
const visit = (node) => {
|
|
@@ -62,15 +69,72 @@ function scanSource(code, id) {
|
|
|
62
69
|
discovered,
|
|
63
70
|
decoratorResolutionCache
|
|
64
71
|
});
|
|
65
|
-
else if (ts.isCallExpression(node))
|
|
72
|
+
else if (ts.isCallExpression(node)) {
|
|
73
|
+
processLazyCall(node, id, sourceFile, lazyRefs);
|
|
74
|
+
if (scanFactoryProviders) handleFactoryProviderCall(node, {
|
|
75
|
+
id,
|
|
76
|
+
sourceFile,
|
|
77
|
+
fileImports,
|
|
78
|
+
factoryProviders
|
|
79
|
+
});
|
|
80
|
+
}
|
|
66
81
|
ts.forEachChild(node, visit);
|
|
67
82
|
};
|
|
68
83
|
ts.forEachChild(sourceFile, visit);
|
|
69
84
|
return {
|
|
70
85
|
metas: Array.from(discovered.values()),
|
|
71
|
-
lazyClassKeys: lazyRefs
|
|
86
|
+
lazyClassKeys: lazyRefs,
|
|
87
|
+
factoryProviders
|
|
72
88
|
};
|
|
73
89
|
}
|
|
90
|
+
function handleFactoryProviderCall(node, context) {
|
|
91
|
+
if (!isAlloyRuntimeHelper(node.expression, "asFactory", context.fileImports)) return;
|
|
92
|
+
const tokenArg = node.arguments[0];
|
|
93
|
+
if (!tokenArg) return;
|
|
94
|
+
context.factoryProviders.push({
|
|
95
|
+
filePath: context.id,
|
|
96
|
+
tokenExpression: tokenArg.getText(context.sourceFile),
|
|
97
|
+
tokenLabel: createFactoryTokenLabel(tokenArg, context.sourceFile),
|
|
98
|
+
lifecycle: extractFactoryLifecycle(node.arguments[2])
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function isAlloyRuntimeHelper(expression, helperName, fileImports) {
|
|
102
|
+
if (ts.isIdentifier(expression)) {
|
|
103
|
+
const importInfo = fileImports.get(expression.text);
|
|
104
|
+
return Boolean(importInfo && !importInfo.isTypeOnly && importInfo.path === ALLOY_RUNTIME_MODULE && (importInfo.originalName ?? expression.text) === helperName);
|
|
105
|
+
}
|
|
106
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
107
|
+
const importInfo = fileImports.get(expression.expression.text);
|
|
108
|
+
return Boolean(importInfo && !importInfo.isTypeOnly && importInfo.path === ALLOY_RUNTIME_MODULE && importInfo.originalName === "*" && expression.name.text === helperName);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function createFactoryTokenLabel(tokenArg, sourceFile) {
|
|
113
|
+
if (ts.isIdentifier(tokenArg)) return tokenArg.text;
|
|
114
|
+
return tokenArg.getText(sourceFile);
|
|
115
|
+
}
|
|
116
|
+
function extractFactoryLifecycle(optionsArg) {
|
|
117
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return ServiceScope.SINGLETON;
|
|
118
|
+
for (const prop of optionsArg.properties) {
|
|
119
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
|
120
|
+
if (prop.name.text !== "lifecycle") continue;
|
|
121
|
+
return lifecycleExpressionToScope(prop.initializer);
|
|
122
|
+
}
|
|
123
|
+
return ServiceScope.SINGLETON;
|
|
124
|
+
}
|
|
125
|
+
function lifecycleExpressionToScope(expression) {
|
|
126
|
+
if (ts.isStringLiteralLike(expression)) return expression.text;
|
|
127
|
+
if (ts.isCallExpression(expression)) {
|
|
128
|
+
const callTarget = expression.expression;
|
|
129
|
+
if (ts.isPropertyAccessExpression(callTarget) && (callTarget.name.text === "singleton" || callTarget.name.text === "transient")) return callTarget.name.text;
|
|
130
|
+
}
|
|
131
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
132
|
+
const member = expression.name.text;
|
|
133
|
+
if (member === "SINGLETON") return ServiceScope.SINGLETON;
|
|
134
|
+
if (member === "TRANSIENT") return ServiceScope.TRANSIENT;
|
|
135
|
+
}
|
|
136
|
+
return ServiceScope.SINGLETON;
|
|
137
|
+
}
|
|
74
138
|
function handleClassDeclaration(node, context) {
|
|
75
139
|
if (!node.name) return;
|
|
76
140
|
const decoratorMatch = findServiceDecorator(node, context.sourceFile, context.fileImports, context.id, context.decoratorResolutionCache);
|
|
@@ -49,7 +49,7 @@ const RESERVED_IDENTIFIERS = new Set([
|
|
|
49
49
|
* @param input - Discovered metadata, optional lazy keys, and rendering options.
|
|
50
50
|
* @returns The rendered diagram plus simple counts useful for reporting.
|
|
51
51
|
*/
|
|
52
|
-
function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
52
|
+
function generateMermaidDiagram({ metas, lazyClassKeys, factoryProviders, options, scopes }) {
|
|
53
53
|
const mergedOptions = {
|
|
54
54
|
...DEFAULT_OPTIONS,
|
|
55
55
|
...options,
|
|
@@ -69,6 +69,7 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
69
69
|
const nodesByClassName = /* @__PURE__ */ new Map();
|
|
70
70
|
const nodesByFilePath = /* @__PURE__ */ new Map();
|
|
71
71
|
const tokenNodes = /* @__PURE__ */ new Map();
|
|
72
|
+
const factoryNodes = /* @__PURE__ */ new Map();
|
|
72
73
|
const nodeByMeta = /* @__PURE__ */ new Map();
|
|
73
74
|
metas.forEach((meta, index) => {
|
|
74
75
|
const key = createClassKey(meta.filePath, meta.className);
|
|
@@ -94,6 +95,7 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
94
95
|
pathBucket.push(node);
|
|
95
96
|
nodesByFilePath.set(normalizedPath, pathBucket);
|
|
96
97
|
});
|
|
98
|
+
for (const provider of factoryProviders ?? []) ensureFactoryNode(factoryNodes, provider);
|
|
97
99
|
const edges = [];
|
|
98
100
|
const edgeKeys = /* @__PURE__ */ new Set();
|
|
99
101
|
metas.forEach((meta) => {
|
|
@@ -105,7 +107,7 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
105
107
|
if (!identifiers.length) continue;
|
|
106
108
|
const targets = /* @__PURE__ */ new Map();
|
|
107
109
|
for (const ident of identifiers) {
|
|
108
|
-
const resolvedTargets = resolveTargetsForIdentifier(ident, dep.expression, meta, nodesByClassName, nodesByFilePath, tokenNodes);
|
|
110
|
+
const resolvedTargets = resolveTargetsForIdentifier(ident, dep.expression, meta, nodesByClassName, nodesByFilePath, tokenNodes, factoryNodes);
|
|
109
111
|
for (const target of resolvedTargets) {
|
|
110
112
|
if (target.id === sourceNode.id) continue;
|
|
111
113
|
targets.set(target.id, target);
|
|
@@ -140,10 +142,14 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
140
142
|
lines.push(` %% Scope stability: ⚠️ ${VIOLATION_EDGE_COLOR} edge = captive dependency (longer-lived service depends on shorter-lived scope)`);
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
|
-
const allNodes = [
|
|
145
|
+
const allNodes = [
|
|
146
|
+
...serviceNodes,
|
|
147
|
+
...Array.from(factoryNodes.values()),
|
|
148
|
+
...Array.from(tokenNodes.values())
|
|
149
|
+
];
|
|
144
150
|
const groupedNodes = /* @__PURE__ */ new Map();
|
|
145
151
|
const ungroupedNodes = [];
|
|
146
|
-
for (const node of allNodes) if (node.type === "service" && node.scope && customScopes.has(node.scope)) {
|
|
152
|
+
for (const node of allNodes) if ((node.type === "service" || node.type === "factory") && node.scope && customScopes.has(node.scope)) {
|
|
147
153
|
const bucket = groupedNodes.get(node.scope) ?? [];
|
|
148
154
|
bucket.push(node);
|
|
149
155
|
groupedNodes.set(node.scope, bucket);
|
|
@@ -179,7 +185,8 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
179
185
|
diagram: lines.join("\n"),
|
|
180
186
|
nodeCount: allNodes.length,
|
|
181
187
|
edgeCount: edges.length,
|
|
182
|
-
tokenCount: tokenNodes.size
|
|
188
|
+
tokenCount: tokenNodes.size,
|
|
189
|
+
factoryCount: factoryNodes.size
|
|
183
190
|
};
|
|
184
191
|
}
|
|
185
192
|
/**
|
|
@@ -216,12 +223,14 @@ function scopeCode(node) {
|
|
|
216
223
|
}
|
|
217
224
|
/**
|
|
218
225
|
* True when an edge captures a shorter-lived dependency in a longer-lived host,
|
|
219
|
-
* per the declared hierarchy.
|
|
220
|
-
* are
|
|
226
|
+
* per the declared hierarchy. Service dependencies on scoped factory providers
|
|
227
|
+
* are checked too: factory bodies are opaque, but the factory result lifecycle
|
|
228
|
+
* is known and can still be captive.
|
|
221
229
|
*/
|
|
222
230
|
function isStabilityViolation(from, to, scopes) {
|
|
223
231
|
if (!scopes) return false;
|
|
224
|
-
if (from.type !== "service"
|
|
232
|
+
if (from.type !== "service") return false;
|
|
233
|
+
if (to.type !== "service" && to.type !== "factory") return false;
|
|
225
234
|
if (!from.scope || !to.scope) return false;
|
|
226
235
|
return !isDependencyAllowed(from.scope, to.scope, scopes);
|
|
227
236
|
}
|
|
@@ -238,6 +247,7 @@ function describeEdge(from, to) {
|
|
|
238
247
|
*/
|
|
239
248
|
function nodeFill(node, opts) {
|
|
240
249
|
if (node.type === "token") return `fill:${opts.tokenNodeFill}`;
|
|
250
|
+
if (node.type === "factory") return `fill:${opts.factoryNodeFill}`;
|
|
241
251
|
if (node.hasFactory) return `fill:${opts.factoryNodeFill}`;
|
|
242
252
|
if (node.isLazyOnly) return `fill:${opts.lazyNodeFill}`;
|
|
243
253
|
const scopeFill = node.scope ? opts.scopeColors[node.scope] : void 0;
|
|
@@ -278,14 +288,17 @@ function inferIdentifiersFromExpression(expression) {
|
|
|
278
288
|
/**
|
|
279
289
|
* Resolves a dependency identifier to known service nodes, or creates a token node when unresolved.
|
|
280
290
|
*/
|
|
281
|
-
function resolveTargetsForIdentifier(identifier, fallbackExpression, meta, nodesByClassName, nodesByFilePath, tokenNodes) {
|
|
291
|
+
function resolveTargetsForIdentifier(identifier, fallbackExpression, meta, nodesByClassName, nodesByFilePath, tokenNodes, factoryNodes) {
|
|
282
292
|
const serviceMatches = resolveServiceTargets(identifier, meta, nodesByClassName, nodesByFilePath);
|
|
283
293
|
if (serviceMatches.length) {
|
|
284
294
|
const deduped = /* @__PURE__ */ new Map();
|
|
285
295
|
for (const node of serviceMatches) deduped.set(node.id, node);
|
|
286
296
|
return Array.from(deduped.values());
|
|
287
297
|
}
|
|
288
|
-
|
|
298
|
+
const tokenLabel = createTokenLabel(identifier || fallbackExpression);
|
|
299
|
+
const factoryNode = factoryNodes.get(tokenLabel);
|
|
300
|
+
if (factoryNode) return [factoryNode];
|
|
301
|
+
return [ensureTokenNode(tokenNodes, tokenLabel)];
|
|
289
302
|
}
|
|
290
303
|
/**
|
|
291
304
|
* Attempts to find service nodes that match an identifier via import metadata or class names.
|
|
@@ -340,6 +353,23 @@ function ensureTokenNode(tokenNodes, label) {
|
|
|
340
353
|
tokenNodes.set(label, node);
|
|
341
354
|
return node;
|
|
342
355
|
}
|
|
356
|
+
function ensureFactoryNode(factoryNodes, provider) {
|
|
357
|
+
const label = createTokenLabel(provider.tokenLabel || provider.tokenExpression);
|
|
358
|
+
const existing = factoryNodes.get(label);
|
|
359
|
+
if (existing) return existing;
|
|
360
|
+
const node = {
|
|
361
|
+
id: sanitizeMermaidId(`factory:${provider.filePath}:${provider.tokenExpression}`, factoryNodes.size),
|
|
362
|
+
label: `Factory: ${label}`,
|
|
363
|
+
key: label,
|
|
364
|
+
scope: provider.lifecycle,
|
|
365
|
+
type: "factory",
|
|
366
|
+
isLazyOnly: false,
|
|
367
|
+
hasFactory: true,
|
|
368
|
+
filePath: normalizeImportPath(provider.filePath)
|
|
369
|
+
};
|
|
370
|
+
factoryNodes.set(label, node);
|
|
371
|
+
return node;
|
|
372
|
+
}
|
|
343
373
|
/**
|
|
344
374
|
* Condenses arbitrary strings into stable token labels, truncating overly long values.
|
|
345
375
|
*/
|
|
@@ -26,6 +26,7 @@ function alloy(options = {}) {
|
|
|
26
26
|
let isDevMode;
|
|
27
27
|
const lazyServiceKeys = new Set((options.lazyServices ?? []).map(toLazyServiceKey));
|
|
28
28
|
const discoveryRuntime = createDiscoveryRuntime();
|
|
29
|
+
const shouldTrackFactoryProviders = () => Boolean(resolvedVisualization);
|
|
29
30
|
return {
|
|
30
31
|
name: "vite-plugin-alloy",
|
|
31
32
|
enforce: "pre",
|
|
@@ -59,7 +60,7 @@ function alloy(options = {}) {
|
|
|
59
60
|
exclude: [/\.d\.ts$/i, /node_modules/]
|
|
60
61
|
} },
|
|
61
62
|
handler(code, id) {
|
|
62
|
-
discoveryRuntime.processUpdate(id, code);
|
|
63
|
+
discoveryRuntime.processUpdate(id, code, { factoryProviders: shouldTrackFactoryProviders() });
|
|
63
64
|
return null;
|
|
64
65
|
}
|
|
65
66
|
},
|
|
@@ -76,7 +77,7 @@ function alloy(options = {}) {
|
|
|
76
77
|
} catch {
|
|
77
78
|
return;
|
|
78
79
|
}
|
|
79
|
-
discoveryChanged = discoveryRuntime.processUpdate(file, code);
|
|
80
|
+
discoveryChanged = discoveryRuntime.processUpdate(file, code, { factoryProviders: shouldTrackFactoryProviders() });
|
|
80
81
|
}
|
|
81
82
|
if (!discoveryChanged) return;
|
|
82
83
|
invalidateContainerModule(ctx.server, resolvedVirtualModuleId);
|
|
@@ -85,11 +86,17 @@ function alloy(options = {}) {
|
|
|
85
86
|
},
|
|
86
87
|
buildStart() {
|
|
87
88
|
discoveryRuntime.clear();
|
|
88
|
-
for (const ref of providerModuleRefs)
|
|
89
|
+
for (const ref of providerModuleRefs) {
|
|
90
|
+
this.addWatchFile(ref.absPath);
|
|
91
|
+
if (shouldTrackFactoryProviders()) try {
|
|
92
|
+
const code = fs.readFileSync(ref.absPath, "utf-8");
|
|
93
|
+
discoveryRuntime.processUpdate(ref.absPath, code, { factoryProviders: true });
|
|
94
|
+
} catch {}
|
|
95
|
+
}
|
|
89
96
|
const files = walkSync(path.join(resolvedRoot, "src"));
|
|
90
97
|
for (const file of files) if (/\.(tsx?|ts)$/i.test(file) && !file.endsWith(".d.ts")) try {
|
|
91
98
|
const code = fs.readFileSync(file, "utf-8");
|
|
92
|
-
discoveryRuntime.processUpdate(file, code);
|
|
99
|
+
discoveryRuntime.processUpdate(file, code, { factoryProviders: shouldTrackFactoryProviders() });
|
|
93
100
|
} catch {}
|
|
94
101
|
},
|
|
95
102
|
load: {
|
|
@@ -101,6 +108,7 @@ function alloy(options = {}) {
|
|
|
101
108
|
lazyReferencedClassKeys: discoveryRuntime.lazyReferencedClassKeys,
|
|
102
109
|
manifests: options.manifests ?? [],
|
|
103
110
|
providerImportPaths: providerModuleRefs.map((ref) => ref.importPath),
|
|
111
|
+
factoryProviders: shouldTrackFactoryProviders() ? Array.from(discoveryRuntime.factoryProvidersByFile.values()).flat() : [],
|
|
104
112
|
lazyServiceKeys,
|
|
105
113
|
packageName,
|
|
106
114
|
resolvedRoot,
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Newable, Token, createToken } from "./lib/types.js";
|
|
2
2
|
import { AlloyScopes, ResolutionContext, ServiceScope } from "./lib/scope.js";
|
|
3
3
|
import { ServiceIdentifier, clearServiceIdentifierRegistry, getConstructorByIdentifier, getServiceIdentifier, registerServiceIdentifier } from "./lib/service-identifiers.js";
|
|
4
|
-
import { Container } from "./lib/container.js";
|
|
4
|
+
import { Container, FactoryFn } from "./lib/container.js";
|
|
5
5
|
import { EnvDetectionOverrides, setEnvDetectionOverrides } from "./lib/env-detection.js";
|
|
6
6
|
import { LAZY_IDENTIFIER, Lazy } from "./lib/lazy.js";
|
|
7
7
|
import { Injectable, Singleton, assertDeps, dependenciesRegistry, deps } from "./lib/decorators.js";
|
|
8
|
-
import { ProviderDefinitions, applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
-
export { type AlloyScopes, Container, type EnvDetectionOverrides, Injectable, LAZY_IDENTIFIER, Lazy, type Lazy as LazyInterface, type Newable, type ProviderDefinitions, type ResolutionContext, type ServiceIdentifier, ServiceScope, Singleton, type Token, applyProviders, asClass, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
|
8
|
+
import { FactoryProviderDescriptor, ProviderDefinitions, applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
+
export { type AlloyScopes, Container, type EnvDetectionOverrides, type FactoryFn, type FactoryProviderDescriptor, Injectable, LAZY_IDENTIFIER, Lazy, type Lazy as LazyInterface, type Newable, type ProviderDefinitions, type ResolutionContext, type ServiceIdentifier, ServiceScope, Singleton, type Token, applyProviders, asClass, asFactory, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
package/dist/runtime.js
CHANGED
|
@@ -5,5 +5,5 @@ import { Injectable, Singleton, assertDeps, dependenciesRegistry, deps } from ".
|
|
|
5
5
|
import { clearServiceIdentifierRegistry, getConstructorByIdentifier, getServiceIdentifier, registerServiceIdentifier } from "./lib/service-identifiers.js";
|
|
6
6
|
import { setEnvDetectionOverrides } from "./lib/env-detection.js";
|
|
7
7
|
import { Container } from "./lib/container.js";
|
|
8
|
-
import { applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
-
export { Container, Injectable, LAZY_IDENTIFIER, Lazy, ServiceScope, Singleton, applyProviders, asClass, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
|
8
|
+
import { applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
+
export { Container, Injectable, LAZY_IDENTIFIER, Lazy, ServiceScope, Singleton, applyProviders, asClass, asFactory, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
package/dist/scopes.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Constructor, Newable, Token } from "./lib/types.js";
|
|
2
|
-
import { ResolutionContext, ServiceScope } from "./lib/scope.js";
|
|
2
|
+
import { FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope } from "./lib/scope.js";
|
|
3
3
|
import { ServiceIdentifier } from "./lib/service-identifiers.js";
|
|
4
4
|
import { Container } from "./lib/container.js";
|
|
5
5
|
|
|
@@ -14,6 +14,8 @@ declare class Scope implements ResolutionContext {
|
|
|
14
14
|
private readonly cached;
|
|
15
15
|
private readonly pending;
|
|
16
16
|
private readonly valueProviders;
|
|
17
|
+
private readonly factoryValues;
|
|
18
|
+
private readonly factoryPending;
|
|
17
19
|
private readonly activeChildren;
|
|
18
20
|
private readonly instantiatedInstances;
|
|
19
21
|
constructor(parent: ResolutionContext, scopeName: ServiceScope);
|
|
@@ -29,6 +31,11 @@ declare class Scope implements ResolutionContext {
|
|
|
29
31
|
deletePending(target: Constructor): void;
|
|
30
32
|
getProvider(tokenId: symbol): unknown;
|
|
31
33
|
hasProvider(tokenId: symbol): boolean;
|
|
34
|
+
getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
|
|
35
|
+
setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
|
|
36
|
+
getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
|
|
37
|
+
setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
|
|
38
|
+
deleteFactoryPending(tokenId: symbol, generation: number): void;
|
|
32
39
|
/**
|
|
33
40
|
* Register a value provider for a token scoped to this context.
|
|
34
41
|
*/
|
package/dist/scopes.js
CHANGED
|
@@ -9,6 +9,8 @@ var Scope = class Scope {
|
|
|
9
9
|
cached = /* @__PURE__ */ new Map();
|
|
10
10
|
pending = /* @__PURE__ */ new Map();
|
|
11
11
|
valueProviders = /* @__PURE__ */ new Map();
|
|
12
|
+
factoryValues = /* @__PURE__ */ new Map();
|
|
13
|
+
factoryPending = /* @__PURE__ */ new Map();
|
|
12
14
|
activeChildren = /* @__PURE__ */ new Set();
|
|
13
15
|
instantiatedInstances = [];
|
|
14
16
|
constructor(parent, scopeName) {
|
|
@@ -48,6 +50,28 @@ var Scope = class Scope {
|
|
|
48
50
|
if (this.valueProviders.has(tokenId)) return true;
|
|
49
51
|
return this.parent.hasProvider(tokenId);
|
|
50
52
|
}
|
|
53
|
+
getFactoryValue(tokenId) {
|
|
54
|
+
return this.factoryValues.get(tokenId);
|
|
55
|
+
}
|
|
56
|
+
setFactoryValue(tokenId, generation, value) {
|
|
57
|
+
this.factoryValues.set(tokenId, {
|
|
58
|
+
generation,
|
|
59
|
+
value
|
|
60
|
+
});
|
|
61
|
+
this.instantiatedInstances.push(value);
|
|
62
|
+
}
|
|
63
|
+
getFactoryPending(tokenId) {
|
|
64
|
+
return this.factoryPending.get(tokenId);
|
|
65
|
+
}
|
|
66
|
+
setFactoryPending(tokenId, generation, promise) {
|
|
67
|
+
this.factoryPending.set(tokenId, {
|
|
68
|
+
generation,
|
|
69
|
+
promise
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
deleteFactoryPending(tokenId, generation) {
|
|
73
|
+
if (this.factoryPending.get(tokenId)?.generation === generation) this.factoryPending.delete(tokenId);
|
|
74
|
+
}
|
|
51
75
|
/**
|
|
52
76
|
* Register a value provider for a token scoped to this context.
|
|
53
77
|
*/
|
|
@@ -111,6 +135,8 @@ var Scope = class Scope {
|
|
|
111
135
|
this.cached.clear();
|
|
112
136
|
this.pending.clear();
|
|
113
137
|
this.valueProviders.clear();
|
|
138
|
+
this.factoryValues.clear();
|
|
139
|
+
this.factoryPending.clear();
|
|
114
140
|
if (this.parent instanceof Scope) this.parent.activeChildren.delete(this);
|
|
115
141
|
}
|
|
116
142
|
if (errors.length > 0) {
|
package/dist/test.d.ts
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
import { Newable, Token, createToken } from "./lib/types.js";
|
|
2
|
+
import { ServiceScope } from "./lib/scope.js";
|
|
2
3
|
import { ServiceIdentifier } from "./lib/service-identifiers.js";
|
|
3
|
-
import { Container } from "./lib/container.js";
|
|
4
|
+
import { Container, FactoryFn } from "./lib/container.js";
|
|
4
5
|
import { ProviderDefinitions } from "./lib/providers.js";
|
|
6
|
+
import { Scope } from "./scopes.js";
|
|
5
7
|
import { MockOf } from "./lib/testing/mocking.js";
|
|
6
8
|
import { vi } from "vitest";
|
|
7
9
|
|
|
8
10
|
//#region src/test.d.ts
|
|
11
|
+
type FactoryOverrideSpec<T = unknown> = [token: Token<T>, factory: FactoryFn<T>, options?: {
|
|
12
|
+
lifecycle?: ServiceScope;
|
|
13
|
+
}];
|
|
14
|
+
type TestScopeHierarchy = Record<string, {
|
|
15
|
+
parent?: ServiceScope;
|
|
16
|
+
}>;
|
|
9
17
|
interface OverrideSpec {
|
|
10
18
|
/** Class constructor instance overrides */
|
|
11
19
|
instances?: Array<[Newable<unknown>, unknown]>;
|
|
12
20
|
/** Token value overrides */
|
|
13
21
|
tokens?: Array<[Token<unknown>, unknown]>;
|
|
22
|
+
/**
|
|
23
|
+
* Token factory overrides. These are explicit token-keyed overrides and are
|
|
24
|
+
* not affected by autoMock, which only mocks class constructor dependencies.
|
|
25
|
+
* Token value overrides still take precedence for the same token.
|
|
26
|
+
*/
|
|
27
|
+
factories?: FactoryOverrideSpec[];
|
|
14
28
|
}
|
|
15
29
|
interface CreateTestContainerOptions {
|
|
16
30
|
overrides?: OverrideSpec;
|
|
17
31
|
autoMock?: boolean;
|
|
18
32
|
target?: Newable<unknown>;
|
|
19
33
|
providers?: ProviderDefinitions | ProviderDefinitions[];
|
|
34
|
+
/** Custom scope hierarchy for tests, matching alloy({ scopes }) shape. */
|
|
35
|
+
scopes?: TestScopeHierarchy;
|
|
20
36
|
}
|
|
21
37
|
interface TestContainerHandle {
|
|
22
38
|
container: Container;
|
|
@@ -26,6 +42,16 @@ interface TestContainerHandle {
|
|
|
26
42
|
getToken<T>(token: Token<T>): T;
|
|
27
43
|
/** Provide a token value into the container */
|
|
28
44
|
provideToken?<T>(token: Token<T>, value: T): void;
|
|
45
|
+
/** Provide a token factory into the container */
|
|
46
|
+
provideFactory?<T>(token: Token<T>, factory: FactoryFn<T>, options?: {
|
|
47
|
+
lifecycle?: ServiceScope;
|
|
48
|
+
}): void;
|
|
49
|
+
/** Alias for provideFactory when replacing an existing test factory. */
|
|
50
|
+
overrideFactory?<T>(token: Token<T>, factory: FactoryFn<T>, options?: {
|
|
51
|
+
lifecycle?: ServiceScope;
|
|
52
|
+
}): void;
|
|
53
|
+
/** Create a scope under the test container. */
|
|
54
|
+
createScope?(scopeName: ServiceScope): Scope;
|
|
29
55
|
/** Placeholder restore hook (future phases may implement overlay stacks). */
|
|
30
56
|
restore(): void;
|
|
31
57
|
/** Retrieve a single class mock (if autoMock enabled). */
|
|
@@ -39,12 +65,20 @@ interface TestContainerHandle {
|
|
|
39
65
|
}
|
|
40
66
|
/**
|
|
41
67
|
* Create a test-focused container with manual overrides.
|
|
42
|
-
* -
|
|
68
|
+
* - Auto-mocking, when enabled, only mocks class constructor dependencies.
|
|
69
|
+
* - Token factories are explicit overrides and are applied after provider blocks.
|
|
43
70
|
*/
|
|
44
71
|
declare function createTestContainer(opts?: CreateTestContainerOptions | OverrideSpec): TestContainerHandle & {
|
|
45
72
|
getMock<T>(ctor: Newable<T>): MockOf<T> | undefined;
|
|
46
73
|
getMocks<T extends readonly Newable<unknown>[]>(ctors: T): { [K in keyof T]: T[K] extends Newable<infer I> ? MockOf<I> | undefined : never };
|
|
47
74
|
provideToken<T>(token: Token<T>, value: T): void;
|
|
75
|
+
provideFactory<T>(token: Token<T>, factory: FactoryFn<T>, options?: {
|
|
76
|
+
lifecycle?: ServiceScope;
|
|
77
|
+
}): void;
|
|
78
|
+
overrideFactory<T>(token: Token<T>, factory: FactoryFn<T>, options?: {
|
|
79
|
+
lifecycle?: ServiceScope;
|
|
80
|
+
}): void;
|
|
81
|
+
createScope(scopeName: ServiceScope): Scope;
|
|
48
82
|
};
|
|
49
83
|
//#endregion
|
|
50
|
-
export { CreateTestContainerOptions, type MockOf, OverrideSpec, TestContainerHandle, createTestContainer, createToken };
|
|
84
|
+
export { CreateTestContainerOptions, FactoryOverrideSpec, type MockOf, OverrideSpec, TestContainerHandle, TestScopeHierarchy, createTestContainer, createToken };
|
package/dist/test.js
CHANGED
|
@@ -3,18 +3,27 @@ import { Container } from "./lib/container.js";
|
|
|
3
3
|
import { applyProviders } from "./lib/providers.js";
|
|
4
4
|
import { restoreRegistry, snapshotRegistry } from "./lib/testing/registry.js";
|
|
5
5
|
import { applyAutoMocks } from "./lib/testing/mocking.js";
|
|
6
|
+
import { createScope } from "./scopes.js";
|
|
6
7
|
//#region src/test.ts
|
|
8
|
+
function normalizeScopeHierarchy(scopes) {
|
|
9
|
+
const hierarchy = {};
|
|
10
|
+
for (const [scopeName, config] of Object.entries(scopes)) hierarchy[scopeName] = config.parent ?? "singleton";
|
|
11
|
+
return hierarchy;
|
|
12
|
+
}
|
|
7
13
|
/**
|
|
8
14
|
* Create a test-focused container with manual overrides.
|
|
9
|
-
* -
|
|
15
|
+
* - Auto-mocking, when enabled, only mocks class constructor dependencies.
|
|
16
|
+
* - Token factories are explicit overrides and are applied after provider blocks.
|
|
10
17
|
*/
|
|
11
18
|
function createTestContainer(opts) {
|
|
12
|
-
const isLegacy = !!opts && !("autoMock" in opts) && !("target" in opts) && !("overrides" in opts);
|
|
19
|
+
const isLegacy = !!opts && !("autoMock" in opts) && !("target" in opts) && !("overrides" in opts) && !("providers" in opts) && !("scopes" in opts);
|
|
13
20
|
const normalizedOpts = isLegacy ? { overrides: opts } : opts ?? {};
|
|
14
21
|
const overrides = normalizedOpts.overrides ?? (isLegacy ? opts : void 0);
|
|
15
22
|
const container = new Container();
|
|
16
23
|
const snapshot = snapshotRegistry();
|
|
24
|
+
if (normalizedOpts.scopes) container._registerScopeHierarchy(normalizeScopeHierarchy(normalizedOpts.scopes));
|
|
17
25
|
if (normalizedOpts.providers) applyProviders(container, normalizedOpts.providers);
|
|
26
|
+
for (const [tok, factory, options] of overrides?.factories ?? []) container.provideFactory(tok, factory, options);
|
|
18
27
|
for (const [tok, value] of overrides?.tokens ?? []) container.provideValue(tok, value);
|
|
19
28
|
const overriddenCtors = new Set(overrides?.instances?.map(([c]) => c) ?? []);
|
|
20
29
|
for (const [ctor, instance] of overrides?.instances ?? []) container.overrideInstance(ctor, instance);
|
|
@@ -39,6 +48,15 @@ function createTestContainer(opts) {
|
|
|
39
48
|
provideToken: (token, value) => {
|
|
40
49
|
container.provideValue(token, value);
|
|
41
50
|
},
|
|
51
|
+
provideFactory: (token, factory, options) => {
|
|
52
|
+
container.provideFactory(token, factory, options);
|
|
53
|
+
},
|
|
54
|
+
overrideFactory: (token, factory, options) => {
|
|
55
|
+
container.provideFactory(token, factory, options);
|
|
56
|
+
},
|
|
57
|
+
createScope: (scopeName) => {
|
|
58
|
+
return createScope(container, scopeName);
|
|
59
|
+
},
|
|
42
60
|
getMock: (ctor) => {
|
|
43
61
|
return mocks?.get(ctor) ?? void 0;
|
|
44
62
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"root":["../src/entry-points.test.ts","../src/fixture-subpaths.test.ts","../src/rollup.ts","../src/runtime.ts","../src/scopes.ts","../src/test.ts","../src/vite.ts","../src/lib/container.identifiers.test.ts","../src/lib/container.internals.test.ts","../src/lib/container.test.ts","../src/lib/container.testing-features.test.ts","../src/lib/container.ts","../src/lib/decorators.runtime.test.ts","../src/lib/decorators.test-d.ts","../src/lib/decorators.ts","../src/lib/dependency-error.ts","../src/lib/env-detection.test.ts","../src/lib/env-detection.ts","../src/lib/lazy-retry.test.ts","../src/lib/lazy.ts","../src/lib/providers.test.ts","../src/lib/providers.ts","../src/lib/scope.ts","../src/lib/scopes.test.ts","../src/lib/service-identifiers.test.ts","../src/lib/service-identifiers.ts","../src/lib/tokens.test.ts","../src/lib/types.ts","../src/lib/testing/mocking.test.ts","../src/lib/testing/mocking.ts","../src/lib/testing/registry.test.ts","../src/lib/testing/registry.ts","../src/plugins/core/codegen.test.ts","../src/plugins/core/codegen.ts","../src/plugins/core/container-loader.test.ts","../src/plugins/core/container-loader.ts","../src/plugins/core/decorators.helpers.test.ts","../src/plugins/core/decorators.ts","../src/plugins/core/discovery-runtime.ts","../src/plugins/core/discovery-store.test.ts","../src/plugins/core/discovery-store.ts","../src/plugins/core/identifier-resolver.test.ts","../src/plugins/core/identifier-resolver.ts","../src/plugins/core/lazy-utils.ts","../src/plugins/core/lazy.helpers.test.ts","../src/plugins/core/lazy.ts","../src/plugins/core/manifest-utils.ts","../src/plugins/core/manifest-utils.validation.test.ts","../src/plugins/core/scanner.test.ts","../src/plugins/core/scanner.ts","../src/plugins/core/scopes-validation.test.ts","../src/plugins/core/scopes-validation.ts","../src/plugins/core/types.ts","../src/plugins/core/utils.ts","../src/plugins/core/utils.walk.test.ts","../src/plugins/core/utils.write.test.ts","../src/plugins/core/visualization-utils.ts","../src/plugins/core/visualizer.test.ts","../src/plugins/core/visualizer.ts","../src/plugins/rollup-plugin/barrel-exports.test.ts","../src/plugins/rollup-plugin/barrel-exports.ts","../src/plugins/rollup-plugin/build-utils.test.ts","../src/plugins/rollup-plugin/build-utils.ts","../src/plugins/rollup-plugin/dependency-resolution.test.ts","../src/plugins/rollup-plugin/dependency-resolution.ts","../src/plugins/rollup-plugin/index.test.ts","../src/plugins/rollup-plugin/index.ts","../src/plugins/rollup-plugin/manifest-deps.test.ts","../src/plugins/rollup-plugin/manifest-deps.ts","../src/plugins/rollup-plugin/package-exports.test.ts","../src/plugins/rollup-plugin/package-exports.ts","../src/plugins/rollup-plugin/types.ts","../src/plugins/vite-plugin/duplicate-registrations.test.ts","../src/plugins/vite-plugin/index.ts","../src/plugins/vite-plugin/lazy-services.test.ts","../src/plugins/vite-plugin/lifecycle-and-hmr.test.ts","../src/plugins/vite-plugin/module-generation.test.ts","../src/plugins/vite-plugin/module-invalidation.ts","../src/plugins/vite-plugin/test-utils.ts","../src/plugins/vite-plugin/transform-guards.test.ts","../src/plugins/vite-plugin/visualization-option.test.ts"],"version":"6.0.3"}
|
|
1
|
+
{"root":["../src/entry-points.test.ts","../src/fixture-subpaths.test.ts","../src/rollup.ts","../src/runtime.ts","../src/scopes.ts","../src/test.test.ts","../src/test.ts","../src/vite.ts","../src/lib/container.factories-scoped.test.ts","../src/lib/container.factories.test.ts","../src/lib/container.identifiers.test.ts","../src/lib/container.internals.test.ts","../src/lib/container.test.ts","../src/lib/container.testing-features.test.ts","../src/lib/container.ts","../src/lib/decorators.runtime.test.ts","../src/lib/decorators.test-d.ts","../src/lib/decorators.ts","../src/lib/dependency-error.ts","../src/lib/env-detection.test.ts","../src/lib/env-detection.ts","../src/lib/lazy-retry.test.ts","../src/lib/lazy.ts","../src/lib/providers.test.ts","../src/lib/providers.ts","../src/lib/scope.ts","../src/lib/scopes.test.ts","../src/lib/service-identifiers.test.ts","../src/lib/service-identifiers.ts","../src/lib/tokens.test.ts","../src/lib/types.ts","../src/lib/testing/mocking.test.ts","../src/lib/testing/mocking.ts","../src/lib/testing/registry.test.ts","../src/lib/testing/registry.ts","../src/plugins/core/codegen.test.ts","../src/plugins/core/codegen.ts","../src/plugins/core/container-loader.test.ts","../src/plugins/core/container-loader.ts","../src/plugins/core/decorators.helpers.test.ts","../src/plugins/core/decorators.ts","../src/plugins/core/discovery-runtime.ts","../src/plugins/core/discovery-store.test.ts","../src/plugins/core/discovery-store.ts","../src/plugins/core/identifier-resolver.test.ts","../src/plugins/core/identifier-resolver.ts","../src/plugins/core/lazy-utils.ts","../src/plugins/core/lazy.helpers.test.ts","../src/plugins/core/lazy.ts","../src/plugins/core/manifest-utils.ts","../src/plugins/core/manifest-utils.validation.test.ts","../src/plugins/core/scanner.test.ts","../src/plugins/core/scanner.ts","../src/plugins/core/scopes-validation.test.ts","../src/plugins/core/scopes-validation.ts","../src/plugins/core/types.ts","../src/plugins/core/utils.ts","../src/plugins/core/utils.walk.test.ts","../src/plugins/core/utils.write.test.ts","../src/plugins/core/visualization-utils.ts","../src/plugins/core/visualizer.test.ts","../src/plugins/core/visualizer.ts","../src/plugins/rollup-plugin/barrel-exports.test.ts","../src/plugins/rollup-plugin/barrel-exports.ts","../src/plugins/rollup-plugin/build-utils.test.ts","../src/plugins/rollup-plugin/build-utils.ts","../src/plugins/rollup-plugin/dependency-resolution.test.ts","../src/plugins/rollup-plugin/dependency-resolution.ts","../src/plugins/rollup-plugin/index.test.ts","../src/plugins/rollup-plugin/index.ts","../src/plugins/rollup-plugin/manifest-deps.test.ts","../src/plugins/rollup-plugin/manifest-deps.ts","../src/plugins/rollup-plugin/package-exports.test.ts","../src/plugins/rollup-plugin/package-exports.ts","../src/plugins/rollup-plugin/types.ts","../src/plugins/vite-plugin/duplicate-registrations.test.ts","../src/plugins/vite-plugin/index.ts","../src/plugins/vite-plugin/lazy-services.test.ts","../src/plugins/vite-plugin/lifecycle-and-hmr.test.ts","../src/plugins/vite-plugin/module-generation.test.ts","../src/plugins/vite-plugin/module-invalidation.ts","../src/plugins/vite-plugin/test-utils.ts","../src/plugins/vite-plugin/transform-guards.test.ts","../src/plugins/vite-plugin/visualization-option.test.ts"],"version":"6.0.3"}
|