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,98 @@
1
+ import { createClassKey } from "./utils.js";
2
+ import ts from "typescript";
3
+ import path from "path";
4
+
5
+ //#region src/plugins/core/lazy.ts
6
+ const RESOLVED_EXTENSIONS = [
7
+ "",
8
+ ".ts",
9
+ ".tsx",
10
+ ".js",
11
+ ".jsx",
12
+ ".mts",
13
+ ".cts"
14
+ ];
15
+ function processLazyCall(node, fileId, sourceFile, localLazyRefs) {
16
+ if (node.expression.getText(sourceFile) !== "Lazy") return;
17
+ const classKeys = resolveLazyTarget(node, fileId);
18
+ if (classKeys) for (const key of classKeys) localLazyRefs.add(key);
19
+ }
20
+ function resolveLazyTarget(node, fileId) {
21
+ if (node.arguments.length === 0) return;
22
+ const factory = node.arguments[0];
23
+ if (!(ts.isArrowFunction(factory) || ts.isFunctionExpression(factory))) return;
24
+ const body = getReturnedExpression(factory);
25
+ if (!body) return;
26
+ const importInfo = extractImportInfo(body);
27
+ if (!importInfo) return;
28
+ const resolvedPaths = resolveModuleSpecifierCandidates(fileId, importInfo.specifier);
29
+ if (!resolvedPaths.length) return;
30
+ const exportName = importInfo.exportName;
31
+ if (!exportName) return;
32
+ return resolvedPaths.map((candidate) => createClassKey(candidate, exportName));
33
+ }
34
+ function getReturnedExpression(fn) {
35
+ if (ts.isBlock(fn.body)) {
36
+ for (const statement of fn.body.statements) if (ts.isReturnStatement(statement) && statement.expression) return statement.expression;
37
+ return;
38
+ }
39
+ return fn.body;
40
+ }
41
+ function extractImportInfo(expr) {
42
+ if (ts.isCallExpression(expr)) {
43
+ if (isDynamicImport(expr)) {
44
+ const spec = getImportSpecifier(expr.arguments[0]);
45
+ if (!spec) return;
46
+ return {
47
+ specifier: spec,
48
+ exportName: void 0
49
+ };
50
+ }
51
+ if (ts.isPropertyAccessExpression(expr.expression) && expr.expression.name.text === "then") {
52
+ const importCall = expr.expression.expression;
53
+ if (!ts.isCallExpression(importCall) || !isDynamicImport(importCall)) return;
54
+ const spec = getImportSpecifier(importCall.arguments[0]);
55
+ if (!spec) return;
56
+ const callback = expr.arguments[0];
57
+ return {
58
+ specifier: spec,
59
+ exportName: callback ? extractExportName(callback) : void 0
60
+ };
61
+ }
62
+ }
63
+ }
64
+ function isDynamicImport(node) {
65
+ return node.expression.kind === ts.SyntaxKind.ImportKeyword;
66
+ }
67
+ function getImportSpecifier(node) {
68
+ if (!node) return;
69
+ if (ts.isStringLiteralLike(node)) return node.text;
70
+ }
71
+ function extractExportName(callback) {
72
+ if (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback)) {
73
+ const body = getReturnedExpression(callback);
74
+ if (!body) return;
75
+ return extractExportNameFromExpression(body);
76
+ }
77
+ return extractExportNameFromExpression(callback);
78
+ }
79
+ function extractExportNameFromExpression(expr) {
80
+ if (ts.isPropertyAccessExpression(expr)) return expr.name.text;
81
+ if (ts.isIdentifier(expr)) return expr.text;
82
+ if (ts.isNewExpression(expr) && expr.expression) return extractExportNameFromExpression(expr.expression);
83
+ }
84
+ function resolveModuleSpecifierCandidates(fromId, specifier) {
85
+ if (!specifier.startsWith(".")) return [];
86
+ const baseDir = path.dirname(fromId);
87
+ const resolvedBase = path.resolve(baseDir, specifier);
88
+ const candidates = [];
89
+ if (Boolean(path.extname(resolvedBase))) candidates.push(resolvedBase);
90
+ else {
91
+ for (const ext of RESOLVED_EXTENSIONS) candidates.push(resolvedBase + ext);
92
+ for (const ext of RESOLVED_EXTENSIONS) candidates.push(path.join(resolvedBase, "index" + ext));
93
+ }
94
+ return candidates;
95
+ }
96
+
97
+ //#endregion
98
+ export { processLazyCall };
@@ -0,0 +1,93 @@
1
+ import { createClassKey, createSymbolKey } from "./utils.js";
2
+ import { extractServiceMetadata } from "./decorators.js";
3
+ import { processLazyCall } from "./lazy.js";
4
+ import ts, { SyntaxKind } from "typescript";
5
+
6
+ //#region src/plugins/core/scanner.ts
7
+ function collectFileImports(sourceFile) {
8
+ const imports = /* @__PURE__ */ new Map();
9
+ for (const statement of sourceFile.statements) if (ts.isImportDeclaration(statement) && statement.importClause && ts.isStringLiteral(statement.moduleSpecifier)) {
10
+ const path = statement.moduleSpecifier.text;
11
+ const clause = statement.importClause;
12
+ const isTypeOnly = clause.phaseModifier === SyntaxKind.TypeKeyword;
13
+ if (clause.name) imports.set(clause.name.text, {
14
+ path,
15
+ originalName: "default",
16
+ isTypeOnly
17
+ });
18
+ if (clause.namedBindings) {
19
+ if (ts.isNamedImports(clause.namedBindings)) for (const element of clause.namedBindings.elements) {
20
+ const localName = element.name.text;
21
+ const originalName = element.propertyName ? element.propertyName.text : localName;
22
+ const elementIsTypeOnly = isTypeOnly || element.isTypeOnly;
23
+ imports.set(localName, {
24
+ path,
25
+ originalName,
26
+ isTypeOnly: elementIsTypeOnly
27
+ });
28
+ }
29
+ else if (ts.isNamespaceImport(clause.namedBindings)) imports.set(clause.namedBindings.name.text, {
30
+ path,
31
+ originalName: "*",
32
+ isTypeOnly
33
+ });
34
+ }
35
+ }
36
+ return imports;
37
+ }
38
+ function scanSource(code, id) {
39
+ const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.ESNext, true);
40
+ const discovered = /* @__PURE__ */ new Map();
41
+ const lazyRefs = /* @__PURE__ */ new Set();
42
+ const fileImports = collectFileImports(sourceFile);
43
+ const visit = (node) => {
44
+ if (!ts.isClassDeclaration(node) || !node.name) {
45
+ if (ts.isCallExpression(node)) processLazyCall(node, id, sourceFile, lazyRefs);
46
+ ts.forEachChild(node, visit);
47
+ return;
48
+ }
49
+ const targetDecorator = (ts.getDecorators ? ts.getDecorators(node) : [])?.find((d) => {
50
+ if (!ts.isCallExpression(d.expression)) return false;
51
+ const name = d.expression.expression.getText(sourceFile);
52
+ return name.endsWith("Injectable") || name.endsWith("Singleton");
53
+ });
54
+ if (!targetDecorator || !ts.isCallExpression(targetDecorator.expression)) {
55
+ ts.forEachChild(node, visit);
56
+ return;
57
+ }
58
+ const decoratorName = targetDecorator.expression.expression.getText(sourceFile);
59
+ const className = node.name.getText(sourceFile);
60
+ const callExpression = targetDecorator.expression;
61
+ const metadata = extractServiceMetadata(decoratorName, callExpression, sourceFile);
62
+ const referencedImports = [];
63
+ const seenIdentifiers = /* @__PURE__ */ new Set();
64
+ for (const dep of metadata.dependencies) for (const ident of dep.referencedIdentifiers) {
65
+ if (seenIdentifiers.has(ident)) continue;
66
+ seenIdentifiers.add(ident);
67
+ const importInfo = fileImports.get(ident);
68
+ if (importInfo) referencedImports.push({
69
+ name: ident,
70
+ path: importInfo.path,
71
+ originalName: importInfo.originalName,
72
+ isTypeOnly: importInfo.isTypeOnly
73
+ });
74
+ }
75
+ const classKey = createClassKey(id, className);
76
+ discovered.set(classKey, {
77
+ className,
78
+ filePath: id,
79
+ identifierKey: createSymbolKey(id, className),
80
+ metadata,
81
+ referencedImports
82
+ });
83
+ ts.forEachChild(node, visit);
84
+ };
85
+ ts.forEachChild(sourceFile, visit);
86
+ return {
87
+ metas: Array.from(discovered.values()),
88
+ lazyClassKeys: lazyRefs
89
+ };
90
+ }
91
+
92
+ //#endregion
93
+ export { scanSource };
@@ -0,0 +1,44 @@
1
+ import { ServiceScope } from "../../lib/scope.js";
2
+
3
+ //#region src/plugins/core/types.d.ts
4
+
5
+ interface ManifestServiceDescriptor {
6
+ exportName: string;
7
+ importPath: string;
8
+ /**
9
+ * Stable, unique key used to generate the ServiceIdentifier.
10
+ * Format: `alloy:<package-name>/<relative-path>#<ClassName>`
11
+ */
12
+ symbolKey: string;
13
+ scope: ServiceScope;
14
+ deps: string[];
15
+ /** Token dependencies (non-service identifiers) exported publicly by the package. */
16
+ tokenDeps?: {
17
+ exportName: string;
18
+ importPath: string;
19
+ }[];
20
+ lazyDeps: {
21
+ exportName: string;
22
+ importPath: string;
23
+ retry?: {
24
+ retries: number;
25
+ backoffMs?: number;
26
+ factor?: number;
27
+ };
28
+ }[];
29
+ }
30
+ interface AlloyManifest {
31
+ schemaVersion: number;
32
+ packageName: string;
33
+ buildMode: "preserve-modules" | "bundled" | "chunks";
34
+ services: ManifestServiceDescriptor[];
35
+ /** Optional provider module import specifiers (internal library-provided). */
36
+ providers?: string[];
37
+ diagnostics?: {
38
+ barrelFallback?: boolean;
39
+ duplicateServices?: string[];
40
+ missingExports?: string[];
41
+ };
42
+ }
43
+ //#endregion
44
+ export { AlloyManifest };
@@ -0,0 +1,45 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+
4
+ //#region src/plugins/core/utils.ts
5
+ const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:[\\/]/;
6
+ function normalizeImportPath(p) {
7
+ const raw = p.trim();
8
+ if (!raw) return raw;
9
+ const startsWithSlash = raw.startsWith("/") || raw.startsWith("\\");
10
+ const startsWithDot = raw.startsWith(".");
11
+ const startsWithTilde = raw.startsWith("~");
12
+ const isWindowsDrive = WINDOWS_DRIVE_PATTERN.test(raw);
13
+ const containsBackslash = raw.includes("\\");
14
+ if (!startsWithSlash && !startsWithDot && !startsWithTilde && !isWindowsDrive && !containsBackslash) return raw;
15
+ let out = raw.replace(/\\/g, "/");
16
+ out = out.replace(/^\/+/g, "/");
17
+ if (!out.startsWith("/")) out = "/" + out;
18
+ return out;
19
+ }
20
+ function hashString(value) {
21
+ let hash = 0;
22
+ for (let i = 0; i < value.length; i++) hash = hash * 31 + value.charCodeAt(i) | 0;
23
+ return Math.abs(hash).toString(36);
24
+ }
25
+ function createClassKey(filePath, className) {
26
+ return `${filePath}::${className}`;
27
+ }
28
+ function createAliasName(className, filePath) {
29
+ return `${className}__${hashString(normalizeImportPath(filePath))}`;
30
+ }
31
+ function createSymbolKey(filePath, className) {
32
+ return `alloy:${normalizeImportPath(filePath)}#${className}`;
33
+ }
34
+ function walkSync(dir, fileList = []) {
35
+ if (!fs.existsSync(dir)) return fileList;
36
+ fs.readdirSync(dir).forEach((file) => {
37
+ const filePath = path.join(dir, file);
38
+ if (fs.statSync(filePath).isDirectory()) walkSync(filePath, fileList);
39
+ else fileList.push(filePath);
40
+ });
41
+ return fileList;
42
+ }
43
+
44
+ //#endregion
45
+ export { createAliasName, createClassKey, createSymbolKey, hashString, normalizeImportPath, walkSync };
@@ -0,0 +1,49 @@
1
+ import { normalizeImportPath } from "../core/utils.js";
2
+ import path from "node:path";
3
+
4
+ //#region src/plugins/rollup-plugin/build-utils.ts
5
+ /**
6
+ * Determines build mode from Rollup/Rolldown output configuration + count of discovered services.
7
+ * Logic: preserveModules wins first; otherwise if >1 service we treat as chunked; else bundled.
8
+ *
9
+ * @param preserveModules Whether the bundler output preserves modules (Rollup output.preserveModules).
10
+ * @param discoveredServiceCount Total number of decorated Alloy services found during scanning.
11
+ * @returns Resolved BuildMode used for import path derivation and manifest shape.
12
+ */
13
+ function determineBuildMode(preserveModules, discoveredServiceCount) {
14
+ if (preserveModules) return "preserve-modules";
15
+ return !preserveModules && discoveredServiceCount > 1 ? "chunks" : "bundled";
16
+ }
17
+ /**
18
+ * Derives the public import path for a source file based on build mode.
19
+ *
20
+ * Rules:
21
+ * - preserve-modules: subpath mirrors /src/ relative path with extension stripped.
22
+ * - chunks: base filename becomes the subpath (extension stripped).
23
+ * - bundled: all services accessible from the package root (no subpath suffix).
24
+ *
25
+ * @param targetPath Absolute filesystem path of the source file defining the service.
26
+ * @param packageName NPM package name as resolved from library package.json.
27
+ * @param buildMode Previously resolved build mode.
28
+ * @returns Public import specifier consumers will use (e.g. `@scope/pkg/service-a`).
29
+ */
30
+ function resolveImportPathForBuild(targetPath, packageName, buildMode) {
31
+ const normalized = normalizeImportPath(targetPath);
32
+ if (buildMode === "preserve-modules") {
33
+ const srcIndex = normalized.lastIndexOf("/src/");
34
+ if (srcIndex !== -1) {
35
+ let sub = normalized.slice(srcIndex + 5);
36
+ sub = sub.replace(/\.(tsx?|ts|js|jsx|mts|cts)$/i, "");
37
+ return `${packageName}/${sub}`;
38
+ }
39
+ return packageName;
40
+ }
41
+ if (buildMode === "chunks") return `${packageName}/${path.basename(targetPath).replace(/\.(tsx?|ts|js|jsx|mts|cts)$/i, "")}`;
42
+ return packageName;
43
+ }
44
+ function hasPreserveModules(o) {
45
+ return typeof o === "object" && o !== null && "preserveModules" in o;
46
+ }
47
+
48
+ //#endregion
49
+ export { determineBuildMode, hasPreserveModules, resolveImportPathForBuild };
@@ -0,0 +1,30 @@
1
+ //#region src/plugins/rollup-plugin/index.d.ts
2
+ interface AlloyManifestPluginOptions {
3
+ /** Optional override for emitted filename. Defaults to 'alloy.manifest.mjs'. */
4
+ fileName?: string;
5
+ /** Relative or absolute path to package.json if not at cwd root. */
6
+ packageJsonPath?: string;
7
+ /**
8
+ * Optional list of provider module source paths to include in the manifest.
9
+ * These should be file paths within the library (e.g., 'src/providers.ts').
10
+ * In `preserveModules` builds, import specifiers will be derived and emitted
11
+ * so consumer apps can import and apply them automatically.
12
+ */
13
+ providers?: string[];
14
+ }
15
+ /**
16
+ * Rollup/Rolldown plugin that scans decorated Alloy services and emits an ESM manifest.
17
+ */
18
+ interface MinimalRollupPlugin {
19
+ name: string;
20
+ transform?(code: string, id: string): unknown;
21
+ generateBundle?(outputOptions: unknown): void;
22
+ emitFile?(file: {
23
+ type: "asset";
24
+ fileName: string;
25
+ source: string;
26
+ }): void;
27
+ }
28
+ declare function alloy(options?: AlloyManifestPluginOptions): MinimalRollupPlugin;
29
+ //#endregion
30
+ export { alloy };
@@ -0,0 +1,225 @@
1
+ import { ServiceScope } from "../../lib/scope.js";
2
+ import { createDiscoveryStore } from "../core/discovery-store.js";
3
+ import { determineBuildMode, hasPreserveModules, resolveImportPathForBuild } from "./build-utils.js";
4
+ import path from "node:path";
5
+ import fs from "node:fs";
6
+ import ts from "typescript";
7
+
8
+ //#region src/plugins/rollup-plugin/index.ts
9
+ function alloy(options = {}) {
10
+ const fileName = options.fileName ?? "alloy.manifest.mjs";
11
+ const packageJsonFile = options.packageJsonPath ? path.isAbsolute(options.packageJsonPath) ? options.packageJsonPath : path.resolve(process.cwd(), options.packageJsonPath) : path.resolve(process.cwd(), "package.json");
12
+ let packageName = "UNKNOWN_PACKAGE";
13
+ try {
14
+ const pkgRaw = fs.readFileSync(packageJsonFile, "utf8");
15
+ const pkg = JSON.parse(pkgRaw);
16
+ if (typeof pkg.name === "string") packageName = pkg.name;
17
+ } catch {}
18
+ const packageRoot = path.dirname(packageJsonFile);
19
+ const discovery = createDiscoveryStore({ trackSources: true });
20
+ /**
21
+ * Determines the build mode based on output options and discovered services.
22
+ *
23
+ * Build modes affect import path resolution:
24
+ * - `preserve-modules`: Each source file becomes a separate output module with subpath imports
25
+ * - `chunks`: Multiple services with chunked output (multi-entry)
26
+ * - `bundled`: All services bundled into a single entry point
27
+ *
28
+ * @param outputOptions - Rollup/Rolldown output configuration
29
+ * @returns Build mode identifier
30
+ */
31
+ function getBuildMode(outputOptions) {
32
+ return determineBuildMode(hasPreserveModules(outputOptions) ? Boolean(outputOptions.preserveModules) : false, [...discovery.fileMetas.values()].reduce((acc, metas) => acc + metas.length, 0));
33
+ }
34
+ /**
35
+ * Parses the barrel export file (index.ts) to extract all publicly exported symbol names.
36
+ * Used in bundled/chunks modes to detect services that aren't properly exported.
37
+ *
38
+ * Looks for:
39
+ * - `export class Foo`
40
+ * - `export const bar`
41
+ * - `export function baz`
42
+ * - `export { Foo, Bar }`
43
+ *
44
+ * @returns Set of exported symbol names found in the barrel file
45
+ */
46
+ function parseExportedNames() {
47
+ let exportedNames = /* @__PURE__ */ new Set();
48
+ const sources = discovery.fileSources;
49
+ if (!sources) return exportedNames;
50
+ const barrelEntry = [...sources.keys()].find((p) => /\/src\/index\.(tsx?|mts|cts)$/i.test(p)) ?? [...sources.keys()].find((p) => /\/index\.(tsx?|ts)$/i.test(p));
51
+ const sourceText = barrelEntry ? sources.get(barrelEntry) : void 0;
52
+ if (!barrelEntry || !sourceText) return exportedNames;
53
+ const sf = ts.createSourceFile(barrelEntry, sourceText, ts.ScriptTarget.ESNext, true);
54
+ const names = /* @__PURE__ */ new Set();
55
+ const hasExportModifier = (node) => {
56
+ return !!(ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
57
+ };
58
+ const visit = (node) => {
59
+ if (ts.isClassDeclaration(node) && node.name && hasExportModifier(node)) names.add(node.name.text);
60
+ if (ts.isVariableStatement(node) && hasExportModifier(node)) {
61
+ for (const decl of node.declarationList.declarations) if (ts.isIdentifier(decl.name)) names.add(decl.name.text);
62
+ }
63
+ if (ts.isFunctionDeclaration(node) && node.name && hasExportModifier(node)) names.add(node.name.text);
64
+ if (ts.isExportDeclaration(node) && node.exportClause && ts.isNamedExports(node.exportClause)) for (const el of node.exportClause.elements) names.add(el.name.text);
65
+ ts.forEachChild(node, visit);
66
+ };
67
+ ts.forEachChild(sf, visit);
68
+ exportedNames = names;
69
+ return exportedNames;
70
+ }
71
+ /**
72
+ * Resolves the public import path for a service based on build mode and source location.
73
+ *
74
+ * Resolution strategies:
75
+ * - `preserve-modules`: Derives subpath from /src/ directory structure
76
+ * - `chunks`: Uses base filename as subpath
77
+ * - `bundled`: Uses package root
78
+ *
79
+ * @param targetPath - Absolute path to source file
80
+ * @param buildMode - Current build mode
81
+ * @returns Public import specifier (e.g., "@myorg/pkg/subpath")
82
+ */
83
+ function resolveImportPath(targetPath, buildMode) {
84
+ return resolveImportPathForBuild(targetPath, packageName, buildMode);
85
+ }
86
+ return {
87
+ name: "alloy-manifest",
88
+ transform(code, id) {
89
+ const isTS = /\.(tsx?|mts|cts)$/i.test(id);
90
+ const isDeclaration = id.endsWith(".d.ts");
91
+ if (!isTS || isDeclaration) return null;
92
+ discovery.updateFile(id, code);
93
+ return null;
94
+ },
95
+ generateBundle(outputOptions) {
96
+ const buildMode = getBuildMode(outputOptions);
97
+ const services = [];
98
+ const missingExports = [];
99
+ const exportedNames = buildMode === "preserve-modules" ? /* @__PURE__ */ new Set() : parseExportedNames();
100
+ for (const metas of discovery.fileMetas.values()) for (const meta of metas) {
101
+ const scope = meta.metadata.scope ?? ServiceScope.TRANSIENT;
102
+ const importPath = resolveImportPath(meta.filePath, buildMode);
103
+ let relPath = path.relative(packageRoot, meta.filePath);
104
+ if (path.sep === "\\") relPath = relPath.split(path.sep).join("/");
105
+ const symbolKey = `alloy:${packageName}/${relPath}#${meta.className}`;
106
+ services.push({
107
+ exportName: meta.className,
108
+ importPath,
109
+ symbolKey,
110
+ scope,
111
+ deps: [],
112
+ lazyDeps: []
113
+ });
114
+ if (buildMode !== "preserve-modules" && !exportedNames.has(meta.className)) missingExports.push(meta.className);
115
+ }
116
+ const serviceByName = /* @__PURE__ */ new Map();
117
+ for (const s of services) serviceByName.set(s.exportName, s);
118
+ const serviceTokenDeps = /* @__PURE__ */ new Map();
119
+ for (const metas of discovery.fileMetas.values()) for (const meta of metas) {
120
+ const svc = serviceByName.get(meta.className);
121
+ if (!svc) continue;
122
+ 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
+ }
130
+ }
131
+ }
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
+ const manifest = {
159
+ schemaVersion: 1,
160
+ packageName,
161
+ buildMode,
162
+ services,
163
+ diagnostics: {
164
+ barrelFallback: buildMode !== "preserve-modules",
165
+ missingExports: missingExports.length ? missingExports : void 0
166
+ }
167
+ };
168
+ const nameOccurrences = /* @__PURE__ */ new Map();
169
+ for (const svc of services) {
170
+ const arr = nameOccurrences.get(svc.exportName);
171
+ if (arr) arr.push(svc);
172
+ else nameOccurrences.set(svc.exportName, [svc]);
173
+ }
174
+ for (const arr of nameOccurrences.values()) if (arr.length > 1) {
175
+ if (!manifest.diagnostics) manifest.diagnostics = {};
176
+ const dup = manifest.diagnostics.duplicateServices ?? [];
177
+ for (const svc of arr) dup.push(`${svc.exportName}|${svc.importPath}`);
178
+ manifest.diagnostics.duplicateServices = dup;
179
+ }
180
+ const providerPaths = Array.isArray(options.providers) ? options.providers : [];
181
+ if (providerPaths.length) {
182
+ if (buildMode !== "preserve-modules") throw new Error("Alloy manifest plugin: 'providers' requires preserveModules=true to emit stable public import specifiers. Enable preserveModules in your library build, or expose provider modules via root exports and omit 'providers' here.");
183
+ const resolvedProviders = [];
184
+ for (const p of providerPaths) {
185
+ const spec = resolveImportPath(path.isAbsolute(p) ? p : path.resolve(process.cwd(), p), "preserve-modules");
186
+ resolvedProviders.push(spec);
187
+ }
188
+ manifest.providers = resolvedProviders;
189
+ }
190
+ const code = `// Generated Alloy manifest (v1)\nexport const manifest = ${JSON.stringify(manifest, null, 2)};\n`;
191
+ const identifiersCode = ["// Generated Alloy Service Identifiers", ...services.map((s) => `export const ${s.exportName}Identifier = Symbol.for("${s.symbolKey}");`)].join("\n");
192
+ if (this.emitFile) {
193
+ this.emitFile({
194
+ type: "asset",
195
+ fileName,
196
+ source: code
197
+ });
198
+ this.emitFile({
199
+ type: "asset",
200
+ fileName: "service-identifiers.mjs",
201
+ source: identifiersCode
202
+ });
203
+ } else try {
204
+ fs.writeFileSync(path.resolve(process.cwd(), fileName), code, "utf8");
205
+ fs.writeFileSync(path.resolve(process.cwd(), "service-identifiers.mjs"), identifiersCode, "utf8");
206
+ } catch {}
207
+ checkPackageExports(packageJsonFile, fileName);
208
+ }
209
+ };
210
+ }
211
+ function checkPackageExports(packageJsonPath, manifestFileName) {
212
+ try {
213
+ const pkgRaw = fs.readFileSync(packageJsonPath, "utf8");
214
+ const pkg = JSON.parse(pkgRaw);
215
+ if (!pkg.exports) return;
216
+ const exports = pkg.exports;
217
+ const hasManifest = Object.values(exports).some((e) => typeof e === "string" && e.includes(manifestFileName) || typeof e === "object" && e !== null && Object.values(e).some((v) => String(v).includes(manifestFileName)));
218
+ const hasIdentifiers = Object.values(exports).some((e) => typeof e === "string" && e.includes("service-identifiers.mjs") || typeof e === "object" && e !== null && Object.values(e).some((v) => String(v).includes("service-identifiers.mjs")));
219
+ if (!hasManifest) console.warn(`[alloy] Warning: ${manifestFileName} is not exposed in package.json "exports". Consumers may not be able to access the manifest.`);
220
+ if (!hasIdentifiers) console.warn(`[alloy] Warning: service-identifiers.mjs is not exposed in package.json "exports". Consumers may not be able to access the generated identifiers helper.`);
221
+ } catch {}
222
+ }
223
+
224
+ //#endregion
225
+ export { alloy };
@@ -0,0 +1,25 @@
1
+ import { ServiceIdentifier } from "../../lib/service-identifiers.js";
2
+ import { AlloyManifest } from "../core/types.js";
3
+ import { Plugin } from "vite";
4
+
5
+ //#region src/plugins/vite-plugin/index.d.ts
6
+ interface AlloyPluginOptions {
7
+ providers?: string[];
8
+ /** Optional list of manifest objects to ingest */
9
+ manifests?: AlloyManifest[];
10
+ /** List of ServiceIdentifiers to mark as instantiation-lazy (adds factory Lazy wrapper) */
11
+ lazyServices?: ServiceIdentifier[];
12
+ /**
13
+ * Output directory for the generated `virtual-container.d.ts` file.
14
+ * Relative paths are resolved against the project root.
15
+ * Defaults to "./src".
16
+ */
17
+ containerDeclarationDir?: string;
18
+ }
19
+ /**
20
+ * Creates the Alloy Vite plugin that statically discovers injectable classes
21
+ * and exposes them through a virtual container module at build time.
22
+ */
23
+ declare function alloy(options?: AlloyPluginOptions): Plugin;
24
+ //#endregion
25
+ export { AlloyPluginOptions, alloy };