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.
Files changed (45) hide show
  1. package/README.md +12 -7
  2. package/bin/alloy.js +9 -0
  3. package/dist/cli.d.ts +24 -0
  4. package/dist/cli.js +168 -0
  5. package/dist/generate.d.ts +28 -0
  6. package/dist/generate.js +46 -0
  7. package/dist/lib/container.d.ts +78 -3
  8. package/dist/lib/container.js +130 -6
  9. package/dist/lib/lazy.d.ts +5 -1
  10. package/dist/lib/providers.d.ts +26 -2
  11. package/dist/lib/providers.js +23 -1
  12. package/dist/lib/scope.d.ts +21 -1
  13. package/dist/lib/types.d.ts +3 -1
  14. package/dist/plugins/consumer-plugin.d.ts +40 -0
  15. package/dist/plugins/consumer-plugin.js +101 -0
  16. package/dist/plugins/core/codegen.js +3 -3
  17. package/dist/plugins/core/container-loader.js +32 -17
  18. package/dist/plugins/core/discovery-runtime.js +19 -4
  19. package/dist/plugins/core/discovery-store.js +20 -6
  20. package/dist/plugins/core/generation-inputs.js +41 -0
  21. package/dist/plugins/core/scanner.js +71 -7
  22. package/dist/plugins/core/utils.js +1 -1
  23. package/dist/plugins/core/visualization-utils.js +1 -1
  24. package/dist/plugins/core/visualizer.js +40 -10
  25. package/dist/plugins/rollup-plugin/index.js +3 -5
  26. package/dist/plugins/vite-plugin/index.d.ts +2 -29
  27. package/dist/plugins/vite-plugin/index.js +19 -65
  28. package/dist/plugins/webpack-like-plugin.d.ts +32 -0
  29. package/dist/plugins/webpack-like-plugin.js +86 -0
  30. package/dist/rspack.d.ts +7 -0
  31. package/dist/rspack.js +10 -0
  32. package/dist/runtime.d.ts +6 -6
  33. package/dist/runtime.js +4 -4
  34. package/dist/scopes.d.ts +9 -1
  35. package/dist/scopes.js +29 -0
  36. package/dist/tsconfig.tsbuildinfo +1 -1
  37. package/dist/vite.d.ts +2 -1
  38. package/dist/webpack.d.ts +7 -0
  39. package/dist/webpack.js +10 -0
  40. package/package.json +28 -12
  41. package/dist/lib/testing/mocking.d.ts +0 -12
  42. package/dist/lib/testing/mocking.js +0 -107
  43. package/dist/lib/testing/registry.js +0 -17
  44. package/dist/test.d.ts +0 -50
  45. 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 mayContainDiscoverableSyntax(code) {
45
- return code.includes("@") || code.includes("Lazy");
45
+ function shouldScanFactoryProviders(options) {
46
+ return options?.factoryProviders ?? true;
46
47
  }
47
- function scanSource(code, id) {
48
- if (!mayContainDiscoverableSyntax(code)) return {
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)) processLazyCall(node, id, sourceFile, lazyRefs);
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);
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import fs from "node:fs";
2
+ import path from "node:path";
3
3
  //#region src/plugins/core/utils.ts
4
4
  const WINDOWS_DRIVE_PATTERN = /^[A-Za-z]:[\\/]/;
5
5
  function normalizeImportPath(p) {
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import fs from "node:fs";
2
+ import path from "node:path";
3
3
  //#region src/plugins/core/visualization-utils.ts
4
4
  const DEFAULT_MERMAID_FILENAME = "alloy-di.mmd";
5
5
  function resolveVisualizationOptions(input, projectRoot) {
@@ -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 = [...serviceNodes, ...Array.from(tokenNodes.values())];
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. Only service-to-service edges with known scopes
220
- * are considered, and only when a custom hierarchy is configured.
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" || to.type !== "service") return false;
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
- return [ensureTokenNode(tokenNodes, createTokenLabel(identifier || fallbackExpression))];
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
- validateScopesConfig(options.scopes);
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 { AlloyPluginOptions, alloy };
12
+ export { alloy };
@@ -1,56 +1,28 @@
1
- import { normalizeImportPath, walkSync } from "../core/utils.js";
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
- function toLazyServiceKey(identifier) {
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 virtualModuleId = "virtual:alloy-container";
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
- resolvedRoot = config.root ?? process.cwd();
34
- isDevMode = !config.isProduction;
35
- try {
36
- const pkgPath = path.resolve(resolvedRoot, "package.json");
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
- discoveryRuntime.processUpdate(id, code);
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 (!isDiscoverableFile(file)) return;
41
+ if (!isAlloyDiscoverableFile(file)) return;
70
42
  let discoveryChanged;
71
- if (ctx.type === "delete") discoveryChanged = discoveryRuntime.removeDiscoveredFile(file);
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 = discoveryRuntime.processUpdate(file, code);
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
- discoveryRuntime.clear();
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 loadVirtualContainerModule({
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 };
@@ -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 { Container } from "./lib/container.js";
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 };