alloy-di 1.5.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 (39) 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 +2 -1
  8. package/dist/lib/container.js +5 -4
  9. package/dist/lib/lazy.d.ts +5 -1
  10. package/dist/lib/scope.d.ts +1 -0
  11. package/dist/lib/types.d.ts +3 -1
  12. package/dist/plugins/consumer-plugin.d.ts +40 -0
  13. package/dist/plugins/consumer-plugin.js +101 -0
  14. package/dist/plugins/core/codegen.js +1 -1
  15. package/dist/plugins/core/container-loader.js +30 -16
  16. package/dist/plugins/core/generation-inputs.js +41 -0
  17. package/dist/plugins/core/utils.js +1 -1
  18. package/dist/plugins/core/visualization-utils.js +1 -1
  19. package/dist/plugins/rollup-plugin/index.js +3 -5
  20. package/dist/plugins/vite-plugin/index.d.ts +2 -29
  21. package/dist/plugins/vite-plugin/index.js +19 -73
  22. package/dist/plugins/webpack-like-plugin.d.ts +32 -0
  23. package/dist/plugins/webpack-like-plugin.js +86 -0
  24. package/dist/rspack.d.ts +7 -0
  25. package/dist/rspack.js +10 -0
  26. package/dist/runtime.d.ts +4 -4
  27. package/dist/runtime.js +3 -3
  28. package/dist/scopes.d.ts +2 -1
  29. package/dist/scopes.js +3 -0
  30. package/dist/tsconfig.tsbuildinfo +1 -1
  31. package/dist/vite.d.ts +2 -1
  32. package/dist/webpack.d.ts +7 -0
  33. package/dist/webpack.js +10 -0
  34. package/package.json +28 -12
  35. package/dist/lib/testing/mocking.d.ts +0 -12
  36. package/dist/lib/testing/mocking.js +0 -107
  37. package/dist/lib/testing/registry.js +0 -17
  38. package/dist/test.d.ts +0 -84
  39. package/dist/test.js +0 -83
package/README.md CHANGED
@@ -1,13 +1,13 @@
1
1
  # alloy-di
2
2
 
3
- `alloy-di` is a build-time dependency injection toolkit for Vite. It scans your TypeScript during build, generates a static container, and ships a tiny runtime so you get dependency injection without reflection overhead.
3
+ `alloy-di` is a build-time dependency injection toolkit for TypeScript apps. It scans your TypeScript during build, generates a static container, and ships a tiny runtime so you get dependency injection without reflection overhead.
4
4
 
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
+ - **Visualize your DI graph** – enable the build plugin’s `visualize` option to emit a Mermaid diagram (`./alloy-di.mmd` by default) that captures scopes, lazy edges, and tokens for easy review.
9
9
  - **First-class lazy loading** – use `Lazy()` or provider-based lazy registrations to keep optional features in separate chunks.
10
- - **Framework agnostic** – works anywhere Vite runs: React, Vue, Svelte, SSR, libraries, and plain TS apps.
10
+ - **Bundler support** – use the generated container from Vite, webpack, or Rspack applications.
11
11
  - **Type safe** – generates `serviceIdentifiers` and manifest declarations for precise inference.
12
12
 
13
13
  ## Install
@@ -51,13 +51,17 @@ pnpm add -D alloy-di
51
51
  const app = await container.get(serviceIdentifiers.AppService);
52
52
  ```
53
53
 
54
- > **Build tip:** The default Vite scaffold (`pnpm create vite@latest`) wires `"build": "tsc && vite build"`. Alloy writes its ambient declarations during `vite build`, so running `tsc` first can fail on fresh trees. Swap the order (`vite build && tsc`), or manually run `vite build` to generate the declarations first.
54
+ > **Build tip:** The default Vite scaffold (`pnpm create vite@latest`) wires `"build": "tsc && vite build"`. Run `alloy generate` before type-checking so fresh checkouts and CI have Alloy's ambient declarations available, for example `"build": "alloy generate && tsc && vite build"`.
55
+
56
+ By default Alloy scans `src` for decorated services. If your services live elsewhere, configure `sourceDirs` in the Vite plugin; `alloy generate` will reuse that config.
57
+
58
+ Webpack and Rspack apps can use `alloy-di/webpack` or `alloy-di/rspack` with the same options. Run `alloy generate --bundler webpack` or `alloy generate --bundler rspack` before type-checking.
55
59
 
56
60
  Need manifests, providers, or testing utilities? See the docs site for complete guides.
57
61
 
58
62
  ## Visualize your dependency graph
59
63
 
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.
64
+ Enable the build 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
65
 
62
66
  ```ts
63
67
  import { defineConfig } from "vite";
@@ -90,8 +94,9 @@ The site covers getting started, plugin options, manifest authoring, lazy loadin
90
94
 
91
95
  ## Examples in this repo
92
96
 
93
- - `packages/examples/app` – React + Vite app consuming decorated services, manifests, and providers.
97
+ - `packages/examples/app-vite` – React + Vite app consuming decorated services, manifests, and providers.
98
+ - `packages/examples/app-webpack-rspack` – the same React app source built with webpack and Rspack.
94
99
  - `packages/examples/library-internal` – monorepo library that emits `alloy.manifest.mjs` via the Rolldown plugin.
95
100
  - `packages/examples/library-external` – plain classes registered through providers.
96
101
 
97
- Clone the repo, run `pnpm install`, then `pnpm --filter @alloy-di/example-app dev` to explore.
102
+ Clone the repo, run `pnpm install`, then `pnpm --filter @alloy-di/example-app-vite dev` to explore.
package/bin/alloy.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { runCli } from "../dist/cli.js";
3
+
4
+ try {
5
+ await runCli();
6
+ } catch (error) {
7
+ console.error(error instanceof Error ? error.message : String(error));
8
+ process.exitCode = 1;
9
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ import { AlloyPluginOptions } from "./plugins/consumer-plugin.js";
2
+ import { AlloyGenerateOptions, generate } from "./generate.js";
3
+ //#region src/cli.d.ts
4
+ type GenerateBundler = "vite" | "webpack" | "rspack" | "none";
5
+ interface GenerateCliOptions {
6
+ root?: string;
7
+ configFile?: string;
8
+ bundler?: GenerateBundler;
9
+ mode?: string;
10
+ help?: boolean;
11
+ }
12
+ interface RunCliDependencies {
13
+ generate: typeof generate;
14
+ resolveGenerateOptions: typeof resolveGenerateOptions;
15
+ log: (message: string) => void;
16
+ }
17
+ declare function printUsage(log?: (message: string) => void): void;
18
+ declare function readOptionValue(args: string[], index: number, optionName: string): string;
19
+ declare function parseGenerateArgs(args: string[]): GenerateCliOptions;
20
+ declare function getAlloyOptions(plugin: unknown): AlloyPluginOptions | undefined;
21
+ declare function resolveGenerateOptions(cliOptions: GenerateCliOptions): Promise<AlloyGenerateOptions>;
22
+ declare function runCli(args?: string[], deps?: RunCliDependencies): Promise<void>;
23
+ //#endregion
24
+ export { GenerateBundler, GenerateCliOptions, RunCliDependencies, getAlloyOptions, parseGenerateArgs, printUsage, readOptionValue, resolveGenerateOptions, runCli };
package/dist/cli.js ADDED
@@ -0,0 +1,168 @@
1
+ #!/usr/bin/env node
2
+ import { generate } from "./generate.js";
3
+ import { ALLOY_PLUGIN_OPTIONS } from "./plugins/consumer-plugin.js";
4
+ import { ALLOY_VITE_PLUGIN_OPTIONS } from "./plugins/vite-plugin/index.js";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { fileURLToPath, pathToFileURL } from "node:url";
8
+ //#region src/cli.ts
9
+ const defaultRunCliDependencies = {
10
+ generate,
11
+ resolveGenerateOptions,
12
+ log: (message) => console.log(message)
13
+ };
14
+ function printUsage(log = console.log) {
15
+ log(`Usage:
16
+ alloy generate [--root <dir>] [--config <file>] [--bundler <vite|webpack|rspack|none>] [--mode <mode>]
17
+
18
+ Commands:
19
+ generate Generate Alloy declaration files without bundling.
20
+
21
+ Options:
22
+ --root Project root. Defaults to the current working directory.
23
+ --config Bundler config file to load. Pass "false" to skip config loading.
24
+ --bundler Config loader to use. Defaults to "vite" unless --config false is set.
25
+ --mode Mode passed to webpack/Rspack config functions. Defaults to "production".`);
26
+ }
27
+ function readOptionValue(args, index, optionName) {
28
+ const value = args[index + 1];
29
+ if (!value || value.startsWith("-")) throw new Error(`[alloy] ${optionName} requires a value.`);
30
+ return value;
31
+ }
32
+ function parseGenerateArgs(args) {
33
+ const options = {};
34
+ for (let i = 0; i < args.length; i++) {
35
+ const arg = args[i];
36
+ if (arg === "--root" || arg === "-r") {
37
+ options.root = readOptionValue(args, i, arg);
38
+ i++;
39
+ } else if (arg === "--config" || arg === "-c") {
40
+ options.configFile = readOptionValue(args, i, arg);
41
+ i++;
42
+ } else if (arg === "--bundler" || arg === "-b") {
43
+ const bundler = readOptionValue(args, i, arg);
44
+ if (bundler !== "vite" && bundler !== "webpack" && bundler !== "rspack" && bundler !== "none") throw new Error(`[alloy] --bundler must be one of "vite", "webpack", "rspack", or "none".`);
45
+ options.bundler = bundler;
46
+ i++;
47
+ } else if (arg === "--mode" || arg === "-m") {
48
+ options.mode = readOptionValue(args, i, arg);
49
+ i++;
50
+ } else if (arg === "--help" || arg === "-h") {
51
+ options.help = true;
52
+ return options;
53
+ } else throw new Error(`[alloy] Unknown option "${arg}".`);
54
+ }
55
+ return options;
56
+ }
57
+ function getAlloyOptions(plugin) {
58
+ if (!plugin || typeof plugin !== "object") return;
59
+ const candidate = plugin;
60
+ return candidate[ALLOY_PLUGIN_OPTIONS] ?? candidate[ALLOY_VITE_PLUGIN_OPTIONS];
61
+ }
62
+ async function resolveGenerateOptions(cliOptions) {
63
+ const root = cliOptions.root ? path.resolve(cliOptions.root) : process.cwd();
64
+ const bundler = resolveBundler(cliOptions);
65
+ if (bundler === "none") return { root };
66
+ if (bundler === "vite") return resolveViteGenerateOptions(cliOptions, root);
67
+ return resolveWebpackLikeGenerateOptions(cliOptions, root, bundler);
68
+ }
69
+ function resolveBundler(cliOptions) {
70
+ if (cliOptions.configFile === "false") return "none";
71
+ return cliOptions.bundler ?? "vite";
72
+ }
73
+ async function resolveViteGenerateOptions(cliOptions, root) {
74
+ let vite;
75
+ try {
76
+ vite = await import("vite");
77
+ } catch {
78
+ throw new Error("[alloy] Could not load Vite. Install vite or run with --config false and provide options programmatically.");
79
+ }
80
+ const inlineConfig = {
81
+ root,
82
+ configFile: cliOptions.configFile
83
+ };
84
+ const config = await vite.resolveConfig(inlineConfig, "build", "production", "production");
85
+ const alloyOptions = config.plugins.map((plugin) => getAlloyOptions(plugin)).find((options) => Boolean(options));
86
+ if (!alloyOptions) throw new Error("[alloy] Could not find alloy() in the resolved Vite plugins.");
87
+ return {
88
+ ...alloyOptions,
89
+ root: config.root
90
+ };
91
+ }
92
+ async function resolveWebpackLikeGenerateOptions(cliOptions, root, bundler) {
93
+ const alloyOptions = normalizeWebpackLikeConfigs(await evaluateWebpackLikeConfig(await loadBundlerConfig(resolveBundlerConfigFile(root, bundler, cliOptions.configFile), bundler), cliOptions.mode)).flatMap((config) => Array.isArray(config.plugins) ? config.plugins : []).map((plugin) => getAlloyOptions(plugin)).find((options) => Boolean(options));
94
+ if (!alloyOptions) throw new Error(`[alloy] Could not find alloy() in the resolved ${bundlerLabel(bundler)} plugins.`);
95
+ return {
96
+ ...alloyOptions,
97
+ root
98
+ };
99
+ }
100
+ function resolveBundlerConfigFile(root, bundler, configFile) {
101
+ if (configFile) return path.isAbsolute(configFile) ? configFile : path.resolve(root, configFile);
102
+ for (const extension of [
103
+ "js",
104
+ "mjs",
105
+ "cjs"
106
+ ]) {
107
+ const candidate = path.resolve(root, `${bundler}.config.${extension}`);
108
+ try {
109
+ if (fsExists(candidate)) return candidate;
110
+ } catch {}
111
+ }
112
+ throw new Error(`[alloy] Could not find ${bundlerLabel(bundler)} config. Pass --config <file> or --config false.`);
113
+ }
114
+ function fsExists(filePath) {
115
+ return fs.existsSync(filePath);
116
+ }
117
+ async function loadBundlerConfig(configFile, bundler) {
118
+ if (/\.[cm]?ts$/i.test(configFile)) throw new Error(`[alloy] ${bundlerLabel(bundler)} TypeScript config loading is not supported by alloy generate yet. Use a JavaScript config wrapper or pass --config false.`);
119
+ try {
120
+ const imported = await import(pathToFileURL(configFile).href);
121
+ return imported.default ?? imported;
122
+ } catch (error) {
123
+ throw new Error(`[alloy] Could not load ${bundlerLabel(bundler)} config at ${configFile}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
124
+ }
125
+ }
126
+ async function evaluateWebpackLikeConfig(config, mode = "production") {
127
+ if (typeof config !== "function") return config;
128
+ return config({
129
+ mode,
130
+ production: mode === "production"
131
+ }, {
132
+ mode,
133
+ env: { mode }
134
+ });
135
+ }
136
+ function normalizeWebpackLikeConfigs(config) {
137
+ return (Array.isArray(config) ? config : [config]).filter((candidate) => Boolean(candidate) && typeof candidate === "object");
138
+ }
139
+ function bundlerLabel(bundler) {
140
+ return bundler === "rspack" ? "Rspack" : "webpack";
141
+ }
142
+ async function runCli(args = process.argv.slice(2), deps = defaultRunCliDependencies) {
143
+ const [command, ...commandArgs] = args;
144
+ if (!command || command === "--help" || command === "-h") {
145
+ printUsage(deps.log);
146
+ return;
147
+ }
148
+ if (command !== "generate") throw new Error(`[alloy] Unknown command "${command}".`);
149
+ const cliOptions = parseGenerateArgs(commandArgs);
150
+ if (cliOptions.help) {
151
+ printUsage(deps.log);
152
+ return;
153
+ }
154
+ const options = await deps.resolveGenerateOptions(cliOptions);
155
+ const result = await deps.generate(options);
156
+ deps.log(`[alloy] Generated declarations for ${result.serviceCount} service(s) in ${result.declarationDir}.`);
157
+ }
158
+ function isMainModule() {
159
+ return process.argv[1] ? path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) : false;
160
+ }
161
+ if (isMainModule()) try {
162
+ await runCli();
163
+ } catch (error) {
164
+ console.error(error instanceof Error ? error.message : String(error));
165
+ process.exitCode = 1;
166
+ }
167
+ //#endregion
168
+ export { getAlloyOptions, parseGenerateArgs, printUsage, readOptionValue, resolveGenerateOptions, runCli };
@@ -0,0 +1,28 @@
1
+ import { AlloyPluginOptions } from "./plugins/consumer-plugin.js";
2
+
3
+ //#region src/generate.d.ts
4
+ interface AlloyGenerateOptions extends AlloyPluginOptions {
5
+ /**
6
+ * Project root to scan. Defaults to `process.cwd()`.
7
+ */
8
+ root?: string;
9
+ /**
10
+ * Override the package name used for generated service identifier keys.
11
+ * Defaults to the nearest `package.json` name under `root`.
12
+ */
13
+ packageName?: string;
14
+ }
15
+ interface AlloyGenerateResult {
16
+ root: string;
17
+ declarationDir: string;
18
+ serviceCount: number;
19
+ manifestCount: number;
20
+ }
21
+ /**
22
+ * Generates Alloy's ambient TypeScript declarations without running a Vite
23
+ * build. This is intended for fresh checkouts and CI pipelines that type-check
24
+ * before bundling.
25
+ */
26
+ declare function generate(options?: AlloyGenerateOptions): Promise<AlloyGenerateResult>;
27
+ //#endregion
28
+ export { AlloyGenerateOptions, AlloyGenerateResult, generate };
@@ -0,0 +1,46 @@
1
+ import { normalizeImportPath } from "./plugins/core/utils.js";
2
+ import { prepareContainerData, writeDeclarationArtifacts } from "./plugins/core/container-loader.js";
3
+ import { DEFAULT_SOURCE_DIRS, createDiscoveryRuntimeForSourceDirs, readPackageName, toLazyServiceKey } from "./plugins/core/generation-inputs.js";
4
+ import path from "node:path";
5
+ //#region src/generate.ts
6
+ /**
7
+ * Generates Alloy's ambient TypeScript declarations without running a Vite
8
+ * build. This is intended for fresh checkouts and CI pipelines that type-check
9
+ * before bundling.
10
+ */
11
+ async function generate(options = {}) {
12
+ const root = path.resolve(options.root ?? process.cwd());
13
+ const packageName = options.packageName ?? readPackageName(root);
14
+ const discoveryRuntime = createDiscoveryRuntimeForSourceDirs(root, options.sourceDirs ?? DEFAULT_SOURCE_DIRS, { factoryProviders: false });
15
+ const providerImportPaths = (options.providers ?? []).map((entry) => {
16
+ return normalizeImportPath(path.isAbsolute(entry) ? entry : path.resolve(root, entry));
17
+ });
18
+ const prepared = await prepareContainerData({
19
+ localMetas: Array.from(discoveryRuntime.discoveredClasses.values()),
20
+ lazyReferencedClassKeys: discoveryRuntime.lazyReferencedClassKeys,
21
+ manifests: options.manifests ?? [],
22
+ providerImportPaths,
23
+ factoryProviders: [],
24
+ lazyServiceKeys: new Set((options.lazyServices ?? []).map(toLazyServiceKey)),
25
+ packageName,
26
+ resolvedRoot: root,
27
+ containerDeclarationDir: options.containerDeclarationDir,
28
+ resolvedVisualization: null,
29
+ scopes: options.scopes
30
+ });
31
+ writeDeclarationArtifacts({
32
+ metas: prepared.metas,
33
+ loadedManifests: prepared.loadedManifests,
34
+ resolvedRoot: root,
35
+ containerDeclarationDir: options.containerDeclarationDir,
36
+ scopes: options.scopes
37
+ });
38
+ return {
39
+ root,
40
+ declarationDir: path.resolve(root, options.containerDeclarationDir ?? "./src"),
41
+ serviceCount: prepared.metas.length,
42
+ manifestCount: prepared.loadedManifests.length
43
+ };
44
+ }
45
+ //#endregion
46
+ export { generate };
@@ -1,6 +1,6 @@
1
1
  import { Constructor, Newable, Token } from "./types.js";
2
- import { FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope } from "./scope.js";
3
2
  import { ServiceIdentifier } from "./service-identifiers.js";
3
+ import { FactoryCacheEntry, FactoryPendingEntry, ResolutionContext, ServiceScope } from "./scope.js";
4
4
 
5
5
  //#region src/lib/container.d.ts
6
6
  /**
@@ -36,6 +36,7 @@ declare class Container implements ResolutionContext {
36
36
  readonly scopeName: ServiceScope;
37
37
  readonly parent: ResolutionContext | null;
38
38
  getCached(target: Constructor): unknown;
39
+ hasCached(target: Constructor): boolean;
39
40
  setCached(target: Constructor, instance: unknown): void;
40
41
  getPending(target: Constructor): Promise<unknown> | undefined;
41
42
  setPending(target: Constructor, promise: Promise<unknown>): void;
@@ -53,6 +53,9 @@ var Container = class {
53
53
  getCached(target) {
54
54
  return this.singletons.get(target);
55
55
  }
56
+ hasCached(target) {
57
+ return this.singletons.has(target);
58
+ }
56
59
  setCached(target, instance) {
57
60
  this.singletons.set(target, instance);
58
61
  }
@@ -202,8 +205,7 @@ var Container = class {
202
205
  * @throws Error if a circular dependency is detected
203
206
  */
204
207
  async resolve(target, resolutionStack, context) {
205
- const overridden = this.instanceOverrides.get(target);
206
- if (overridden) return overridden;
208
+ if (this.instanceOverrides.has(target)) return this.instanceOverrides.get(target);
207
209
  if (resolutionStack.includes(target)) throw new DependencyResolutionError(`Circular dependency detected: ${[...resolutionStack.map((t) => t.name), target.name].join(" -> ")}`, {
208
210
  target,
209
211
  resolutionStack,
@@ -232,8 +234,7 @@ var Container = class {
232
234
  return startingContext;
233
235
  }
234
236
  async resolveCached(target, metadata, resolutionStack, targetCtx) {
235
- const cached = targetCtx.getCached(target);
236
- if (cached) return cached;
237
+ if (targetCtx.hasCached(target)) return targetCtx.getCached(target);
237
238
  const pending = targetCtx.getPending(target);
238
239
  if (pending) return await pending;
239
240
  const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory, targetCtx).then((instance) => {
@@ -14,6 +14,10 @@ interface Lazy<T> {
14
14
  factor?: number;
15
15
  };
16
16
  }
17
+ /**
18
+ * Narrow an unknown value to a `Lazy` wrapper based on the hidden identifier symbol.
19
+ */
20
+ declare function isLazy(value: unknown): value is Lazy<unknown>;
17
21
  /**
18
22
  * Helper type to extract the instance type from a Newable constructor.
19
23
  */
@@ -36,4 +40,4 @@ declare function Lazy<F extends () => Promise<Newable<any> | {
36
40
  }>>(importer: F, retry?: Lazy<any>["retry"]): Lazy<InferLazyType<F>>;
37
41
  declare function Lazy<T>(importer: Importer<T>, retry?: Lazy<T>["retry"]): Lazy<T>;
38
42
  //#endregion
39
- export { LAZY_IDENTIFIER, Lazy };
43
+ export { LAZY_IDENTIFIER, Lazy, isLazy };
@@ -27,6 +27,7 @@ interface FactoryPendingEntry {
27
27
  interface ResolutionContext {
28
28
  readonly scopeName: ServiceScope;
29
29
  getCached(target: Constructor): unknown;
30
+ hasCached(target: Constructor): boolean;
30
31
  setCached(target: Constructor, instance: unknown): void;
31
32
  getPending(target: Constructor): Promise<unknown> | undefined;
32
33
  setPending(target: Constructor, promise: Promise<unknown>): void;
@@ -18,5 +18,7 @@ interface Token<T> {
18
18
  }
19
19
  /** Create a unique typed token for values or abstractions. */
20
20
  declare function createToken<T>(description?: string): Token<T>;
21
+ declare function isToken(value: unknown): value is Token<unknown>;
22
+ declare function isConstructor(value: unknown): value is Constructor;
21
23
  //#endregion
22
- export { Constructor, Newable, Token, createToken };
24
+ export { Constructor, Newable, Token, createToken, isConstructor, isToken };
@@ -0,0 +1,40 @@
1
+ import { ServiceIdentifier } from "../lib/service-identifiers.js";
2
+ import { AlloyManifest } from "./core/types.js";
3
+ import { AlloyScopesConfig } from "./core/scopes-validation.js";
4
+ import { AlloyMermaidVisualizerOptions, AlloyVisualizationOptions } from "./core/visualization-utils.js";
5
+
6
+ //#region src/plugins/consumer-plugin.d.ts
7
+ interface AlloyPluginOptions {
8
+ providers?: string[];
9
+ /**
10
+ * Source directories to scan for decorated services before the virtual
11
+ * container is loaded. Relative paths are resolved against the project root.
12
+ * Defaults to ["src"].
13
+ */
14
+ sourceDirs?: string[];
15
+ /** Optional list of manifest objects to ingest */
16
+ manifests?: AlloyManifest[];
17
+ /** List of ServiceIdentifiers to mark as instantiation-lazy (adds factory Lazy wrapper) */
18
+ lazyServices?: ServiceIdentifier[];
19
+ /**
20
+ * Output directory for the generated `virtual-container.d.ts` file.
21
+ * Relative paths are resolved against the project root.
22
+ * Defaults to "./src".
23
+ */
24
+ containerDeclarationDir?: string;
25
+ /**
26
+ * Emit dependency graph artifacts. When `true`, writes a Mermaid diagram to
27
+ * `${projectRoot}/alloy-di.mmd`. Provide an object to customize output.
28
+ */
29
+ visualize?: boolean | AlloyVisualizationOptions;
30
+ /**
31
+ * Declares custom, application-defined scopes and their parent ordering, e.g.
32
+ * `{ session: { parent: 'singleton' }, request: { parent: 'session' } }`.
33
+ * Drives type-safe scope names (emitted into the generated declaration),
34
+ * runtime hierarchy registration, and build-time scope-stability validation.
35
+ */
36
+ scopes?: AlloyScopesConfig;
37
+ }
38
+ declare const ALLOY_PLUGIN_OPTIONS: unique symbol;
39
+ //#endregion
40
+ export { ALLOY_PLUGIN_OPTIONS, AlloyPluginOptions };
@@ -0,0 +1,101 @@
1
+ import { normalizeImportPath, writeFileIfChanged } 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";
5
+ import { DEFAULT_SOURCE_DIRS, readPackageName, scanSourceDirectories, toLazyServiceKey } from "./core/generation-inputs.js";
6
+ import fs from "node:fs";
7
+ import path from "node:path";
8
+ //#region src/plugins/consumer-plugin.ts
9
+ const ALLOY_PLUGIN_OPTIONS = Symbol.for("alloy-di.plugin-options");
10
+ function createConsumerPluginContext(options = {}) {
11
+ const configuredProviderEntries = Array.from(options.providers ?? []);
12
+ const configuredSourceDirs = Array.from(options.sourceDirs ?? DEFAULT_SOURCE_DIRS);
13
+ const providerModuleRefs = [];
14
+ const lazyServiceKeys = new Set((options.lazyServices ?? []).map(toLazyServiceKey));
15
+ const discoveryRuntime = createDiscoveryRuntime();
16
+ let resolvedRoot = process.cwd();
17
+ let packageName = "UNKNOWN_PACKAGE";
18
+ let resolvedVisualization = null;
19
+ let isDevMode;
20
+ function configure(config = {}) {
21
+ resolvedRoot = config.root ?? process.cwd();
22
+ isDevMode = config.isDevMode;
23
+ packageName = readPackageName(resolvedRoot);
24
+ providerModuleRefs.length = 0;
25
+ for (const entry of configuredProviderEntries) {
26
+ const absPath = path.isAbsolute(entry) ? entry : path.resolve(resolvedRoot, entry);
27
+ providerModuleRefs.push({
28
+ absPath,
29
+ importPath: normalizeImportPath(absPath)
30
+ });
31
+ }
32
+ resolvedVisualization = resolveVisualizationOptions(options.visualize, resolvedRoot);
33
+ }
34
+ async function loadContainer() {
35
+ return loadVirtualContainerModule({
36
+ localMetas: Array.from(discoveryRuntime.discoveredClasses.values()),
37
+ lazyReferencedClassKeys: discoveryRuntime.lazyReferencedClassKeys,
38
+ manifests: options.manifests ?? [],
39
+ providerImportPaths: providerModuleRefs.map((ref) => ref.importPath),
40
+ factoryProviders: shouldTrackFactoryProviders() ? Array.from(discoveryRuntime.factoryProvidersByFile.values()).flat() : [],
41
+ lazyServiceKeys,
42
+ packageName,
43
+ resolvedRoot,
44
+ containerDeclarationDir: options.containerDeclarationDir,
45
+ resolvedVisualization,
46
+ isDevMode,
47
+ scopes: options.scopes
48
+ });
49
+ }
50
+ function shouldTrackFactoryProviders() {
51
+ return Boolean(resolvedVisualization);
52
+ }
53
+ return {
54
+ options,
55
+ virtualModuleId: "virtual:alloy-container",
56
+ resolvedVirtualModuleId: "\0virtual:alloy-container",
57
+ get root() {
58
+ return resolvedRoot;
59
+ },
60
+ sourceDirs: configuredSourceDirs,
61
+ configure,
62
+ shouldTrackFactoryProviders,
63
+ processTransform(code, id) {
64
+ return discoveryRuntime.processUpdate(id, code, { factoryProviders: shouldTrackFactoryProviders() });
65
+ },
66
+ processFileUpdate(file, code) {
67
+ return discoveryRuntime.processUpdate(file, code, { factoryProviders: shouldTrackFactoryProviders() });
68
+ },
69
+ removeFile(file) {
70
+ return discoveryRuntime.removeDiscoveredFile(file);
71
+ },
72
+ buildStart(addWatchFile) {
73
+ discoveryRuntime.clear();
74
+ for (const ref of providerModuleRefs) {
75
+ addWatchFile?.(ref.absPath);
76
+ if (shouldTrackFactoryProviders()) try {
77
+ const code = fs.readFileSync(ref.absPath, "utf-8");
78
+ discoveryRuntime.processUpdate(ref.absPath, code, { factoryProviders: true });
79
+ } catch {}
80
+ }
81
+ scanSourceDirectories(discoveryRuntime, resolvedRoot, configuredSourceDirs, { factoryProviders: shouldTrackFactoryProviders() });
82
+ },
83
+ loadContainer,
84
+ async writeContainerCache(filePath) {
85
+ const result = await loadContainer();
86
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
87
+ return writeFileIfChanged(filePath, result.code);
88
+ },
89
+ getWatchFiles() {
90
+ return providerModuleRefs.map((ref) => ref.absPath);
91
+ },
92
+ getWatchDirectories() {
93
+ return configuredSourceDirs.map((sourceDir) => path.isAbsolute(sourceDir) ? sourceDir : path.resolve(resolvedRoot, sourceDir));
94
+ }
95
+ };
96
+ }
97
+ function isAlloyDiscoverableFile(file) {
98
+ return isDiscoverableFile(file);
99
+ }
100
+ //#endregion
101
+ export { ALLOY_PLUGIN_OPTIONS, createConsumerPluginContext, isAlloyDiscoverableFile };
@@ -171,7 +171,7 @@ function reconstructOptionsText(meta, importMap, serviceRenames) {
171
171
  const expr = reconstructDependencyExpression(factory, () => "", path.dirname(meta.filePath));
172
172
  parts.push(`factory: ${expr}`);
173
173
  }
174
- if (scope === "singleton") parts.push(`scope: 'singleton'`);
174
+ if (scope && scope !== "transient") parts.push(`scope: '${escapeSingleQuotes(scope)}'`);
175
175
  if (dependencies && dependencies.length > 0) {
176
176
  const depExprs = dependencies.map((dep) => {
177
177
  return reconstructDependencyExpression(dep, (ident) => {
@@ -5,10 +5,29 @@ import { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateM
5
5
  import { validateScopeStability, validateScopesConfig } from "./scopes-validation.js";
6
6
  import { generateMermaidDiagram } from "./visualizer.js";
7
7
  import { ensureDirectoryForFile } from "./visualization-utils.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/core/container-loader.ts
11
11
  async function loadVirtualContainerModule(options) {
12
+ const prepared = await prepareContainerData(options);
13
+ const code = generateContainerModule(prepared.metas, prepared.lazyClassKeys, prepared.providerImports, {
14
+ isDev: options.isDevMode,
15
+ scopes: options.scopes
16
+ });
17
+ writeDeclarationArtifacts({
18
+ metas: prepared.metas,
19
+ loadedManifests: prepared.loadedManifests,
20
+ resolvedRoot: options.resolvedRoot,
21
+ containerDeclarationDir: options.containerDeclarationDir,
22
+ scopes: options.scopes
23
+ });
24
+ writeVisualizationArtifact(prepared.metas, prepared.lazyClassKeys, options.factoryProviders, options.resolvedVisualization, options.scopes);
25
+ return {
26
+ code,
27
+ moduleType: "js"
28
+ };
29
+ }
30
+ async function prepareContainerData(options) {
12
31
  const metas = options.localMetas.map((meta) => ({
13
32
  ...meta,
14
33
  metadata: { ...meta.metadata }
@@ -33,19 +52,13 @@ async function loadVirtualContainerModule(options) {
33
52
  const providerImports = Array.from(new Set([...options.providerImportPaths, ...manifestData.providers]));
34
53
  reconcileLazySet(metas, lazyClassKeys, collectEagerReferencedNames(metas));
35
54
  augmentFactoryLazyServices(metas, options.lazyServiceKeys);
36
- if (options.scopes && Object.keys(options.scopes).length > 0) {
37
- validateScopesConfig(options.scopes);
38
- validateScopeStability(metas, options.scopes);
39
- }
40
- const code = generateContainerModule(metas, lazyClassKeys, providerImports, {
41
- isDev: options.isDevMode,
42
- scopes: options.scopes
43
- });
44
- writeTypeDefinitions(metas, loadedManifests, options.resolvedRoot, options.containerDeclarationDir, options.scopes);
45
- writeVisualizationArtifact(metas, lazyClassKeys, options.factoryProviders, options.resolvedVisualization, options.scopes);
55
+ if (options.scopes && Object.keys(options.scopes).length > 0) validateScopesConfig(options.scopes);
56
+ validateScopeStability(metas, options.scopes);
46
57
  return {
47
- code,
48
- moduleType: "js"
58
+ metas,
59
+ loadedManifests,
60
+ lazyClassKeys,
61
+ providerImports
49
62
  };
50
63
  }
51
64
  function assignIdentifierKeys(metas, packageName, resolvedRoot) {
@@ -70,13 +83,14 @@ function assertNoDuplicateManifestServices(metas, manifestServices) {
70
83
  "Resolve by removing one source (local or manifest) to avoid ambiguous DI keys."
71
84
  ].join("\n"));
72
85
  }
73
- function writeTypeDefinitions(metas, loadedManifests, resolvedRoot, containerDeclarationDir, scopes) {
86
+ function writeDeclarationArtifacts(options) {
87
+ const { metas, loadedManifests, resolvedRoot, containerDeclarationDir } = options;
74
88
  const dtsDir = path.resolve(resolvedRoot, containerDeclarationDir ?? "./src");
75
89
  const dtsContent = generateContainerTypeDefinition(metas, (filePath) => resolveDeclarationImportPath(dtsDir, filePath));
76
90
  if (!fs.existsSync(dtsDir)) fs.mkdirSync(dtsDir, { recursive: true });
77
91
  writeFileIfChanged(path.join(dtsDir, "alloy-container.d.ts"), dtsContent);
78
92
  const scopeAugmentationPath = path.join(dtsDir, "alloy-scopes.d.ts");
79
- const scopeAugmentation = generateScopeAugmentationDefinition(scopes ? Object.keys(scopes) : []);
93
+ const scopeAugmentation = generateScopeAugmentationDefinition(options.scopes ? Object.keys(options.scopes) : []);
80
94
  if (scopeAugmentation) writeFileIfChanged(scopeAugmentationPath, scopeAugmentation);
81
95
  else if (fs.existsSync(scopeAugmentationPath)) fs.rmSync(scopeAugmentationPath);
82
96
  if (loadedManifests.length === 0) return;
@@ -106,4 +120,4 @@ function writeVisualizationArtifact(metas, lazyReferencedClassKeys, factoryProvi
106
120
  writeFileIfChanged(resolvedVisualization.outputPath, `%% ${GENERATED_FILE_NOTICE}\n${artifact.diagram}\n`);
107
121
  }
108
122
  //#endregion
109
- export { loadVirtualContainerModule };
123
+ export { loadVirtualContainerModule, prepareContainerData, writeDeclarationArtifacts };