alloy-di 1.2.2 → 1.2.3

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.
@@ -89,8 +89,12 @@ declare class Container {
89
89
  */
90
90
  private resolveParam;
91
91
  /**
92
- * Execute a lazy importer with optional retry/backoff semantics.
93
- * Implements exponential backoff for transient network failures.
92
+ * Execute a lazy importer with optional retry/backoff semantics, then
93
+ * validate that the imported value is a constructor.
94
+ *
95
+ * Only the dynamic import itself is retried; a successful import that does
96
+ * not yield a constructor is deterministic and fails immediately without
97
+ * backoff or re-wrapping.
94
98
  *
95
99
  * @param lazyDep - Lazy dependency wrapper with importer function and retry config
96
100
  * @param target - Service being resolved (for error messages)
@@ -99,6 +103,10 @@ declare class Container {
99
103
  * @throws Error if all retry attempts exhausted or import returns non-constructor
100
104
  */
101
105
  private importWithRetry;
106
+ /**
107
+ * Run a lazy importer, retrying failed imports with exponential backoff.
108
+ */
109
+ private runImporterWithRetry;
102
110
  /**
103
111
  * Resolve a token dependency via registered value providers.
104
112
  */
@@ -184,8 +184,12 @@ var Container = class {
184
184
  return classification;
185
185
  }
186
186
  /**
187
- * Execute a lazy importer with optional retry/backoff semantics.
188
- * Implements exponential backoff for transient network failures.
187
+ * Execute a lazy importer with optional retry/backoff semantics, then
188
+ * validate that the imported value is a constructor.
189
+ *
190
+ * Only the dynamic import itself is retried; a successful import that does
191
+ * not yield a constructor is deterministic and fails immediately without
192
+ * backoff or re-wrapping.
189
193
  *
190
194
  * @param lazyDep - Lazy dependency wrapper with importer function and retry config
191
195
  * @param target - Service being resolved (for error messages)
@@ -194,23 +198,28 @@ var Container = class {
194
198
  * @throws Error if all retry attempts exhausted or import returns non-constructor
195
199
  */
196
200
  async importWithRetry(lazyDep, target, resolutionStack) {
197
- const runImport = async () => await lazyDep.importer();
201
+ const module = await this.runImporterWithRetry(lazyDep, target, resolutionStack);
202
+ const depClass = typeof module === "object" && module !== null && "default" in module ? module.default : module;
203
+ if (!isConstructor(depClass)) {
204
+ const stackPath = this.formatStackPath(target, resolutionStack);
205
+ throw new DependencyResolutionError(`Lazy importer did not return a class for dependency while resolving ${target.name}. Resolution stack: ${stackPath}. Received type: ${typeof depClass}`, {
206
+ target,
207
+ resolutionStack,
208
+ failedDependency: depClass
209
+ });
210
+ }
211
+ return depClass;
212
+ }
213
+ /**
214
+ * Run a lazy importer, retrying failed imports with exponential backoff.
215
+ */
216
+ async runImporterWithRetry(lazyDep, target, resolutionStack) {
198
217
  const retries = lazyDep.retry?.retries ?? 0;
199
218
  const baseDelay = lazyDep.retry?.backoffMs ?? 0;
200
219
  const factor = lazyDep.retry?.factor ?? 2;
201
220
  let attempt = 0;
202
221
  while (true) try {
203
- const module = await runImport();
204
- const depClass = typeof module === "object" && module !== null && "default" in module ? module.default : module;
205
- if (!isConstructor(depClass)) {
206
- const stackPath = this.formatStackPath(target, resolutionStack);
207
- throw new DependencyResolutionError(`Lazy importer did not return a class for dependency while resolving ${target.name}. Resolution stack: ${stackPath}. Received type: ${typeof depClass}`, {
208
- target,
209
- resolutionStack,
210
- failedDependency: depClass
211
- });
212
- }
213
- return depClass;
222
+ return await lazyDep.importer();
214
223
  } catch (err) {
215
224
  if (attempt >= retries) {
216
225
  const stackPath = this.formatStackPath(target, resolutionStack);
@@ -251,10 +260,14 @@ var Container = class {
251
260
  const cached = this.metadataCache.get(target);
252
261
  if (cached) return cached;
253
262
  const registryEntry = dependenciesRegistry.get(target);
263
+ if (!registryEntry) return {
264
+ scope: ServiceScope.TRANSIENT,
265
+ dependencies: []
266
+ };
254
267
  const metadata = {
255
- scope: registryEntry?.scope ?? ServiceScope.TRANSIENT,
256
- dependencies: (registryEntry?.dependencies ?? (() => []))(),
257
- factory: registryEntry?.factory
268
+ scope: registryEntry.scope ?? ServiceScope.TRANSIENT,
269
+ dependencies: (registryEntry.dependencies ?? (() => []))(),
270
+ factory: registryEntry.factory
258
271
  };
259
272
  this.metadataCache.set(target, metadata);
260
273
  return metadata;
@@ -65,11 +65,44 @@ function createDecoratorWithoutDeps(scope) {
65
65
  function isDependenciesArg(value) {
66
66
  return typeof value === "function" || Array.isArray(value);
67
67
  }
68
+ /**
69
+ * Detect a class constructor passed where dependency metadata was expected.
70
+ *
71
+ * Class constructors have a non-writable `prototype`; dependency thunks
72
+ * (arrow functions or plain functions) either lack a `prototype` descriptor
73
+ * or have a writable one.
74
+ *
75
+ * @param value - The argument supplied to `@Injectable`/`@Singleton`.
76
+ * @returns True if the value is a class constructor.
77
+ * @internal
78
+ */
79
+ function isClassConstructor(value) {
80
+ if (typeof value !== "function") return false;
81
+ const prototypeDescriptor = Object.getOwnPropertyDescriptor(value, "prototype");
82
+ return prototypeDescriptor !== void 0 && !prototypeDescriptor.writable;
83
+ }
84
+ /**
85
+ * Reject the bare-decorator misuse `@Injectable` / `@Singleton` (no parens).
86
+ *
87
+ * Applied bare, the decorator factory itself receives the class, mistakes it
88
+ * for a dependencies thunk, and returns a decorator function — which legacy
89
+ * decorator semantics then substitute for the class. Throwing here turns that
90
+ * silent class replacement into an immediate, actionable error.
91
+ *
92
+ * @param value - The first argument received by the decorator factory.
93
+ * @param decoratorName - Public decorator name for the error message.
94
+ * @internal
95
+ */
96
+ function assertNotBareDecorator(value, decoratorName) {
97
+ if (isClassConstructor(value)) throw new TypeError(`@${decoratorName} must be called — use @${decoratorName}() instead of @${decoratorName}`);
98
+ }
68
99
  function Injectable(depsOrScope, scopeOverride) {
100
+ assertNotBareDecorator(depsOrScope, "Injectable");
69
101
  if (isDependenciesArg(depsOrScope)) return createDecoratorWithDeps(scopeOverride ?? ServiceScope.TRANSIENT, depsOrScope);
70
102
  return createDecoratorWithoutDeps((typeof depsOrScope === "string" ? depsOrScope : void 0) ?? scopeOverride ?? ServiceScope.TRANSIENT);
71
103
  }
72
104
  function Singleton(dependencies) {
105
+ assertNotBareDecorator(dependencies, "Singleton");
73
106
  if (isDependenciesArg(dependencies)) return createDecoratorWithDeps(ServiceScope.SINGLETON, dependencies);
74
107
  return createDecoratorWithoutDeps(ServiceScope.SINGLETON);
75
108
  }
@@ -13,8 +13,11 @@ type ServiceIdentifier<T = unknown> = symbol & {
13
13
  /**
14
14
  * Associates a constructor with a stable identifier. When an explicit identifier
15
15
  * is provided (e.g., by generated metadata), it becomes canonical for the
16
- * constructor. Attempting to reuse an identifier with a different constructor
17
- * throws to surface manifest/config mismatches early.
16
+ * constructor when the constructor is first registered. If the constructor was
17
+ * already assigned an auto-generated identifier, later explicit identifiers are
18
+ * recorded as aliases so both symbols resolve to the same constructor.
19
+ * Attempting to reuse an identifier with a different constructor throws to
20
+ * surface manifest/config mismatches early.
18
21
  */
19
22
  declare function registerServiceIdentifier<T>(ctor: Constructor, explicitIdentifier?: ServiceIdentifier<T>): ServiceIdentifier<T>;
20
23
  /**
@@ -11,8 +11,11 @@ function createServiceIdentifier(ctor) {
11
11
  /**
12
12
  * Associates a constructor with a stable identifier. When an explicit identifier
13
13
  * is provided (e.g., by generated metadata), it becomes canonical for the
14
- * constructor. Attempting to reuse an identifier with a different constructor
15
- * throws to surface manifest/config mismatches early.
14
+ * constructor when the constructor is first registered. If the constructor was
15
+ * already assigned an auto-generated identifier, later explicit identifiers are
16
+ * recorded as aliases so both symbols resolve to the same constructor.
17
+ * Attempting to reuse an identifier with a different constructor throws to
18
+ * surface manifest/config mismatches early.
16
19
  */
17
20
  function registerServiceIdentifier(ctor, explicitIdentifier) {
18
21
  const current = ctorToIdentifier.get(ctor);
@@ -20,6 +23,7 @@ function registerServiceIdentifier(ctor, explicitIdentifier) {
20
23
  if (explicitIdentifier && explicitIdentifier !== current) {
21
24
  const owner = identifierToCtor.get(explicitIdentifier);
22
25
  if (owner && owner !== ctor) throw new Error("Attempted to reassign an existing ServiceIdentifier to a different constructor.");
26
+ identifierToCtor.set(explicitIdentifier, ctor);
23
27
  }
24
28
  return current;
25
29
  }
@@ -18,30 +18,73 @@ function createSymbolDescription(meta) {
18
18
  return createSymbolKey(meta.filePath, meta.className);
19
19
  }
20
20
  /**
21
+ * Allocates module-local names from a single shared pool so the generated
22
+ * module's independent naming domains (runtime helpers, service imports,
23
+ * factory-lazy stubs, dependency imports, identifier consts, generated
24
+ * locals) can never collide.
25
+ */
26
+ function createNamePool(reservedNames) {
27
+ const used = new Set(reservedNames);
28
+ return { claim(base) {
29
+ let candidate = base;
30
+ let suffix = 1;
31
+ while (used.has(candidate)) {
32
+ candidate = `${base}_${suffix}`;
33
+ suffix++;
34
+ }
35
+ used.add(candidate);
36
+ return candidate;
37
+ } };
38
+ }
39
+ function stripModuleExtension(importPath) {
40
+ return importPath.replace(/\.[cm]?[jt]sx?$/i, "");
41
+ }
42
+ /**
43
+ * Identity of an imported binding: the module (extension-insensitive, since
44
+ * references may resolve extensionless while service paths keep theirs) plus
45
+ * the export name.
46
+ */
47
+ function createBindingKey(importPath, exportName) {
48
+ return `${stripModuleExtension(importPath)}::${exportName}`;
49
+ }
50
+ /** Resolves a referenced import's specifier to a normalized module path. */
51
+ function resolveReferencePath(ref, meta) {
52
+ return normalizeImportPath(ref.path.startsWith(".") ? path.resolve(path.dirname(meta.filePath), ref.path) : ref.path);
53
+ }
54
+ function resolveDependencyImport(ref, normalizedPath, pool, serviceBindings, runtimeImports) {
55
+ if (normalizedPath === "alloy-di/runtime" && ref.originalName && ref.name === ref.originalName && runtimeImports.has(ref.originalName)) return {
56
+ localName: ref.originalName,
57
+ importPath: normalizedPath,
58
+ originalName: ref.originalName,
59
+ reusesExistingBinding: true
60
+ };
61
+ const serviceLocalName = serviceBindings.get(createBindingKey(normalizedPath, ref.originalName ?? "default"));
62
+ if (serviceLocalName) return {
63
+ localName: serviceLocalName,
64
+ importPath: normalizedPath,
65
+ originalName: ref.originalName,
66
+ reusesExistingBinding: true
67
+ };
68
+ return {
69
+ localName: pool.claim(ref.name),
70
+ importPath: normalizedPath,
71
+ originalName: ref.originalName
72
+ };
73
+ }
74
+ /**
21
75
  * Analyzes dependencies across all discovered services and resolves imports.
22
- * Deduplicates imports and handles naming collisions by generating unique local names.
76
+ * Deduplicates imports by binding identity, reuses bindings the module
77
+ * already declares (runtime helpers, service imports, factory-lazy stubs),
78
+ * and claims fresh local names from the shared pool otherwise.
23
79
  */
24
- function resolveDependencyImports(metas) {
80
+ function resolveDependencyImports(metas, pool, serviceBindings, runtimeImports) {
25
81
  const importMap = /* @__PURE__ */ new Map();
26
- const nameCounts = /* @__PURE__ */ new Map();
27
- const getUniqueName = (name) => {
28
- const count = nameCounts.get(name) ?? 0;
29
- nameCounts.set(name, count + 1);
30
- return count === 0 ? name : `${name}_${count}`;
31
- };
32
- for (const meta of metas) {
33
- if (!meta.referencedImports?.length) continue;
34
- for (const ref of meta.referencedImports) {
35
- if (ref.isTypeOnly) continue;
36
- const normalizedPath = normalizeImportPath(ref.path.startsWith(".") ? path.resolve(path.dirname(meta.filePath), ref.path) : ref.path);
37
- const key = `${normalizedPath}::${ref.originalName ?? "default"}`;
38
- if (importMap.has(key)) continue;
39
- importMap.set(key, {
40
- localName: getUniqueName(ref.name),
41
- importPath: normalizedPath,
42
- originalName: ref.originalName
43
- });
44
- }
82
+ for (const meta of metas) for (const ref of meta.referencedImports ?? []) {
83
+ if (ref.isTypeOnly) continue;
84
+ const normalizedPath = resolveReferencePath(ref, meta);
85
+ const key = createBindingKey(normalizedPath, ref.originalName ?? "default");
86
+ if (importMap.has(key)) continue;
87
+ importMap.set(key, resolveDependencyImport(ref, normalizedPath, pool, serviceBindings, runtimeImports));
45
88
  }
46
89
  return {
47
90
  dependencyImports: Array.from(importMap.values()),
@@ -60,7 +103,7 @@ function reconstructDependencyExpression(dep, rewriter, contextDir) {
60
103
  });
61
104
  return expr;
62
105
  }
63
- function reconstructOptionsText(meta, importMap) {
106
+ function reconstructOptionsText(meta, importMap, serviceRenames) {
64
107
  const { scope, dependencies, factory } = meta.metadata;
65
108
  const parts = [];
66
109
  if (factory) {
@@ -73,12 +116,11 @@ function reconstructOptionsText(meta, importMap) {
73
116
  return reconstructDependencyExpression(dep, (ident) => {
74
117
  const ref = meta.referencedImports?.find((r) => r.name === ident && !r.isTypeOnly);
75
118
  if (ref) {
76
- const dir = path.dirname(meta.filePath);
77
- const key = `${normalizeImportPath(ref.path.startsWith(".") ? path.resolve(dir, ref.path) : ref.path)}::${ref.originalName ?? "default"}`;
119
+ const key = createBindingKey(resolveReferencePath(ref, meta), ref.originalName ?? "default");
78
120
  const resolved = importMap.get(key);
79
121
  return resolved ? resolved.localName : ident;
80
122
  }
81
- return ident;
123
+ return serviceRenames.get(ident) ?? ident;
82
124
  }, path.dirname(meta.filePath));
83
125
  });
84
126
  parts.push(`dependencies: () => [${depExprs.join(", ")}]`);
@@ -86,13 +128,38 @@ function reconstructOptionsText(meta, importMap) {
86
128
  if (parts.length === 0) return "{}";
87
129
  return `{ ${parts.join(", ")} }`;
88
130
  }
89
- function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, hasProviderModules) {
131
+ function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths) {
132
+ const hasProviderModules = providerModulePaths.length > 0;
90
133
  const activeMetas = filterActiveMetas(metas, lazyReferencedClassKeys);
91
- const { dependencyImports, importMap } = resolveDependencyImports(activeMetas);
92
- const resolvedRegistrations = enrichRegistrations(activeMetas, new IdentifierResolver(activeMetas), importMap);
93
- const runtimeImports = computeRuntimeImports(resolvedRegistrations, hasProviderModules);
134
+ const resolver = new IdentifierResolver(activeMetas);
135
+ const runtimeImports = computeRuntimeImports(activeMetas, hasProviderModules);
136
+ const pool = createNamePool([
137
+ ...runtimeImports,
138
+ "registrations",
139
+ "container",
140
+ "providerDefinitions",
141
+ ...providerModulePaths.map((_, idx) => `providers_${idx}`)
142
+ ]);
143
+ const serviceNames = /* @__PURE__ */ new Map();
144
+ const serviceRenames = /* @__PURE__ */ new Map();
145
+ const serviceBindings = /* @__PURE__ */ new Map();
146
+ for (const meta of activeMetas) {
147
+ const baseName = resolver.resolve(meta.className, meta.filePath);
148
+ const name = pool.claim(baseName);
149
+ serviceNames.set(meta, name);
150
+ if (name !== baseName) serviceRenames.set(baseName, name);
151
+ serviceBindings.set(createBindingKey(getServiceImportPath(meta), meta.className), name);
152
+ }
153
+ const { dependencyImports, importMap } = resolveDependencyImports(activeMetas, pool, serviceBindings, runtimeImports);
154
+ const resolvedRegistrations = enrichRegistrations(activeMetas, {
155
+ resolver,
156
+ serviceNames,
157
+ serviceRenames,
158
+ importMap,
159
+ pool
160
+ });
94
161
  const runtimeImportStatement = formatRuntimeImportStatement(runtimeImports);
95
- const stubsBlock = createStubBlock(dependencyImports, resolvedRegistrations, runtimeImports);
162
+ const stubsBlock = createStubBlock(dependencyImports, resolvedRegistrations);
96
163
  return {
97
164
  runtimeImportStatement,
98
165
  registrationsBlock: createRegistrationsBlock(buildRegistrationEntries(resolvedRegistrations)),
@@ -103,13 +170,14 @@ function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, hasProvide
103
170
  function filterActiveMetas(metas, lazyReferencedClassKeys) {
104
171
  return metas.filter((meta) => !lazyReferencedClassKeys.has(createClassKey(meta.filePath, meta.className)));
105
172
  }
106
- function enrichRegistrations(activeMetas, resolver, importMap) {
173
+ function enrichRegistrations(activeMetas, naming) {
174
+ const { resolver, serviceNames, serviceRenames, importMap, pool } = naming;
107
175
  return activeMetas.map((meta) => {
108
- const importName = resolver.resolve(meta.className, meta.filePath);
109
- const identifierConst = `${importName}Identifier`;
176
+ const importName = serviceNames.get(meta) ?? meta.className;
177
+ const identifierConst = pool.claim(`${importName}Identifier`);
110
178
  const exportKey = createIdentifierExportKey(meta, resolver);
111
179
  const symbolDescription = meta.identifierKey ?? createSymbolDescription(meta);
112
- const optionsText = reconstructOptionsText(meta, importMap);
180
+ const optionsText = reconstructOptionsText(meta, importMap, serviceRenames);
113
181
  return {
114
182
  ...meta,
115
183
  importName,
@@ -121,34 +189,29 @@ function enrichRegistrations(activeMetas, resolver, importMap) {
121
189
  };
122
190
  });
123
191
  }
124
- function computeRuntimeImports(registrations, hasProviderModules) {
192
+ function computeRuntimeImports(activeMetas, hasProviderModules) {
125
193
  const imports = new Set(["Container", "dependenciesRegistry"]);
126
- const needsLazyImport = registrations.some((m) => m.metadata.dependencies.some((d) => d.isLazy) || !!m.metadata.factory);
194
+ const needsLazyImport = activeMetas.some((m) => m.metadata.dependencies.some((d) => d.isLazy) || !!m.metadata.factory);
127
195
  if (hasProviderModules) imports.add("applyProviders");
128
196
  if (needsLazyImport) imports.add("Lazy");
129
- if (registrations.length) imports.add("registerServiceIdentifier");
197
+ if (activeMetas.length) imports.add("registerServiceIdentifier");
130
198
  return imports;
131
199
  }
132
200
  function formatRuntimeImportStatement(imports) {
133
201
  return `\nimport { ${Array.from(imports).join(", ")} } from 'alloy-di/runtime';\n`;
134
202
  }
135
- function createStubBlock(dependencyImports, registrations, runtimeImports) {
203
+ function createStubBlock(dependencyImports, registrations) {
136
204
  const statements = [];
137
- const importedNames = new Set(runtimeImports);
138
205
  for (const dep of dependencyImports) {
139
- if (dep.importPath === "alloy-di/runtime" && dep.originalName && dep.localName === dep.originalName && runtimeImports.has(dep.originalName)) continue;
140
- if (importedNames.has(dep.localName)) continue;
206
+ if (dep.reusesExistingBinding) continue;
141
207
  statements.push(createDependencyImportStatement(dep));
142
- importedNames.add(dep.localName);
143
208
  }
144
209
  for (const meta of registrations) {
145
210
  if (meta.isFactoryLazy) {
146
211
  statements.push(`class ${meta.importName} {}`);
147
212
  continue;
148
213
  }
149
- if (importedNames.has(meta.importName)) continue;
150
214
  statements.push(createServiceImportStatement(meta));
151
- importedNames.add(meta.importName);
152
215
  }
153
216
  return statements.length ? `${statements.join("\n")}\n` : "";
154
217
  }
@@ -158,8 +221,11 @@ function createDependencyImportStatement(dep) {
158
221
  if (dep.originalName && dep.originalName !== dep.localName) return `import { ${dep.originalName} as ${dep.localName} } from '${dep.importPath}';`;
159
222
  return `import { ${dep.localName} } from '${dep.importPath}';`;
160
223
  }
224
+ function getServiceImportPath(meta) {
225
+ return !/^(\/|[A-Za-z]:\\|\.|~)/.test(meta.filePath) && !meta.filePath.includes("\\") ? meta.filePath : normalizeImportPath(meta.filePath);
226
+ }
161
227
  function createServiceImportStatement(meta) {
162
- const importPath = !/^(\/|[A-Za-z]:\\|\.|~)/.test(meta.filePath) && !meta.filePath.includes("\\") ? meta.filePath : normalizeImportPath(meta.filePath);
228
+ const importPath = getServiceImportPath(meta);
163
229
  if (meta.importName === meta.className) return `import { ${meta.className} } from '${importPath}';`;
164
230
  return `import { ${meta.className} as ${meta.importName} } from '${importPath}';`;
165
231
  }
@@ -193,7 +259,7 @@ function createIdentifierExports(registrations) {
193
259
  */
194
260
  function generateContainerModule(metas, lazyReferencedClassKeys, providerModulePaths) {
195
261
  const hasProviderModules = providerModulePaths.length > 0;
196
- const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, hasProviderModules);
262
+ const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, providerModulePaths);
197
263
  let providerImportBlock = "";
198
264
  let providerInvocationBlock = "";
199
265
  if (hasProviderModules) {
@@ -224,10 +290,15 @@ export default container;
224
290
  */
225
291
  function generateContainerTypeDefinition(metas, pathResolver) {
226
292
  const resolver = new IdentifierResolver(metas);
293
+ const pool = createNamePool([
294
+ "Container",
295
+ "ServiceIdentifier",
296
+ "container"
297
+ ]);
227
298
  const imports = [];
228
299
  const interfaceMembers = [];
229
300
  for (const meta of metas) {
230
- const importName = resolver.resolve(meta.className, meta.filePath);
301
+ const importName = pool.claim(resolver.resolve(meta.className, meta.filePath));
231
302
  const importPath = pathResolver(meta.filePath);
232
303
  if (importName === meta.className) imports.push(`import { ${meta.className} } from '${importPath}';`);
233
304
  else imports.push(`import { ${meta.className} as ${importName} } from '${importPath}';`);
@@ -80,7 +80,10 @@ function findServiceDecorator(node, sourceFile, fileImports, id, resolutionCache
80
80
  const decorators = ts.getDecorators ? ts.getDecorators(node) : void 0;
81
81
  if (!decorators?.length) return;
82
82
  for (const decorator of decorators) {
83
- if (!ts.isCallExpression(decorator.expression)) continue;
83
+ if (!ts.isCallExpression(decorator.expression)) {
84
+ warnOnBareAlloyDecorator(decorator, sourceFile, fileImports, id, resolutionCache);
85
+ continue;
86
+ }
84
87
  const decoratorName = resolveDecoratorName(decorator.expression.expression, fileImports, id, new Set([id]), resolutionCache);
85
88
  if (decoratorName) return {
86
89
  decoratorCall: decorator.expression,
@@ -88,6 +91,20 @@ function findServiceDecorator(node, sourceFile, fileImports, id, resolutionCache
88
91
  };
89
92
  }
90
93
  }
94
+ /**
95
+ * Warn when an alloy decorator is applied bare (`@Injectable` instead of
96
+ * `@Injectable()`). The scanner only registers call-expression decorators, so
97
+ * the service would silently vanish from the container — and at runtime the
98
+ * factory throws. Surfacing the location here makes the misuse findable at
99
+ * build time.
100
+ */
101
+ function warnOnBareAlloyDecorator(decorator, sourceFile, fileImports, id, resolutionCache) {
102
+ if (!ts.isIdentifier(decorator.expression) && !ts.isPropertyAccessExpression(decorator.expression)) return;
103
+ if (!resolveDecoratorName(decorator.expression, fileImports, id, new Set([id]), resolutionCache)) return;
104
+ const { line } = sourceFile.getLineAndCharacterOfPosition(decorator.getStart(sourceFile));
105
+ const appliedText = decorator.expression.getText(sourceFile);
106
+ console.warn(`[alloy] ${id}:${line + 1} applies @${appliedText} without calling it — use @${appliedText}(). The class will not be registered.`);
107
+ }
91
108
  function resolveDecoratorName(expression, fileImports, id, visitedModules, resolutionCache) {
92
109
  if (ts.isIdentifier(expression)) {
93
110
  const importInfo = fileImports.get(expression.text);
@@ -104,11 +121,11 @@ function resolveImportedDecorator(importPath, requestedName, fromId, visitedModu
104
121
  if (importPath === ALLOY_RUNTIME_MODULE) return isAlloyDecoratorName(requestedName) ? requestedName : void 0;
105
122
  if (!importPath.startsWith(".")) return;
106
123
  for (const candidate of resolveModuleSpecifierCandidates(fromId, importPath)) {
107
- if (visitedModules.has(candidate) || !fs.existsSync(candidate)) continue;
108
124
  const cacheKey = `${candidate}:${requestedName}`;
109
125
  const cached = resolutionCache.get(cacheKey);
110
126
  if (cached) return cached;
111
127
  if (cached === null) continue;
128
+ if (visitedModules.has(candidate) || !fs.existsSync(candidate)) continue;
112
129
  visitedModules.add(candidate);
113
130
  try {
114
131
  const source = fs.readFileSync(candidate, "utf8");
@@ -18,7 +18,7 @@ function ensureLeadingSlash(value) {
18
18
  }
19
19
  function hashString(value) {
20
20
  let hash = 0;
21
- for (let i = 0; i < value.length; i++) hash = Math.trunc(hash * 31 + value.charCodeAt(i));
21
+ for (let i = 0; i < value.length; i++) hash = hash * 31 + value.charCodeAt(i) | 0;
22
22
  return Math.abs(hash).toString(36);
23
23
  }
24
24
  function createClassKey(filePath, className) {
@@ -8,7 +8,11 @@ import path from "node:path";
8
8
  import fs from "node:fs";
9
9
  //#region src/plugins/vite-plugin/container-loader.ts
10
10
  async function loadVirtualContainerModule(options) {
11
- const metas = options.localMetas;
11
+ const metas = options.localMetas.map((meta) => ({
12
+ ...meta,
13
+ metadata: { ...meta.metadata }
14
+ }));
15
+ const lazyClassKeys = new Set(options.lazyReferencedClassKeys);
12
16
  assignIdentifierKeys(metas, options.packageName, options.resolvedRoot);
13
17
  const manifestData = await readManifests(options.manifests);
14
18
  const manifestServices = manifestData.services;
@@ -24,14 +28,13 @@ async function loadVirtualContainerModule(options) {
24
28
  }))];
25
29
  const resolver = new IdentifierResolver(combinedMetas);
26
30
  const metasByName = groupMetasByName(combinedMetas);
27
- for (const svc of manifestServices) metas.push(toMetaFromManifest(svc, metasByName, resolver, options.lazyReferencedClassKeys));
31
+ for (const svc of manifestServices) metas.push(toMetaFromManifest(svc, metasByName, resolver, lazyClassKeys));
28
32
  const providerImports = Array.from(new Set([...options.providerImportPaths, ...manifestData.providers]));
29
- const eagerReferencedNames = collectEagerReferencedNames(metas);
30
- reconcileLazySet(metas, options.lazyReferencedClassKeys, eagerReferencedNames);
33
+ reconcileLazySet(metas, lazyClassKeys, collectEagerReferencedNames(metas));
31
34
  augmentFactoryLazyServices(metas, options.lazyServiceKeys);
32
- const code = generateContainerModule(metas, new Set(options.lazyReferencedClassKeys), providerImports);
35
+ const code = generateContainerModule(metas, lazyClassKeys, providerImports);
33
36
  writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir);
34
- writeVisualizationArtifact(metas, options.lazyReferencedClassKeys, options.resolvedVisualization);
37
+ writeVisualizationArtifact(metas, lazyClassKeys, options.resolvedVisualization);
35
38
  return {
36
39
  code,
37
40
  moduleType: "js"
@@ -1 +1 @@
1
- {"root":["../src/entry-points.test.ts","../src/rollup.ts","../src/runtime.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/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/decorators.helpers.test.ts","../src/plugins/core/decorators.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/scanner.test.ts","../src/plugins/core/scanner.ts","../src/plugins/core/types.ts","../src/plugins/core/utils.ts","../src/plugins/rollup-plugin/build-utils.ts","../src/plugins/rollup-plugin/index.ts","../src/plugins/rollup-plugin/rollup-plugin.test.ts","../src/plugins/vite-plugin/codegen.specifier.test.ts","../src/plugins/vite-plugin/container-loader.ts","../src/plugins/vite-plugin/discovery-runtime.ts","../src/plugins/vite-plugin/duplicate-registrations.test.ts","../src/plugins/vite-plugin/fixture-subpaths.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/manifest-utils.ts","../src/plugins/vite-plugin/manifest-utils.validation.test.ts","../src/plugins/vite-plugin/module-generation.test.ts","../src/plugins/vite-plugin/test-utils.ts","../src/plugins/vite-plugin/transform-guards.test.ts","../src/plugins/vite-plugin/visualization-option.test.ts","../src/plugins/vite-plugin/visualization-utils.ts","../src/plugins/vite-plugin/visualizer.test.ts","../src/plugins/vite-plugin/visualizer.ts"],"version":"6.0.3"}
1
+ {"root":["../src/entry-points.test.ts","../src/rollup.ts","../src/runtime.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/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/decorators.helpers.test.ts","../src/plugins/core/decorators.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/scanner.test.ts","../src/plugins/core/scanner.ts","../src/plugins/core/types.ts","../src/plugins/core/utils.ts","../src/plugins/rollup-plugin/build-utils.ts","../src/plugins/rollup-plugin/index.ts","../src/plugins/rollup-plugin/rollup-plugin.test.ts","../src/plugins/vite-plugin/codegen.specifier.test.ts","../src/plugins/vite-plugin/container-loader.test.ts","../src/plugins/vite-plugin/container-loader.ts","../src/plugins/vite-plugin/discovery-runtime.ts","../src/plugins/vite-plugin/duplicate-registrations.test.ts","../src/plugins/vite-plugin/fixture-subpaths.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/manifest-utils.ts","../src/plugins/vite-plugin/manifest-utils.validation.test.ts","../src/plugins/vite-plugin/module-generation.test.ts","../src/plugins/vite-plugin/test-utils.ts","../src/plugins/vite-plugin/transform-guards.test.ts","../src/plugins/vite-plugin/visualization-option.test.ts","../src/plugins/vite-plugin/visualization-utils.ts","../src/plugins/vite-plugin/visualizer.test.ts","../src/plugins/vite-plugin/visualizer.ts"],"version":"6.0.3"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alloy-di",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "A compile-time dependency injection plugin for Vite",
5
5
  "keywords": [
6
6
  "dependency-injection",