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
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,8 +1,20 @@
1
1
  import { Constructor, Newable, Token } from "./types.js";
2
- import { 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
+ /**
7
+ * The container-like surface a factory receives. Both the root `Container` and
8
+ * a `Scope` satisfy it, so a factory can resolve its own dependencies against
9
+ * whichever context owns its lifecycle (root for `singleton`, the scope for a
10
+ * custom-scoped factory).
11
+ */
12
+ interface FactoryContext {
13
+ get<T>(target: Newable<T> | ServiceIdentifier<T>): Promise<T>;
14
+ getToken<T>(token: Token<T>): T;
15
+ }
16
+ /** A token-bound factory function executed at resolution time. */
17
+ type FactoryFn<T = unknown> = (context: FactoryContext) => T | Promise<T>;
6
18
  /**
7
19
  * Runtime dependency injection container used by generated modules and tests.
8
20
  *
@@ -15,17 +27,27 @@ declare class Container implements ResolutionContext {
15
27
  private readonly instanceOverrides;
16
28
  private readonly metadataCache;
17
29
  private readonly valueProviders;
30
+ private readonly factoryRegistry;
31
+ private readonly factoryValues;
32
+ private readonly factoryPending;
33
+ private factoryGeneration;
18
34
  private readonly factoryWarningCache;
19
35
  private readonly scopeHierarchy;
20
36
  readonly scopeName: ServiceScope;
21
37
  readonly parent: ResolutionContext | null;
22
38
  getCached(target: Constructor): unknown;
39
+ hasCached(target: Constructor): boolean;
23
40
  setCached(target: Constructor, instance: unknown): void;
24
41
  getPending(target: Constructor): Promise<unknown> | undefined;
25
42
  setPending(target: Constructor, promise: Promise<unknown>): void;
26
43
  deletePending(target: Constructor): void;
27
44
  getProvider(tokenId: symbol): unknown;
28
45
  hasProvider(tokenId: symbol): boolean;
46
+ getFactoryValue(tokenId: symbol): FactoryCacheEntry | undefined;
47
+ setFactoryValue(tokenId: symbol, generation: number, value: unknown): void;
48
+ getFactoryPending(tokenId: symbol): FactoryPendingEntry | undefined;
49
+ setFactoryPending(tokenId: symbol, generation: number, promise: Promise<unknown>): void;
50
+ deleteFactoryPending(tokenId: symbol, generation: number): void;
29
51
  /**
30
52
  * Resolve (and construct) the requested service.
31
53
  *
@@ -64,6 +86,23 @@ declare class Container implements ResolutionContext {
64
86
  * @param value - The value that should be injected when the token is requested.
65
87
  */
66
88
  provideValue<T>(token: Token<T>, value: T): void;
89
+ /**
90
+ * Register a factory function for an injection token. The factory runs at
91
+ * resolution time and receives the container so it can resolve its own
92
+ * dependencies.
93
+ *
94
+ * This is the imperative escape hatch backing the declarative `asFactory`
95
+ * provider; prefer `asFactory` + `applyProviders` for static registration.
96
+ *
97
+ * @param token - The token created via `createToken`.
98
+ * @param fn - Factory invoked with the resolving context; may be async.
99
+ * @param options.lifecycle - `singleton` (default, cached on the root),
100
+ * `transient` (re-run on every resolution), or a custom scope name (cached
101
+ * once per matching scope instance and disposed with it).
102
+ */
103
+ provideFactory<T>(token: Token<T>, fn: FactoryFn<T>, options?: {
104
+ lifecycle?: ServiceScope;
105
+ }): void;
67
106
  /**
68
107
  * Retrieve a provided value for a token from this container.
69
108
  * Throws if no provider is registered for the token.
@@ -125,9 +164,45 @@ declare class Container implements ResolutionContext {
125
164
  */
126
165
  private runImporterWithRetry;
127
166
  /**
128
- * Resolve a token dependency via registered value providers.
167
+ * Resolve a token dependency via registered value providers or factories.
129
168
  */
130
169
  private resolveTokenLike;
170
+ /**
171
+ * Execute a token-bound factory according to its lifecycle, caching its
172
+ * result on the context that owns that lifecycle:
173
+ *
174
+ * - `transient`: run `fn` on every call against the current context; nothing
175
+ * is cached.
176
+ * - `singleton`: cache on the root container.
177
+ * - custom scope: cache on the nearest ancestor context whose `scopeName`
178
+ * matches; if none is active, fall back to transient behaviour (mirrors how
179
+ * class resolution treats a custom scope with no matching context).
180
+ */
181
+ private resolveFactory;
182
+ /**
183
+ * Resolve a factory whose result is cached on `targetCtx`, keyed by token id.
184
+ * Mirrors `resolveCached` for classes: return the cached value, else coalesce
185
+ * concurrent first-resolutions onto a single `pending` promise, else run the
186
+ * factory and cache. `pending` is cleared in `finally` so a rejected factory
187
+ * can be retried rather than caching the failure.
188
+ */
189
+ private resolveFactoryCached;
190
+ /**
191
+ * Invoke a factory's function against the context that owns its lifecycle,
192
+ * with a best-effort synchronous re-entrancy guard.
193
+ *
194
+ * `executing` is set only for the synchronous duration of `fn(...)` — long
195
+ * enough to catch a factory that resolves its own token before returning (a
196
+ * synchronous self-cycle, or a synchronous mutual cycle between factories),
197
+ * but cleared the moment `fn` returns so that concurrent resolutions which
198
+ * legitimately coalesce on an in-flight result are never mistaken for a cycle.
199
+ *
200
+ * Known limitation: a factory that re-enters its own token *after* awaiting
201
+ * (an async cross-await self-reference) is indistinguishable from valid
202
+ * coalescing without async-context tracking, so that case is not caught here
203
+ * — it is documented in the factory-providers guide.
204
+ */
205
+ private runFactory;
131
206
  /**
132
207
  * Format a readable representation of the resolution stack for error messages.
133
208
  */
@@ -138,4 +213,4 @@ declare class Container implements ResolutionContext {
138
213
  private getServiceMetadata;
139
214
  }
140
215
  //#endregion
141
- export { Container };
216
+ export { Container, FactoryFn };
@@ -42,6 +42,10 @@ var Container = class {
42
42
  instanceOverrides = /* @__PURE__ */ new Map();
43
43
  metadataCache = /* @__PURE__ */ new Map();
44
44
  valueProviders = /* @__PURE__ */ new Map();
45
+ factoryRegistry = /* @__PURE__ */ new Map();
46
+ factoryValues = /* @__PURE__ */ new Map();
47
+ factoryPending = /* @__PURE__ */ new Map();
48
+ factoryGeneration = 0;
45
49
  factoryWarningCache = /* @__PURE__ */ new WeakSet();
46
50
  scopeHierarchy = /* @__PURE__ */ new Map();
47
51
  scopeName = ServiceScope.SINGLETON;
@@ -49,6 +53,9 @@ var Container = class {
49
53
  getCached(target) {
50
54
  return this.singletons.get(target);
51
55
  }
56
+ hasCached(target) {
57
+ return this.singletons.has(target);
58
+ }
52
59
  setCached(target, instance) {
53
60
  this.singletons.set(target, instance);
54
61
  }
@@ -67,6 +74,27 @@ var Container = class {
67
74
  hasProvider(tokenId) {
68
75
  return this.valueProviders.has(tokenId);
69
76
  }
77
+ getFactoryValue(tokenId) {
78
+ return this.factoryValues.get(tokenId);
79
+ }
80
+ setFactoryValue(tokenId, generation, value) {
81
+ this.factoryValues.set(tokenId, {
82
+ generation,
83
+ value
84
+ });
85
+ }
86
+ getFactoryPending(tokenId) {
87
+ return this.factoryPending.get(tokenId);
88
+ }
89
+ setFactoryPending(tokenId, generation, promise) {
90
+ this.factoryPending.set(tokenId, {
91
+ generation,
92
+ promise
93
+ });
94
+ }
95
+ deleteFactoryPending(tokenId, generation) {
96
+ if (this.factoryPending.get(tokenId)?.generation === generation) this.factoryPending.delete(tokenId);
97
+ }
70
98
  async get(targetOrIdentifier) {
71
99
  if (typeof targetOrIdentifier === "symbol") return this.getByIdentifier(targetOrIdentifier);
72
100
  return this.getByConstructor(targetOrIdentifier, this);
@@ -123,11 +151,38 @@ var Container = class {
123
151
  this.valueProviders.set(token.id, value);
124
152
  }
125
153
  /**
154
+ * Register a factory function for an injection token. The factory runs at
155
+ * resolution time and receives the container so it can resolve its own
156
+ * dependencies.
157
+ *
158
+ * This is the imperative escape hatch backing the declarative `asFactory`
159
+ * provider; prefer `asFactory` + `applyProviders` for static registration.
160
+ *
161
+ * @param token - The token created via `createToken`.
162
+ * @param fn - Factory invoked with the resolving context; may be async.
163
+ * @param options.lifecycle - `singleton` (default, cached on the root),
164
+ * `transient` (re-run on every resolution), or a custom scope name (cached
165
+ * once per matching scope instance and disposed with it).
166
+ */
167
+ provideFactory(token, fn, options) {
168
+ this.factoryRegistry.set(token.id, {
169
+ fn,
170
+ lifecycle: options?.lifecycle ?? ServiceScope.SINGLETON,
171
+ description: token.description,
172
+ generation: ++this.factoryGeneration,
173
+ executing: false
174
+ });
175
+ }
176
+ /**
126
177
  * Retrieve a provided value for a token from this container.
127
178
  * Throws if no provider is registered for the token.
128
179
  */
129
180
  getToken(token) {
130
- if (!this.valueProviders.has(token.id)) throw new Error(`No provider registered for token ${token.description ?? String(token.id)}`);
181
+ if (!this.valueProviders.has(token.id)) {
182
+ const label = token.description ?? String(token.id);
183
+ if (this.factoryRegistry.has(token.id)) throw new Error(`Token ${label} is registered as a factory provider, which cannot be retrieved synchronously via getToken(). Declare it as a dependency so it resolves through the async resolution path instead.`);
184
+ throw new Error(`No provider registered for token ${label}`);
185
+ }
131
186
  return this.valueProviders.get(token.id);
132
187
  }
133
188
  async getByConstructor(target, context, options) {
@@ -150,8 +205,7 @@ var Container = class {
150
205
  * @throws Error if a circular dependency is detected
151
206
  */
152
207
  async resolve(target, resolutionStack, context) {
153
- const overridden = this.instanceOverrides.get(target);
154
- if (overridden) return overridden;
208
+ if (this.instanceOverrides.has(target)) return this.instanceOverrides.get(target);
155
209
  if (resolutionStack.includes(target)) throw new DependencyResolutionError(`Circular dependency detected: ${[...resolutionStack.map((t) => t.name), target.name].join(" -> ")}`, {
156
210
  target,
157
211
  resolutionStack,
@@ -180,8 +234,7 @@ var Container = class {
180
234
  return startingContext;
181
235
  }
182
236
  async resolveCached(target, metadata, resolutionStack, targetCtx) {
183
- const cached = targetCtx.getCached(target);
184
- if (cached) return cached;
237
+ if (targetCtx.hasCached(target)) return targetCtx.getCached(target);
185
238
  const pending = targetCtx.getPending(target);
186
239
  if (pending) return await pending;
187
240
  const creation = this.createInstance(target, metadata.dependencies, resolutionStack, metadata.factory, targetCtx).then((instance) => {
@@ -292,7 +345,7 @@ var Container = class {
292
345
  }
293
346
  }
294
347
  /**
295
- * Resolve a token dependency via registered value providers.
348
+ * Resolve a token dependency via registered value providers or factories.
296
349
  */
297
350
  resolveTokenLike(tok, target, resolutionStack, context) {
298
351
  let current = context;
@@ -300,6 +353,8 @@ var Container = class {
300
353
  if (current.hasProvider(tok.id)) return current.getProvider(tok.id);
301
354
  current = current.parent;
302
355
  }
356
+ const factory = this.factoryRegistry.get(tok.id);
357
+ if (factory) return this.resolveFactory(tok.id, factory, context);
303
358
  const stackPath = this.formatStackPath(target, resolutionStack);
304
359
  throw new DependencyResolutionError(`No provider registered for token ${tok.description ?? String(tok.id)} while resolving ${target.name}. Resolution stack: ${stackPath}`, {
305
360
  target,
@@ -308,6 +363,75 @@ var Container = class {
308
363
  });
309
364
  }
310
365
  /**
366
+ * Execute a token-bound factory according to its lifecycle, caching its
367
+ * result on the context that owns that lifecycle:
368
+ *
369
+ * - `transient`: run `fn` on every call against the current context; nothing
370
+ * is cached.
371
+ * - `singleton`: cache on the root container.
372
+ * - custom scope: cache on the nearest ancestor context whose `scopeName`
373
+ * matches; if none is active, fall back to transient behaviour (mirrors how
374
+ * class resolution treats a custom scope with no matching context).
375
+ */
376
+ resolveFactory(tokenId, descriptor, context) {
377
+ if (descriptor.lifecycle === ServiceScope.TRANSIENT) return this.runFactory(descriptor, context);
378
+ const targetCtx = this.findContextForScope(descriptor.lifecycle, context);
379
+ if (descriptor.lifecycle !== ServiceScope.SINGLETON && targetCtx.scopeName !== descriptor.lifecycle) return this.runFactory(descriptor, context);
380
+ return this.resolveFactoryCached(tokenId, descriptor, targetCtx);
381
+ }
382
+ /**
383
+ * Resolve a factory whose result is cached on `targetCtx`, keyed by token id.
384
+ * Mirrors `resolveCached` for classes: return the cached value, else coalesce
385
+ * concurrent first-resolutions onto a single `pending` promise, else run the
386
+ * factory and cache. `pending` is cleared in `finally` so a rejected factory
387
+ * can be retried rather than caching the failure.
388
+ */
389
+ resolveFactoryCached(tokenId, descriptor, targetCtx) {
390
+ const generation = descriptor.generation;
391
+ const cached = targetCtx.getFactoryValue(tokenId);
392
+ if (cached?.generation === generation) return cached.value;
393
+ const pending = targetCtx.getFactoryPending(tokenId);
394
+ if (pending?.generation === generation) return pending.promise;
395
+ const creation = (async () => {
396
+ try {
397
+ const value = await this.runFactory(descriptor, targetCtx);
398
+ targetCtx.setFactoryValue(tokenId, generation, value);
399
+ return value;
400
+ } finally {
401
+ targetCtx.deleteFactoryPending(tokenId, generation);
402
+ }
403
+ })();
404
+ targetCtx.setFactoryPending(tokenId, generation, creation);
405
+ return creation;
406
+ }
407
+ /**
408
+ * Invoke a factory's function against the context that owns its lifecycle,
409
+ * with a best-effort synchronous re-entrancy guard.
410
+ *
411
+ * `executing` is set only for the synchronous duration of `fn(...)` — long
412
+ * enough to catch a factory that resolves its own token before returning (a
413
+ * synchronous self-cycle, or a synchronous mutual cycle between factories),
414
+ * but cleared the moment `fn` returns so that concurrent resolutions which
415
+ * legitimately coalesce on an in-flight result are never mistaken for a cycle.
416
+ *
417
+ * Known limitation: a factory that re-enters its own token *after* awaiting
418
+ * (an async cross-await self-reference) is indistinguishable from valid
419
+ * coalescing without async-context tracking, so that case is not caught here
420
+ * — it is documented in the factory-providers guide.
421
+ */
422
+ runFactory(descriptor, context) {
423
+ if (descriptor.executing) {
424
+ const label = descriptor.description ?? "<token>";
425
+ throw new Error(`Circular factory dependency detected: the factory for token ${label} resolved its own token while it was still being constructed. Break the cycle by restructuring the factory or resolving the dependency lazily.`);
426
+ }
427
+ descriptor.executing = true;
428
+ try {
429
+ return descriptor.fn(context);
430
+ } finally {
431
+ descriptor.executing = false;
432
+ }
433
+ }
434
+ /**
311
435
  * Format a readable representation of the resolution stack for error messages.
312
436
  */
313
437
  formatStackPath(target, resolutionStack) {
@@ -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 };