alloy-di 1.2.0 → 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.
@@ -1,8 +1,10 @@
1
1
  import { createClassKey, createSymbolKey } from "./utils.js";
2
2
  import { extractServiceMetadata } from "./decorators.js";
3
- import { processLazyCall } from "./lazy.js";
3
+ import { processLazyCall, resolveModuleSpecifierCandidates } from "./lazy.js";
4
+ import fs from "node:fs";
4
5
  import ts, { SyntaxKind } from "typescript";
5
6
  //#region src/plugins/core/scanner.ts
7
+ const ALLOY_RUNTIME_MODULE = "alloy-di/runtime";
6
8
  function collectFileImports(sourceFile) {
7
9
  const imports = /* @__PURE__ */ new Map();
8
10
  for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && statement.importClause && ts.isStringLiteral(statement.moduleSpecifier)) {
@@ -39,12 +41,14 @@ function scanSource(code, id) {
39
41
  const discovered = /* @__PURE__ */ new Map();
40
42
  const lazyRefs = /* @__PURE__ */ new Set();
41
43
  const fileImports = collectFileImports(sourceFile);
44
+ const decoratorResolutionCache = /* @__PURE__ */ new Map();
42
45
  const visit = (node) => {
43
46
  if (ts.isClassDeclaration(node)) handleClassDeclaration(node, {
44
47
  id,
45
48
  sourceFile,
46
49
  fileImports,
47
- discovered
50
+ discovered,
51
+ decoratorResolutionCache
48
52
  });
49
53
  else if (ts.isCallExpression(node)) processLazyCall(node, id, sourceFile, lazyRefs);
50
54
  ts.forEachChild(node, visit);
@@ -57,9 +61,9 @@ function scanSource(code, id) {
57
61
  }
58
62
  function handleClassDeclaration(node, context) {
59
63
  if (!node.name) return;
60
- const decoratorCall = findServiceDecorator(node, context.sourceFile);
61
- if (!decoratorCall) return;
62
- const decoratorName = decoratorCall.expression.getText(context.sourceFile);
64
+ const decoratorMatch = findServiceDecorator(node, context.sourceFile, context.fileImports, context.id, context.decoratorResolutionCache);
65
+ if (!decoratorMatch) return;
66
+ const { decoratorCall, decoratorName } = decoratorMatch;
63
67
  const className = node.name.getText(context.sourceFile);
64
68
  const metadata = extractServiceMetadata(decoratorName, decoratorCall, context.sourceFile);
65
69
  const referencedImports = collectReferencedImports(metadata, context.fileImports);
@@ -72,15 +76,98 @@ function handleClassDeclaration(node, context) {
72
76
  referencedImports
73
77
  });
74
78
  }
75
- function findServiceDecorator(node, sourceFile) {
79
+ function findServiceDecorator(node, sourceFile, fileImports, id, resolutionCache) {
76
80
  const decorators = ts.getDecorators ? ts.getDecorators(node) : void 0;
77
81
  if (!decorators?.length) return;
78
82
  for (const decorator of decorators) {
79
- if (!ts.isCallExpression(decorator.expression)) continue;
80
- const name = decorator.expression.expression.getText(sourceFile);
81
- if (name.endsWith("Injectable") || name.endsWith("Singleton")) return decorator.expression;
83
+ if (!ts.isCallExpression(decorator.expression)) {
84
+ warnOnBareAlloyDecorator(decorator, sourceFile, fileImports, id, resolutionCache);
85
+ continue;
86
+ }
87
+ const decoratorName = resolveDecoratorName(decorator.expression.expression, fileImports, id, new Set([id]), resolutionCache);
88
+ if (decoratorName) return {
89
+ decoratorCall: decorator.expression,
90
+ decoratorName
91
+ };
92
+ }
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
+ }
108
+ function resolveDecoratorName(expression, fileImports, id, visitedModules, resolutionCache) {
109
+ if (ts.isIdentifier(expression)) {
110
+ const importInfo = fileImports.get(expression.text);
111
+ if (!importInfo || importInfo.isTypeOnly) return;
112
+ return resolveImportedDecorator(importInfo.path, importInfo.originalName ?? expression.text, id, visitedModules, resolutionCache);
113
+ }
114
+ if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
115
+ const importInfo = fileImports.get(expression.expression.text);
116
+ if (!importInfo || importInfo.isTypeOnly || importInfo.originalName !== "*") return;
117
+ return resolveImportedDecorator(importInfo.path, expression.name.text, id, visitedModules, resolutionCache);
118
+ }
119
+ }
120
+ function resolveImportedDecorator(importPath, requestedName, fromId, visitedModules, resolutionCache) {
121
+ if (importPath === ALLOY_RUNTIME_MODULE) return isAlloyDecoratorName(requestedName) ? requestedName : void 0;
122
+ if (!importPath.startsWith(".")) return;
123
+ for (const candidate of resolveModuleSpecifierCandidates(fromId, importPath)) {
124
+ const cacheKey = `${candidate}:${requestedName}`;
125
+ const cached = resolutionCache.get(cacheKey);
126
+ if (cached) return cached;
127
+ if (cached === null) continue;
128
+ if (visitedModules.has(candidate) || !fs.existsSync(candidate)) continue;
129
+ visitedModules.add(candidate);
130
+ try {
131
+ const source = fs.readFileSync(candidate, "utf8");
132
+ const sourceFile = ts.createSourceFile(candidate, source, ts.ScriptTarget.ESNext, true);
133
+ const resolved = resolveDecoratorExport(requestedName, sourceFile, collectFileImports(sourceFile), candidate, visitedModules, resolutionCache);
134
+ resolutionCache.set(cacheKey, resolved ?? null);
135
+ if (resolved) return resolved;
136
+ } catch {
137
+ continue;
138
+ } finally {
139
+ visitedModules.delete(candidate);
140
+ }
82
141
  }
83
142
  }
143
+ function resolveDecoratorExport(requestedName, sourceFile, fileImports, id, visitedModules, resolutionCache) {
144
+ for (const statement of sourceFile.statements) {
145
+ if (!ts.isExportDeclaration(statement)) continue;
146
+ const moduleSpecifier = statement.moduleSpecifier && ts.isStringLiteral(statement.moduleSpecifier) ? statement.moduleSpecifier.text : void 0;
147
+ if (!statement.exportClause) {
148
+ if (!moduleSpecifier) continue;
149
+ const resolved = resolveImportedDecorator(moduleSpecifier, requestedName, id, visitedModules, resolutionCache);
150
+ if (resolved) return resolved;
151
+ continue;
152
+ }
153
+ if (!ts.isNamedExports(statement.exportClause)) continue;
154
+ for (const element of statement.exportClause.elements) {
155
+ if (element.name.text !== requestedName) continue;
156
+ const resolved = resolveNamedExportElement(element, moduleSpecifier, fileImports, id, visitedModules, resolutionCache);
157
+ if (resolved) return resolved;
158
+ }
159
+ }
160
+ }
161
+ function resolveNamedExportElement(element, moduleSpecifier, fileImports, id, visitedModules, resolutionCache) {
162
+ const sourceName = element.propertyName?.text ?? element.name.text;
163
+ if (moduleSpecifier) return resolveImportedDecorator(moduleSpecifier, sourceName, id, visitedModules, resolutionCache);
164
+ const importInfo = fileImports.get(sourceName);
165
+ if (!importInfo || importInfo.isTypeOnly) return;
166
+ return resolveImportedDecorator(importInfo.path, importInfo.originalName ?? sourceName, id, visitedModules, resolutionCache);
167
+ }
168
+ function isAlloyDecoratorName(name) {
169
+ return name === "Injectable" || name === "Singleton";
170
+ }
84
171
  function collectReferencedImports(metadata, fileImports) {
85
172
  const referenced = [];
86
173
  const seen = /* @__PURE__ */ new Set();
@@ -1,7 +1,31 @@
1
1
  import { ServiceScope } from "../../lib/scope.js";
2
2
 
3
3
  //#region src/plugins/core/types.d.ts
4
- interface ManifestServiceDescriptor {
4
+ interface ManifestTokenDependency {
5
+ exportName: string;
6
+ importPath: string;
7
+ }
8
+ interface ManifestLazyDependency {
9
+ exportName: string;
10
+ importPath: string;
11
+ retry?: {
12
+ retries: number;
13
+ backoffMs?: number;
14
+ factor?: number;
15
+ };
16
+ }
17
+ interface ManifestClassDependencyEntry {
18
+ kind: "class";
19
+ exportName: string;
20
+ }
21
+ interface ManifestTokenDependencyEntry extends ManifestTokenDependency {
22
+ kind: "token";
23
+ }
24
+ interface ManifestLazyDependencyEntry extends ManifestLazyDependency {
25
+ kind: "lazy";
26
+ }
27
+ type ManifestDependencyEntry = ManifestClassDependencyEntry | ManifestTokenDependencyEntry | ManifestLazyDependencyEntry;
28
+ interface ManifestServiceDescriptorBase {
5
29
  exportName: string;
6
30
  importPath: string;
7
31
  /**
@@ -10,22 +34,17 @@ interface ManifestServiceDescriptor {
10
34
  */
11
35
  symbolKey: string;
12
36
  scope: ServiceScope;
37
+ }
38
+ interface ManifestServiceDescriptorV1 extends ManifestServiceDescriptorBase {
13
39
  deps: string[];
14
40
  /** Token dependencies (non-service identifiers) exported publicly by the package. */
15
- tokenDeps?: {
16
- exportName: string;
17
- importPath: string;
18
- }[];
19
- lazyDeps: {
20
- exportName: string;
21
- importPath: string;
22
- retry?: {
23
- retries: number;
24
- backoffMs?: number;
25
- factor?: number;
26
- };
27
- }[];
41
+ tokenDeps?: ManifestTokenDependency[];
42
+ lazyDeps: ManifestLazyDependency[];
43
+ }
44
+ interface ManifestServiceDescriptorV2 extends ManifestServiceDescriptorBase {
45
+ deps: ManifestDependencyEntry[];
28
46
  }
47
+ type ManifestServiceDescriptor = ManifestServiceDescriptorV1 | ManifestServiceDescriptorV2;
29
48
  interface AlloyManifest {
30
49
  schemaVersion: number;
31
50
  packageName: string;
@@ -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) {
@@ -1,4 +1,5 @@
1
1
  import { ServiceScope } from "../../lib/scope.js";
2
+ import { parseLazyDependencyExpression, resolveModuleSpecifierCandidates } from "../core/lazy.js";
2
3
  import { createDiscoveryStore } from "../core/discovery-store.js";
3
4
  import { determineBuildMode, hasPreserveModules, resolveImportPathForBuild } from "./build-utils.js";
4
5
  import path from "node:path";
@@ -9,6 +10,31 @@ import ts from "typescript";
9
10
  function hasExportModifier(node) {
10
11
  return !!(ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
11
12
  }
13
+ function getDependencyImports(meta, dep) {
14
+ const imports = meta.referencedImports ?? [];
15
+ if (imports.length === 0 || dep.referencedIdentifiers.length === 0) return [];
16
+ const identifiers = new Set(dep.referencedIdentifiers);
17
+ return imports.filter((entry) => identifiers.has(entry.name));
18
+ }
19
+ function getDependencyReferenceName(expression) {
20
+ const statement = ts.createSourceFile("dependency.ts", `const __dep = (${expression});`, ts.ScriptTarget.ESNext, true, ts.ScriptKind.TS).statements[0];
21
+ if (!statement || !ts.isVariableStatement(statement)) return;
22
+ const initializer = statement.declarationList.declarations[0]?.initializer;
23
+ if (!initializer) return;
24
+ return extractReferenceNameFromExpression(initializer);
25
+ }
26
+ function extractReferenceNameFromExpression(expression) {
27
+ if (ts.isIdentifier(expression)) return expression.text;
28
+ if (ts.isPropertyAccessExpression(expression)) return expression.name.text;
29
+ if (ts.isAsExpression(expression) || ts.isParenthesizedExpression(expression) || ts.isNonNullExpression(expression) || ts.isTypeAssertionExpression(expression)) return extractReferenceNameFromExpression(expression.expression);
30
+ }
31
+ function resolveClassDependencyName(dep, meta, knownServiceNames) {
32
+ const imports = getDependencyImports(meta, dep);
33
+ for (const entry of imports) if (entry.originalName && knownServiceNames.has(entry.originalName)) return entry.originalName;
34
+ const referenceName = getDependencyReferenceName(dep.expression);
35
+ if (referenceName && knownServiceNames.has(referenceName)) return referenceName;
36
+ return dep.referencedIdentifiers.find((name) => knownServiceNames.has(name));
37
+ }
12
38
  function alloy(options = {}) {
13
39
  const fileName = options.fileName ?? "alloy.manifest.mjs";
14
40
  const packageJsonFile = options.packageJsonPath ? path.isAbsolute(options.packageJsonPath) ? options.packageJsonPath : path.resolve(process.cwd(), options.packageJsonPath) : path.resolve(process.cwd(), "package.json");
@@ -83,6 +109,38 @@ function alloy(options = {}) {
83
109
  function resolveImportPath(targetPath, buildMode) {
84
110
  return resolveImportPathForBuild(targetPath, packageName, buildMode);
85
111
  }
112
+ function resolveDependencyImportPath(specifier, sourceFilePath, buildMode) {
113
+ if (specifier.startsWith(".")) return resolveImportPath(resolveModuleSpecifierCandidates(sourceFilePath, specifier)[0] ?? path.resolve(path.dirname(sourceFilePath), specifier), buildMode);
114
+ if (path.isAbsolute(specifier)) return resolveImportPath(specifier, buildMode);
115
+ return specifier;
116
+ }
117
+ function createTokenDependency(dep, meta, buildMode) {
118
+ const preferredImport = getDependencyImports(meta, dep).find((entry) => entry.originalName !== "*");
119
+ return {
120
+ kind: "token",
121
+ exportName: preferredImport?.originalName ?? getDependencyReferenceName(dep.expression) ?? dep.referencedIdentifiers[0] ?? dep.expression,
122
+ importPath: preferredImport ? resolveDependencyImportPath(preferredImport.path, meta.filePath, buildMode) : packageName
123
+ };
124
+ }
125
+ function createManifestDependency(dep, meta, knownServiceNames, buildMode) {
126
+ if (dep.isLazy) {
127
+ const parsedLazy = parseLazyDependencyExpression(dep.expression, meta.filePath);
128
+ if (!parsedLazy) return null;
129
+ const entry = {
130
+ kind: "lazy",
131
+ exportName: parsedLazy.exportName,
132
+ importPath: resolveDependencyImportPath(parsedLazy.specifier, meta.filePath, buildMode)
133
+ };
134
+ if (parsedLazy.retry) entry.retry = parsedLazy.retry;
135
+ return entry;
136
+ }
137
+ const className = resolveClassDependencyName(dep, meta, knownServiceNames);
138
+ if (className) return {
139
+ kind: "class",
140
+ exportName: className
141
+ };
142
+ return createTokenDependency(dep, meta, buildMode);
143
+ }
86
144
  return {
87
145
  name: "alloy-manifest",
88
146
  transform(code, id) {
@@ -108,55 +166,26 @@ function alloy(options = {}) {
108
166
  importPath,
109
167
  symbolKey,
110
168
  scope,
111
- deps: [],
112
- lazyDeps: []
169
+ deps: []
113
170
  });
114
171
  if (buildMode !== "preserve-modules" && !exportedNames.has(meta.className)) missingExports.push(meta.className);
115
172
  }
116
173
  const serviceByName = /* @__PURE__ */ new Map();
117
- for (const s of services) serviceByName.set(s.exportName, s);
118
- const serviceTokenDeps = /* @__PURE__ */ new Map();
174
+ const knownServiceNames = /* @__PURE__ */ new Set();
175
+ for (const service of services) {
176
+ serviceByName.set(service.exportName, service);
177
+ knownServiceNames.add(service.exportName);
178
+ }
119
179
  for (const metas of discovery.fileMetas.values()) for (const meta of metas) {
120
180
  const svc = serviceByName.get(meta.className);
121
181
  if (!svc) continue;
122
182
  for (const dep of meta.metadata.dependencies) {
123
- if (dep.isLazy) continue;
124
- for (const name of dep.referencedIdentifiers) if (serviceByName.has(name)) svc.deps.push(name);
125
- else {
126
- const set = serviceTokenDeps.get(svc.exportName) ?? /* @__PURE__ */ new Set();
127
- set.add(name);
128
- serviceTokenDeps.set(svc.exportName, set);
129
- }
183
+ const manifestDep = createManifestDependency(dep, meta, knownServiceNames, buildMode);
184
+ if (manifestDep) svc.deps.push(manifestDep);
130
185
  }
131
186
  }
132
- for (const [id, refs] of discovery.fileLazyRefs.entries()) {
133
- const metas = discovery.fileMetas.get(id) ?? [];
134
- for (const meta of metas) {
135
- const svc = serviceByName.get(meta.className);
136
- if (!svc) continue;
137
- const firstForExport = /* @__PURE__ */ new Map();
138
- for (const key of refs) {
139
- const [targetPath, exportName] = key.split("::");
140
- if (!targetPath || !exportName) continue;
141
- const importPath = resolveImportPath(targetPath, buildMode);
142
- if (!firstForExport.has(exportName)) firstForExport.set(exportName, importPath);
143
- }
144
- for (const [exportName, importPath] of firstForExport.entries()) svc.lazyDeps.push({
145
- exportName,
146
- importPath
147
- });
148
- }
149
- }
150
- for (const [exportName, tokens] of serviceTokenDeps.entries()) {
151
- const svc = serviceByName.get(exportName);
152
- if (!svc || !tokens.size) continue;
153
- svc.tokenDeps = Array.from(tokens).map((t) => ({
154
- exportName: t,
155
- importPath: packageName
156
- }));
157
- }
158
187
  const manifest = {
159
- schemaVersion: 1,
188
+ schemaVersion: 2,
160
189
  packageName,
161
190
  buildMode,
162
191
  services,
@@ -187,7 +216,7 @@ function alloy(options = {}) {
187
216
  }
188
217
  manifest.providers = resolvedProviders;
189
218
  }
190
- const code = `// Generated Alloy manifest (v1)\nexport const manifest = ${JSON.stringify(manifest, null, 2)};\n`;
219
+ const code = `// Generated Alloy manifest (v2)\nexport const manifest = ${JSON.stringify(manifest, null, 2)};\n`;
191
220
  const identifiersCode = ["// Generated Alloy Service Identifiers", ...services.map((s) => `export const ${s.exportName}Identifier = Symbol.for("${s.symbolKey}");`)].join("\n");
192
221
  if (this.emitFile) {
193
222
  this.emitFile({
@@ -0,0 +1,95 @@
1
+ import { normalizeImportPath } from "../core/utils.js";
2
+ import { IdentifierResolver } from "../core/identifier-resolver.js";
3
+ import { generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition } from "../core/codegen.js";
4
+ import { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateManifestServices, groupMetasByName, readManifests, reconcileLazySet, toMetaFromManifest } from "./manifest-utils.js";
5
+ import { generateMermaidDiagram } from "./visualizer.js";
6
+ import { ensureDirectoryForFile } from "./visualization-utils.js";
7
+ import path from "node:path";
8
+ import fs from "node:fs";
9
+ //#region src/plugins/vite-plugin/container-loader.ts
10
+ async function loadVirtualContainerModule(options) {
11
+ const metas = options.localMetas.map((meta) => ({
12
+ ...meta,
13
+ metadata: { ...meta.metadata }
14
+ }));
15
+ const lazyClassKeys = new Set(options.lazyReferencedClassKeys);
16
+ assignIdentifierKeys(metas, options.packageName, options.resolvedRoot);
17
+ const manifestData = await readManifests(options.manifests);
18
+ const manifestServices = manifestData.services;
19
+ const loadedManifests = manifestData.loadedManifests;
20
+ assertNoDuplicateManifestServices(metas, manifestServices);
21
+ const combinedMetas = [...metas, ...manifestServices.map((svc) => ({
22
+ className: svc.exportName,
23
+ filePath: svc.importPath,
24
+ metadata: {
25
+ scope: svc.scope,
26
+ dependencies: []
27
+ }
28
+ }))];
29
+ const resolver = new IdentifierResolver(combinedMetas);
30
+ const metasByName = groupMetasByName(combinedMetas);
31
+ for (const svc of manifestServices) metas.push(toMetaFromManifest(svc, metasByName, resolver, lazyClassKeys));
32
+ const providerImports = Array.from(new Set([...options.providerImportPaths, ...manifestData.providers]));
33
+ reconcileLazySet(metas, lazyClassKeys, collectEagerReferencedNames(metas));
34
+ augmentFactoryLazyServices(metas, options.lazyServiceKeys);
35
+ const code = generateContainerModule(metas, lazyClassKeys, providerImports);
36
+ writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir);
37
+ writeVisualizationArtifact(metas, lazyClassKeys, options.resolvedVisualization);
38
+ return {
39
+ code,
40
+ moduleType: "js"
41
+ };
42
+ }
43
+ function assignIdentifierKeys(metas, packageName, resolvedRoot) {
44
+ for (const meta of metas) {
45
+ const normalizedMetaPath = normalizeImportPath(meta.filePath);
46
+ const trimmedNormalizedMetaPath = normalizedMetaPath.replaceAll(/^\/+/g, "");
47
+ const looksRootRelative = normalizedMetaPath === "/src" || normalizedMetaPath.startsWith("/src/");
48
+ let relPath = path.relative(resolvedRoot, meta.filePath);
49
+ if (path.sep === "\\") relPath = relPath.split(path.sep).join("/");
50
+ if (looksRootRelative || !relPath || relPath.startsWith("..") || relPath.startsWith("\\")) relPath = trimmedNormalizedMetaPath || normalizedMetaPath.replaceAll(/^\/+/g, "");
51
+ meta.identifierKey = `alloy:${packageName}/${relPath}#${meta.className}`;
52
+ }
53
+ }
54
+ function assertNoDuplicateManifestServices(metas, manifestServices) {
55
+ if (!metas.length || !manifestServices.length) return;
56
+ const duplicates = findDuplicateManifestServices(metas, manifestServices);
57
+ if (!duplicates.length) return;
58
+ const details = duplicates.map((d) => `- ${d.exportName}: local [${d.localPaths.join(", ")}] vs manifest '${d.manifestImport}'`).join("\n");
59
+ throw new Error([
60
+ "[alloy] Duplicate service registrations detected.",
61
+ details,
62
+ "Resolve by removing one source (local or manifest) to avoid ambiguous DI keys."
63
+ ].join("\n"));
64
+ }
65
+ function writeTypeDefinitions(metas, loadedManifests, resolvedRoot, containerDeclarationDir) {
66
+ const dtsDir = path.resolve(resolvedRoot, containerDeclarationDir ?? "./src");
67
+ const dtsContent = generateContainerTypeDefinition(metas, (filePath) => resolveDeclarationImportPath(dtsDir, filePath));
68
+ if (!fs.existsSync(dtsDir)) fs.mkdirSync(dtsDir, { recursive: true });
69
+ fs.writeFileSync(path.join(dtsDir, "alloy-container.d.ts"), dtsContent);
70
+ if (loadedManifests.length === 0) return;
71
+ const manifestsDts = generateManifestTypeDefinition(loadedManifests.map((m) => ({
72
+ packageName: m.packageName,
73
+ services: m.services
74
+ })));
75
+ fs.writeFileSync(path.join(dtsDir, "alloy-manifests.d.ts"), manifestsDts);
76
+ }
77
+ function resolveDeclarationImportPath(dtsDir, filePath) {
78
+ if (!path.isAbsolute(filePath)) return filePath;
79
+ let rel = path.relative(dtsDir, filePath);
80
+ rel = rel.split(path.sep).join(path.posix.sep);
81
+ if (!rel.startsWith(".")) rel = "./" + rel;
82
+ return rel;
83
+ }
84
+ function writeVisualizationArtifact(metas, lazyReferencedClassKeys, resolvedVisualization) {
85
+ if (!resolvedVisualization) return;
86
+ const artifact = generateMermaidDiagram({
87
+ metas,
88
+ lazyClassKeys: new Set(lazyReferencedClassKeys),
89
+ options: resolvedVisualization.mermaidOptions
90
+ });
91
+ ensureDirectoryForFile(resolvedVisualization.outputPath);
92
+ fs.writeFileSync(resolvedVisualization.outputPath, `${artifact.diagram}\n`);
93
+ }
94
+ //#endregion
95
+ export { loadVirtualContainerModule };
@@ -0,0 +1,76 @@
1
+ import { createClassKey } from "../core/utils.js";
2
+ import { createDiscoveryStore } from "../core/discovery-store.js";
3
+ //#region src/plugins/vite-plugin/discovery-runtime.ts
4
+ /** Files the discovery scanner processes (mirrors the transform hook filter). */
5
+ function isDiscoverableFile(file) {
6
+ return /\.tsx?$/i.test(file) && !/\.d\.ts$/i.test(file) && !file.includes("node_modules");
7
+ }
8
+ /**
9
+ * Serializes the codegen-relevant fields of a file's discovered metas so two
10
+ * scans can be compared. Changes here (added/removed services, scope, deps,
11
+ * factory, or resolved imports) mean the generated container must be rebuilt;
12
+ * edits that leave them untouched (e.g. a method body) should not.
13
+ */
14
+ function metasSignature(metas) {
15
+ return JSON.stringify(metas.map((m) => ({
16
+ className: m.className,
17
+ filePath: m.filePath,
18
+ scope: m.metadata.scope,
19
+ factory: m.metadata.factory?.expression ?? null,
20
+ dependencies: m.metadata.dependencies.map((d) => ({
21
+ expression: d.expression,
22
+ isLazy: d.isLazy,
23
+ referencedIdentifiers: d.referencedIdentifiers
24
+ })),
25
+ referencedImports: (m.referencedImports ?? []).map((r) => ({
26
+ name: r.name,
27
+ path: r.path,
28
+ originalName: r.originalName ?? null,
29
+ isTypeOnly: Boolean(r.isTypeOnly)
30
+ }))
31
+ })));
32
+ }
33
+ function lazyKeysSignature(keys) {
34
+ if (!keys || keys.size === 0) return "";
35
+ return Array.from(keys).toSorted().join("|");
36
+ }
37
+ function createDiscoveryRuntime() {
38
+ const discovery = createDiscoveryStore();
39
+ const discoveredClasses = /* @__PURE__ */ new Map();
40
+ const lazyReferencedClassKeys = /* @__PURE__ */ new Set();
41
+ return {
42
+ discoveredClasses,
43
+ lazyReferencedClassKeys,
44
+ processUpdate(id, code) {
45
+ const { metas, lazyClassKeys, previousMetas, previousLazyClassKeys } = discovery.updateFile(id, code);
46
+ if (previousMetas) for (const meta of previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
47
+ for (const meta of metas) discoveredClasses.set(createClassKey(meta.filePath, meta.className), meta);
48
+ if (previousLazyClassKeys) for (const key of previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
49
+ if (lazyClassKeys.size) for (const key of lazyClassKeys) lazyReferencedClassKeys.add(key);
50
+ return metasSignature(previousMetas ?? []) !== metasSignature(metas) || lazyKeysSignature(previousLazyClassKeys) !== lazyKeysSignature(lazyClassKeys);
51
+ },
52
+ removeDiscoveredFile(file) {
53
+ const removed = discovery.removeFile(file);
54
+ if (removed.previousMetas) for (const meta of removed.previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
55
+ if (removed.previousLazyClassKeys) for (const key of removed.previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
56
+ return Boolean(removed.previousMetas?.length || removed.previousLazyClassKeys?.size);
57
+ },
58
+ clear() {
59
+ discovery.clear();
60
+ discoveredClasses.clear();
61
+ lazyReferencedClassKeys.clear();
62
+ }
63
+ };
64
+ }
65
+ /**
66
+ * Invalidate the generated container module in every environment's module
67
+ * graph so its `load` hook re-runs and regenerates from current discovery.
68
+ */
69
+ function invalidateContainerModule(server, resolvedVirtualModuleId) {
70
+ for (const environment of Object.values(server.environments)) {
71
+ const mod = environment.moduleGraph.getModuleById(resolvedVirtualModuleId);
72
+ if (mod) environment.moduleGraph.invalidateModule(mod);
73
+ }
74
+ }
75
+ //#endregion
76
+ export { createDiscoveryRuntime, invalidateContainerModule, isDiscoverableFile };
@@ -1,19 +1,9 @@
1
1
  import { ServiceIdentifier } from "../../lib/service-identifiers.js";
2
2
  import { AlloyManifest } from "../core/types.js";
3
- import { MermaidDiagramOptions } from "./visualizer.js";
3
+ import { AlloyMermaidVisualizerOptions, AlloyVisualizationOptions } from "./visualization-utils.js";
4
4
  import { Plugin } from "vite";
5
5
 
6
6
  //#region src/plugins/vite-plugin/index.d.ts
7
- interface AlloyMermaidVisualizerOptions extends MermaidDiagramOptions {
8
- outputPath?: string;
9
- }
10
- interface AlloyVisualizationOptions {
11
- /**
12
- * Configure Mermaid diagram emission. Use `true` for defaults or provide
13
- * overrides for layout, colors, or output path.
14
- */
15
- mermaid?: boolean | AlloyMermaidVisualizerOptions;
16
- }
17
7
  interface AlloyPluginOptions {
18
8
  providers?: string[];
19
9
  /** Optional list of manifest objects to ingest */