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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # alloy-di
2
2
 
3
- `alloy-di` is a compile-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.
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
 
@@ -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
- * Resolve a singleton service with caching and in-flight creation coalescing.
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.
@@ -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
- if (metadata.scope === ServiceScope.SINGLETON) return this.resolveSingleton(target, metadata, nextStack);
121
- return this.createInstance(target, metadata.dependencies, nextStack, metadata.factory);
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
- * Resolve a singleton service with caching and in-flight creation coalescing.
125
- */
126
- async resolveSingleton(target, metadata, resolutionStack) {
127
- const cached = this.singletons.get(target);
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 = this.pendingSingletons.get(target);
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
- this.singletons.set(target, instance);
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
- this.pendingSingletons.set(target, creation);
191
+ targetCtx.setPending(target, creation);
136
192
  try {
137
193
  return await creation;
138
194
  } finally {
139
- this.pendingSingletons.delete(target);
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
- if (this.valueProviders.has(tok.id)) return this.valueProviders.get(tok.id);
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,
@@ -1,6 +1,6 @@
1
1
  import { Newable, Token } from "./types.js";
2
- import { Lazy } from "./lazy.js";
3
2
  import { ServiceScope } from "./scope.js";
3
+ import { Lazy } from "./lazy.js";
4
4
 
5
5
  //#region src/lib/decorators.d.ts
6
6
  /**
@@ -1,7 +1,7 @@
1
1
  import { Newable, Token } from "./types.js";
2
+ import { ServiceScope } from "./scope.js";
2
3
  import { Container } from "./container.js";
3
4
  import { Lazy } from "./lazy.js";
4
- import { ServiceScope } from "./scope.js";
5
5
 
6
6
  //#region src/lib/providers.d.ts
7
7
  /**
@@ -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
- type ServiceScope = (typeof ServiceScope)[keyof typeof ServiceScope];
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
- const code = generateContainerModule(metas, lazyClassKeys, providerImports, { isDev: options.isDevMode });
36
- writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir);
37
- writeVisualizationArtifact(metas, lazyClassKeys, options.resolvedVisualization);
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
- let scope = ServiceScope.TRANSIENT;
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.isStringLiteral(prop.initializer)) {
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
- if (decoratorName.endsWith("Singleton")) scope = ServiceScope.SINGLETON;
89
- return {
90
- scope,
91
- dependencies
92
- };
93
- }
94
- let depsNode;
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 (depsNode) dependencies = parseDependencies(depsNode, sourceFile);
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 };