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.
- package/README.md +12 -7
- package/bin/alloy.js +9 -0
- package/dist/cli.d.ts +24 -0
- package/dist/cli.js +168 -0
- package/dist/generate.d.ts +28 -0
- package/dist/generate.js +46 -0
- package/dist/lib/container.d.ts +2 -1
- package/dist/lib/container.js +5 -4
- package/dist/lib/lazy.d.ts +5 -1
- package/dist/lib/scope.d.ts +1 -0
- package/dist/lib/types.d.ts +3 -1
- package/dist/plugins/consumer-plugin.d.ts +40 -0
- package/dist/plugins/consumer-plugin.js +101 -0
- package/dist/plugins/core/codegen.js +1 -1
- package/dist/plugins/core/container-loader.js +30 -16
- package/dist/plugins/core/generation-inputs.js +41 -0
- package/dist/plugins/core/utils.js +1 -1
- package/dist/plugins/core/visualization-utils.js +1 -1
- package/dist/plugins/rollup-plugin/index.js +3 -5
- package/dist/plugins/vite-plugin/index.d.ts +2 -29
- package/dist/plugins/vite-plugin/index.js +19 -73
- package/dist/plugins/webpack-like-plugin.d.ts +32 -0
- package/dist/plugins/webpack-like-plugin.js +86 -0
- package/dist/rspack.d.ts +7 -0
- package/dist/rspack.js +10 -0
- package/dist/runtime.d.ts +4 -4
- package/dist/runtime.js +3 -3
- package/dist/scopes.d.ts +2 -1
- package/dist/scopes.js +3 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/vite.d.ts +2 -1
- package/dist/webpack.d.ts +7 -0
- package/dist/webpack.js +10 -0
- package/package.json +28 -12
- package/dist/lib/testing/mocking.d.ts +0 -12
- package/dist/lib/testing/mocking.js +0 -107
- package/dist/lib/testing/registry.js +0 -17
- package/dist/test.d.ts +0 -84
- 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
|
|
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
|
|
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
|
-
- **
|
|
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"`.
|
|
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
|
|
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
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 };
|
package/dist/generate.js
ADDED
|
@@ -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 };
|
package/dist/lib/container.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/container.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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) => {
|
package/dist/lib/lazy.d.ts
CHANGED
|
@@ -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 };
|
package/dist/lib/scope.d.ts
CHANGED
|
@@ -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;
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
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
|
|
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 };
|