alloy-di 0.1.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 (44) hide show
  1. package/README.md +69 -0
  2. package/dist/lib/container.d.ts +117 -0
  3. package/dist/lib/container.js +266 -0
  4. package/dist/lib/decorators.d.ts +177 -0
  5. package/dist/lib/decorators.js +126 -0
  6. package/dist/lib/dependency-error.js +31 -0
  7. package/dist/lib/env-detection.js +44 -0
  8. package/dist/lib/lazy.d.ts +39 -0
  9. package/dist/lib/lazy.js +18 -0
  10. package/dist/lib/providers.d.ts +112 -0
  11. package/dist/lib/providers.js +166 -0
  12. package/dist/lib/scope.d.ts +8 -0
  13. package/dist/lib/scope.js +8 -0
  14. package/dist/lib/service-identifiers.d.ts +34 -0
  15. package/dist/lib/service-identifiers.js +54 -0
  16. package/dist/lib/testing/mocking.d.ts +14 -0
  17. package/dist/lib/testing/mocking.js +109 -0
  18. package/dist/lib/testing/registry.js +19 -0
  19. package/dist/lib/types.d.ts +22 -0
  20. package/dist/lib/types.js +21 -0
  21. package/dist/plugins/core/codegen.js +288 -0
  22. package/dist/plugins/core/decorators.js +90 -0
  23. package/dist/plugins/core/discovery-store.js +78 -0
  24. package/dist/plugins/core/identifier-resolver.js +22 -0
  25. package/dist/plugins/core/lazy.js +98 -0
  26. package/dist/plugins/core/scanner.js +93 -0
  27. package/dist/plugins/core/types.d.ts +44 -0
  28. package/dist/plugins/core/utils.js +45 -0
  29. package/dist/plugins/rollup-plugin/build-utils.js +49 -0
  30. package/dist/plugins/rollup-plugin/index.d.ts +30 -0
  31. package/dist/plugins/rollup-plugin/index.js +225 -0
  32. package/dist/plugins/vite-plugin/index.d.ts +25 -0
  33. package/dist/plugins/vite-plugin/index.js +154 -0
  34. package/dist/plugins/vite-plugin/manifest-utils.js +213 -0
  35. package/dist/rollup.d.ts +2 -0
  36. package/dist/rollup.js +7 -0
  37. package/dist/runtime.d.ts +7 -0
  38. package/dist/runtime.js +8 -0
  39. package/dist/test.d.ts +50 -0
  40. package/dist/test.js +67 -0
  41. package/dist/tsconfig.tsbuildinfo +1 -0
  42. package/dist/vite.d.ts +2 -0
  43. package/dist/vite.js +7 -0
  44. package/package.json +69 -0
@@ -0,0 +1,109 @@
1
+ import { isConstructor } from "../types.js";
2
+ import { isLazy } from "../lazy.js";
3
+ import { getRawDependencies } from "./registry.js";
4
+ import { vi } from "vitest";
5
+
6
+ //#region src/lib/testing/mocking.ts
7
+ /** Create a lightweight auto-mock instance for a class constructor. */
8
+ function mockClass(ctor) {
9
+ const proto = ctor.prototype;
10
+ const spies = {};
11
+ const mockObj = {
12
+ spies,
13
+ __target: ctor
14
+ };
15
+ for (const key of Object.getOwnPropertyNames(proto)) {
16
+ if (key === "constructor") continue;
17
+ if (typeof proto[key] === "function") {
18
+ const fn = vi.fn();
19
+ spies[key] = fn;
20
+ mockObj[key] = fn;
21
+ }
22
+ }
23
+ return {
24
+ target: ctor,
25
+ mock: mockObj
26
+ };
27
+ }
28
+ function collectDependencyGraph(target) {
29
+ const constructors = /* @__PURE__ */ new Set();
30
+ const lazyDependencies = [];
31
+ const queue = [target];
32
+ const visited = /* @__PURE__ */ new Set();
33
+ while (queue.length) {
34
+ const current = queue.shift();
35
+ if (!current || visited.has(current)) continue;
36
+ visited.add(current);
37
+ constructors.add(current);
38
+ const deps = getRawDependencies(current);
39
+ for (const dep of deps) {
40
+ if (isLazy(dep)) {
41
+ lazyDependencies.push({ lazy: dep });
42
+ continue;
43
+ }
44
+ if (isConstructor(dep)) queue.push(dep);
45
+ }
46
+ }
47
+ return {
48
+ constructors,
49
+ lazyDependencies
50
+ };
51
+ }
52
+ function createMocksForConstructors(constructors, target, overrides) {
53
+ const mocks = /* @__PURE__ */ new Map();
54
+ for (const ctor of constructors) {
55
+ if (ctor === target) continue;
56
+ if (overrides.has(ctor)) continue;
57
+ const classMock = mockClass(ctor);
58
+ mocks.set(ctor, classMock.mock);
59
+ }
60
+ return mocks;
61
+ }
62
+ function applyMocksToContainer(container, mocks) {
63
+ for (const [ctor, mock] of mocks.entries()) container.overrideInstance(ctor, mock);
64
+ }
65
+ function patchLazyDependencies(lazyDeps, mocks, overrides) {
66
+ const patches = [];
67
+ for (const { lazy } of lazyDeps) {
68
+ const originalImporter = lazy.importer;
69
+ lazy.importer = async () => {
70
+ const realCtor = await originalImporter();
71
+ let mockObj = mocks.get(realCtor);
72
+ if (!overrides.has(realCtor) && !mockObj) {
73
+ mockObj = mockClass(realCtor).mock;
74
+ mocks.set(realCtor, mockObj);
75
+ }
76
+ if (!mockObj) return realCtor;
77
+ return buildMockCtorFrom(realCtor, mockObj);
78
+ };
79
+ patches.push({
80
+ lazy,
81
+ originalImporter
82
+ });
83
+ }
84
+ return patches;
85
+ }
86
+ /** Traverse dependency graph (deep) and create class mocks, including lazy deps. */
87
+ function applyAutoMocks(options) {
88
+ const { target, container, overridesCtors } = options;
89
+ const graph = collectDependencyGraph(target);
90
+ const mocks = createMocksForConstructors(graph.constructors, target, overridesCtors);
91
+ applyMocksToContainer(container, mocks);
92
+ return {
93
+ mocks,
94
+ lazyPatches: patchLazyDependencies(graph.lazyDependencies, mocks, overridesCtors)
95
+ };
96
+ }
97
+ /** Build a class constructor that exposes spies from a mock object via prototype methods */
98
+ function buildMockCtorFrom(realCtor, mock) {
99
+ function MockCtor() {}
100
+ const proto = realCtor.prototype;
101
+ for (const key of Object.getOwnPropertyNames(proto)) {
102
+ if (key === "constructor") continue;
103
+ if (typeof proto[key] === "function" && key in mock.spies) MockCtor.prototype[key] = mock.spies[key];
104
+ }
105
+ return MockCtor;
106
+ }
107
+
108
+ //#endregion
109
+ export { applyAutoMocks };
@@ -0,0 +1,19 @@
1
+ import { dependenciesRegistry } from "../decorators.js";
2
+
3
+ //#region src/lib/testing/registry.ts
4
+ /** Take a deep(ish) snapshot of current dependency registry state. */
5
+ function snapshotRegistry() {
6
+ return new Map(dependenciesRegistry);
7
+ }
8
+ /** Restore dependency registry from a prior snapshot. */
9
+ function restoreRegistry(snapshot) {
10
+ dependenciesRegistry.clear();
11
+ for (const [k, v] of snapshot.entries()) dependenciesRegistry.set(k, v);
12
+ }
13
+ /** Get raw declared dependencies including constructors, Lazy and Tokens */
14
+ function getRawDependencies(target) {
15
+ return (dependenciesRegistry.get(target)?.dependencies ?? (() => []))();
16
+ }
17
+
18
+ //#endregion
19
+ export { getRawDependencies, restoreRegistry, snapshotRegistry };
@@ -0,0 +1,22 @@
1
+ //#region src/lib/types.d.ts
2
+ /**
3
+ * Represents a class constructor with arbitrary parameters.
4
+ * We intentionally use `any[]` here because the DI container never inspects
5
+ * constructor parameter types at runtime and widening to `unknown[]` causes
6
+ * TypeScript incompatibilities (strict contravariance) when passing concrete
7
+ * constructor signatures. Using `any` preserves ergonomic usage while remaining
8
+ * safe: instantiation is delegated to the actual class.
9
+ */
10
+ type Newable<T> = new (...args: any[]) => T;
11
+ /** Generic constructor shape (helper for internal checks). */
12
+ type Constructor = new (...args: any[]) => unknown;
13
+ /** Typed token for non-class values or abstract contracts. */
14
+ interface Token<T> {
15
+ readonly id: symbol;
16
+ readonly description?: string;
17
+ readonly __type?: T;
18
+ }
19
+ /** Create a unique typed token for values or abstractions. */
20
+ declare function createToken<T>(description?: string): Token<T>;
21
+ //#endregion
22
+ export { Constructor, Newable, Token, createToken };
@@ -0,0 +1,21 @@
1
+ //#region src/lib/types.ts
2
+ /** Create a unique typed token for values or abstractions. */
3
+ function createToken(description) {
4
+ return {
5
+ id: Symbol(description),
6
+ description
7
+ };
8
+ }
9
+ function isToken(value) {
10
+ return typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "id") && typeof value.id === "symbol";
11
+ }
12
+ function isConstructor(value) {
13
+ if (typeof value !== "function") return false;
14
+ const proto = value.prototype;
15
+ if (!proto || typeof proto !== "object") return false;
16
+ if (proto.constructor !== value) return false;
17
+ return true;
18
+ }
19
+
20
+ //#endregion
21
+ export { createToken, isConstructor, isToken };
@@ -0,0 +1,288 @@
1
+ import { createClassKey, createSymbolKey, hashString, normalizeImportPath } from "./utils.js";
2
+ import { IdentifierResolver } from "./identifier-resolver.js";
3
+ import path from "node:path";
4
+
5
+ //#region src/plugins/core/codegen.ts
6
+ function escapeSingleQuotes(value) {
7
+ return value.replace(/'/g, "\\'");
8
+ }
9
+ /**
10
+ * Generates a unique export key for the service identifier map.
11
+ * If there are name collisions (multiple classes with same name), it appends a hash of the file path.
12
+ */
13
+ function createIdentifierExportKey(meta, resolver) {
14
+ if (resolver.count(meta.className) <= 1) return meta.className;
15
+ const hash = hashString(normalizeImportPath(meta.filePath));
16
+ return `${meta.className}_${hash}`;
17
+ }
18
+ function createSymbolDescription(meta) {
19
+ return createSymbolKey(meta.filePath, meta.className);
20
+ }
21
+ /**
22
+ * Analyzes dependencies across all discovered services and resolves imports.
23
+ * Deduplicates imports and handles naming collisions by generating unique local names.
24
+ */
25
+ function resolveDependencyImports(metas) {
26
+ const importMap = /* @__PURE__ */ new Map();
27
+ const nameCounts = /* @__PURE__ */ new Map();
28
+ const getUniqueName = (name) => {
29
+ const count = nameCounts.get(name) || 0;
30
+ nameCounts.set(name, count + 1);
31
+ return count === 0 ? name : `${name}_${count}`;
32
+ };
33
+ for (const meta of metas) if (meta.referencedImports) for (const ref of meta.referencedImports) {
34
+ if (ref.isTypeOnly) continue;
35
+ const dir = path.dirname(meta.filePath);
36
+ const normalizedPath = normalizeImportPath(ref.path.startsWith(".") ? path.resolve(dir, ref.path) : ref.path);
37
+ const key = `${normalizedPath}::${ref.originalName ?? "default"}`;
38
+ let resolved = importMap.get(key);
39
+ if (!resolved) {
40
+ resolved = {
41
+ localName: getUniqueName(ref.name),
42
+ importPath: normalizedPath,
43
+ originalName: ref.originalName
44
+ };
45
+ importMap.set(key, resolved);
46
+ }
47
+ }
48
+ return {
49
+ dependencyImports: Array.from(importMap.values()),
50
+ importMap
51
+ };
52
+ }
53
+ function reconstructDependencyExpression(dep, rewriter, contextDir) {
54
+ let expr = dep.expression;
55
+ for (const ident of dep.referencedIdentifiers) {
56
+ const replacement = rewriter(ident);
57
+ if (replacement && replacement !== ident) expr = expr.replace(new RegExp(`\\b${ident}\\b`, "g"), replacement);
58
+ }
59
+ if (dep.isLazy) expr = expr.replace(/import\s*\(\s*(['"])(.+?)\1\s*\)/g, (match, quote, importPath) => {
60
+ if (importPath.startsWith(".")) return `import(${quote}${normalizeImportPath(path.resolve(contextDir, importPath))}${quote})`;
61
+ return match;
62
+ });
63
+ return expr;
64
+ }
65
+ function reconstructOptionsText(meta, importMap) {
66
+ const { scope, dependencies, factory } = meta.metadata;
67
+ const parts = [];
68
+ if (factory) {
69
+ const expr = reconstructDependencyExpression(factory, () => "", path.dirname(meta.filePath));
70
+ parts.push(`factory: ${expr}`);
71
+ }
72
+ if (scope === "singleton") parts.push(`scope: 'singleton'`);
73
+ if (dependencies && dependencies.length > 0) {
74
+ const depExprs = dependencies.map((dep) => {
75
+ return reconstructDependencyExpression(dep, (ident) => {
76
+ const ref = meta.referencedImports?.find((r) => r.name === ident && !r.isTypeOnly);
77
+ if (ref) {
78
+ const dir = path.dirname(meta.filePath);
79
+ const key = `${normalizeImportPath(ref.path.startsWith(".") ? path.resolve(dir, ref.path) : ref.path)}::${ref.originalName ?? "default"}`;
80
+ const resolved = importMap.get(key);
81
+ return resolved ? resolved.localName : ident;
82
+ }
83
+ return ident;
84
+ }, path.dirname(meta.filePath));
85
+ });
86
+ parts.push(`dependencies: () => [${depExprs.join(", ")}]`);
87
+ }
88
+ if (parts.length === 0) return "{}";
89
+ return `{ ${parts.join(", ")} }`;
90
+ }
91
+ function buildImportsAndRegistrations(metas, lazyReferencedClassKeys, hasProviderModules) {
92
+ const activeMetas = metas.filter((meta) => !lazyReferencedClassKeys.has(createClassKey(meta.filePath, meta.className)));
93
+ const { dependencyImports, importMap } = resolveDependencyImports(activeMetas);
94
+ const resolver = new IdentifierResolver(activeMetas);
95
+ const resolvedImports = activeMetas.map((meta) => {
96
+ const importName = resolver.resolve(meta.className, meta.filePath);
97
+ const isFactoryLazy = !!meta.metadata.factory;
98
+ const identifierConst = `${importName}Identifier`;
99
+ const exportKey = createIdentifierExportKey(meta, resolver);
100
+ const symbolDescription = meta.identifierKey ?? createSymbolDescription(meta);
101
+ const optionsText = reconstructOptionsText(meta, importMap);
102
+ return {
103
+ ...meta,
104
+ importName,
105
+ isFactoryLazy,
106
+ identifierConst,
107
+ exportKey,
108
+ symbolDescription,
109
+ optionsText
110
+ };
111
+ });
112
+ const needsLazyImport = activeMetas.some((m) => m.metadata.dependencies.some((d) => d.isLazy) || !!m.metadata.factory);
113
+ const runtimeImports = new Set(["Container", "dependenciesRegistry"]);
114
+ if (hasProviderModules) runtimeImports.add("applyProviders");
115
+ if (needsLazyImport) runtimeImports.add("Lazy");
116
+ if (resolvedImports.length) runtimeImports.add("registerServiceIdentifier");
117
+ const runtimeImportStatement = `\nimport { ${Array.from(runtimeImports).join(", ")} } from 'alloy-di/runtime';\n`;
118
+ const stubDeclarations = [];
119
+ const importedNames = new Set(runtimeImports);
120
+ for (const dep of dependencyImports) {
121
+ if (dep.importPath === "alloy-di/runtime" && dep.originalName && dep.localName === dep.originalName && runtimeImports.has(dep.originalName)) continue;
122
+ if (importedNames.has(dep.localName)) continue;
123
+ if (dep.originalName === "default") stubDeclarations.push(`import ${dep.localName} from '${dep.importPath}';`);
124
+ else if (dep.originalName === "*") stubDeclarations.push(`import * as ${dep.localName} from '${dep.importPath}';`);
125
+ else if (dep.originalName && dep.originalName !== dep.localName) stubDeclarations.push(`import { ${dep.originalName} as ${dep.localName} } from '${dep.importPath}';`);
126
+ else stubDeclarations.push(`import { ${dep.localName} } from '${dep.importPath}';`);
127
+ importedNames.add(dep.localName);
128
+ }
129
+ for (const meta of resolvedImports) {
130
+ if (meta.isFactoryLazy) {
131
+ stubDeclarations.push(`class ${meta.importName} {}`);
132
+ continue;
133
+ }
134
+ if (importedNames.has(meta.importName)) continue;
135
+ const importPath = !/^(\/|[A-Za-z]:\\|\.|~)/.test(meta.filePath) && !meta.filePath.includes("\\") ? meta.filePath : normalizeImportPath(meta.filePath);
136
+ if (meta.importName === meta.className) stubDeclarations.push(`import { ${meta.className} } from '${importPath}';`);
137
+ else stubDeclarations.push(`import { ${meta.className} as ${meta.importName} } from '${importPath}';`);
138
+ importedNames.add(meta.importName);
139
+ }
140
+ const registrationEntries = resolvedImports.map((m) => ` { ctor: ${m.importName}, meta: ${m.optionsText} }`).join(",\n");
141
+ const registrationsBlock = registrationEntries ? `const registrations = [\n${registrationEntries}\n];` : `const registrations = [];`;
142
+ const stubsBlock = stubDeclarations.length ? `${stubDeclarations.join("\n")}\n` : "";
143
+ const identifierDeclarations = resolvedImports.map((meta) => `const ${meta.identifierConst} = registerServiceIdentifier(${meta.importName}, Symbol.for('${escapeSingleQuotes(meta.symbolDescription)}'));`).join("\n");
144
+ const identifierExportEntries = resolvedImports.map((meta) => ` '${meta.exportKey}': ${meta.identifierConst}`).join(",\n");
145
+ return {
146
+ runtimeImportStatement,
147
+ registrationsBlock,
148
+ stubsBlock,
149
+ identifierDeclarations,
150
+ identifierExportBlock: resolvedImports.length ? `${identifierDeclarations}\n\nexport const serviceIdentifiers = {\n${identifierExportEntries}\n};\n` : `export const serviceIdentifiers = {};\n`
151
+ };
152
+ }
153
+ /**
154
+ * Generates the virtual container module code.
155
+ * This module:
156
+ * 1. Imports the runtime container and necessary helpers.
157
+ * 2. Imports all discovered service classes (or creates stubs for factory-lazy services).
158
+ * 3. Registers each service with the global `dependenciesRegistry`.
159
+ * 4. Applies any configured providers.
160
+ * 5. Exports the configured `Container` instance as default.
161
+ * 6. Exports `serviceIdentifiers` map for consumers to use safe injection keys.
162
+ *
163
+ * @param metas - List of discovered services.
164
+ * @param lazyReferencedClassKeys - Set of service keys that are referenced ONLY lazily (and thus should not be imported/registered eagerly in this bundle).
165
+ * @param providerModulePaths - List of provider modules to import and apply.
166
+ */
167
+ function generateContainerModule(metas, lazyReferencedClassKeys, providerModulePaths) {
168
+ const hasProviderModules = providerModulePaths.length > 0;
169
+ const { runtimeImportStatement, registrationsBlock, stubsBlock, identifierExportBlock } = buildImportsAndRegistrations(metas, lazyReferencedClassKeys, hasProviderModules);
170
+ let providerImportBlock = "";
171
+ let providerInvocationBlock = "";
172
+ if (hasProviderModules) {
173
+ const aliasNames = providerModulePaths.map((_, idx) => `providers_${idx}`);
174
+ providerImportBlock = providerModulePaths.map((p, idx) => `import ${aliasNames[idx]} from '${p}';`).join("\n") + "\n";
175
+ providerInvocationBlock = `\nconst providerDefinitions = [${aliasNames.join(", ")}];\nfor (const definition of providerDefinitions) {\n applyProviders(container, definition);\n}\n`;
176
+ }
177
+ return `
178
+ ${runtimeImportStatement}${stubsBlock}
179
+ ${providerImportBlock}
180
+ ${registrationsBlock}
181
+
182
+ const container = new Container();
183
+
184
+ for (const entry of registrations) {
185
+ dependenciesRegistry.set(entry.ctor, entry.meta);
186
+ }
187
+ ${providerInvocationBlock}${identifierExportBlock}
188
+ export default container;
189
+ `;
190
+ }
191
+ /**
192
+ * Generates the TypeScript declaration definition (`.d.ts`) for the virtual container module.
193
+ * It exports the `ServiceIdentifiers` interface matching the runtime exports.
194
+ *
195
+ * @param metas - List of discovered services.
196
+ * @param pathResolver - Function to resolve absolute file paths to import paths relative to the declaration file location.
197
+ */
198
+ function generateContainerTypeDefinition(metas, pathResolver) {
199
+ const resolver = new IdentifierResolver(metas);
200
+ const imports = [];
201
+ const interfaceMembers = [];
202
+ for (const meta of metas) {
203
+ const importName = resolver.resolve(meta.className, meta.filePath);
204
+ const importPath = pathResolver(meta.filePath);
205
+ if (importName === meta.className) imports.push(`import { ${meta.className} } from '${importPath}';`);
206
+ else imports.push(`import { ${meta.className} as ${importName} } from '${importPath}';`);
207
+ const exportKey = createIdentifierExportKey(meta, resolver);
208
+ interfaceMembers.push(`${exportKey}: ServiceIdentifier<${importName}>;`);
209
+ }
210
+ return `
211
+ declare module "virtual:alloy-container" {
212
+ import { Container, ServiceIdentifier } from "alloy-di/runtime";
213
+ ${imports.length ? imports.join("\n") + "\n" : ""}
214
+ export interface ServiceIdentifiers {
215
+ ${interfaceMembers.length ? interfaceMembers.join("\n ") : ""}
216
+ }
217
+
218
+ export const serviceIdentifiers: ServiceIdentifiers;
219
+
220
+ const container: Container;
221
+ export default container;
222
+ }
223
+ `;
224
+ }
225
+ /**
226
+ * Generates ambient type declarations for external Alloy manifests consumed by the project.
227
+ * Creates:
228
+ * 1. `declare module "PKG/manifest"` typed as `LibraryManifest`.
229
+ * 2. `declare module "PKG/service-identifiers"` exporting typed `ServiceIdentifier` constants.
230
+ *
231
+ * @param manifests - List of loaded manifest info (packageName and services).
232
+ */
233
+ function generateManifestTypeDefinition(manifests) {
234
+ return manifests.map((m) => {
235
+ const serviceIdentifiers = m.services.map((s) => ` export const ${s.exportName}Identifier: ServiceIdentifier;`).join("\n");
236
+ return `
237
+ declare module "${m.packageName}/manifest" {
238
+ type ServiceScope = "singleton" | "transient";
239
+
240
+ interface ManifestLazyDependency {
241
+ exportName: string;
242
+ importPath: string;
243
+ retry?: {
244
+ retries: number;
245
+ backoffMs?: number;
246
+ factor?: number;
247
+ };
248
+ }
249
+
250
+ interface ManifestTokenDependency {
251
+ exportName: string;
252
+ importPath: string;
253
+ symbolKey?: string;
254
+ }
255
+
256
+ interface ManifestService {
257
+ exportName: string;
258
+ importPath: string;
259
+ symbolKey: string;
260
+ scope: ServiceScope;
261
+ deps: string[];
262
+ lazyDeps: ManifestLazyDependency[];
263
+ tokenDeps?: ManifestTokenDependency[];
264
+ }
265
+
266
+ interface LibraryManifest {
267
+ schemaVersion: number;
268
+ packageName: string;
269
+ buildMode: "preserve-modules" | "bundled" | "chunks";
270
+ services: ManifestService[];
271
+ providers: string[];
272
+ diagnostics?: Record<string, unknown>;
273
+ }
274
+
275
+ export const manifest: LibraryManifest;
276
+ export default manifest;
277
+ }
278
+
279
+ declare module "${m.packageName}/service-identifiers" {
280
+ import { ServiceIdentifier } from "alloy-di/runtime";
281
+ ${serviceIdentifiers}
282
+ }
283
+ `;
284
+ }).join("\n");
285
+ }
286
+
287
+ //#endregion
288
+ export { generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition };
@@ -0,0 +1,90 @@
1
+ import { ServiceScope } from "../../lib/scope.js";
2
+ import ts from "typescript";
3
+
4
+ //#region src/plugins/core/decorators.ts
5
+ function extractRefs(node, sourceFile, identifiers) {
6
+ if (ts.isPropertyAssignment(node)) {
7
+ extractRefs(node.initializer, sourceFile, identifiers);
8
+ if (ts.isComputedPropertyName(node.name)) extractRefs(node.name.expression, sourceFile, identifiers);
9
+ return;
10
+ }
11
+ if (ts.isIdentifier(node)) {
12
+ identifiers.add(node.text);
13
+ return;
14
+ }
15
+ if (ts.isCallExpression(node)) {
16
+ const name = node.expression.getText(sourceFile);
17
+ if (name === "Lazy" || name.endsWith(".Lazy")) {
18
+ node.arguments.forEach((arg) => extractRefs(arg, sourceFile, identifiers));
19
+ return;
20
+ }
21
+ }
22
+ ts.forEachChild(node, (n) => extractRefs(n, sourceFile, identifiers));
23
+ }
24
+ function createDependencyDescriptor(node, sourceFile) {
25
+ const expression = node.getText(sourceFile);
26
+ const referencedIdentifiers = /* @__PURE__ */ new Set();
27
+ let isLazy = false;
28
+ if (ts.isCallExpression(node)) {
29
+ const callName = node.expression.getText(sourceFile);
30
+ if (callName === "Lazy" || callName.endsWith(".Lazy")) isLazy = true;
31
+ }
32
+ extractRefs(node, sourceFile, referencedIdentifiers);
33
+ return {
34
+ expression,
35
+ referencedIdentifiers: Array.from(referencedIdentifiers),
36
+ isLazy
37
+ };
38
+ }
39
+ function parseDependencies(node, sourceFile) {
40
+ if (ts.isArrowFunction(node)) return parseDependencies(node.body, sourceFile);
41
+ if (ts.isParenthesizedExpression(node)) return parseDependencies(node.expression, sourceFile);
42
+ if (ts.isCallExpression(node)) return node.arguments.map((arg) => createDependencyDescriptor(arg, sourceFile));
43
+ if (ts.isArrayLiteralExpression(node)) return node.elements.map((arg) => createDependencyDescriptor(arg, sourceFile));
44
+ return [];
45
+ }
46
+ function extractServiceMetadata(decoratorName, callExpression, sourceFile) {
47
+ let scope = ServiceScope.TRANSIENT;
48
+ let dependencies = [];
49
+ if (decoratorName.endsWith("Singleton")) scope = ServiceScope.SINGLETON;
50
+ const args = callExpression.arguments;
51
+ if (args.length === 0) return {
52
+ scope,
53
+ dependencies
54
+ };
55
+ const firstArg = args[0];
56
+ if (ts.isObjectLiteralExpression(firstArg)) {
57
+ for (const prop of firstArg.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
58
+ if (prop.name.text === "scope") {
59
+ if (ts.isStringLiteral(prop.initializer)) {
60
+ const val = prop.initializer.text;
61
+ if (val === "singleton") scope = ServiceScope.SINGLETON;
62
+ else if (val === "transient") scope = ServiceScope.TRANSIENT;
63
+ }
64
+ } else if (prop.name.text === "dependencies") dependencies = parseDependencies(prop.initializer, sourceFile);
65
+ }
66
+ if (decoratorName.endsWith("Singleton")) scope = ServiceScope.SINGLETON;
67
+ return {
68
+ scope,
69
+ dependencies
70
+ };
71
+ }
72
+ let depsNode;
73
+ if (ts.isStringLiteralLike(firstArg)) {
74
+ if (firstArg.text === "singleton") scope = ServiceScope.SINGLETON;
75
+ } else depsNode = firstArg;
76
+ if (args.length > 1) {
77
+ const secondArg = args[1];
78
+ if (ts.isStringLiteralLike(secondArg)) {
79
+ if (secondArg.text === "singleton") scope = ServiceScope.SINGLETON;
80
+ }
81
+ }
82
+ if (depsNode) dependencies = parseDependencies(depsNode, sourceFile);
83
+ return {
84
+ scope,
85
+ dependencies
86
+ };
87
+ }
88
+
89
+ //#endregion
90
+ export { extractServiceMetadata };
@@ -0,0 +1,78 @@
1
+ import { scanSource } from "./scanner.js";
2
+
3
+ //#region src/plugins/core/discovery-store.ts
4
+ /**
5
+ * Maintains a per-file cache of discovered DI metadata, lazy references, and
6
+ * optionally source snapshots to drive incremental recompilation inside the
7
+ * Alloy discovery pipeline.
8
+ */
9
+ /**
10
+ * Creates a file-scoped discovery store that caches scanner output and
11
+ * optionally the original source for diagnostics or incremental rebuilds.
12
+ *
13
+ * @param options.trackSources - When true, persist the full source text.
14
+ * @returns An object exposing cache maps plus mutation helpers.
15
+ */
16
+ function createDiscoveryStore(options = {}) {
17
+ const fileMetas = /* @__PURE__ */ new Map();
18
+ const fileLazyRefs = /* @__PURE__ */ new Map();
19
+ const fileSources = options.trackSources ? /* @__PURE__ */ new Map() : void 0;
20
+ /**
21
+ * Scan and cache the latest metadata for a file, returning both the fresh
22
+ * scan results and whatever was previously stored for diff consumers.
23
+ *
24
+ * @param id - Module identifier or path.
25
+ * @param code - Current file contents to analyze.
26
+ */
27
+ function updateFile(id, code) {
28
+ const previousMetas = fileMetas.get(id);
29
+ const previousLazyClassKeys = fileLazyRefs.get(id);
30
+ if (fileSources) fileSources.set(id, code);
31
+ const { metas, lazyClassKeys } = scanSource(code, id);
32
+ if (metas.length) fileMetas.set(id, metas);
33
+ else fileMetas.delete(id);
34
+ if (lazyClassKeys.size) fileLazyRefs.set(id, lazyClassKeys);
35
+ else fileLazyRefs.delete(id);
36
+ return {
37
+ metas,
38
+ lazyClassKeys,
39
+ previousMetas,
40
+ previousLazyClassKeys
41
+ };
42
+ }
43
+ /**
44
+ * Purge all cached information for a given file.
45
+ *
46
+ * @param id - Module identifier or path being removed.
47
+ */
48
+ function removeFile(id) {
49
+ const previousMetas = fileMetas.get(id);
50
+ const previousLazyClassKeys = fileLazyRefs.get(id);
51
+ fileMetas.delete(id);
52
+ fileLazyRefs.delete(id);
53
+ if (fileSources) fileSources.delete(id);
54
+ return {
55
+ previousMetas,
56
+ previousLazyClassKeys
57
+ };
58
+ }
59
+ /**
60
+ * Clear every cached entry, including optional source snapshots.
61
+ */
62
+ function clear() {
63
+ fileMetas.clear();
64
+ fileLazyRefs.clear();
65
+ fileSources?.clear();
66
+ }
67
+ return {
68
+ fileMetas,
69
+ fileLazyRefs,
70
+ fileSources,
71
+ updateFile,
72
+ removeFile,
73
+ clear
74
+ };
75
+ }
76
+
77
+ //#endregion
78
+ export { createDiscoveryStore };
@@ -0,0 +1,22 @@
1
+ import { createAliasName } from "./utils.js";
2
+
3
+ //#region src/plugins/core/identifier-resolver.ts
4
+ var IdentifierResolver = class {
5
+ counts;
6
+ constructor(metas) {
7
+ this.counts = /* @__PURE__ */ new Map();
8
+ for (const meta of metas) {
9
+ const current = this.counts.get(meta.className) ?? 0;
10
+ this.counts.set(meta.className, current + 1);
11
+ }
12
+ }
13
+ count(className) {
14
+ return this.counts.get(className) ?? 0;
15
+ }
16
+ resolve(className, importPath) {
17
+ return this.count(className) > 1 ? createAliasName(className, importPath) : className;
18
+ }
19
+ };
20
+
21
+ //#endregion
22
+ export { IdentifierResolver };