alloy-di 1.2.3 → 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.
Files changed (39) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/container.d.ts +22 -5
  3. package/dist/lib/container.js +85 -25
  4. package/dist/lib/decorators.d.ts +1 -1
  5. package/dist/lib/env-detection.d.ts +24 -0
  6. package/dist/lib/env-detection.js +22 -9
  7. package/dist/lib/providers.d.ts +1 -1
  8. package/dist/lib/scope.d.ts +19 -2
  9. package/dist/plugins/core/codegen.js +118 -14
  10. package/dist/plugins/{vite-plugin → core}/container-loader.js +26 -13
  11. package/dist/plugins/core/decorators.js +12 -22
  12. package/dist/plugins/{vite-plugin → core}/discovery-runtime.js +4 -14
  13. package/dist/plugins/core/discovery-store.js +15 -0
  14. package/dist/plugins/{vite-plugin → core}/manifest-utils.js +14 -4
  15. package/dist/plugins/core/scanner.js +12 -0
  16. package/dist/plugins/core/scopes-validation.d.ts +19 -0
  17. package/dist/plugins/core/scopes-validation.js +225 -0
  18. package/dist/plugins/core/types.d.ts +9 -4
  19. package/dist/plugins/core/utils.js +45 -8
  20. package/dist/plugins/{vite-plugin → core}/visualization-utils.d.ts +1 -1
  21. package/dist/plugins/{vite-plugin → core}/visualization-utils.js +1 -1
  22. package/dist/plugins/{vite-plugin → core}/visualizer.d.ts +3 -3
  23. package/dist/plugins/{vite-plugin → core}/visualizer.js +92 -20
  24. package/dist/plugins/rollup-plugin/barrel-exports.js +51 -0
  25. package/dist/plugins/rollup-plugin/dependency-resolution.js +42 -0
  26. package/dist/plugins/rollup-plugin/index.d.ts +2 -23
  27. package/dist/plugins/rollup-plugin/index.js +15 -128
  28. package/dist/plugins/rollup-plugin/manifest-deps.js +48 -0
  29. package/dist/plugins/rollup-plugin/package-exports.js +20 -0
  30. package/dist/plugins/rollup-plugin/types.d.ts +36 -0
  31. package/dist/plugins/vite-plugin/index.d.ts +10 -2
  32. package/dist/plugins/vite-plugin/index.js +9 -4
  33. package/dist/plugins/vite-plugin/module-invalidation.js +13 -0
  34. package/dist/runtime.d.ts +3 -1
  35. package/dist/runtime.js +3 -1
  36. package/dist/scopes.d.ts +66 -0
  37. package/dist/scopes.js +141 -0
  38. package/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +7 -2
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
  /**
@@ -0,0 +1,24 @@
1
+ //#region src/lib/env-detection.d.ts
2
+ type ImportMetaEnvShape = {
3
+ MODE?: string;
4
+ PROD?: boolean;
5
+ NODE_ENV?: string;
6
+ [key: string]: unknown;
7
+ };
8
+ type EnvDetectionOverrides = {
9
+ /**
10
+ * Explicit import.meta.env replacement. Use `null` to force "no env" behavior.
11
+ */
12
+ importMetaEnv?: ImportMetaEnvShape | null;
13
+ /**
14
+ * Explicit NODE_ENV replacement. Use `null` to ignore process.env.
15
+ */
16
+ nodeEnv?: string | null;
17
+ /**
18
+ * Short-circuit the entire detection logic with a predetermined boolean.
19
+ */
20
+ isDev?: boolean;
21
+ };
22
+ declare function setEnvDetectionOverrides(overrides: EnvDetectionOverrides | undefined): void;
23
+ //#endregion
24
+ export { EnvDetectionOverrides, setEnvDetectionOverrides };
@@ -2,11 +2,20 @@
2
2
  function isRecord(value) {
3
3
  return typeof value === "object" && value !== null;
4
4
  }
5
+ /**
6
+ * Build-time-injected detection overrides.
7
+ *
8
+ * The Vite plugin emits a `setEnvDetectionOverrides({ isDev: ... })` call
9
+ * into the generated container module so detection in plugin-driven setups
10
+ * uses the bundler's authoritative mode instead of runtime sniffing.
11
+ */
12
+ let injectedOverrides;
13
+ function setEnvDetectionOverrides(overrides) {
14
+ injectedOverrides = overrides;
15
+ }
5
16
  function readImportMetaEnvFromRuntime() {
6
17
  if (typeof import.meta === "undefined") return;
7
- const candidate = import.meta;
8
- if (!isRecord(candidate) || !("env" in candidate)) return;
9
- const envValue = candidate.env;
18
+ const envValue = import.meta.env;
10
19
  if (!isRecord(envValue)) return;
11
20
  const env = {};
12
21
  if (typeof envValue.MODE === "string") env.MODE = envValue.MODE;
@@ -15,8 +24,11 @@ function readImportMetaEnvFromRuntime() {
15
24
  return env;
16
25
  }
17
26
  function readProcessNodeEnv() {
18
- if (typeof process === "undefined") return;
19
- return "development";
27
+ try {
28
+ return "development";
29
+ } catch {
30
+ return;
31
+ }
20
32
  }
21
33
  function getImportMetaEnv(overrides) {
22
34
  if (overrides?.importMetaEnv === null) return;
@@ -29,14 +41,15 @@ function getNodeEnv(overrides) {
29
41
  return readProcessNodeEnv();
30
42
  }
31
43
  function isDevEnvironment(overrides) {
32
- if (typeof overrides?.isDev === "boolean") return overrides.isDev;
33
- const nodeEnv = getNodeEnv(overrides);
44
+ const effective = overrides ?? injectedOverrides;
45
+ if (typeof effective?.isDev === "boolean") return effective.isDev;
46
+ const nodeEnv = getNodeEnv(effective);
34
47
  if (typeof nodeEnv === "string") return nodeEnv !== "production";
35
- const importMetaEnv = getImportMetaEnv(overrides);
48
+ const importMetaEnv = getImportMetaEnv(effective);
36
49
  if (typeof importMetaEnv?.PROD === "boolean") return !importMetaEnv.PROD;
37
50
  if (typeof importMetaEnv?.MODE === "string") return importMetaEnv.MODE !== "production";
38
51
  if (typeof importMetaEnv?.NODE_ENV === "string") return importMetaEnv.NODE_ENV !== "production";
39
52
  return true;
40
53
  }
41
54
  //#endregion
42
- export { isDevEnvironment };
55
+ export { isDevEnvironment, setEnvDetectionOverrides };
@@ -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 };
@@ -1,6 +1,7 @@
1
1
  import { createClassKey, createSymbolKey, hashString, normalizeImportPath } from "./utils.js";
2
2
  import { IdentifierResolver } from "./identifier-resolver.js";
3
3
  import path from "node:path";
4
+ import ts from "typescript";
4
5
  //#region src/plugins/core/codegen.ts
5
6
  function escapeSingleQuotes(value) {
6
7
  return value.replaceAll("'", "\\'");
@@ -91,12 +92,72 @@ function resolveDependencyImports(metas, pool, serviceBindings, runtimeImports)
91
92
  importMap
92
93
  };
93
94
  }
95
+ /**
96
+ * True when an identifier occupies a name position rather than referencing a
97
+ * binding: property-access names (`ns.Api`), object keys — including method
98
+ * and accessor keys (`{ Api() {} }`, `{ get Api() {} }`) — class member
99
+ * names, qualified names, and destructuring property names.
100
+ */
101
+ function isNonReferencePosition(node) {
102
+ const parent = node.parent;
103
+ if (ts.isPropertyAccessExpression(parent)) return parent.name === node;
104
+ if (ts.isQualifiedName(parent)) return parent.right === node;
105
+ if (ts.isBindingElement(parent)) return parent.propertyName === node;
106
+ if (ts.isPropertyAssignment(parent) || ts.isMethodDeclaration(parent) || ts.isGetAccessorDeclaration(parent) || ts.isSetAccessorDeclaration(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodSignature(parent) || ts.isPropertySignature(parent)) return parent.name === node;
107
+ return false;
108
+ }
109
+ /**
110
+ * Decide how to rewrite one identifier node, or skip it.
111
+ *
112
+ * Only binding references are rewritten: name positions and string/comment
113
+ * content keep their text. Shorthand properties expand
114
+ * (`{ Api }` -> `{ Api: Api_1 }`) so the key survives the rename.
115
+ */
116
+ function createIdentifierEdit(node, source, replacement) {
117
+ const parent = node.parent;
118
+ if (isNonReferencePosition(node)) return;
119
+ const start = node.getStart(source);
120
+ if (ts.isShorthandPropertyAssignment(parent) && parent.name === node) return {
121
+ start,
122
+ end: node.end,
123
+ text: `${node.text}: ${replacement}`
124
+ };
125
+ return {
126
+ start,
127
+ end: node.end,
128
+ text: replacement
129
+ };
130
+ }
131
+ /**
132
+ * Rewrite identifier references inside a reconstructed dependency expression.
133
+ *
134
+ * The expression is parsed and identifier nodes are replaced by position, so
135
+ * `$`-prefixed names rewrite correctly and occurrences inside string literals
136
+ * (e.g. lazy `import('/src/Api')` specifiers) and comments are untouched —
137
+ * the previous `\b`-regex text replacement got both wrong.
138
+ */
139
+ function rewriteReferencedIdentifiers(expression, referenced, rewriter) {
140
+ const wrapped = `(${expression});`;
141
+ const source = ts.createSourceFile("alloy-dependency-expression.ts", wrapped, ts.ScriptTarget.ESNext, true);
142
+ const edits = [];
143
+ const visit = (node) => {
144
+ if (ts.isIdentifier(node) && referenced.has(node.text)) {
145
+ const replacement = rewriter(node.text);
146
+ if (replacement && replacement !== node.text) {
147
+ const edit = createIdentifierEdit(node, source, replacement);
148
+ if (edit) edits.push(edit);
149
+ }
150
+ }
151
+ ts.forEachChild(node, visit);
152
+ };
153
+ visit(source);
154
+ let result = wrapped;
155
+ for (const edit of edits.toSorted((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
156
+ return result.slice(1, -2);
157
+ }
94
158
  function reconstructDependencyExpression(dep, rewriter, contextDir) {
95
159
  let expr = dep.expression;
96
- for (const ident of dep.referencedIdentifiers) {
97
- const replacement = rewriter(ident);
98
- if (replacement && replacement !== ident) expr = expr.replaceAll(new RegExp(`\\b${ident}\\b`, "g"), replacement);
99
- }
160
+ if (dep.referencedIdentifiers.length > 0) expr = rewriteReferencedIdentifiers(expr, new Set(dep.referencedIdentifiers), rewriter);
100
161
  if (dep.isLazy) expr = expr.replaceAll(/import\s*\(\s*(['"])(.+?)\1\s*\)/g, (match, quote, importPath) => {
101
162
  if (importPath.startsWith(".")) return `import(${quote}${normalizeImportPath(path.resolve(contextDir, importPath))}${quote})`;
102
163
  return match;
@@ -128,11 +189,11 @@ function reconstructOptionsText(meta, importMap, serviceRenames) {
128
189
  if (parts.length === 0) return "{}";
129
190
  return `{ ${parts.join(", ")} }`;
130
191
  }
131
- function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths) {
192
+ function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths, options) {
132
193
  const hasProviderModules = providerModulePaths.length > 0;
133
194
  const activeMetas = filterActiveMetas(metas, lazyReferencedClassKeys);
134
195
  const resolver = new IdentifierResolver(activeMetas);
135
- const runtimeImports = computeRuntimeImports(activeMetas, hasProviderModules);
196
+ const runtimeImports = computeRuntimeImports(activeMetas, hasProviderModules, options?.isDev !== void 0);
136
197
  const pool = createNamePool([
137
198
  ...runtimeImports,
138
199
  "registrations",
@@ -189,8 +250,9 @@ function enrichRegistrations(activeMetas, naming) {
189
250
  };
190
251
  });
191
252
  }
192
- function computeRuntimeImports(activeMetas, hasProviderModules) {
253
+ function computeRuntimeImports(activeMetas, hasProviderModules, hasEnvOverrides = false) {
193
254
  const imports = new Set(["Container", "dependenciesRegistry"]);
255
+ if (hasEnvOverrides) imports.add("setEnvDetectionOverrides");
194
256
  const needsLazyImport = activeMetas.some((m) => m.metadata.dependencies.some((d) => d.isLazy) || !!m.metadata.factory);
195
257
  if (hasProviderModules) imports.add("applyProviders");
196
258
  if (needsLazyImport) imports.add("Lazy");
@@ -244,6 +306,18 @@ function createIdentifierExports(registrations) {
244
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`;
245
307
  }
246
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
+ /**
247
321
  * Generates the virtual container module code.
248
322
  * This module:
249
323
  * 1. Imports the runtime container and necessary helpers.
@@ -257,9 +331,11 @@ function createIdentifierExports(registrations) {
257
331
  * @param lazyReferencedClassKeys - Set of service keys that are referenced ONLY lazily (and thus should not be imported/registered eagerly in this bundle).
258
332
  * @param providerModulePaths - List of provider modules to import and apply.
259
333
  */
260
- function generateContainerModule(metas, lazyReferencedClassKeys, providerModulePaths) {
334
+ function generateContainerModule(metas, lazyReferencedClassKeys, providerModulePaths, options) {
261
335
  const hasProviderModules = providerModulePaths.length > 0;
262
- const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths);
336
+ const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths, options);
337
+ const envOverridesBlock = options?.isDev === void 0 ? "" : `\nsetEnvDetectionOverrides({ isDev: ${options.isDev} });\n`;
338
+ const scopeHierarchyBlock = createScopeHierarchyBlock(options?.scopes);
263
339
  let providerImportBlock = "";
264
340
  let providerInvocationBlock = "";
265
341
  if (hasProviderModules) {
@@ -268,12 +344,12 @@ function generateContainerModule(metas, lazyReferencedClassKeys, providerModuleP
268
344
  providerInvocationBlock = `\nconst providerDefinitions = [${aliasNames.join(", ")}];\nfor (const definition of providerDefinitions) {\n applyProviders(container, definition);\n}\n`;
269
345
  }
270
346
  return `
271
- ${runtimeImportStatement}${stubsBlock}
347
+ ${runtimeImportStatement}${envOverridesBlock}${stubsBlock}
272
348
  ${providerImportBlock}
273
349
  ${registrationsBlock}
274
350
 
275
351
  const container = new Container();
276
-
352
+ ${scopeHierarchyBlock}
277
353
  for (const entry of registrations) {
278
354
  dependenciesRegistry.set(entry.ctor, entry.meta);
279
355
  }
@@ -281,6 +357,11 @@ ${providerInvocationBlock}${identifierExportBlock}
281
357
  export default container;
282
358
  `;
283
359
  }
360
+ const GENERATED_FILE_NOTICE = "This file was auto-generated by Alloy. Manual changes will be overwritten.";
361
+ const GENERATED_FILE_HEADER = `/**
362
+ * ${GENERATED_FILE_NOTICE}
363
+ */
364
+ `;
284
365
  /**
285
366
  * Generates the TypeScript declaration definition (`.d.ts`) for the virtual container module.
286
367
  * It exports the `ServiceIdentifiers` interface matching the runtime exports.
@@ -305,7 +386,7 @@ function generateContainerTypeDefinition(metas, pathResolver) {
305
386
  const exportKey = createIdentifierExportKey(meta, resolver);
306
387
  interfaceMembers.push(`${exportKey}: ServiceIdentifier<${importName}>;`);
307
388
  }
308
- return `
389
+ return `${GENERATED_FILE_HEADER}
309
390
  declare module "virtual:alloy-container" {
310
391
  import { Container, ServiceIdentifier } from "alloy-di/runtime";
311
392
  ${imports.length ? imports.join("\n") + "\n" : ""}
@@ -321,6 +402,29 @@ declare module "virtual:alloy-container" {
321
402
  `;
322
403
  }
323
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
+ /**
324
428
  * Generates ambient type declarations for external Alloy manifests consumed by the project.
325
429
  * Creates:
326
430
  * 1. `declare module "PKG/manifest"` typed as `LibraryManifest`.
@@ -329,7 +433,7 @@ declare module "virtual:alloy-container" {
329
433
  * @param manifests - List of loaded manifest info (packageName and services).
330
434
  */
331
435
  function generateManifestTypeDefinition(manifests) {
332
- return manifests.map((m) => {
436
+ return GENERATED_FILE_HEADER + manifests.map((m) => {
333
437
  const serviceIdentifiers = m.services.map((s) => ` export const ${s.exportName}Identifier: ServiceIdentifier;`).join("\n");
334
438
  return `
335
439
  declare module "${m.packageName}/manifest" {
@@ -420,4 +524,4 @@ ${serviceIdentifiers}
420
524
  }).join("\n");
421
525
  }
422
526
  //#endregion
423
- export { generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition };
527
+ export { GENERATED_FILE_NOTICE, generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition, generateScopeAugmentationDefinition };