alloy-di 1.4.0 → 2.0.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/README.md +12 -7
- package/bin/alloy.js +9 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +168 -0
- package/dist/generate.d.ts +28 -0
- package/dist/generate.js +46 -0
- package/dist/lib/container.d.ts +78 -3
- package/dist/lib/container.js +130 -6
- package/dist/lib/lazy.d.ts +5 -1
- package/dist/lib/providers.d.ts +26 -2
- package/dist/lib/providers.js +23 -1
- package/dist/lib/scope.d.ts +21 -1
- package/dist/lib/types.d.ts +3 -1
- package/dist/plugins/consumer-plugin.d.ts +40 -0
- package/dist/plugins/consumer-plugin.js +101 -0
- package/dist/plugins/core/codegen.js +3 -3
- package/dist/plugins/core/container-loader.js +32 -17
- package/dist/plugins/core/discovery-runtime.js +19 -4
- package/dist/plugins/core/discovery-store.js +20 -6
- package/dist/plugins/core/generation-inputs.js +41 -0
- package/dist/plugins/core/scanner.js +71 -7
- package/dist/plugins/core/utils.js +1 -1
- package/dist/plugins/core/visualization-utils.js +1 -1
- package/dist/plugins/core/visualizer.js +40 -10
- package/dist/plugins/rollup-plugin/index.js +3 -5
- package/dist/plugins/vite-plugin/index.d.ts +2 -29
- package/dist/plugins/vite-plugin/index.js +19 -65
- package/dist/plugins/webpack-like-plugin.d.ts +32 -0
- package/dist/plugins/webpack-like-plugin.js +86 -0
- package/dist/rspack.d.ts +7 -0
- package/dist/rspack.js +10 -0
- package/dist/runtime.d.ts +6 -6
- package/dist/runtime.js +4 -4
- package/dist/scopes.d.ts +9 -1
- package/dist/scopes.js +29 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vite.d.ts +2 -1
- package/dist/webpack.d.ts +7 -0
- package/dist/webpack.js +10 -0
- package/package.json +28 -12
- package/dist/lib/testing/mocking.d.ts +0 -12
- package/dist/lib/testing/mocking.js +0 -107
- package/dist/lib/testing/registry.js +0 -17
- package/dist/test.d.ts +0 -50
- package/dist/test.js +0 -65
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createClassKey, createSymbolKey } from "./utils.js";
|
|
2
|
+
import { ServiceScope } from "../../lib/scope.js";
|
|
2
3
|
import { extractServiceMetadata } from "./decorators.js";
|
|
3
4
|
import { processLazyCall, resolveModuleSpecifierCandidates } from "./lazy.js";
|
|
4
5
|
import fs from "node:fs";
|
|
@@ -41,17 +42,23 @@ function collectFileImports(sourceFile) {
|
|
|
41
42
|
* discovery results: decorators require an `@` and lazy references require
|
|
42
43
|
* the `Lazy` identifier.
|
|
43
44
|
*/
|
|
44
|
-
function
|
|
45
|
-
return
|
|
45
|
+
function shouldScanFactoryProviders(options) {
|
|
46
|
+
return options?.factoryProviders ?? true;
|
|
46
47
|
}
|
|
47
|
-
function
|
|
48
|
-
|
|
48
|
+
function mayContainDiscoverableSyntax(code, options) {
|
|
49
|
+
return code.includes("@") || code.includes("Lazy") || shouldScanFactoryProviders(options) && code.includes("asFactory");
|
|
50
|
+
}
|
|
51
|
+
function scanSource(code, id, options) {
|
|
52
|
+
const scanFactoryProviders = shouldScanFactoryProviders(options);
|
|
53
|
+
if (!mayContainDiscoverableSyntax(code, options)) return {
|
|
49
54
|
metas: [],
|
|
50
|
-
lazyClassKeys: /* @__PURE__ */ new Set()
|
|
55
|
+
lazyClassKeys: /* @__PURE__ */ new Set(),
|
|
56
|
+
factoryProviders: []
|
|
51
57
|
};
|
|
52
58
|
const sourceFile = ts.createSourceFile(id, code, ts.ScriptTarget.ESNext, true);
|
|
53
59
|
const discovered = /* @__PURE__ */ new Map();
|
|
54
60
|
const lazyRefs = /* @__PURE__ */ new Set();
|
|
61
|
+
const factoryProviders = [];
|
|
55
62
|
const fileImports = collectFileImports(sourceFile);
|
|
56
63
|
const decoratorResolutionCache = /* @__PURE__ */ new Map();
|
|
57
64
|
const visit = (node) => {
|
|
@@ -62,15 +69,72 @@ function scanSource(code, id) {
|
|
|
62
69
|
discovered,
|
|
63
70
|
decoratorResolutionCache
|
|
64
71
|
});
|
|
65
|
-
else if (ts.isCallExpression(node))
|
|
72
|
+
else if (ts.isCallExpression(node)) {
|
|
73
|
+
processLazyCall(node, id, sourceFile, lazyRefs);
|
|
74
|
+
if (scanFactoryProviders) handleFactoryProviderCall(node, {
|
|
75
|
+
id,
|
|
76
|
+
sourceFile,
|
|
77
|
+
fileImports,
|
|
78
|
+
factoryProviders
|
|
79
|
+
});
|
|
80
|
+
}
|
|
66
81
|
ts.forEachChild(node, visit);
|
|
67
82
|
};
|
|
68
83
|
ts.forEachChild(sourceFile, visit);
|
|
69
84
|
return {
|
|
70
85
|
metas: Array.from(discovered.values()),
|
|
71
|
-
lazyClassKeys: lazyRefs
|
|
86
|
+
lazyClassKeys: lazyRefs,
|
|
87
|
+
factoryProviders
|
|
72
88
|
};
|
|
73
89
|
}
|
|
90
|
+
function handleFactoryProviderCall(node, context) {
|
|
91
|
+
if (!isAlloyRuntimeHelper(node.expression, "asFactory", context.fileImports)) return;
|
|
92
|
+
const tokenArg = node.arguments[0];
|
|
93
|
+
if (!tokenArg) return;
|
|
94
|
+
context.factoryProviders.push({
|
|
95
|
+
filePath: context.id,
|
|
96
|
+
tokenExpression: tokenArg.getText(context.sourceFile),
|
|
97
|
+
tokenLabel: createFactoryTokenLabel(tokenArg, context.sourceFile),
|
|
98
|
+
lifecycle: extractFactoryLifecycle(node.arguments[2])
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function isAlloyRuntimeHelper(expression, helperName, fileImports) {
|
|
102
|
+
if (ts.isIdentifier(expression)) {
|
|
103
|
+
const importInfo = fileImports.get(expression.text);
|
|
104
|
+
return Boolean(importInfo && !importInfo.isTypeOnly && importInfo.path === ALLOY_RUNTIME_MODULE && (importInfo.originalName ?? expression.text) === helperName);
|
|
105
|
+
}
|
|
106
|
+
if (ts.isPropertyAccessExpression(expression) && ts.isIdentifier(expression.expression)) {
|
|
107
|
+
const importInfo = fileImports.get(expression.expression.text);
|
|
108
|
+
return Boolean(importInfo && !importInfo.isTypeOnly && importInfo.path === ALLOY_RUNTIME_MODULE && importInfo.originalName === "*" && expression.name.text === helperName);
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
function createFactoryTokenLabel(tokenArg, sourceFile) {
|
|
113
|
+
if (ts.isIdentifier(tokenArg)) return tokenArg.text;
|
|
114
|
+
return tokenArg.getText(sourceFile);
|
|
115
|
+
}
|
|
116
|
+
function extractFactoryLifecycle(optionsArg) {
|
|
117
|
+
if (!optionsArg || !ts.isObjectLiteralExpression(optionsArg)) return ServiceScope.SINGLETON;
|
|
118
|
+
for (const prop of optionsArg.properties) {
|
|
119
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
|
|
120
|
+
if (prop.name.text !== "lifecycle") continue;
|
|
121
|
+
return lifecycleExpressionToScope(prop.initializer);
|
|
122
|
+
}
|
|
123
|
+
return ServiceScope.SINGLETON;
|
|
124
|
+
}
|
|
125
|
+
function lifecycleExpressionToScope(expression) {
|
|
126
|
+
if (ts.isStringLiteralLike(expression)) return expression.text;
|
|
127
|
+
if (ts.isCallExpression(expression)) {
|
|
128
|
+
const callTarget = expression.expression;
|
|
129
|
+
if (ts.isPropertyAccessExpression(callTarget) && (callTarget.name.text === "singleton" || callTarget.name.text === "transient")) return callTarget.name.text;
|
|
130
|
+
}
|
|
131
|
+
if (ts.isPropertyAccessExpression(expression)) {
|
|
132
|
+
const member = expression.name.text;
|
|
133
|
+
if (member === "SINGLETON") return ServiceScope.SINGLETON;
|
|
134
|
+
if (member === "TRANSIENT") return ServiceScope.TRANSIENT;
|
|
135
|
+
}
|
|
136
|
+
return ServiceScope.SINGLETON;
|
|
137
|
+
}
|
|
74
138
|
function handleClassDeclaration(node, context) {
|
|
75
139
|
if (!node.name) return;
|
|
76
140
|
const decoratorMatch = findServiceDecorator(node, context.sourceFile, context.fileImports, context.id, context.decoratorResolutionCache);
|
|
@@ -49,7 +49,7 @@ const RESERVED_IDENTIFIERS = new Set([
|
|
|
49
49
|
* @param input - Discovered metadata, optional lazy keys, and rendering options.
|
|
50
50
|
* @returns The rendered diagram plus simple counts useful for reporting.
|
|
51
51
|
*/
|
|
52
|
-
function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
52
|
+
function generateMermaidDiagram({ metas, lazyClassKeys, factoryProviders, options, scopes }) {
|
|
53
53
|
const mergedOptions = {
|
|
54
54
|
...DEFAULT_OPTIONS,
|
|
55
55
|
...options,
|
|
@@ -69,6 +69,7 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
69
69
|
const nodesByClassName = /* @__PURE__ */ new Map();
|
|
70
70
|
const nodesByFilePath = /* @__PURE__ */ new Map();
|
|
71
71
|
const tokenNodes = /* @__PURE__ */ new Map();
|
|
72
|
+
const factoryNodes = /* @__PURE__ */ new Map();
|
|
72
73
|
const nodeByMeta = /* @__PURE__ */ new Map();
|
|
73
74
|
metas.forEach((meta, index) => {
|
|
74
75
|
const key = createClassKey(meta.filePath, meta.className);
|
|
@@ -94,6 +95,7 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
94
95
|
pathBucket.push(node);
|
|
95
96
|
nodesByFilePath.set(normalizedPath, pathBucket);
|
|
96
97
|
});
|
|
98
|
+
for (const provider of factoryProviders ?? []) ensureFactoryNode(factoryNodes, provider);
|
|
97
99
|
const edges = [];
|
|
98
100
|
const edgeKeys = /* @__PURE__ */ new Set();
|
|
99
101
|
metas.forEach((meta) => {
|
|
@@ -105,7 +107,7 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
105
107
|
if (!identifiers.length) continue;
|
|
106
108
|
const targets = /* @__PURE__ */ new Map();
|
|
107
109
|
for (const ident of identifiers) {
|
|
108
|
-
const resolvedTargets = resolveTargetsForIdentifier(ident, dep.expression, meta, nodesByClassName, nodesByFilePath, tokenNodes);
|
|
110
|
+
const resolvedTargets = resolveTargetsForIdentifier(ident, dep.expression, meta, nodesByClassName, nodesByFilePath, tokenNodes, factoryNodes);
|
|
109
111
|
for (const target of resolvedTargets) {
|
|
110
112
|
if (target.id === sourceNode.id) continue;
|
|
111
113
|
targets.set(target.id, target);
|
|
@@ -140,10 +142,14 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
140
142
|
lines.push(` %% Scope stability: ⚠️ ${VIOLATION_EDGE_COLOR} edge = captive dependency (longer-lived service depends on shorter-lived scope)`);
|
|
141
143
|
}
|
|
142
144
|
}
|
|
143
|
-
const allNodes = [
|
|
145
|
+
const allNodes = [
|
|
146
|
+
...serviceNodes,
|
|
147
|
+
...Array.from(factoryNodes.values()),
|
|
148
|
+
...Array.from(tokenNodes.values())
|
|
149
|
+
];
|
|
144
150
|
const groupedNodes = /* @__PURE__ */ new Map();
|
|
145
151
|
const ungroupedNodes = [];
|
|
146
|
-
for (const node of allNodes) if (node.type === "service" && node.scope && customScopes.has(node.scope)) {
|
|
152
|
+
for (const node of allNodes) if ((node.type === "service" || node.type === "factory") && node.scope && customScopes.has(node.scope)) {
|
|
147
153
|
const bucket = groupedNodes.get(node.scope) ?? [];
|
|
148
154
|
bucket.push(node);
|
|
149
155
|
groupedNodes.set(node.scope, bucket);
|
|
@@ -179,7 +185,8 @@ function generateMermaidDiagram({ metas, lazyClassKeys, options, scopes }) {
|
|
|
179
185
|
diagram: lines.join("\n"),
|
|
180
186
|
nodeCount: allNodes.length,
|
|
181
187
|
edgeCount: edges.length,
|
|
182
|
-
tokenCount: tokenNodes.size
|
|
188
|
+
tokenCount: tokenNodes.size,
|
|
189
|
+
factoryCount: factoryNodes.size
|
|
183
190
|
};
|
|
184
191
|
}
|
|
185
192
|
/**
|
|
@@ -216,12 +223,14 @@ function scopeCode(node) {
|
|
|
216
223
|
}
|
|
217
224
|
/**
|
|
218
225
|
* True when an edge captures a shorter-lived dependency in a longer-lived host,
|
|
219
|
-
* per the declared hierarchy.
|
|
220
|
-
* are
|
|
226
|
+
* per the declared hierarchy. Service dependencies on scoped factory providers
|
|
227
|
+
* are checked too: factory bodies are opaque, but the factory result lifecycle
|
|
228
|
+
* is known and can still be captive.
|
|
221
229
|
*/
|
|
222
230
|
function isStabilityViolation(from, to, scopes) {
|
|
223
231
|
if (!scopes) return false;
|
|
224
|
-
if (from.type !== "service"
|
|
232
|
+
if (from.type !== "service") return false;
|
|
233
|
+
if (to.type !== "service" && to.type !== "factory") return false;
|
|
225
234
|
if (!from.scope || !to.scope) return false;
|
|
226
235
|
return !isDependencyAllowed(from.scope, to.scope, scopes);
|
|
227
236
|
}
|
|
@@ -238,6 +247,7 @@ function describeEdge(from, to) {
|
|
|
238
247
|
*/
|
|
239
248
|
function nodeFill(node, opts) {
|
|
240
249
|
if (node.type === "token") return `fill:${opts.tokenNodeFill}`;
|
|
250
|
+
if (node.type === "factory") return `fill:${opts.factoryNodeFill}`;
|
|
241
251
|
if (node.hasFactory) return `fill:${opts.factoryNodeFill}`;
|
|
242
252
|
if (node.isLazyOnly) return `fill:${opts.lazyNodeFill}`;
|
|
243
253
|
const scopeFill = node.scope ? opts.scopeColors[node.scope] : void 0;
|
|
@@ -278,14 +288,17 @@ function inferIdentifiersFromExpression(expression) {
|
|
|
278
288
|
/**
|
|
279
289
|
* Resolves a dependency identifier to known service nodes, or creates a token node when unresolved.
|
|
280
290
|
*/
|
|
281
|
-
function resolveTargetsForIdentifier(identifier, fallbackExpression, meta, nodesByClassName, nodesByFilePath, tokenNodes) {
|
|
291
|
+
function resolveTargetsForIdentifier(identifier, fallbackExpression, meta, nodesByClassName, nodesByFilePath, tokenNodes, factoryNodes) {
|
|
282
292
|
const serviceMatches = resolveServiceTargets(identifier, meta, nodesByClassName, nodesByFilePath);
|
|
283
293
|
if (serviceMatches.length) {
|
|
284
294
|
const deduped = /* @__PURE__ */ new Map();
|
|
285
295
|
for (const node of serviceMatches) deduped.set(node.id, node);
|
|
286
296
|
return Array.from(deduped.values());
|
|
287
297
|
}
|
|
288
|
-
|
|
298
|
+
const tokenLabel = createTokenLabel(identifier || fallbackExpression);
|
|
299
|
+
const factoryNode = factoryNodes.get(tokenLabel);
|
|
300
|
+
if (factoryNode) return [factoryNode];
|
|
301
|
+
return [ensureTokenNode(tokenNodes, tokenLabel)];
|
|
289
302
|
}
|
|
290
303
|
/**
|
|
291
304
|
* Attempts to find service nodes that match an identifier via import metadata or class names.
|
|
@@ -340,6 +353,23 @@ function ensureTokenNode(tokenNodes, label) {
|
|
|
340
353
|
tokenNodes.set(label, node);
|
|
341
354
|
return node;
|
|
342
355
|
}
|
|
356
|
+
function ensureFactoryNode(factoryNodes, provider) {
|
|
357
|
+
const label = createTokenLabel(provider.tokenLabel || provider.tokenExpression);
|
|
358
|
+
const existing = factoryNodes.get(label);
|
|
359
|
+
if (existing) return existing;
|
|
360
|
+
const node = {
|
|
361
|
+
id: sanitizeMermaidId(`factory:${provider.filePath}:${provider.tokenExpression}`, factoryNodes.size),
|
|
362
|
+
label: `Factory: ${label}`,
|
|
363
|
+
key: label,
|
|
364
|
+
scope: provider.lifecycle,
|
|
365
|
+
type: "factory",
|
|
366
|
+
isLazyOnly: false,
|
|
367
|
+
hasFactory: true,
|
|
368
|
+
filePath: normalizeImportPath(provider.filePath)
|
|
369
|
+
};
|
|
370
|
+
factoryNodes.set(label, node);
|
|
371
|
+
return node;
|
|
372
|
+
}
|
|
343
373
|
/**
|
|
344
374
|
* Condenses arbitrary strings into stable token labels, truncating overly long values.
|
|
345
375
|
*/
|
|
@@ -5,8 +5,8 @@ import { determineBuildMode, hasPreserveModules, resolveImportPathForBuild } fro
|
|
|
5
5
|
import { parseExportedNames } from "./barrel-exports.js";
|
|
6
6
|
import { createManifestDependency } from "./manifest-deps.js";
|
|
7
7
|
import { checkPackageExports } from "./package-exports.js";
|
|
8
|
-
import path from "node:path";
|
|
9
8
|
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
10
|
//#region src/plugins/rollup-plugin/index.ts
|
|
11
11
|
/**
|
|
12
12
|
* Rollup/Rolldown plugin that scans decorated Alloy services and emits an ESM manifest.
|
|
@@ -47,10 +47,8 @@ function alloy(options = {}) {
|
|
|
47
47
|
},
|
|
48
48
|
generateBundle(outputOptions) {
|
|
49
49
|
const buildMode = getBuildMode(outputOptions);
|
|
50
|
-
if (options.scopes && Object.keys(options.scopes).length > 0)
|
|
51
|
-
|
|
52
|
-
validateScopeStability([...discovery.fileMetas.values()].flat(), options.scopes);
|
|
53
|
-
}
|
|
50
|
+
if (options.scopes && Object.keys(options.scopes).length > 0) validateScopesConfig(options.scopes);
|
|
51
|
+
validateScopeStability([...discovery.fileMetas.values()].flat(), options.scopes);
|
|
54
52
|
const services = [];
|
|
55
53
|
const missingExports = [];
|
|
56
54
|
const exportedNames = buildMode === "preserve-modules" ? /* @__PURE__ */ new Set() : parseExportedNames(discovery.fileSources);
|
|
@@ -1,39 +1,12 @@
|
|
|
1
|
-
import { AlloyManifest } from "../core/types.js";
|
|
2
|
-
import { AlloyScopesConfig } from "../core/scopes-validation.js";
|
|
3
|
-
import { ServiceIdentifier } from "../../lib/service-identifiers.js";
|
|
4
1
|
import { AlloyMermaidVisualizerOptions, AlloyVisualizationOptions } from "../core/visualization-utils.js";
|
|
2
|
+
import { AlloyPluginOptions } from "../consumer-plugin.js";
|
|
5
3
|
import { Plugin } from "vite";
|
|
6
4
|
|
|
7
5
|
//#region src/plugins/vite-plugin/index.d.ts
|
|
8
|
-
interface AlloyPluginOptions {
|
|
9
|
-
providers?: string[];
|
|
10
|
-
/** Optional list of manifest objects to ingest */
|
|
11
|
-
manifests?: AlloyManifest[];
|
|
12
|
-
/** List of ServiceIdentifiers to mark as instantiation-lazy (adds factory Lazy wrapper) */
|
|
13
|
-
lazyServices?: ServiceIdentifier[];
|
|
14
|
-
/**
|
|
15
|
-
* Output directory for the generated `virtual-container.d.ts` file.
|
|
16
|
-
* Relative paths are resolved against the project root.
|
|
17
|
-
* Defaults to "./src".
|
|
18
|
-
*/
|
|
19
|
-
containerDeclarationDir?: string;
|
|
20
|
-
/**
|
|
21
|
-
* Emit dependency graph artifacts. When `true`, writes a Mermaid diagram to
|
|
22
|
-
* `${projectRoot}/alloy-di.mmd`. Provide an object to customize output.
|
|
23
|
-
*/
|
|
24
|
-
visualize?: boolean | AlloyVisualizationOptions;
|
|
25
|
-
/**
|
|
26
|
-
* Declares custom, application-defined scopes and their parent ordering, e.g.
|
|
27
|
-
* `{ session: { parent: 'singleton' }, request: { parent: 'session' } }`.
|
|
28
|
-
* Drives type-safe scope names (emitted into the generated declaration),
|
|
29
|
-
* runtime hierarchy registration, and build-time scope-stability validation.
|
|
30
|
-
*/
|
|
31
|
-
scopes?: AlloyScopesConfig;
|
|
32
|
-
}
|
|
33
6
|
/**
|
|
34
7
|
* Creates the Alloy Vite plugin that statically discovers injectable classes
|
|
35
8
|
* and exposes them through a virtual container module at build time.
|
|
36
9
|
*/
|
|
37
10
|
declare function alloy(options?: AlloyPluginOptions): Plugin;
|
|
38
11
|
//#endregion
|
|
39
|
-
export {
|
|
12
|
+
export { alloy };
|
|
@@ -1,56 +1,28 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { resolveVisualizationOptions } from "../core/visualization-utils.js";
|
|
3
|
-
import { loadVirtualContainerModule } from "../core/container-loader.js";
|
|
4
|
-
import { createDiscoveryRuntime, isDiscoverableFile } from "../core/discovery-runtime.js";
|
|
1
|
+
import { ALLOY_PLUGIN_OPTIONS, createConsumerPluginContext, isAlloyDiscoverableFile } from "../consumer-plugin.js";
|
|
5
2
|
import { invalidateContainerModule } from "./module-invalidation.js";
|
|
6
|
-
import path from "node:path";
|
|
7
|
-
import fs from "node:fs";
|
|
8
3
|
//#region src/plugins/vite-plugin/index.ts
|
|
9
|
-
|
|
10
|
-
const description = identifier.description;
|
|
11
|
-
if (!description || !description.startsWith("alloy:")) throw new Error("[alloy] lazyServices entries must be serviceIdentifiers exported by Alloy manifests.");
|
|
12
|
-
return description;
|
|
13
|
-
}
|
|
4
|
+
const ALLOY_VITE_PLUGIN_OPTIONS = Symbol.for("alloy-di.vite-plugin-options");
|
|
14
5
|
/**
|
|
15
6
|
* Creates the Alloy Vite plugin that statically discovers injectable classes
|
|
16
7
|
* and exposes them through a virtual container module at build time.
|
|
17
8
|
*/
|
|
18
9
|
function alloy(options = {}) {
|
|
19
|
-
const
|
|
20
|
-
const resolvedVirtualModuleId = "\0virtual:alloy-container";
|
|
21
|
-
const configuredProviderEntries = Array.from(options.providers ?? []);
|
|
22
|
-
const providerModuleRefs = [];
|
|
23
|
-
let resolvedRoot = process.cwd();
|
|
24
|
-
let packageName = "UNKNOWN_PACKAGE";
|
|
25
|
-
let resolvedVisualization = null;
|
|
26
|
-
let isDevMode;
|
|
27
|
-
const lazyServiceKeys = new Set((options.lazyServices ?? []).map(toLazyServiceKey));
|
|
28
|
-
const discoveryRuntime = createDiscoveryRuntime();
|
|
10
|
+
const context = createConsumerPluginContext(options);
|
|
29
11
|
return {
|
|
30
12
|
name: "vite-plugin-alloy",
|
|
31
13
|
enforce: "pre",
|
|
14
|
+
[ALLOY_VITE_PLUGIN_OPTIONS]: options,
|
|
15
|
+
[ALLOY_PLUGIN_OPTIONS]: options,
|
|
32
16
|
configResolved(config) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
38
|
-
if (typeof pkg.name === "string") packageName = pkg.name;
|
|
39
|
-
} catch {}
|
|
40
|
-
providerModuleRefs.length = 0;
|
|
41
|
-
for (const entry of configuredProviderEntries) {
|
|
42
|
-
const absPath = path.isAbsolute(entry) ? entry : path.resolve(resolvedRoot, entry);
|
|
43
|
-
providerModuleRefs.push({
|
|
44
|
-
absPath,
|
|
45
|
-
importPath: normalizeImportPath(absPath)
|
|
46
|
-
});
|
|
47
|
-
}
|
|
48
|
-
resolvedVisualization = resolveVisualizationOptions(options.visualize, resolvedRoot);
|
|
17
|
+
context.configure({
|
|
18
|
+
root: config.root ?? process.cwd(),
|
|
19
|
+
isDevMode: !config.isProduction
|
|
20
|
+
});
|
|
49
21
|
},
|
|
50
22
|
resolveId: {
|
|
51
23
|
filter: { id: { include: [/^virtual:alloy-container$/] } },
|
|
52
24
|
handler(id) {
|
|
53
|
-
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
|
25
|
+
if (id === context.virtualModuleId) return context.resolvedVirtualModuleId;
|
|
54
26
|
}
|
|
55
27
|
},
|
|
56
28
|
transform: {
|
|
@@ -59,16 +31,16 @@ function alloy(options = {}) {
|
|
|
59
31
|
exclude: [/\.d\.ts$/i, /node_modules/]
|
|
60
32
|
} },
|
|
61
33
|
handler(code, id) {
|
|
62
|
-
|
|
34
|
+
context.processTransform(code, id);
|
|
63
35
|
return null;
|
|
64
36
|
}
|
|
65
37
|
},
|
|
66
38
|
async hotUpdate(ctx) {
|
|
67
39
|
if (this.environment.name !== "client") return;
|
|
68
40
|
const { file } = ctx;
|
|
69
|
-
if (!
|
|
41
|
+
if (!isAlloyDiscoverableFile(file)) return;
|
|
70
42
|
let discoveryChanged;
|
|
71
|
-
if (ctx.type === "delete") discoveryChanged =
|
|
43
|
+
if (ctx.type === "delete") discoveryChanged = context.removeFile(file);
|
|
72
44
|
else {
|
|
73
45
|
let code;
|
|
74
46
|
try {
|
|
@@ -76,42 +48,24 @@ function alloy(options = {}) {
|
|
|
76
48
|
} catch {
|
|
77
49
|
return;
|
|
78
50
|
}
|
|
79
|
-
discoveryChanged =
|
|
51
|
+
discoveryChanged = context.processFileUpdate(file, code);
|
|
80
52
|
}
|
|
81
53
|
if (!discoveryChanged) return;
|
|
82
|
-
invalidateContainerModule(ctx.server, resolvedVirtualModuleId);
|
|
54
|
+
invalidateContainerModule(ctx.server, context.resolvedVirtualModuleId);
|
|
83
55
|
this.environment.hot.send({ type: "full-reload" });
|
|
84
56
|
return [];
|
|
85
57
|
},
|
|
86
58
|
buildStart() {
|
|
87
|
-
|
|
88
|
-
for (const ref of providerModuleRefs) this.addWatchFile(ref.absPath);
|
|
89
|
-
const files = walkSync(path.join(resolvedRoot, "src"));
|
|
90
|
-
for (const file of files) if (/\.(tsx?|ts)$/i.test(file) && !file.endsWith(".d.ts")) try {
|
|
91
|
-
const code = fs.readFileSync(file, "utf-8");
|
|
92
|
-
discoveryRuntime.processUpdate(file, code);
|
|
93
|
-
} catch {}
|
|
59
|
+
context.buildStart((file) => this.addWatchFile(file));
|
|
94
60
|
},
|
|
95
61
|
load: {
|
|
96
62
|
filter: { id: { include: [/^\0virtual:alloy-container$/] } },
|
|
97
63
|
async handler(id) {
|
|
98
|
-
if (id !== resolvedVirtualModuleId) return;
|
|
99
|
-
return
|
|
100
|
-
localMetas: Array.from(discoveryRuntime.discoveredClasses.values()),
|
|
101
|
-
lazyReferencedClassKeys: discoveryRuntime.lazyReferencedClassKeys,
|
|
102
|
-
manifests: options.manifests ?? [],
|
|
103
|
-
providerImportPaths: providerModuleRefs.map((ref) => ref.importPath),
|
|
104
|
-
lazyServiceKeys,
|
|
105
|
-
packageName,
|
|
106
|
-
resolvedRoot,
|
|
107
|
-
containerDeclarationDir: options.containerDeclarationDir,
|
|
108
|
-
resolvedVisualization,
|
|
109
|
-
isDevMode,
|
|
110
|
-
scopes: options.scopes
|
|
111
|
-
});
|
|
64
|
+
if (id !== context.resolvedVirtualModuleId) return;
|
|
65
|
+
return context.loadContainer();
|
|
112
66
|
}
|
|
113
67
|
}
|
|
114
68
|
};
|
|
115
69
|
}
|
|
116
70
|
//#endregion
|
|
117
|
-
export { alloy };
|
|
71
|
+
export { ALLOY_VITE_PLUGIN_OPTIONS, alloy };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ALLOY_PLUGIN_OPTIONS, AlloyPluginOptions } from "./consumer-plugin.js";
|
|
2
|
+
|
|
3
|
+
//#region src/plugins/webpack-like-plugin.d.ts
|
|
4
|
+
interface TapHook {
|
|
5
|
+
tap?(name: string, handler: (...args: unknown[]) => unknown): void;
|
|
6
|
+
tapPromise?(name: string, handler: (...args: unknown[]) => Promise<unknown>): void;
|
|
7
|
+
}
|
|
8
|
+
interface WebpackLikeCompiler {
|
|
9
|
+
context?: string;
|
|
10
|
+
options?: {
|
|
11
|
+
context?: string;
|
|
12
|
+
mode?: string;
|
|
13
|
+
resolve?: {
|
|
14
|
+
alias?: Record<string, string | false | string[]> | {
|
|
15
|
+
name?: string;
|
|
16
|
+
alias?: string | false | string[];
|
|
17
|
+
}[];
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
hooks?: {
|
|
21
|
+
beforeCompile?: TapHook;
|
|
22
|
+
thisCompilation?: TapHook;
|
|
23
|
+
normalModuleFactory?: TapHook;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
interface AlloyWebpackLikePlugin {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly [ALLOY_PLUGIN_OPTIONS]: AlloyPluginOptions;
|
|
29
|
+
apply(compiler: WebpackLikeCompiler): void;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { AlloyWebpackLikePlugin };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ALLOY_PLUGIN_OPTIONS, createConsumerPluginContext } from "./consumer-plugin.js";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/plugins/webpack-like-plugin.ts
|
|
4
|
+
function addDependency(deps, file) {
|
|
5
|
+
deps?.add(file);
|
|
6
|
+
}
|
|
7
|
+
function addContextDependency(deps, dir) {
|
|
8
|
+
deps?.add(dir);
|
|
9
|
+
}
|
|
10
|
+
function isObject(value) {
|
|
11
|
+
return Boolean(value) && typeof value === "object";
|
|
12
|
+
}
|
|
13
|
+
function isWebpackLikeNormalModuleFactory(value) {
|
|
14
|
+
return isObject(value) && isObject(value.hooks);
|
|
15
|
+
}
|
|
16
|
+
function isResolveRequest(value) {
|
|
17
|
+
return value === void 0 || isObject(value);
|
|
18
|
+
}
|
|
19
|
+
function isWebpackLikeCompilation(value) {
|
|
20
|
+
return isObject(value);
|
|
21
|
+
}
|
|
22
|
+
function createWebpackLikeAlloyPlugin(options, pluginOptions) {
|
|
23
|
+
const context = createConsumerPluginContext(options);
|
|
24
|
+
let cacheFilePath = "";
|
|
25
|
+
async function writeContainer() {
|
|
26
|
+
context.buildStart();
|
|
27
|
+
await context.writeContainerCache(cacheFilePath);
|
|
28
|
+
}
|
|
29
|
+
function configureAlias(compiler) {
|
|
30
|
+
compiler.options ??= {};
|
|
31
|
+
compiler.options.resolve ??= {};
|
|
32
|
+
const alias = compiler.options.resolve.alias;
|
|
33
|
+
if (Array.isArray(alias)) {
|
|
34
|
+
const existing = alias.find((entry) => entry.name === context.virtualModuleId);
|
|
35
|
+
if (existing) existing.alias = cacheFilePath;
|
|
36
|
+
else alias.push({
|
|
37
|
+
name: context.virtualModuleId,
|
|
38
|
+
alias: cacheFilePath
|
|
39
|
+
});
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
compiler.options.resolve.alias = {
|
|
43
|
+
...alias,
|
|
44
|
+
[context.virtualModuleId]: cacheFilePath
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function configureVirtualResolve(compiler) {
|
|
48
|
+
compiler.hooks?.normalModuleFactory?.tap?.(pluginOptions.name, (factory) => {
|
|
49
|
+
if (!isWebpackLikeNormalModuleFactory(factory)) return;
|
|
50
|
+
factory.hooks?.beforeResolve?.tap?.(pluginOptions.name, (request) => {
|
|
51
|
+
if (!isResolveRequest(request)) return;
|
|
52
|
+
const resolveRequest = request;
|
|
53
|
+
if (resolveRequest?.request === context.virtualModuleId) resolveRequest.request = cacheFilePath;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
function configureLifecycleHooks(compiler) {
|
|
58
|
+
compiler.hooks?.beforeCompile?.tapPromise?.(pluginOptions.name, async () => {
|
|
59
|
+
await writeContainer();
|
|
60
|
+
});
|
|
61
|
+
compiler.hooks?.thisCompilation?.tap?.(pluginOptions.name, (compilation) => {
|
|
62
|
+
if (!isWebpackLikeCompilation(compilation)) return;
|
|
63
|
+
addDependency(compilation.fileDependencies, cacheFilePath);
|
|
64
|
+
for (const watchFile of context.getWatchFiles()) addDependency(compilation.fileDependencies, watchFile);
|
|
65
|
+
for (const watchRoot of context.getWatchDirectories()) addContextDependency(compilation.contextDependencies, watchRoot);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
name: pluginOptions.name,
|
|
70
|
+
[ALLOY_PLUGIN_OPTIONS]: options,
|
|
71
|
+
apply(compiler) {
|
|
72
|
+
const root = compiler.options?.context ?? compiler.context ?? process.cwd();
|
|
73
|
+
const isDevMode = compiler.options?.mode ? compiler.options.mode !== "production" : void 0;
|
|
74
|
+
context.configure({
|
|
75
|
+
root,
|
|
76
|
+
isDevMode
|
|
77
|
+
});
|
|
78
|
+
cacheFilePath = path.resolve(root, "node_modules/.cache/alloy-di", pluginOptions.cacheFileName);
|
|
79
|
+
configureAlias(compiler);
|
|
80
|
+
configureVirtualResolve(compiler);
|
|
81
|
+
configureLifecycleHooks(compiler);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
export { createWebpackLikeAlloyPlugin };
|
package/dist/rspack.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { AlloyPluginOptions } from "./plugins/consumer-plugin.js";
|
|
2
|
+
import { AlloyWebpackLikePlugin } from "./plugins/webpack-like-plugin.js";
|
|
3
|
+
|
|
4
|
+
//#region src/rspack.d.ts
|
|
5
|
+
declare function alloy(options?: AlloyPluginOptions): AlloyWebpackLikePlugin;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { type AlloyPluginOptions, alloy, alloy as default };
|
package/dist/rspack.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { createWebpackLikeAlloyPlugin } from "./plugins/webpack-like-plugin.js";
|
|
2
|
+
//#region src/rspack.ts
|
|
3
|
+
function alloy(options = {}) {
|
|
4
|
+
return createWebpackLikeAlloyPlugin(options, {
|
|
5
|
+
name: "alloy-di-rspack",
|
|
6
|
+
cacheFileName: "rspack-virtual-container.js"
|
|
7
|
+
});
|
|
8
|
+
}
|
|
9
|
+
//#endregion
|
|
10
|
+
export { alloy, alloy as default };
|
package/dist/runtime.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Newable, Token, createToken } from "./lib/types.js";
|
|
2
|
-
import { AlloyScopes, ResolutionContext, ServiceScope } from "./lib/scope.js";
|
|
1
|
+
import { Constructor, Newable, Token, createToken, isConstructor, isToken } from "./lib/types.js";
|
|
3
2
|
import { ServiceIdentifier, clearServiceIdentifierRegistry, getConstructorByIdentifier, getServiceIdentifier, registerServiceIdentifier } from "./lib/service-identifiers.js";
|
|
4
|
-
import {
|
|
3
|
+
import { AlloyScopes, ResolutionContext, ServiceScope } from "./lib/scope.js";
|
|
4
|
+
import { Container, FactoryFn } from "./lib/container.js";
|
|
5
5
|
import { EnvDetectionOverrides, setEnvDetectionOverrides } from "./lib/env-detection.js";
|
|
6
|
-
import { LAZY_IDENTIFIER, Lazy } from "./lib/lazy.js";
|
|
6
|
+
import { LAZY_IDENTIFIER, Lazy, isLazy } from "./lib/lazy.js";
|
|
7
7
|
import { Injectable, Singleton, assertDeps, dependenciesRegistry, deps } from "./lib/decorators.js";
|
|
8
|
-
import { ProviderDefinitions, applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
-
export { type AlloyScopes, Container, type EnvDetectionOverrides, Injectable, LAZY_IDENTIFIER, Lazy, type Lazy as LazyInterface, type Newable, type ProviderDefinitions, type ResolutionContext, type ServiceIdentifier, ServiceScope, Singleton, type Token, applyProviders, asClass, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
|
8
|
+
import { FactoryProviderDescriptor, ProviderDefinitions, applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
+
export { type AlloyScopes, type Constructor, Container, type EnvDetectionOverrides, type FactoryFn, type FactoryProviderDescriptor, Injectable, LAZY_IDENTIFIER, Lazy, type Lazy as LazyInterface, type Newable, type ProviderDefinitions, type ResolutionContext, type ServiceIdentifier, ServiceScope, Singleton, type Token, applyProviders, asClass, asFactory, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, isConstructor, isLazy, isToken, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
package/dist/runtime.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ServiceScope } from "./lib/scope.js";
|
|
2
|
-
import { createToken } from "./lib/types.js";
|
|
3
|
-
import { LAZY_IDENTIFIER, Lazy } from "./lib/lazy.js";
|
|
2
|
+
import { createToken, isConstructor, isToken } from "./lib/types.js";
|
|
3
|
+
import { LAZY_IDENTIFIER, Lazy, isLazy } from "./lib/lazy.js";
|
|
4
4
|
import { Injectable, Singleton, assertDeps, dependenciesRegistry, deps } from "./lib/decorators.js";
|
|
5
5
|
import { clearServiceIdentifierRegistry, getConstructorByIdentifier, getServiceIdentifier, registerServiceIdentifier } from "./lib/service-identifiers.js";
|
|
6
6
|
import { setEnvDetectionOverrides } from "./lib/env-detection.js";
|
|
7
7
|
import { Container } from "./lib/container.js";
|
|
8
|
-
import { applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
-
export { Container, Injectable, LAZY_IDENTIFIER, Lazy, ServiceScope, Singleton, applyProviders, asClass, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|
|
8
|
+
import { applyProviders, asClass, asFactory, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
9
|
+
export { Container, Injectable, LAZY_IDENTIFIER, Lazy, ServiceScope, Singleton, applyProviders, asClass, asFactory, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, isConstructor, isLazy, isToken, lifecycle, registerServiceIdentifier, setEnvDetectionOverrides };
|