alloy-di 0.1.1 → 1.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.
- package/LICENSE.md +21 -0
- package/README.md +26 -0
- package/dist/plugins/core/decorators.js +30 -7
- package/dist/plugins/vite-plugin/index.d.ts +16 -0
- package/dist/plugins/vite-plugin/index.js +35 -0
- package/dist/plugins/vite-plugin/visualizer.d.ts +18 -0
- package/dist/plugins/vite-plugin/visualizer.js +289 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +18 -14
package/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 The Alloy DI Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
## Highlights
|
|
6
6
|
|
|
7
7
|
- **Build-time graph** – services, scopes, and dependencies are resolved while bundling, so runtime work stays minimal.
|
|
8
|
+
- **Visualize your DI graph** – enable the Vite plugin’s `visualize` option to emit a Mermaid diagram (`./alloy-di.mmd` by default) that captures scopes, lazy edges, and tokens for easy review.
|
|
8
9
|
- **First-class lazy loading** – use `Lazy()` or provider-based lazy registrations to keep optional features in separate chunks.
|
|
9
10
|
- **Framework agnostic** – works anywhere Vite runs: React, Vue, Svelte, SSR, libraries, and plain TS apps.
|
|
10
11
|
- **Type safe** – generates `serviceIdentifiers` and manifest declarations for precise inference.
|
|
@@ -54,6 +55,31 @@ pnpm add -D alloy-di
|
|
|
54
55
|
|
|
55
56
|
Need manifests, providers, or testing utilities? See the docs site for complete guides.
|
|
56
57
|
|
|
58
|
+
## Visualize your dependency graph
|
|
59
|
+
|
|
60
|
+
Enable the Vite plugin’s `visualize` option to have Alloy emit a Mermaid diagram that reflects every discovered service, scope, lazy edge, and token. By default the graph is written to `./alloy-di.mmd`, but you can customize the output path, color palette, or layout direction to fit your workflow.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import { defineConfig } from "vite";
|
|
64
|
+
import alloy from "alloy-di/vite";
|
|
65
|
+
|
|
66
|
+
export default defineConfig({
|
|
67
|
+
plugins: [
|
|
68
|
+
alloy({
|
|
69
|
+
visualize: {
|
|
70
|
+
mermaid: {
|
|
71
|
+
outputPath: "./docs/di-graph.mmd",
|
|
72
|
+
direction: "TB",
|
|
73
|
+
includeLegend: false,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
}),
|
|
77
|
+
],
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Commit the artifact for PR reviews, or generate ad-hoc previews locally with any Mermaid-friendly tool (for example VS Code’s Mermaid extension, GitHub’s Markdown preview, or `npx @mermaid-js/mermaid-cli -i docs/di-graph.mmd -o graph.svg`). The diagram highlights scopes, lazy edges, factory nodes, and tokens so you can inspect DI wiring at a glance.
|
|
82
|
+
|
|
57
83
|
## Documentation
|
|
58
84
|
|
|
59
85
|
- **Website**: https://alloy-di.dev (generated from `/docs`)
|
|
@@ -2,37 +2,60 @@ import { ServiceScope } from "../../lib/scope.js";
|
|
|
2
2
|
import ts from "typescript";
|
|
3
3
|
|
|
4
4
|
//#region src/plugins/core/decorators.ts
|
|
5
|
-
function
|
|
5
|
+
function collectBindingIdentifiers(name, ignored) {
|
|
6
|
+
if (ts.isIdentifier(name)) {
|
|
7
|
+
ignored.add(name.text);
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
name.elements.forEach((element) => {
|
|
11
|
+
if (ts.isOmittedExpression(element)) return;
|
|
12
|
+
collectBindingIdentifiers(element.name, ignored);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function isDynamicImportExpression(node) {
|
|
16
|
+
return node.expression.kind === ts.SyntaxKind.ImportKeyword;
|
|
17
|
+
}
|
|
18
|
+
function extractRefs(node, sourceFile, identifiers, ignored) {
|
|
6
19
|
if (ts.isPropertyAssignment(node)) {
|
|
7
|
-
extractRefs(node.initializer, sourceFile, identifiers);
|
|
8
|
-
if (ts.isComputedPropertyName(node.name)) extractRefs(node.name.expression, sourceFile, identifiers);
|
|
20
|
+
extractRefs(node.initializer, sourceFile, identifiers, ignored);
|
|
21
|
+
if (ts.isComputedPropertyName(node.name)) extractRefs(node.name.expression, sourceFile, identifiers, ignored);
|
|
9
22
|
return;
|
|
10
23
|
}
|
|
24
|
+
if (ts.isParameter(node)) {
|
|
25
|
+
collectBindingIdentifiers(node.name, ignored);
|
|
26
|
+
if (node.initializer) extractRefs(node.initializer, sourceFile, identifiers, ignored);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
30
|
+
if (ts.isCallExpression(node.expression) && isDynamicImportExpression(node.expression) && ts.isIdentifier(node.name)) ignored.add(node.name.text);
|
|
31
|
+
}
|
|
11
32
|
if (ts.isIdentifier(node)) {
|
|
12
|
-
identifiers.add(node.text);
|
|
33
|
+
if (!ignored.has(node.text)) identifiers.add(node.text);
|
|
13
34
|
return;
|
|
14
35
|
}
|
|
15
36
|
if (ts.isCallExpression(node)) {
|
|
16
37
|
const name = node.expression.getText(sourceFile);
|
|
17
38
|
if (name === "Lazy" || name.endsWith(".Lazy")) {
|
|
18
|
-
node.arguments.forEach((arg) => extractRefs(arg, sourceFile, identifiers));
|
|
39
|
+
node.arguments.forEach((arg) => extractRefs(arg, sourceFile, identifiers, ignored));
|
|
19
40
|
return;
|
|
20
41
|
}
|
|
21
42
|
}
|
|
22
|
-
ts.forEachChild(node, (n) => extractRefs(n, sourceFile, identifiers));
|
|
43
|
+
ts.forEachChild(node, (n) => extractRefs(n, sourceFile, identifiers, ignored));
|
|
23
44
|
}
|
|
24
45
|
function createDependencyDescriptor(node, sourceFile) {
|
|
25
46
|
const expression = node.getText(sourceFile);
|
|
26
47
|
const referencedIdentifiers = /* @__PURE__ */ new Set();
|
|
48
|
+
const ignoredIdentifiers = /* @__PURE__ */ new Set();
|
|
27
49
|
let isLazy = false;
|
|
28
50
|
if (ts.isCallExpression(node)) {
|
|
29
51
|
const callName = node.expression.getText(sourceFile);
|
|
30
52
|
if (callName === "Lazy" || callName.endsWith(".Lazy")) isLazy = true;
|
|
31
53
|
}
|
|
32
|
-
extractRefs(node, sourceFile, referencedIdentifiers);
|
|
54
|
+
extractRefs(node, sourceFile, referencedIdentifiers, ignoredIdentifiers);
|
|
33
55
|
return {
|
|
34
56
|
expression,
|
|
35
57
|
referencedIdentifiers: Array.from(referencedIdentifiers),
|
|
58
|
+
ignoredIdentifiers: ignoredIdentifiers.size ? Array.from(ignoredIdentifiers) : void 0,
|
|
36
59
|
isLazy
|
|
37
60
|
};
|
|
38
61
|
}
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
import { ServiceIdentifier } from "../../lib/service-identifiers.js";
|
|
2
2
|
import { AlloyManifest } from "../core/types.js";
|
|
3
|
+
import { MermaidDiagramOptions } from "./visualizer.js";
|
|
3
4
|
import { Plugin } from "vite";
|
|
4
5
|
|
|
5
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
|
+
}
|
|
6
17
|
interface AlloyPluginOptions {
|
|
7
18
|
providers?: string[];
|
|
8
19
|
/** Optional list of manifest objects to ingest */
|
|
@@ -15,6 +26,11 @@ interface AlloyPluginOptions {
|
|
|
15
26
|
* Defaults to "./src".
|
|
16
27
|
*/
|
|
17
28
|
containerDeclarationDir?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Emit dependency graph artifacts. When `true`, writes a Mermaid diagram to
|
|
31
|
+
* `${projectRoot}/alloy-di.mmd`. Provide an object to customize output.
|
|
32
|
+
*/
|
|
33
|
+
visualize?: boolean | AlloyVisualizationOptions;
|
|
18
34
|
}
|
|
19
35
|
/**
|
|
20
36
|
* Creates the Alloy Vite plugin that statically discovers injectable classes
|
|
@@ -3,10 +3,12 @@ import { IdentifierResolver } from "../core/identifier-resolver.js";
|
|
|
3
3
|
import { generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition } from "../core/codegen.js";
|
|
4
4
|
import { createDiscoveryStore } from "../core/discovery-store.js";
|
|
5
5
|
import { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateManifestServices, groupMetasByName, readManifests, reconcileLazySet, toMetaFromManifest } from "./manifest-utils.js";
|
|
6
|
+
import { generateMermaidDiagram } from "./visualizer.js";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import fs from "node:fs";
|
|
8
9
|
|
|
9
10
|
//#region src/plugins/vite-plugin/index.ts
|
|
11
|
+
const DEFAULT_MERMAID_FILENAME = "alloy-di.mmd";
|
|
10
12
|
function toLazyServiceKey(identifier) {
|
|
11
13
|
const description = identifier.description;
|
|
12
14
|
if (!description || !description.startsWith("alloy:")) throw new Error("[alloy] lazyServices entries must be serviceIdentifiers exported by Alloy manifests.");
|
|
@@ -23,6 +25,7 @@ function alloy(options = {}) {
|
|
|
23
25
|
const providerModuleRefs = [];
|
|
24
26
|
let resolvedRoot = process.cwd();
|
|
25
27
|
let packageName = "UNKNOWN_PACKAGE";
|
|
28
|
+
let resolvedVisualization = null;
|
|
26
29
|
const lazyServiceKeys = new Set((options.lazyServices ?? []).map(toLazyServiceKey));
|
|
27
30
|
const discovery = createDiscoveryStore();
|
|
28
31
|
const discoveredClasses = /* @__PURE__ */ new Map();
|
|
@@ -51,6 +54,7 @@ function alloy(options = {}) {
|
|
|
51
54
|
importPath: normalizeImportPath(absPath)
|
|
52
55
|
});
|
|
53
56
|
}
|
|
57
|
+
resolvedVisualization = resolveVisualizationOptions(options.visualize, resolvedRoot);
|
|
54
58
|
},
|
|
55
59
|
resolveId(id) {
|
|
56
60
|
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
|
@@ -145,10 +149,41 @@ function alloy(options = {}) {
|
|
|
145
149
|
const manifestsDtsPath = path.join(dtsDir, "alloy-manifests.d.ts");
|
|
146
150
|
fs.writeFileSync(manifestsDtsPath, manifestsDts);
|
|
147
151
|
}
|
|
152
|
+
if (resolvedVisualization) {
|
|
153
|
+
const artifact = generateMermaidDiagram({
|
|
154
|
+
metas,
|
|
155
|
+
lazyClassKeys: new Set(lazyReferencedClassKeys),
|
|
156
|
+
options: resolvedVisualization.mermaidOptions
|
|
157
|
+
});
|
|
158
|
+
ensureDirectoryForFile(resolvedVisualization.outputPath);
|
|
159
|
+
fs.writeFileSync(resolvedVisualization.outputPath, `${artifact.diagram}\n`);
|
|
160
|
+
}
|
|
148
161
|
return code;
|
|
149
162
|
}
|
|
150
163
|
};
|
|
151
164
|
}
|
|
165
|
+
function resolveVisualizationOptions(input, projectRoot) {
|
|
166
|
+
if (!input) return null;
|
|
167
|
+
if (typeof input === "boolean") return {
|
|
168
|
+
outputPath: path.resolve(projectRoot, DEFAULT_MERMAID_FILENAME),
|
|
169
|
+
mermaidOptions: void 0
|
|
170
|
+
};
|
|
171
|
+
const mermaidConfig = input.mermaid;
|
|
172
|
+
if (!mermaidConfig) return null;
|
|
173
|
+
if (mermaidConfig === true) return {
|
|
174
|
+
outputPath: path.resolve(projectRoot, DEFAULT_MERMAID_FILENAME),
|
|
175
|
+
mermaidOptions: void 0
|
|
176
|
+
};
|
|
177
|
+
const { outputPath, ...rest } = mermaidConfig;
|
|
178
|
+
return {
|
|
179
|
+
outputPath: path.resolve(projectRoot, outputPath ?? DEFAULT_MERMAID_FILENAME),
|
|
180
|
+
mermaidOptions: Object.keys(rest).length > 0 ? rest : void 0
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function ensureDirectoryForFile(filePath) {
|
|
184
|
+
const dir = path.dirname(filePath);
|
|
185
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
186
|
+
}
|
|
152
187
|
|
|
153
188
|
//#endregion
|
|
154
189
|
export { alloy };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ServiceScope } from "../../lib/scope.js";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/vite-plugin/visualizer.d.ts
|
|
4
|
+
interface MermaidDiagramOptions {
|
|
5
|
+
direction?: "LR" | "TB" | "BT" | "RL";
|
|
6
|
+
includeLegend?: boolean;
|
|
7
|
+
scopeColors?: Partial<Record<ServiceScope, string>>;
|
|
8
|
+
lazyNodeFill?: string;
|
|
9
|
+
factoryNodeFill?: string;
|
|
10
|
+
tokenNodeFill?: string;
|
|
11
|
+
nodeStrokeColor?: string;
|
|
12
|
+
nodeTextColor?: string;
|
|
13
|
+
lazyEdgeColor?: string;
|
|
14
|
+
eagerEdgeColor?: string;
|
|
15
|
+
factoryEdgeColor?: string;
|
|
16
|
+
}
|
|
17
|
+
//#endregion
|
|
18
|
+
export { MermaidDiagramOptions };
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { createClassKey, createSymbolKey, hashString, normalizeImportPath } from "../core/utils.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/vite-plugin/visualizer.ts
|
|
5
|
+
const DEFAULT_SCOPE_COLORS = {
|
|
6
|
+
singleton: "#f6c14a",
|
|
7
|
+
transient: "#58a6ff"
|
|
8
|
+
};
|
|
9
|
+
const DEFAULT_OPTIONS = {
|
|
10
|
+
direction: "LR",
|
|
11
|
+
includeLegend: true,
|
|
12
|
+
scopeColors: DEFAULT_SCOPE_COLORS,
|
|
13
|
+
lazyNodeFill: "#e8def8",
|
|
14
|
+
factoryNodeFill: "#ffe0b2",
|
|
15
|
+
tokenNodeFill: "#d1d5db",
|
|
16
|
+
nodeStrokeColor: "#1f2937",
|
|
17
|
+
nodeTextColor: "#111827",
|
|
18
|
+
lazyEdgeColor: "#a855f7",
|
|
19
|
+
eagerEdgeColor: "#6b7280",
|
|
20
|
+
factoryEdgeColor: "#ef6c00"
|
|
21
|
+
};
|
|
22
|
+
const RESERVED_IDENTIFIERS = new Set([
|
|
23
|
+
"Lazy",
|
|
24
|
+
"Symbol",
|
|
25
|
+
"Promise",
|
|
26
|
+
"import",
|
|
27
|
+
"this",
|
|
28
|
+
"arguments"
|
|
29
|
+
]);
|
|
30
|
+
/**
|
|
31
|
+
* Generates a Mermaid diagram depicting the dependency graph for the provided services.
|
|
32
|
+
* @param input - Discovered metadata, optional lazy keys, and rendering options.
|
|
33
|
+
* @returns The rendered diagram plus simple counts useful for reporting.
|
|
34
|
+
*/
|
|
35
|
+
function generateMermaidDiagram({ metas, lazyClassKeys, options }) {
|
|
36
|
+
const mergedOptions = {
|
|
37
|
+
...DEFAULT_OPTIONS,
|
|
38
|
+
...options,
|
|
39
|
+
scopeColors: {
|
|
40
|
+
...DEFAULT_SCOPE_COLORS,
|
|
41
|
+
...options?.scopeColors
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
const lazyKeys = lazyClassKeys ?? /* @__PURE__ */ new Set();
|
|
45
|
+
const serviceNodes = [];
|
|
46
|
+
const nodesByClassName = /* @__PURE__ */ new Map();
|
|
47
|
+
const nodesByFilePath = /* @__PURE__ */ new Map();
|
|
48
|
+
const tokenNodes = /* @__PURE__ */ new Map();
|
|
49
|
+
const nodeByMeta = /* @__PURE__ */ new Map();
|
|
50
|
+
metas.forEach((meta, index) => {
|
|
51
|
+
const key = createClassKey(meta.filePath, meta.className);
|
|
52
|
+
const id = resolveNodeId(meta, index);
|
|
53
|
+
const normalizedPath = normalizeImportPath(meta.filePath);
|
|
54
|
+
const node = {
|
|
55
|
+
id,
|
|
56
|
+
label: meta.className,
|
|
57
|
+
key,
|
|
58
|
+
scope: meta.metadata.scope,
|
|
59
|
+
type: "service",
|
|
60
|
+
isLazyOnly: lazyKeys.has(key),
|
|
61
|
+
hasFactory: Boolean(meta.metadata.factory),
|
|
62
|
+
className: meta.className,
|
|
63
|
+
filePath: normalizedPath
|
|
64
|
+
};
|
|
65
|
+
serviceNodes.push(node);
|
|
66
|
+
nodeByMeta.set(meta, node);
|
|
67
|
+
const classBucket = nodesByClassName.get(meta.className) ?? [];
|
|
68
|
+
classBucket.push(node);
|
|
69
|
+
nodesByClassName.set(meta.className, classBucket);
|
|
70
|
+
const pathBucket = nodesByFilePath.get(normalizedPath) ?? [];
|
|
71
|
+
pathBucket.push(node);
|
|
72
|
+
nodesByFilePath.set(normalizedPath, pathBucket);
|
|
73
|
+
});
|
|
74
|
+
const edges = [];
|
|
75
|
+
const edgeKeys = /* @__PURE__ */ new Set();
|
|
76
|
+
metas.forEach((meta) => {
|
|
77
|
+
const sourceNode = nodeByMeta.get(meta);
|
|
78
|
+
if (!sourceNode) return;
|
|
79
|
+
const dependencies = meta.metadata.dependencies ?? [];
|
|
80
|
+
for (const dep of dependencies) {
|
|
81
|
+
const identifiers = gatherIdentifiers(dep);
|
|
82
|
+
if (!identifiers.length) continue;
|
|
83
|
+
const targets = /* @__PURE__ */ new Map();
|
|
84
|
+
for (const ident of identifiers) {
|
|
85
|
+
const resolvedTargets = resolveTargetsForIdentifier(ident, dep.expression, meta, nodesByClassName, nodesByFilePath, tokenNodes);
|
|
86
|
+
for (const target of resolvedTargets) {
|
|
87
|
+
if (target.id === sourceNode.id) continue;
|
|
88
|
+
targets.set(target.id, target);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!targets.size) continue;
|
|
92
|
+
for (const target of targets.values()) {
|
|
93
|
+
const edgeKey = `${sourceNode.id}->${target.id}|${dep.isLazy}`;
|
|
94
|
+
if (edgeKeys.has(edgeKey)) continue;
|
|
95
|
+
edgeKeys.add(edgeKey);
|
|
96
|
+
edges.push({
|
|
97
|
+
from: sourceNode,
|
|
98
|
+
to: target,
|
|
99
|
+
label: describeEdge(sourceNode, target, dep.isLazy),
|
|
100
|
+
isLazy: dep.isLazy,
|
|
101
|
+
stroke: selectEdgeColor(dep.isLazy, target, mergedOptions)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
const lines = [`graph ${mergedOptions.direction}`];
|
|
107
|
+
if (mergedOptions.includeLegend) {
|
|
108
|
+
lines.push(` %% Legend: singleton=${mergedOptions.scopeColors.singleton}, transient=${mergedOptions.scopeColors.transient}, lazy-only=${mergedOptions.lazyNodeFill}, factory=${mergedOptions.factoryNodeFill}, token=${mergedOptions.tokenNodeFill}`);
|
|
109
|
+
lines.push(` %% Edge colors: eager=${mergedOptions.eagerEdgeColor}, lazy=${mergedOptions.lazyEdgeColor}, factory=${mergedOptions.factoryEdgeColor}`);
|
|
110
|
+
}
|
|
111
|
+
const allNodes = [...serviceNodes, ...Array.from(tokenNodes.values())];
|
|
112
|
+
for (const node of allNodes) {
|
|
113
|
+
const safeLabel = escapeMermaidLabel(node.label);
|
|
114
|
+
lines.push(` ${node.id}["${safeLabel}"]`);
|
|
115
|
+
const styleParts = [
|
|
116
|
+
nodeFill(node, mergedOptions),
|
|
117
|
+
`stroke:${mergedOptions.nodeStrokeColor}`,
|
|
118
|
+
`color:${mergedOptions.nodeTextColor}`
|
|
119
|
+
].filter((part) => Boolean(part));
|
|
120
|
+
if (styleParts.length) lines.push(` style ${node.id} ${styleParts.join(",")}`);
|
|
121
|
+
}
|
|
122
|
+
const linkStyles = [];
|
|
123
|
+
let edgeIndex = 0;
|
|
124
|
+
for (const edge of edges) {
|
|
125
|
+
const arrow = edge.isLazy ? "-.->" : "-->";
|
|
126
|
+
const safeLabel = escapeMermaidLabel(edge.label);
|
|
127
|
+
lines.push(` ${edge.from.id} ${arrow}|${safeLabel}| ${edge.to.id}`);
|
|
128
|
+
linkStyles.push(` linkStyle ${edgeIndex} stroke:${edge.stroke},color:${edge.stroke}`);
|
|
129
|
+
edgeIndex += 1;
|
|
130
|
+
}
|
|
131
|
+
lines.push(...linkStyles);
|
|
132
|
+
return {
|
|
133
|
+
diagram: lines.join("\n"),
|
|
134
|
+
nodeCount: allNodes.length,
|
|
135
|
+
edgeCount: edges.length,
|
|
136
|
+
tokenCount: tokenNodes.size
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Produces a deterministic node identifier for a service, falling back to hashing the symbol key.
|
|
141
|
+
*/
|
|
142
|
+
function resolveNodeId(meta, index) {
|
|
143
|
+
return sanitizeMermaidId(meta.identifierKey ?? createSymbolKey(meta.filePath, meta.className), index);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Normalizes arbitrary strings into Mermaid-safe identifiers, hashing when a leading letter is missing.
|
|
147
|
+
*/
|
|
148
|
+
function sanitizeMermaidId(source, fallbackIndex) {
|
|
149
|
+
const condensed = source.replace(/[^A-Za-z0-9_]/g, "_");
|
|
150
|
+
if (condensed && /^[A-Za-z]/.test(condensed)) return condensed;
|
|
151
|
+
return `n_${hashString(`${fallbackIndex}:${source}`)}`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Escapes problematic characters in labels so Mermaid renders them literally.
|
|
155
|
+
*/
|
|
156
|
+
function escapeMermaidLabel(label) {
|
|
157
|
+
return label.replace(/"/g, "\\\"").replace(/\|/g, "/");
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Builds a human-readable label describing the nature of an edge between two nodes.
|
|
161
|
+
*/
|
|
162
|
+
function describeEdge(from, to, depIsLazy) {
|
|
163
|
+
return `${depIsLazy ? "Lazy" : "Eager"} · ${from.scope ?? "unknown"}→${to.type === "token" ? "token" : to.scope ?? "unknown"} · ${to.type === "token" ? "Token" : to.hasFactory ? "Factory" : "Class"}`;
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Determines the fill color for a node based on its type, scope, and lazy/factory flags.
|
|
167
|
+
*/
|
|
168
|
+
function nodeFill(node, opts) {
|
|
169
|
+
if (node.type === "token") return `fill:${opts.tokenNodeFill}`;
|
|
170
|
+
if (node.hasFactory) return `fill:${opts.factoryNodeFill}`;
|
|
171
|
+
if (node.isLazyOnly) return `fill:${opts.lazyNodeFill}`;
|
|
172
|
+
const scopeFill = node.scope ? opts.scopeColors[node.scope] : void 0;
|
|
173
|
+
if (scopeFill) return `fill:${scopeFill}`;
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Collects unique identifiers for a dependency, falling back to expression parsing if metadata is absent.
|
|
177
|
+
*/
|
|
178
|
+
function gatherIdentifiers(dep) {
|
|
179
|
+
const identifiers = /* @__PURE__ */ new Set();
|
|
180
|
+
const ignored = new Set(dep.ignoredIdentifiers ?? []);
|
|
181
|
+
for (const ident of dep.referencedIdentifiers ?? []) {
|
|
182
|
+
const trimmed = ident.trim();
|
|
183
|
+
if (!trimmed || RESERVED_IDENTIFIERS.has(trimmed) || ignored.has(trimmed)) continue;
|
|
184
|
+
identifiers.add(trimmed);
|
|
185
|
+
}
|
|
186
|
+
if (!identifiers.size) for (const inferred of inferIdentifiersFromExpression(dep.expression)) {
|
|
187
|
+
const trimmed = inferred.trim();
|
|
188
|
+
if (!trimmed || RESERVED_IDENTIFIERS.has(trimmed) || ignored.has(trimmed)) continue;
|
|
189
|
+
identifiers.add(trimmed);
|
|
190
|
+
}
|
|
191
|
+
return Array.from(identifiers);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Heuristically extracts likely identifier names from common Lazy import expressions.
|
|
195
|
+
*/
|
|
196
|
+
function inferIdentifiersFromExpression(expression) {
|
|
197
|
+
const matches = /* @__PURE__ */ new Set();
|
|
198
|
+
const thenPattern = /\.then\(\s*(?:\w+)\s*=>\s*\w+\.([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
199
|
+
let match;
|
|
200
|
+
while ((match = thenPattern.exec(expression)) !== null) matches.add(match[1]);
|
|
201
|
+
if (!matches.size) {
|
|
202
|
+
const simple = expression.match(/([A-Za-z_][A-Za-z0-9_]*)$/);
|
|
203
|
+
if (simple) matches.add(simple[1]);
|
|
204
|
+
}
|
|
205
|
+
return Array.from(matches);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Resolves a dependency identifier to known service nodes, or creates a token node when unresolved.
|
|
209
|
+
*/
|
|
210
|
+
function resolveTargetsForIdentifier(identifier, fallbackExpression, meta, nodesByClassName, nodesByFilePath, tokenNodes) {
|
|
211
|
+
const serviceMatches = resolveServiceTargets(identifier, meta, nodesByClassName, nodesByFilePath);
|
|
212
|
+
if (serviceMatches.length) {
|
|
213
|
+
const deduped = /* @__PURE__ */ new Map();
|
|
214
|
+
for (const node of serviceMatches) deduped.set(node.id, node);
|
|
215
|
+
return Array.from(deduped.values());
|
|
216
|
+
}
|
|
217
|
+
return [ensureTokenNode(tokenNodes, createTokenLabel(identifier || fallbackExpression))];
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Attempts to find service nodes that match an identifier via import metadata or class names.
|
|
221
|
+
*/
|
|
222
|
+
function resolveServiceTargets(identifier, meta, nodesByClassName, nodesByFilePath) {
|
|
223
|
+
const matches = [];
|
|
224
|
+
const importRef = meta.referencedImports?.find((ref) => !ref.isTypeOnly && ref.name === identifier);
|
|
225
|
+
if (importRef) {
|
|
226
|
+
const normalizedPath = resolveImportSpecifierPath(meta.filePath, importRef.path);
|
|
227
|
+
if (normalizedPath) {
|
|
228
|
+
const byPath = nodesByFilePath.get(normalizedPath);
|
|
229
|
+
if (byPath && byPath.length) {
|
|
230
|
+
if (importRef.originalName && importRef.originalName !== "*" && importRef.originalName !== "default") {
|
|
231
|
+
for (const node of byPath) if (node.className === importRef.originalName) matches.push(node);
|
|
232
|
+
}
|
|
233
|
+
if (!matches.length) matches.push(...byPath);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const fallbackName = importRef.originalName && importRef.originalName !== "*" && importRef.originalName !== "default" ? importRef.originalName : identifier;
|
|
237
|
+
const byName = nodesByClassName.get(fallbackName);
|
|
238
|
+
if (byName) matches.push(...byName);
|
|
239
|
+
} else {
|
|
240
|
+
const byName = nodesByClassName.get(identifier);
|
|
241
|
+
if (byName) matches.push(...byName);
|
|
242
|
+
}
|
|
243
|
+
return matches;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Normalizes an import specifier relative to the source file, supporting relative, absolute, and bare paths.
|
|
247
|
+
*/
|
|
248
|
+
function resolveImportSpecifierPath(sourceFilePath, specifier) {
|
|
249
|
+
if (!specifier) return;
|
|
250
|
+
if (specifier.startsWith(".")) return normalizeImportPath(path.resolve(path.dirname(sourceFilePath), specifier));
|
|
251
|
+
if (specifier.startsWith("/")) return normalizeImportPath(specifier);
|
|
252
|
+
return normalizeImportPath(specifier);
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Retrieves an existing token node or creates a new one with a sanitized identifier.
|
|
256
|
+
*/
|
|
257
|
+
function ensureTokenNode(tokenNodes, label) {
|
|
258
|
+
const existing = tokenNodes.get(label);
|
|
259
|
+
if (existing) return existing;
|
|
260
|
+
const node = {
|
|
261
|
+
id: sanitizeMermaidId(`token:${label}`, tokenNodes.size),
|
|
262
|
+
label,
|
|
263
|
+
key: label,
|
|
264
|
+
scope: void 0,
|
|
265
|
+
type: "token",
|
|
266
|
+
isLazyOnly: false,
|
|
267
|
+
hasFactory: false
|
|
268
|
+
};
|
|
269
|
+
tokenNodes.set(label, node);
|
|
270
|
+
return node;
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Condenses arbitrary strings into stable token labels, truncating overly long values.
|
|
274
|
+
*/
|
|
275
|
+
function createTokenLabel(raw) {
|
|
276
|
+
const condensed = raw.replace(/\s+/g, " ").trim();
|
|
277
|
+
if (!condensed) return "anonymous-token";
|
|
278
|
+
return condensed.length > 48 ? `${condensed.slice(0, 45)}…` : condensed;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Chooses the appropriate edge color based on laziness and whether the target is produced by a factory.
|
|
282
|
+
*/
|
|
283
|
+
function selectEdgeColor(isLazy, target, opts) {
|
|
284
|
+
if (target.hasFactory) return opts.factoryEdgeColor;
|
|
285
|
+
return isLazy ? opts.lazyEdgeColor : opts.eagerEdgeColor;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
//#endregion
|
|
289
|
+
export { generateMermaidDiagram };
|
|
@@ -0,0 +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/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/transform-guards.test.ts","../src/plugins/vite-plugin/visualization-option.test.ts","../src/plugins/vite-plugin/visualizer.test.ts","../src/plugins/vite-plugin/visualizer.ts"],"version":"5.9.3"}
|
package/package.json
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "alloy-di",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "A compile-time dependency injection plugin for Vite",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
7
7
|
"type": "module",
|
|
8
|
-
"scripts": {
|
|
9
|
-
"build": "rimraf dist && rolldown -c",
|
|
10
|
-
"dev": "rolldown -c rolldown.config.ts --watch",
|
|
11
|
-
"format": "prettier -u --write ./",
|
|
12
|
-
"format:check": "prettier -u --check ./",
|
|
13
|
-
"lint": "oxlint --fix --type-aware --config ./.oxlintrc.json .",
|
|
14
|
-
"lint:check": "oxlint --type-aware --config ./.oxlintrc.json .",
|
|
15
|
-
"test": "vitest --run",
|
|
16
|
-
"test:cover": "vitest --run --coverage",
|
|
17
|
-
"test:watch": "vitest",
|
|
18
|
-
"typecheck": "tsc -b . --noEmit"
|
|
19
|
-
},
|
|
20
8
|
"exports": {
|
|
21
9
|
"./runtime": {
|
|
22
10
|
"types": "./dist/runtime.d.ts",
|
|
@@ -49,6 +37,10 @@
|
|
|
49
37
|
"url": "https://github.com/ciddan/alloy-di/tree/main/packages/alloy"
|
|
50
38
|
},
|
|
51
39
|
"homepage": "https://alloy-di.dev",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public",
|
|
42
|
+
"provenance": true
|
|
43
|
+
},
|
|
52
44
|
"files": [
|
|
53
45
|
"dist",
|
|
54
46
|
"README.md"
|
|
@@ -70,5 +62,17 @@
|
|
|
70
62
|
"vitest": {
|
|
71
63
|
"optional": true
|
|
72
64
|
}
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "rimraf dist && rolldown -c",
|
|
68
|
+
"dev": "rolldown -c rolldown.config.ts --watch",
|
|
69
|
+
"format": "prettier -u --write ./",
|
|
70
|
+
"format:check": "prettier -u --check ./",
|
|
71
|
+
"lint": "oxlint --fix --type-aware --config ./.oxlintrc.json .",
|
|
72
|
+
"lint:check": "oxlint --type-aware --config ./.oxlintrc.json .",
|
|
73
|
+
"test": "vitest --run",
|
|
74
|
+
"test:cover": "vitest --run --coverage",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"typecheck": "tsc -b . --noEmit"
|
|
73
77
|
}
|
|
74
|
-
}
|
|
78
|
+
}
|