alloy-di 1.3.0 → 1.4.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/README.md +1 -1
- package/dist/lib/container.d.ts +22 -5
- package/dist/lib/container.js +85 -25
- package/dist/lib/decorators.d.ts +1 -1
- package/dist/lib/providers.d.ts +1 -1
- package/dist/lib/scope.d.ts +19 -2
- package/dist/plugins/core/codegen.js +38 -2
- package/dist/plugins/core/container-loader.js +20 -7
- package/dist/plugins/core/decorators.js +12 -22
- package/dist/plugins/core/scopes-validation.d.ts +19 -0
- package/dist/plugins/core/scopes-validation.js +225 -0
- package/dist/plugins/core/types.d.ts +9 -4
- package/dist/plugins/core/visualizer.d.ts +2 -2
- package/dist/plugins/core/visualizer.js +71 -7
- package/dist/plugins/rollup-plugin/barrel-exports.js +51 -0
- package/dist/plugins/rollup-plugin/dependency-resolution.js +42 -0
- package/dist/plugins/rollup-plugin/index.d.ts +2 -23
- package/dist/plugins/rollup-plugin/index.js +15 -128
- package/dist/plugins/rollup-plugin/manifest-deps.js +48 -0
- package/dist/plugins/rollup-plugin/package-exports.js +20 -0
- package/dist/plugins/rollup-plugin/types.d.ts +36 -0
- package/dist/plugins/vite-plugin/index.d.ts +9 -1
- package/dist/plugins/vite-plugin/index.js +2 -1
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.js +2 -1
- package/dist/scopes.d.ts +66 -0
- package/dist/scopes.js +141 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# alloy-di
|
|
2
2
|
|
|
3
|
-
`alloy-di` is a
|
|
3
|
+
`alloy-di` is a build-time dependency injection toolkit for Vite. It scans your TypeScript during build, generates a static container, and ships a tiny runtime so you get dependency injection without reflection overhead.
|
|
4
4
|
|
|
5
5
|
## Highlights
|
|
6
6
|
|
package/dist/lib/container.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Constructor, Newable, Token } from "./types.js";
|
|
2
|
+
import { ResolutionContext, ServiceScope } from "./scope.js";
|
|
2
3
|
import { ServiceIdentifier } from "./service-identifiers.js";
|
|
3
4
|
|
|
4
5
|
//#region src/lib/container.d.ts
|
|
@@ -8,13 +9,23 @@ import { ServiceIdentifier } from "./service-identifiers.js";
|
|
|
8
9
|
* It stores metadata discovered at build time, resolves constructor dependencies,
|
|
9
10
|
* performs singleton caching, and supports token-based value providers.
|
|
10
11
|
*/
|
|
11
|
-
declare class Container {
|
|
12
|
+
declare class Container implements ResolutionContext {
|
|
12
13
|
private readonly singletons;
|
|
13
14
|
private readonly pendingSingletons;
|
|
14
15
|
private readonly instanceOverrides;
|
|
15
16
|
private readonly metadataCache;
|
|
16
17
|
private readonly valueProviders;
|
|
17
18
|
private readonly factoryWarningCache;
|
|
19
|
+
private readonly scopeHierarchy;
|
|
20
|
+
readonly scopeName: ServiceScope;
|
|
21
|
+
readonly parent: ResolutionContext | null;
|
|
22
|
+
getCached(target: Constructor): unknown;
|
|
23
|
+
setCached(target: Constructor, instance: unknown): void;
|
|
24
|
+
getPending(target: Constructor): Promise<unknown> | undefined;
|
|
25
|
+
setPending(target: Constructor, promise: Promise<unknown>): void;
|
|
26
|
+
deletePending(target: Constructor): void;
|
|
27
|
+
getProvider(tokenId: symbol): unknown;
|
|
28
|
+
hasProvider(tokenId: symbol): boolean;
|
|
18
29
|
/**
|
|
19
30
|
* Resolve (and construct) the requested service.
|
|
20
31
|
*
|
|
@@ -23,6 +34,14 @@ declare class Container {
|
|
|
23
34
|
*/
|
|
24
35
|
get<T>(target: Newable<T>): Promise<T>;
|
|
25
36
|
get<T>(identifier: ServiceIdentifier<T>): Promise<T>;
|
|
37
|
+
/** @internal */
|
|
38
|
+
_registerScopeHierarchy(hierarchy: Record<string, string>): void;
|
|
39
|
+
/** @internal */
|
|
40
|
+
_validateScopeParent(childScope: ServiceScope, parentScope: ServiceScope): void;
|
|
41
|
+
/** @internal */
|
|
42
|
+
_resolveInContext<T>(target: Newable<T> | ServiceIdentifier<T>, context: ResolutionContext, options?: {
|
|
43
|
+
skipFactoryWarning?: boolean;
|
|
44
|
+
}): Promise<T>;
|
|
26
45
|
/**
|
|
27
46
|
* Provide a concrete instance override for a class constructor.
|
|
28
47
|
* Used by test utilities to inject mocks/stubs without altering global metadata.
|
|
@@ -62,10 +81,8 @@ declare class Container {
|
|
|
62
81
|
* @throws Error if a circular dependency is detected
|
|
63
82
|
*/
|
|
64
83
|
private resolve;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
*/
|
|
68
|
-
private resolveSingleton;
|
|
84
|
+
private findContextForScope;
|
|
85
|
+
private resolveCached;
|
|
69
86
|
/**
|
|
70
87
|
* Instantiate a class by resolving and injecting all declared dependencies.
|
|
71
88
|
* Handles factory-lazy services by importing the real class before instantiation.
|
package/dist/lib/container.js
CHANGED
|
@@ -43,9 +43,51 @@ var Container = class {
|
|
|
43
43
|
metadataCache = /* @__PURE__ */ new Map();
|
|
44
44
|
valueProviders = /* @__PURE__ */ new Map();
|
|
45
45
|
factoryWarningCache = /* @__PURE__ */ new WeakSet();
|
|
46
|
+
scopeHierarchy = /* @__PURE__ */ new Map();
|
|
47
|
+
scopeName = ServiceScope.SINGLETON;
|
|
48
|
+
parent = null;
|
|
49
|
+
getCached(target) {
|
|
50
|
+
return this.singletons.get(target);
|
|
51
|
+
}
|
|
52
|
+
setCached(target, instance) {
|
|
53
|
+
this.singletons.set(target, instance);
|
|
54
|
+
}
|
|
55
|
+
getPending(target) {
|
|
56
|
+
return this.pendingSingletons.get(target);
|
|
57
|
+
}
|
|
58
|
+
setPending(target, promise) {
|
|
59
|
+
this.pendingSingletons.set(target, promise);
|
|
60
|
+
}
|
|
61
|
+
deletePending(target) {
|
|
62
|
+
this.pendingSingletons.delete(target);
|
|
63
|
+
}
|
|
64
|
+
getProvider(tokenId) {
|
|
65
|
+
return this.valueProviders.get(tokenId);
|
|
66
|
+
}
|
|
67
|
+
hasProvider(tokenId) {
|
|
68
|
+
return this.valueProviders.has(tokenId);
|
|
69
|
+
}
|
|
46
70
|
async get(targetOrIdentifier) {
|
|
47
71
|
if (typeof targetOrIdentifier === "symbol") return this.getByIdentifier(targetOrIdentifier);
|
|
48
|
-
return this.getByConstructor(targetOrIdentifier);
|
|
72
|
+
return this.getByConstructor(targetOrIdentifier, this);
|
|
73
|
+
}
|
|
74
|
+
/** @internal */
|
|
75
|
+
_registerScopeHierarchy(hierarchy) {
|
|
76
|
+
for (const [child, parent] of Object.entries(hierarchy)) this.scopeHierarchy.set(child, parent);
|
|
77
|
+
}
|
|
78
|
+
/** @internal */
|
|
79
|
+
_validateScopeParent(childScope, parentScope) {
|
|
80
|
+
const declaredParent = this.scopeHierarchy.get(childScope);
|
|
81
|
+
if (declaredParent && declaredParent !== parentScope) throw new Error(`[alloy] Invalid scope hierarchy construction: scope '${String(childScope)}' is declared with parent '${String(declaredParent)}', but was constructed with parent scope '${String(parentScope)}'.`);
|
|
82
|
+
}
|
|
83
|
+
/** @internal */
|
|
84
|
+
async _resolveInContext(target, context, options) {
|
|
85
|
+
if (typeof target === "symbol") {
|
|
86
|
+
const ctor = getConstructorByIdentifier(target);
|
|
87
|
+
if (!ctor) throw new Error(`No service registered for identifier ${target.description ?? target.toString()}`);
|
|
88
|
+
return this.getByConstructor(ctor, context, { skipFactoryWarning: true });
|
|
89
|
+
}
|
|
90
|
+
return this.getByConstructor(target, context, options);
|
|
49
91
|
}
|
|
50
92
|
/**
|
|
51
93
|
* Provide a concrete instance override for a class constructor.
|
|
@@ -69,7 +111,7 @@ var Container = class {
|
|
|
69
111
|
async getByIdentifier(identifier) {
|
|
70
112
|
const ctor = getConstructorByIdentifier(identifier);
|
|
71
113
|
if (!ctor) throw new Error(`No service registered for identifier ${identifier.description ?? identifier.toString()}`);
|
|
72
|
-
return this.getByConstructor(ctor, { skipFactoryWarning: true });
|
|
114
|
+
return this.getByConstructor(ctor, this, { skipFactoryWarning: true });
|
|
73
115
|
}
|
|
74
116
|
/**
|
|
75
117
|
* Register a concrete value for an injection token at runtime.
|
|
@@ -88,9 +130,9 @@ var Container = class {
|
|
|
88
130
|
if (!this.valueProviders.has(token.id)) throw new Error(`No provider registered for token ${token.description ?? String(token.id)}`);
|
|
89
131
|
return this.valueProviders.get(token.id);
|
|
90
132
|
}
|
|
91
|
-
async getByConstructor(target, options) {
|
|
133
|
+
async getByConstructor(target, context, options) {
|
|
92
134
|
if (!options?.skipFactoryWarning) this.maybeWarnFactoryLazyConstructorUsage(target);
|
|
93
|
-
return this.resolve(target, []);
|
|
135
|
+
return this.resolve(target, [], context);
|
|
94
136
|
}
|
|
95
137
|
maybeWarnFactoryLazyConstructorUsage(target) {
|
|
96
138
|
if (!isDevEnvironment()) return;
|
|
@@ -107,7 +149,7 @@ var Container = class {
|
|
|
107
149
|
* @returns Promise resolving to the service instance
|
|
108
150
|
* @throws Error if a circular dependency is detected
|
|
109
151
|
*/
|
|
110
|
-
async resolve(target, resolutionStack) {
|
|
152
|
+
async resolve(target, resolutionStack, context) {
|
|
111
153
|
const overridden = this.instanceOverrides.get(target);
|
|
112
154
|
if (overridden) return overridden;
|
|
113
155
|
if (resolutionStack.includes(target)) throw new DependencyResolutionError(`Circular dependency detected: ${[...resolutionStack.map((t) => t.name), target.name].join(" -> ")}`, {
|
|
@@ -117,26 +159,40 @@ var Container = class {
|
|
|
117
159
|
});
|
|
118
160
|
const metadata = this.getServiceMetadata(target);
|
|
119
161
|
const nextStack = [...resolutionStack, target];
|
|
120
|
-
|
|
121
|
-
return this.
|
|
162
|
+
const targetCtx = this.findContextForScope(metadata.scope, context);
|
|
163
|
+
if (metadata.scope === ServiceScope.SINGLETON) return this.resolveCached(target, metadata, nextStack, targetCtx);
|
|
164
|
+
if (metadata.scope === ServiceScope.TRANSIENT) return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory, targetCtx);
|
|
165
|
+
if (targetCtx.scopeName === metadata.scope) return this.resolveCached(target, metadata, nextStack, targetCtx);
|
|
166
|
+
return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory, targetCtx);
|
|
122
167
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
168
|
+
findContextForScope(scope, startingContext) {
|
|
169
|
+
if (scope === ServiceScope.SINGLETON) {
|
|
170
|
+
let current = startingContext;
|
|
171
|
+
while (current.parent) current = current.parent;
|
|
172
|
+
return current;
|
|
173
|
+
}
|
|
174
|
+
if (scope === ServiceScope.TRANSIENT) return startingContext;
|
|
175
|
+
let current = startingContext;
|
|
176
|
+
while (current) {
|
|
177
|
+
if (current.scopeName === scope) return current;
|
|
178
|
+
current = current.parent;
|
|
179
|
+
}
|
|
180
|
+
return startingContext;
|
|
181
|
+
}
|
|
182
|
+
async resolveCached(target, metadata, resolutionStack, targetCtx) {
|
|
183
|
+
const cached = targetCtx.getCached(target);
|
|
128
184
|
if (cached) return cached;
|
|
129
|
-
const pending =
|
|
185
|
+
const pending = targetCtx.getPending(target);
|
|
130
186
|
if (pending) return await pending;
|
|
131
|
-
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory).then((instance) => {
|
|
132
|
-
|
|
187
|
+
const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory, targetCtx).then((instance) => {
|
|
188
|
+
targetCtx.setCached(target, instance);
|
|
133
189
|
return instance;
|
|
134
190
|
});
|
|
135
|
-
|
|
191
|
+
targetCtx.setPending(target, creation);
|
|
136
192
|
try {
|
|
137
193
|
return await creation;
|
|
138
194
|
} finally {
|
|
139
|
-
|
|
195
|
+
targetCtx.deletePending(target);
|
|
140
196
|
}
|
|
141
197
|
}
|
|
142
198
|
/**
|
|
@@ -149,9 +205,9 @@ var Container = class {
|
|
|
149
205
|
* @param factory - Optional lazy factory to import the real class
|
|
150
206
|
* @returns Promise resolving to the instantiated service
|
|
151
207
|
*/
|
|
152
|
-
async createInstance(target, dependencies, resolutionStack, factory) {
|
|
208
|
+
async createInstance(target, dependencies, resolutionStack, factory, context) {
|
|
153
209
|
const ctor = factory ? await this.importWithRetry(factory, target, resolutionStack) : target;
|
|
154
|
-
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack))));
|
|
210
|
+
return new ctor(...await Promise.all(dependencies.map((param) => this.resolveParam(param, ctor, resolutionStack, context))));
|
|
155
211
|
}
|
|
156
212
|
/**
|
|
157
213
|
* Resolve a single dependency entry, handling lazies, tokens, and constructors.
|
|
@@ -163,7 +219,7 @@ var Container = class {
|
|
|
163
219
|
* @returns Promise resolving to the dependency instance
|
|
164
220
|
* @throws Error if dependency type is invalid
|
|
165
221
|
*/
|
|
166
|
-
async resolveParam(param, target, resolutionStack) {
|
|
222
|
+
async resolveParam(param, target, resolutionStack, context) {
|
|
167
223
|
const classification = classifyDependency(param);
|
|
168
224
|
if (!classification) {
|
|
169
225
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
@@ -176,10 +232,10 @@ var Container = class {
|
|
|
176
232
|
switch (classification.kind) {
|
|
177
233
|
case "lazy": {
|
|
178
234
|
const depClass = await this.importWithRetry(classification.lazy, target, resolutionStack);
|
|
179
|
-
return this.resolve(depClass, resolutionStack);
|
|
235
|
+
return this.resolve(depClass, resolutionStack, context);
|
|
180
236
|
}
|
|
181
|
-
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack);
|
|
182
|
-
case "constructor": return this.resolve(classification.ctor, resolutionStack);
|
|
237
|
+
case "token": return this.resolveTokenLike(classification.token, target, resolutionStack, context);
|
|
238
|
+
case "constructor": return this.resolve(classification.ctor, resolutionStack, context);
|
|
183
239
|
}
|
|
184
240
|
return classification;
|
|
185
241
|
}
|
|
@@ -238,8 +294,12 @@ var Container = class {
|
|
|
238
294
|
/**
|
|
239
295
|
* Resolve a token dependency via registered value providers.
|
|
240
296
|
*/
|
|
241
|
-
resolveTokenLike(tok, target, resolutionStack) {
|
|
242
|
-
|
|
297
|
+
resolveTokenLike(tok, target, resolutionStack, context) {
|
|
298
|
+
let current = context;
|
|
299
|
+
while (current) {
|
|
300
|
+
if (current.hasProvider(tok.id)) return current.getProvider(tok.id);
|
|
301
|
+
current = current.parent;
|
|
302
|
+
}
|
|
243
303
|
const stackPath = this.formatStackPath(target, resolutionStack);
|
|
244
304
|
throw new DependencyResolutionError(`No provider registered for token ${tok.description ?? String(tok.id)} while resolving ${target.name}. Resolution stack: ${stackPath}`, {
|
|
245
305
|
target,
|
package/dist/lib/decorators.d.ts
CHANGED
package/dist/lib/providers.d.ts
CHANGED
package/dist/lib/scope.d.ts
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
import { Constructor } from "./types.js";
|
|
2
|
+
|
|
1
3
|
//#region src/lib/scope.d.ts
|
|
2
4
|
declare const ServiceScope: {
|
|
3
5
|
readonly SINGLETON: "singleton";
|
|
4
6
|
readonly TRANSIENT: "transient";
|
|
5
7
|
};
|
|
6
|
-
|
|
8
|
+
interface AlloyScopes {
|
|
9
|
+
singleton: true;
|
|
10
|
+
transient: true;
|
|
11
|
+
}
|
|
12
|
+
type ServiceScope = keyof AlloyScopes;
|
|
13
|
+
interface ResolutionContext {
|
|
14
|
+
readonly scopeName: ServiceScope;
|
|
15
|
+
getCached(target: Constructor): unknown;
|
|
16
|
+
setCached(target: Constructor, instance: unknown): void;
|
|
17
|
+
getPending(target: Constructor): Promise<unknown> | undefined;
|
|
18
|
+
setPending(target: Constructor, promise: Promise<unknown>): void;
|
|
19
|
+
deletePending(target: Constructor): void;
|
|
20
|
+
getProvider(tokenId: symbol): unknown;
|
|
21
|
+
hasProvider(tokenId: symbol): boolean;
|
|
22
|
+
readonly parent: ResolutionContext | null;
|
|
23
|
+
}
|
|
7
24
|
//#endregion
|
|
8
|
-
export { ServiceScope };
|
|
25
|
+
export { AlloyScopes, ResolutionContext, ServiceScope };
|
|
@@ -306,6 +306,18 @@ function createIdentifierExports(registrations) {
|
|
|
306
306
|
return `${registrations.map((meta) => `const ${meta.identifierConst} = registerServiceIdentifier(${meta.importName}, Symbol.for('${escapeSingleQuotes(meta.symbolDescription)}'));`).join("\n")}\n\nexport const serviceIdentifiers = {\n${registrations.map((meta) => ` '${meta.exportKey}': ${meta.identifierConst}`).join(",\n")}\n};\n`;
|
|
307
307
|
}
|
|
308
308
|
/**
|
|
309
|
+
* Builds the runtime scope-hierarchy registration statement. The generated
|
|
310
|
+
* container records the declared parent of each custom scope so child scopes
|
|
311
|
+
* constructed via `alloy-di/scopes` can be validated against the build-time
|
|
312
|
+
* hierarchy. Returns an empty string when no custom scopes are configured.
|
|
313
|
+
*/
|
|
314
|
+
function createScopeHierarchyBlock(scopes) {
|
|
315
|
+
if (!scopes) return "";
|
|
316
|
+
const names = Object.keys(scopes);
|
|
317
|
+
if (!names.length) return "";
|
|
318
|
+
return `\ncontainer._registerScopeHierarchy({\n${names.map((name) => ` ${JSON.stringify(name)}: ${JSON.stringify(scopes[name].parent ?? "singleton")}`).join(",\n")}\n});\n`;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
309
321
|
* Generates the virtual container module code.
|
|
310
322
|
* This module:
|
|
311
323
|
* 1. Imports the runtime container and necessary helpers.
|
|
@@ -323,6 +335,7 @@ function generateContainerModule(metas, lazyReferencedClassKeys, providerModuleP
|
|
|
323
335
|
const hasProviderModules = providerModulePaths.length > 0;
|
|
324
336
|
const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths, options);
|
|
325
337
|
const envOverridesBlock = options?.isDev === void 0 ? "" : `\nsetEnvDetectionOverrides({ isDev: ${options.isDev} });\n`;
|
|
338
|
+
const scopeHierarchyBlock = createScopeHierarchyBlock(options?.scopes);
|
|
326
339
|
let providerImportBlock = "";
|
|
327
340
|
let providerInvocationBlock = "";
|
|
328
341
|
if (hasProviderModules) {
|
|
@@ -336,7 +349,7 @@ ${providerImportBlock}
|
|
|
336
349
|
${registrationsBlock}
|
|
337
350
|
|
|
338
351
|
const container = new Container();
|
|
339
|
-
|
|
352
|
+
${scopeHierarchyBlock}
|
|
340
353
|
for (const entry of registrations) {
|
|
341
354
|
dependenciesRegistry.set(entry.ctor, entry.meta);
|
|
342
355
|
}
|
|
@@ -389,6 +402,29 @@ declare module "virtual:alloy-container" {
|
|
|
389
402
|
`;
|
|
390
403
|
}
|
|
391
404
|
/**
|
|
405
|
+
* Generates a standalone declaration file that augments `AlloyScopes` with the
|
|
406
|
+
* custom scope names, opening the `ServiceScope` union so `@Injectable('<scope>')`
|
|
407
|
+
* type-checks. Returns `undefined` when no custom scopes are configured.
|
|
408
|
+
*
|
|
409
|
+
* This MUST live in its own file: module augmentation requires the file to be a
|
|
410
|
+
* module (hence the trailing `export {}`), whereas the container declaration
|
|
411
|
+
* must stay a global script so `virtual:alloy-container` resolves everywhere.
|
|
412
|
+
* Combining the two in one file turns the container declaration into a module
|
|
413
|
+
* and breaks resolution of the virtual module.
|
|
414
|
+
*/
|
|
415
|
+
function generateScopeAugmentationDefinition(scopeNames) {
|
|
416
|
+
if (!scopeNames.length) return;
|
|
417
|
+
return `${GENERATED_FILE_HEADER}
|
|
418
|
+
declare module "alloy-di/runtime" {
|
|
419
|
+
interface AlloyScopes {
|
|
420
|
+
${scopeNames.map((name) => ` ${JSON.stringify(name)}: true;`).join("\n")}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export {};
|
|
425
|
+
`;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
392
428
|
* Generates ambient type declarations for external Alloy manifests consumed by the project.
|
|
393
429
|
* Creates:
|
|
394
430
|
* 1. `declare module "PKG/manifest"` typed as `LibraryManifest`.
|
|
@@ -488,4 +524,4 @@ ${serviceIdentifiers}
|
|
|
488
524
|
}).join("\n");
|
|
489
525
|
}
|
|
490
526
|
//#endregion
|
|
491
|
-
export { GENERATED_FILE_NOTICE, generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition };
|
|
527
|
+
export { GENERATED_FILE_NOTICE, generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition, generateScopeAugmentationDefinition };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { normalizeImportPath, writeFileIfChanged } from "./utils.js";
|
|
2
2
|
import { IdentifierResolver } from "./identifier-resolver.js";
|
|
3
|
-
import { GENERATED_FILE_NOTICE, generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition } from "./codegen.js";
|
|
3
|
+
import { GENERATED_FILE_NOTICE, generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition, generateScopeAugmentationDefinition } from "./codegen.js";
|
|
4
4
|
import { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateManifestServices, groupMetasByName, readManifests, reconcileLazySet, toMetaFromManifest } from "./manifest-utils.js";
|
|
5
|
+
import { validateScopeStability, validateScopesConfig } from "./scopes-validation.js";
|
|
5
6
|
import { generateMermaidDiagram } from "./visualizer.js";
|
|
6
7
|
import { ensureDirectoryForFile } from "./visualization-utils.js";
|
|
7
8
|
import path from "node:path";
|
|
@@ -32,9 +33,16 @@ async function loadVirtualContainerModule(options) {
|
|
|
32
33
|
const providerImports = Array.from(new Set([...options.providerImportPaths, ...manifestData.providers]));
|
|
33
34
|
reconcileLazySet(metas, lazyClassKeys, collectEagerReferencedNames(metas));
|
|
34
35
|
augmentFactoryLazyServices(metas, options.lazyServiceKeys);
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
36
|
+
if (options.scopes && Object.keys(options.scopes).length > 0) {
|
|
37
|
+
validateScopesConfig(options.scopes);
|
|
38
|
+
validateScopeStability(metas, options.scopes);
|
|
39
|
+
}
|
|
40
|
+
const code = generateContainerModule(metas, lazyClassKeys, providerImports, {
|
|
41
|
+
isDev: options.isDevMode,
|
|
42
|
+
scopes: options.scopes
|
|
43
|
+
});
|
|
44
|
+
writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir, options.scopes);
|
|
45
|
+
writeVisualizationArtifact(metas, lazyClassKeys, options.resolvedVisualization, options.scopes);
|
|
38
46
|
return {
|
|
39
47
|
code,
|
|
40
48
|
moduleType: "js"
|
|
@@ -62,11 +70,15 @@ function assertNoDuplicateManifestServices(metas, manifestServices) {
|
|
|
62
70
|
"Resolve by removing one source (local or manifest) to avoid ambiguous DI keys."
|
|
63
71
|
].join("\n"));
|
|
64
72
|
}
|
|
65
|
-
function writeTypeDefinitions(metas, loadedManifests, resolvedRoot, containerDeclarationDir) {
|
|
73
|
+
function writeTypeDefinitions(metas, loadedManifests, resolvedRoot, containerDeclarationDir, scopes) {
|
|
66
74
|
const dtsDir = path.resolve(resolvedRoot, containerDeclarationDir ?? "./src");
|
|
67
75
|
const dtsContent = generateContainerTypeDefinition(metas, (filePath) => resolveDeclarationImportPath(dtsDir, filePath));
|
|
68
76
|
if (!fs.existsSync(dtsDir)) fs.mkdirSync(dtsDir, { recursive: true });
|
|
69
77
|
writeFileIfChanged(path.join(dtsDir, "alloy-container.d.ts"), dtsContent);
|
|
78
|
+
const scopeAugmentationPath = path.join(dtsDir, "alloy-scopes.d.ts");
|
|
79
|
+
const scopeAugmentation = generateScopeAugmentationDefinition(scopes ? Object.keys(scopes) : []);
|
|
80
|
+
if (scopeAugmentation) writeFileIfChanged(scopeAugmentationPath, scopeAugmentation);
|
|
81
|
+
else if (fs.existsSync(scopeAugmentationPath)) fs.rmSync(scopeAugmentationPath);
|
|
70
82
|
if (loadedManifests.length === 0) return;
|
|
71
83
|
const manifestsDts = generateManifestTypeDefinition(loadedManifests.map((m) => ({
|
|
72
84
|
packageName: m.packageName,
|
|
@@ -81,12 +93,13 @@ function resolveDeclarationImportPath(dtsDir, filePath) {
|
|
|
81
93
|
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
82
94
|
return rel;
|
|
83
95
|
}
|
|
84
|
-
function writeVisualizationArtifact(metas, lazyReferencedClassKeys, resolvedVisualization) {
|
|
96
|
+
function writeVisualizationArtifact(metas, lazyReferencedClassKeys, resolvedVisualization, scopes) {
|
|
85
97
|
if (!resolvedVisualization) return;
|
|
86
98
|
const artifact = generateMermaidDiagram({
|
|
87
99
|
metas,
|
|
88
100
|
lazyClassKeys: new Set(lazyReferencedClassKeys),
|
|
89
|
-
options: resolvedVisualization.mermaidOptions
|
|
101
|
+
options: resolvedVisualization.mermaidOptions,
|
|
102
|
+
scopes
|
|
90
103
|
});
|
|
91
104
|
ensureDirectoryForFile(resolvedVisualization.outputPath);
|
|
92
105
|
writeFileIfChanged(resolvedVisualization.outputPath, `%% ${GENERATED_FILE_NOTICE}\n${artifact.diagram}\n`);
|
|
@@ -66,9 +66,9 @@ function parseDependencies(node, sourceFile) {
|
|
|
66
66
|
return [];
|
|
67
67
|
}
|
|
68
68
|
function extractServiceMetadata(decoratorName, callExpression, sourceFile) {
|
|
69
|
-
|
|
69
|
+
const isSingletonDecorator = decoratorName.endsWith("Singleton");
|
|
70
|
+
let scope = isSingletonDecorator ? ServiceScope.SINGLETON : ServiceScope.TRANSIENT;
|
|
70
71
|
let dependencies = [];
|
|
71
|
-
if (decoratorName.endsWith("Singleton")) scope = ServiceScope.SINGLETON;
|
|
72
72
|
const args = callExpression.arguments;
|
|
73
73
|
if (args.length === 0) return {
|
|
74
74
|
scope,
|
|
@@ -78,30 +78,20 @@ function extractServiceMetadata(decoratorName, callExpression, sourceFile) {
|
|
|
78
78
|
if (ts.isObjectLiteralExpression(firstArg)) {
|
|
79
79
|
for (const prop of firstArg.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
80
80
|
if (prop.name.text === "scope") {
|
|
81
|
-
if (ts.
|
|
82
|
-
const val = prop.initializer.text;
|
|
83
|
-
if (val === "singleton") scope = ServiceScope.SINGLETON;
|
|
84
|
-
else if (val === "transient") scope = ServiceScope.TRANSIENT;
|
|
85
|
-
}
|
|
81
|
+
if (ts.isStringLiteralLike(prop.initializer)) scope = prop.initializer.text;
|
|
86
82
|
} else if (prop.name.text === "dependencies") dependencies = parseDependencies(prop.initializer, sourceFile);
|
|
87
83
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
if (ts.isStringLiteralLike(firstArg)) {
|
|
96
|
-
if (firstArg.text === "singleton") scope = ServiceScope.SINGLETON;
|
|
97
|
-
} else depsNode = firstArg;
|
|
98
|
-
if (args.length > 1) {
|
|
99
|
-
const secondArg = args[1];
|
|
100
|
-
if (ts.isStringLiteralLike(secondArg)) {
|
|
101
|
-
if (secondArg.text === "singleton") scope = ServiceScope.SINGLETON;
|
|
84
|
+
} else {
|
|
85
|
+
let depsNode;
|
|
86
|
+
if (ts.isStringLiteralLike(firstArg)) scope = firstArg.text;
|
|
87
|
+
else depsNode = firstArg;
|
|
88
|
+
if (args.length > 1) {
|
|
89
|
+
const secondArg = args[1];
|
|
90
|
+
if (ts.isStringLiteralLike(secondArg)) scope = secondArg.text;
|
|
102
91
|
}
|
|
92
|
+
if (depsNode) dependencies = parseDependencies(depsNode, sourceFile);
|
|
103
93
|
}
|
|
104
|
-
if (
|
|
94
|
+
if (isSingletonDecorator) scope = ServiceScope.SINGLETON;
|
|
105
95
|
return {
|
|
106
96
|
scope,
|
|
107
97
|
dependencies
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/plugins/core/scopes-validation.d.ts
|
|
2
|
+
/** Declared parent relationship for a single custom scope. */
|
|
3
|
+
interface AlloyScopeConfig {
|
|
4
|
+
/**
|
|
5
|
+
* The next-longer-lived scope: either `"singleton"` or another custom scope.
|
|
6
|
+
* Defaults to `"singleton"` when omitted.
|
|
7
|
+
*/
|
|
8
|
+
parent?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Hierarchy of custom, application-defined scopes keyed by scope name. The two
|
|
12
|
+
* built-in lifecycles (`singleton` as the implicit root, `transient` as the
|
|
13
|
+
* implicit leaf) are never declared here.
|
|
14
|
+
*/
|
|
15
|
+
interface AlloyScopesConfig {
|
|
16
|
+
[scopeName: string]: AlloyScopeConfig;
|
|
17
|
+
}
|
|
18
|
+
//#endregion
|
|
19
|
+
export { AlloyScopesConfig };
|