alloy-di 0.1.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 +69 -0
- package/dist/lib/container.d.ts +117 -0
- package/dist/lib/container.js +266 -0
- package/dist/lib/decorators.d.ts +177 -0
- package/dist/lib/decorators.js +126 -0
- package/dist/lib/dependency-error.js +31 -0
- package/dist/lib/env-detection.js +44 -0
- package/dist/lib/lazy.d.ts +39 -0
- package/dist/lib/lazy.js +18 -0
- package/dist/lib/providers.d.ts +112 -0
- package/dist/lib/providers.js +166 -0
- package/dist/lib/scope.d.ts +8 -0
- package/dist/lib/scope.js +8 -0
- package/dist/lib/service-identifiers.d.ts +34 -0
- package/dist/lib/service-identifiers.js +54 -0
- package/dist/lib/testing/mocking.d.ts +14 -0
- package/dist/lib/testing/mocking.js +109 -0
- package/dist/lib/testing/registry.js +19 -0
- package/dist/lib/types.d.ts +22 -0
- package/dist/lib/types.js +21 -0
- package/dist/plugins/core/codegen.js +288 -0
- package/dist/plugins/core/decorators.js +90 -0
- package/dist/plugins/core/discovery-store.js +78 -0
- package/dist/plugins/core/identifier-resolver.js +22 -0
- package/dist/plugins/core/lazy.js +98 -0
- package/dist/plugins/core/scanner.js +93 -0
- package/dist/plugins/core/types.d.ts +44 -0
- package/dist/plugins/core/utils.js +45 -0
- package/dist/plugins/rollup-plugin/build-utils.js +49 -0
- package/dist/plugins/rollup-plugin/index.d.ts +30 -0
- package/dist/plugins/rollup-plugin/index.js +225 -0
- package/dist/plugins/vite-plugin/index.d.ts +25 -0
- package/dist/plugins/vite-plugin/index.js +154 -0
- package/dist/plugins/vite-plugin/manifest-utils.js +213 -0
- package/dist/rollup.d.ts +2 -0
- package/dist/rollup.js +7 -0
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +8 -0
- package/dist/test.d.ts +50 -0
- package/dist/test.js +67 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/vite.d.ts +2 -0
- package/dist/vite.js +7 -0
- package/package.json +69 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createClassKey, normalizeImportPath, walkSync } from "../core/utils.js";
|
|
2
|
+
import { IdentifierResolver } from "../core/identifier-resolver.js";
|
|
3
|
+
import { generateContainerModule, generateContainerTypeDefinition, generateManifestTypeDefinition } from "../core/codegen.js";
|
|
4
|
+
import { createDiscoveryStore } from "../core/discovery-store.js";
|
|
5
|
+
import { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateManifestServices, groupMetasByName, readManifests, reconcileLazySet, toMetaFromManifest } from "./manifest-utils.js";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import fs from "node:fs";
|
|
8
|
+
|
|
9
|
+
//#region src/plugins/vite-plugin/index.ts
|
|
10
|
+
function toLazyServiceKey(identifier) {
|
|
11
|
+
const description = identifier.description;
|
|
12
|
+
if (!description || !description.startsWith("alloy:")) throw new Error("[alloy] lazyServices entries must be serviceIdentifiers exported by Alloy manifests.");
|
|
13
|
+
return description;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Creates the Alloy Vite plugin that statically discovers injectable classes
|
|
17
|
+
* and exposes them through a virtual container module at build time.
|
|
18
|
+
*/
|
|
19
|
+
function alloy(options = {}) {
|
|
20
|
+
const virtualModuleId = "virtual:alloy-container";
|
|
21
|
+
const resolvedVirtualModuleId = "\0" + virtualModuleId;
|
|
22
|
+
const configuredProviderEntries = Array.from(options.providers ?? []);
|
|
23
|
+
const providerModuleRefs = [];
|
|
24
|
+
let resolvedRoot = process.cwd();
|
|
25
|
+
let packageName = "UNKNOWN_PACKAGE";
|
|
26
|
+
const lazyServiceKeys = new Set((options.lazyServices ?? []).map(toLazyServiceKey));
|
|
27
|
+
const discovery = createDiscoveryStore();
|
|
28
|
+
const discoveredClasses = /* @__PURE__ */ new Map();
|
|
29
|
+
const lazyReferencedClassKeys = /* @__PURE__ */ new Set();
|
|
30
|
+
const processUpdate = (id, code) => {
|
|
31
|
+
const { metas, lazyClassKeys, previousMetas, previousLazyClassKeys } = discovery.updateFile(id, code);
|
|
32
|
+
if (previousMetas) for (const meta of previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
|
|
33
|
+
for (const meta of metas) discoveredClasses.set(createClassKey(meta.filePath, meta.className), meta);
|
|
34
|
+
if (previousLazyClassKeys) for (const key of previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
|
|
35
|
+
if (lazyClassKeys.size) for (const key of lazyClassKeys) lazyReferencedClassKeys.add(key);
|
|
36
|
+
};
|
|
37
|
+
return {
|
|
38
|
+
name: "vite-plugin-alloy",
|
|
39
|
+
configResolved(config) {
|
|
40
|
+
resolvedRoot = config.root ?? process.cwd();
|
|
41
|
+
try {
|
|
42
|
+
const pkgPath = path.resolve(resolvedRoot, "package.json");
|
|
43
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
44
|
+
if (typeof pkg.name === "string") packageName = pkg.name;
|
|
45
|
+
} catch {}
|
|
46
|
+
providerModuleRefs.length = 0;
|
|
47
|
+
for (const entry of configuredProviderEntries) {
|
|
48
|
+
const absPath = path.isAbsolute(entry) ? entry : path.resolve(resolvedRoot, entry);
|
|
49
|
+
providerModuleRefs.push({
|
|
50
|
+
absPath,
|
|
51
|
+
importPath: normalizeImportPath(absPath)
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
resolveId(id) {
|
|
56
|
+
if (id === virtualModuleId) return resolvedVirtualModuleId;
|
|
57
|
+
},
|
|
58
|
+
transform(code, id) {
|
|
59
|
+
if (!/\.(tsx?|ts)$/i.test(id) || id.endsWith(".d.ts")) return null;
|
|
60
|
+
if (id.includes("node_modules")) return null;
|
|
61
|
+
processUpdate(id, code);
|
|
62
|
+
return {
|
|
63
|
+
code,
|
|
64
|
+
map: null
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
handleHotUpdate(ctx) {
|
|
68
|
+
const file = ctx.file;
|
|
69
|
+
if (!ctx.modules.length) {
|
|
70
|
+
const removed = discovery.removeFile(file);
|
|
71
|
+
if (removed.previousMetas) for (const meta of removed.previousMetas) discoveredClasses.delete(createClassKey(meta.filePath, meta.className));
|
|
72
|
+
if (removed.previousLazyClassKeys) for (const key of removed.previousLazyClassKeys) lazyReferencedClassKeys.delete(key);
|
|
73
|
+
}
|
|
74
|
+
return ctx.modules;
|
|
75
|
+
},
|
|
76
|
+
buildStart() {
|
|
77
|
+
discovery.clear();
|
|
78
|
+
discoveredClasses.clear();
|
|
79
|
+
lazyReferencedClassKeys.clear();
|
|
80
|
+
for (const ref of providerModuleRefs) this.addWatchFile(ref.absPath);
|
|
81
|
+
const files = walkSync(path.join(resolvedRoot, "src"));
|
|
82
|
+
for (const file of files) if (/\.(tsx?|ts)$/i.test(file) && !file.endsWith(".d.ts")) try {
|
|
83
|
+
processUpdate(file, fs.readFileSync(file, "utf-8"));
|
|
84
|
+
} catch {}
|
|
85
|
+
},
|
|
86
|
+
async load(id) {
|
|
87
|
+
if (id !== resolvedVirtualModuleId) return;
|
|
88
|
+
const metas = Array.from(discoveredClasses.values());
|
|
89
|
+
for (const meta of metas) {
|
|
90
|
+
const normalizedMetaPath = normalizeImportPath(meta.filePath);
|
|
91
|
+
const trimmedNormalizedMetaPath = normalizedMetaPath.replace(/^\/+/g, "");
|
|
92
|
+
const looksRootRelative = normalizedMetaPath === "/src" || normalizedMetaPath.startsWith("/src/");
|
|
93
|
+
let relPath = path.relative(resolvedRoot, meta.filePath);
|
|
94
|
+
if (path.sep === "\\") relPath = relPath.split(path.sep).join("/");
|
|
95
|
+
if (looksRootRelative || !relPath || relPath.startsWith("..") || relPath.startsWith("\\")) relPath = trimmedNormalizedMetaPath || normalizedMetaPath.replace(/^\/+/g, "");
|
|
96
|
+
meta.identifierKey = `alloy:${packageName}/${relPath}#${meta.className}`;
|
|
97
|
+
}
|
|
98
|
+
const manifestData = await readManifests(options.manifests ?? []);
|
|
99
|
+
const manifestServices = manifestData.services;
|
|
100
|
+
const loadedManifests = manifestData.loadedManifests;
|
|
101
|
+
if (metas.length && manifestServices.length) {
|
|
102
|
+
const duplicates = findDuplicateManifestServices(metas, manifestServices);
|
|
103
|
+
if (duplicates.length) {
|
|
104
|
+
const details = duplicates.map((d) => `- ${d.exportName}: local [${d.localPaths.join(", ")}] vs manifest '${d.manifestImport}'`).join("\n");
|
|
105
|
+
throw new Error([
|
|
106
|
+
"[alloy] Duplicate service registrations detected.",
|
|
107
|
+
details,
|
|
108
|
+
"Resolve by removing one source (local or manifest) to avoid ambiguous DI keys."
|
|
109
|
+
].join("\n"));
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
const combinedMetas = [...metas, ...manifestServices.map((svc) => ({
|
|
113
|
+
className: svc.exportName,
|
|
114
|
+
filePath: svc.importPath,
|
|
115
|
+
metadata: {
|
|
116
|
+
scope: svc.scope,
|
|
117
|
+
dependencies: []
|
|
118
|
+
}
|
|
119
|
+
}))];
|
|
120
|
+
const resolver = new IdentifierResolver(combinedMetas);
|
|
121
|
+
const metasByName = groupMetasByName(combinedMetas);
|
|
122
|
+
for (const svc of manifestServices) metas.push(toMetaFromManifest(svc, metasByName, resolver, lazyReferencedClassKeys));
|
|
123
|
+
const providerImports = Array.from(new Set([...providerModuleRefs.map((ref) => ref.importPath), ...manifestData.providers]));
|
|
124
|
+
reconcileLazySet(metas, lazyReferencedClassKeys, collectEagerReferencedNames(metas));
|
|
125
|
+
augmentFactoryLazyServices(metas, lazyServiceKeys);
|
|
126
|
+
const code = generateContainerModule(metas, new Set(lazyReferencedClassKeys), providerImports);
|
|
127
|
+
const dtsDir = path.resolve(resolvedRoot, options.containerDeclarationDir ?? "./src");
|
|
128
|
+
const dtsContent = generateContainerTypeDefinition(metas, (filePath) => {
|
|
129
|
+
if (path.isAbsolute(filePath)) {
|
|
130
|
+
let rel = path.relative(dtsDir, filePath);
|
|
131
|
+
rel = rel.split(path.sep).join(path.posix.sep);
|
|
132
|
+
if (!rel.startsWith(".")) rel = "./" + rel;
|
|
133
|
+
return rel;
|
|
134
|
+
}
|
|
135
|
+
return filePath;
|
|
136
|
+
});
|
|
137
|
+
if (!fs.existsSync(dtsDir)) fs.mkdirSync(dtsDir, { recursive: true });
|
|
138
|
+
const dtsPath = path.join(dtsDir, "alloy-container.d.ts");
|
|
139
|
+
fs.writeFileSync(dtsPath, dtsContent);
|
|
140
|
+
if (loadedManifests && loadedManifests.length > 0) {
|
|
141
|
+
const manifestsDts = generateManifestTypeDefinition(loadedManifests.map((m) => ({
|
|
142
|
+
packageName: m.packageName,
|
|
143
|
+
services: m.services
|
|
144
|
+
})));
|
|
145
|
+
const manifestsDtsPath = path.join(dtsDir, "alloy-manifests.d.ts");
|
|
146
|
+
fs.writeFileSync(manifestsDtsPath, manifestsDts);
|
|
147
|
+
}
|
|
148
|
+
return code;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
//#endregion
|
|
154
|
+
export { alloy };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { createClassKey, createSymbolKey, normalizeImportPath } from "../core/utils.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/plugins/vite-plugin/manifest-utils.ts
|
|
5
|
+
/**
|
|
6
|
+
* Reads a list of manifest objects and returns aggregated service + provider module specifiers.
|
|
7
|
+
*
|
|
8
|
+
* @param inputs Direct manifest objects.
|
|
9
|
+
* @returns Aggregated arrays of service descriptors and provider specifiers.
|
|
10
|
+
*/
|
|
11
|
+
async function readManifests(inputs) {
|
|
12
|
+
const services = [];
|
|
13
|
+
const providers = [];
|
|
14
|
+
const loadedManifests = [];
|
|
15
|
+
const manifestSchema = z.object({
|
|
16
|
+
schemaVersion: z.number().optional(),
|
|
17
|
+
packageName: z.string(),
|
|
18
|
+
services: z.array(z.object({
|
|
19
|
+
importPath: z.string(),
|
|
20
|
+
exportName: z.string(),
|
|
21
|
+
symbolKey: z.string(),
|
|
22
|
+
scope: z.enum(["singleton", "transient"]),
|
|
23
|
+
deps: z.array(z.string()).default([]),
|
|
24
|
+
tokenDeps: z.array(z.object({
|
|
25
|
+
exportName: z.string(),
|
|
26
|
+
importPath: z.string()
|
|
27
|
+
})).default([]),
|
|
28
|
+
lazyDeps: z.array(z.object({
|
|
29
|
+
importPath: z.string(),
|
|
30
|
+
exportName: z.string(),
|
|
31
|
+
retry: z.object({
|
|
32
|
+
retries: z.number(),
|
|
33
|
+
backoffMs: z.number().optional(),
|
|
34
|
+
factor: z.number().optional()
|
|
35
|
+
}).optional()
|
|
36
|
+
})).default([])
|
|
37
|
+
})).default([]),
|
|
38
|
+
providers: z.array(z.string()).default([])
|
|
39
|
+
});
|
|
40
|
+
for (const manifest of inputs) {
|
|
41
|
+
const parsed = manifestSchema.safeParse(manifest);
|
|
42
|
+
if (!parsed.success) continue;
|
|
43
|
+
loadedManifests.push({
|
|
44
|
+
packageName: parsed.data.packageName,
|
|
45
|
+
services: parsed.data.services,
|
|
46
|
+
providers: parsed.data.providers
|
|
47
|
+
});
|
|
48
|
+
for (const svc of parsed.data.services) services.push(svc);
|
|
49
|
+
for (const p of parsed.data.providers) providers.push(p);
|
|
50
|
+
}
|
|
51
|
+
return Promise.resolve({
|
|
52
|
+
services,
|
|
53
|
+
providers,
|
|
54
|
+
loadedManifests
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Groups metas by class name to support resolving dependencies that reference classes with collisions.
|
|
59
|
+
*
|
|
60
|
+
* @param metas Metas to index.
|
|
61
|
+
* @returns Map className -> list of metas sharing that name.
|
|
62
|
+
*/
|
|
63
|
+
function groupMetasByName(metas) {
|
|
64
|
+
const byName = /* @__PURE__ */ new Map();
|
|
65
|
+
for (const m of metas) {
|
|
66
|
+
const list = byName.get(m.className) ?? [];
|
|
67
|
+
list.push(m);
|
|
68
|
+
byName.set(m.className, list);
|
|
69
|
+
}
|
|
70
|
+
return byName;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Chooses the best meta for a dependency name when there are naming collisions.
|
|
74
|
+
* Preference order:
|
|
75
|
+
* 1. Exact unique match
|
|
76
|
+
* 2. First meta from same package scope (prefix before first slash)
|
|
77
|
+
* 3. Fallback to first candidate
|
|
78
|
+
*/
|
|
79
|
+
function selectMetaForDep(metasByName, depName, currentImportPath) {
|
|
80
|
+
const candidates = metasByName.get(depName);
|
|
81
|
+
if (!candidates || candidates.length === 0) return;
|
|
82
|
+
if (candidates.length === 1) return candidates[0];
|
|
83
|
+
const scopePrefix = currentImportPath.split("/")[0];
|
|
84
|
+
return candidates.find((c) => c.filePath.startsWith(scopePrefix)) ?? candidates[0];
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Converts a manifest service descriptor into a DiscoveredMeta ready for container codegen.
|
|
88
|
+
* Handles:
|
|
89
|
+
* - Eager dependency identifier resolution (with aliasing)
|
|
90
|
+
* - Token dependencies inclusion
|
|
91
|
+
* - Generation of Lazy(...) expressions for lazyDeps (with optional retry config)
|
|
92
|
+
* - Recording lazy dependency keys into provided lazySet
|
|
93
|
+
*
|
|
94
|
+
* @param svc Manifest descriptor
|
|
95
|
+
* @param metasByName Grouped metas for duplicate resolution
|
|
96
|
+
* @param resolver Collision-aware identifier resolver
|
|
97
|
+
* @param lazySet Target set tracking lazy-only class keys
|
|
98
|
+
*/
|
|
99
|
+
function toMetaFromManifest(svc, metasByName, resolver, lazySet) {
|
|
100
|
+
const deps = [];
|
|
101
|
+
const referencedImports = [];
|
|
102
|
+
for (const depName of svc.deps ?? []) {
|
|
103
|
+
const targetMeta = selectMetaForDep(metasByName, depName, svc.importPath);
|
|
104
|
+
if (targetMeta) {
|
|
105
|
+
const expression = resolver.resolve(targetMeta.className, targetMeta.filePath);
|
|
106
|
+
deps.push({
|
|
107
|
+
expression,
|
|
108
|
+
referencedIdentifiers: [expression],
|
|
109
|
+
isLazy: false
|
|
110
|
+
});
|
|
111
|
+
} else deps.push({
|
|
112
|
+
expression: depName,
|
|
113
|
+
referencedIdentifiers: [depName],
|
|
114
|
+
isLazy: false
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(svc.tokenDeps)) for (const tok of svc.tokenDeps) {
|
|
118
|
+
deps.push({
|
|
119
|
+
expression: tok.exportName,
|
|
120
|
+
referencedIdentifiers: [tok.exportName],
|
|
121
|
+
isLazy: false
|
|
122
|
+
});
|
|
123
|
+
referencedImports.push({
|
|
124
|
+
name: tok.exportName,
|
|
125
|
+
path: tok.importPath,
|
|
126
|
+
originalName: tok.exportName
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
for (const lazy of svc.lazyDeps ?? []) {
|
|
130
|
+
const importer = `() => import('${lazy.importPath}').then(m => m.${lazy.exportName})`;
|
|
131
|
+
let expr;
|
|
132
|
+
if (lazy.retry) {
|
|
133
|
+
const opts = [`retries: ${lazy.retry.retries}`];
|
|
134
|
+
if (typeof lazy.retry.backoffMs === "number") opts.push(`backoffMs: ${lazy.retry.backoffMs}`);
|
|
135
|
+
if (typeof lazy.retry.factor === "number") opts.push(`factor: ${lazy.retry.factor}`);
|
|
136
|
+
expr = `Lazy(${importer}, { ${opts.join(", ")} })`;
|
|
137
|
+
} else expr = `Lazy(${importer})`;
|
|
138
|
+
deps.push({
|
|
139
|
+
expression: expr,
|
|
140
|
+
referencedIdentifiers: [],
|
|
141
|
+
isLazy: true
|
|
142
|
+
});
|
|
143
|
+
lazySet.add(createClassKey(lazy.importPath, lazy.exportName));
|
|
144
|
+
}
|
|
145
|
+
const metadata = {
|
|
146
|
+
scope: svc.scope,
|
|
147
|
+
dependencies: deps
|
|
148
|
+
};
|
|
149
|
+
return {
|
|
150
|
+
className: svc.exportName,
|
|
151
|
+
filePath: svc.importPath,
|
|
152
|
+
identifierKey: svc.symbolKey,
|
|
153
|
+
metadata,
|
|
154
|
+
referencedImports
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Extracts class names referenced eagerly (not wrapped in Lazy) from meta metadata blocks.
|
|
159
|
+
* This allows distinguishing services that must remain in static import set.
|
|
160
|
+
*
|
|
161
|
+
* @param metas All metas to scan.
|
|
162
|
+
* @returns Set of class names with at least one eager reference.
|
|
163
|
+
*/
|
|
164
|
+
function collectEagerReferencedNames(metas) {
|
|
165
|
+
const eager = /* @__PURE__ */ new Set();
|
|
166
|
+
for (const meta of metas) for (const dep of meta.metadata.dependencies) if (!dep.isLazy) for (const id of dep.referencedIdentifiers) eager.add(id);
|
|
167
|
+
return eager;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Removes any service key from lazySet if the service is also referenced eagerly.
|
|
171
|
+
* A service is only lazy-only if ALL references are lazy.
|
|
172
|
+
*/
|
|
173
|
+
function reconcileLazySet(metas, lazySet, eagerNames) {
|
|
174
|
+
for (const meta of metas) if (eagerNames.has(meta.className)) {
|
|
175
|
+
const key = createClassKey(meta.filePath, meta.className);
|
|
176
|
+
lazySet.delete(key);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Adds a factory-lazy wrapper to services configured via plugin option `lazyServices`.
|
|
181
|
+
* Injects a `factory: Lazy(() => import(...))` property into the metadata.
|
|
182
|
+
* Safely skips metas already containing a factory property.
|
|
183
|
+
*/
|
|
184
|
+
function augmentFactoryLazyServices(metas, lazyServiceKeys) {
|
|
185
|
+
for (const m of metas) {
|
|
186
|
+
const identifierKey = m.identifierKey ?? createSymbolKey(m.filePath, m.className);
|
|
187
|
+
if (!lazyServiceKeys.has(identifierKey)) continue;
|
|
188
|
+
if (m.metadata.factory) continue;
|
|
189
|
+
const rawPath = m.filePath.startsWith("/@") ? m.filePath.slice(1) : m.filePath;
|
|
190
|
+
const importPath = !/^(\.|\/|[A-Za-z]:\\)/.test(rawPath) ? rawPath : normalizeImportPath(m.filePath);
|
|
191
|
+
const factoryExpr = `Lazy(() => import('${importPath.startsWith("/@") ? importPath.slice(1) : importPath}').then(m => m.${m.className}))`;
|
|
192
|
+
m.metadata.factory = {
|
|
193
|
+
expression: factoryExpr,
|
|
194
|
+
referencedIdentifiers: [],
|
|
195
|
+
isLazy: true
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Detects duplicate service registrations between locally discovered metas and ingested manifests.
|
|
201
|
+
* Returns structured info for error reporting.
|
|
202
|
+
*/
|
|
203
|
+
function findDuplicateManifestServices(localMetas, manifestServices) {
|
|
204
|
+
const discoveredNames = new Set(localMetas.map((m) => m.className));
|
|
205
|
+
return manifestServices.filter((svc) => discoveredNames.has(svc.exportName)).map((d) => ({
|
|
206
|
+
exportName: d.exportName,
|
|
207
|
+
localPaths: localMetas.filter((m) => m.className === d.exportName).map((m) => normalizeImportPath(m.filePath)),
|
|
208
|
+
manifestImport: d.importPath
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
//#endregion
|
|
213
|
+
export { augmentFactoryLazyServices, collectEagerReferencedNames, findDuplicateManifestServices, groupMetasByName, readManifests, reconcileLazySet, toMetaFromManifest };
|
package/dist/rollup.d.ts
ADDED
package/dist/rollup.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Newable, Token, createToken } from "./lib/types.js";
|
|
2
|
+
import { ServiceIdentifier, clearServiceIdentifierRegistry, getConstructorByIdentifier, getServiceIdentifier, registerServiceIdentifier } from "./lib/service-identifiers.js";
|
|
3
|
+
import { Container } from "./lib/container.js";
|
|
4
|
+
import { LAZY_IDENTIFIER, Lazy } from "./lib/lazy.js";
|
|
5
|
+
import { Injectable, Singleton, assertDeps, dependenciesRegistry, deps } from "./lib/decorators.js";
|
|
6
|
+
import { ProviderDefinitions, applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
7
|
+
export { Container, Injectable, LAZY_IDENTIFIER, Lazy, type Lazy as LazyInterface, type Newable, type ProviderDefinitions, type ServiceIdentifier, Singleton, type Token, applyProviders, asClass, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier };
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { createToken } from "./lib/types.js";
|
|
2
|
+
import { LAZY_IDENTIFIER, Lazy } from "./lib/lazy.js";
|
|
3
|
+
import { Injectable, Singleton, assertDeps, dependenciesRegistry, deps } from "./lib/decorators.js";
|
|
4
|
+
import { clearServiceIdentifierRegistry, getConstructorByIdentifier, getServiceIdentifier, registerServiceIdentifier } from "./lib/service-identifiers.js";
|
|
5
|
+
import { Container } from "./lib/container.js";
|
|
6
|
+
import { applyProviders, asClass, asLazyClass, asValue, defineProviders, lifecycle } from "./lib/providers.js";
|
|
7
|
+
|
|
8
|
+
export { Container, Injectable, LAZY_IDENTIFIER, Lazy, Singleton, applyProviders, asClass, asLazyClass, asValue, assertDeps, clearServiceIdentifierRegistry, createToken, defineProviders, dependenciesRegistry, deps, getConstructorByIdentifier, getServiceIdentifier, lifecycle, registerServiceIdentifier };
|
package/dist/test.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Newable, Token, createToken } from "./lib/types.js";
|
|
2
|
+
import { ServiceIdentifier } from "./lib/service-identifiers.js";
|
|
3
|
+
import { Container } from "./lib/container.js";
|
|
4
|
+
import { ProviderDefinitions } from "./lib/providers.js";
|
|
5
|
+
import { MockOf } from "./lib/testing/mocking.js";
|
|
6
|
+
import { vi } from "vitest";
|
|
7
|
+
|
|
8
|
+
//#region src/test.d.ts
|
|
9
|
+
interface OverrideSpec {
|
|
10
|
+
/** Class constructor instance overrides */
|
|
11
|
+
instances?: Array<[Newable<unknown>, unknown]>;
|
|
12
|
+
/** Token value overrides */
|
|
13
|
+
tokens?: Array<[Token<unknown>, unknown]>;
|
|
14
|
+
}
|
|
15
|
+
interface CreateTestContainerOptions {
|
|
16
|
+
overrides?: OverrideSpec;
|
|
17
|
+
autoMock?: boolean;
|
|
18
|
+
target?: Newable<unknown>;
|
|
19
|
+
providers?: ProviderDefinitions | ProviderDefinitions[];
|
|
20
|
+
}
|
|
21
|
+
interface TestContainerHandle {
|
|
22
|
+
container: Container;
|
|
23
|
+
get<T>(target: Newable<T> | ServiceIdentifier<T>): Promise<T>;
|
|
24
|
+
getIdentifier?<T>(ctor: Newable<T>): ServiceIdentifier<T>;
|
|
25
|
+
/** Retrieve a token value via a synthetic classless access */
|
|
26
|
+
getToken<T>(token: Token<T>): T;
|
|
27
|
+
/** Provide a token value into the container */
|
|
28
|
+
provideToken?<T>(token: Token<T>, value: T): void;
|
|
29
|
+
/** Placeholder restore hook (future phases may implement overlay stacks). */
|
|
30
|
+
restore(): void;
|
|
31
|
+
/** Retrieve a single class mock (if autoMock enabled). */
|
|
32
|
+
getMock?<T>(ctor: Newable<T>): MockOf<T> | undefined;
|
|
33
|
+
/** Retrieve multiple class mocks preserving tuple order. */
|
|
34
|
+
getMocks?<T extends readonly Newable<unknown>[]>(ctors: T): { [K in keyof T]: T[K] extends Newable<infer I> ? MockOf<I> | undefined : never };
|
|
35
|
+
/** Convenience: get a specific method spy from a mock. */
|
|
36
|
+
spyOf?<T>(ctor: Newable<T>, method: Extract<keyof T, string>): ReturnType<typeof vi.fn> | undefined;
|
|
37
|
+
/** Convenience: reset all mock spies (calls vi.fn().mockReset()). */
|
|
38
|
+
clearMockSpies?(): void;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Create a test-focused container with manual overrides.
|
|
42
|
+
* - Does not perform auto-mocking (Phase 2 will expand this).
|
|
43
|
+
*/
|
|
44
|
+
declare function createTestContainer(opts?: CreateTestContainerOptions | OverrideSpec): TestContainerHandle & {
|
|
45
|
+
getMock<T>(ctor: Newable<T>): MockOf<T> | undefined;
|
|
46
|
+
getMocks<T extends readonly Newable<unknown>[]>(ctors: T): { [K in keyof T]: T[K] extends Newable<infer I> ? MockOf<I> | undefined : never };
|
|
47
|
+
provideToken<T>(token: Token<T>, value: T): void;
|
|
48
|
+
};
|
|
49
|
+
//#endregion
|
|
50
|
+
export { CreateTestContainerOptions, type MockOf, OverrideSpec, TestContainerHandle, createTestContainer, createToken };
|
package/dist/test.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { createToken } from "./lib/types.js";
|
|
2
|
+
import { Container } from "./lib/container.js";
|
|
3
|
+
import { applyProviders } from "./lib/providers.js";
|
|
4
|
+
import { restoreRegistry, snapshotRegistry } from "./lib/testing/registry.js";
|
|
5
|
+
import { applyAutoMocks } from "./lib/testing/mocking.js";
|
|
6
|
+
|
|
7
|
+
//#region src/test.ts
|
|
8
|
+
/**
|
|
9
|
+
* Create a test-focused container with manual overrides.
|
|
10
|
+
* - Does not perform auto-mocking (Phase 2 will expand this).
|
|
11
|
+
*/
|
|
12
|
+
function createTestContainer(opts) {
|
|
13
|
+
const isLegacy = !!opts && !("autoMock" in opts) && !("target" in opts) && !("overrides" in opts);
|
|
14
|
+
const normalizedOpts = isLegacy ? { overrides: opts } : opts ?? {};
|
|
15
|
+
const overrides = normalizedOpts.overrides ?? (isLegacy ? opts : void 0);
|
|
16
|
+
const container = new Container();
|
|
17
|
+
const snapshot = snapshotRegistry();
|
|
18
|
+
if (normalizedOpts.providers) applyProviders(container, normalizedOpts.providers);
|
|
19
|
+
for (const [tok, value] of overrides?.tokens ?? []) container.provideValue(tok, value);
|
|
20
|
+
const overriddenCtors = new Set(overrides?.instances?.map(([c]) => c) ?? []);
|
|
21
|
+
for (const [ctor, instance] of overrides?.instances ?? []) container.overrideInstance(ctor, instance);
|
|
22
|
+
let mocks;
|
|
23
|
+
let lazyPatches;
|
|
24
|
+
if (normalizedOpts.autoMock && normalizedOpts.target) {
|
|
25
|
+
const auto = applyAutoMocks({
|
|
26
|
+
target: normalizedOpts.target,
|
|
27
|
+
container,
|
|
28
|
+
overridesCtors: overriddenCtors
|
|
29
|
+
});
|
|
30
|
+
mocks = auto.mocks;
|
|
31
|
+
lazyPatches = auto.lazyPatches;
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
container,
|
|
35
|
+
get: (target) => typeof target === "symbol" ? container.getByIdentifier(target) : container.get(target),
|
|
36
|
+
getIdentifier: (ctor) => container.getIdentifier(ctor),
|
|
37
|
+
getToken: (token) => {
|
|
38
|
+
return container.getToken(token);
|
|
39
|
+
},
|
|
40
|
+
provideToken: (token, value) => {
|
|
41
|
+
container.provideValue(token, value);
|
|
42
|
+
},
|
|
43
|
+
getMock: (ctor) => {
|
|
44
|
+
return mocks?.get(ctor) ?? void 0;
|
|
45
|
+
},
|
|
46
|
+
getMocks: (ctors) => {
|
|
47
|
+
return ctors.map((c) => mocks?.get(c));
|
|
48
|
+
},
|
|
49
|
+
spyOf: (ctor, method) => {
|
|
50
|
+
return (mocks?.get(ctor))?.spies[method];
|
|
51
|
+
},
|
|
52
|
+
clearMockSpies: () => {
|
|
53
|
+
if (!mocks) return;
|
|
54
|
+
for (const [, m] of mocks) {
|
|
55
|
+
const spies = m.spies;
|
|
56
|
+
for (const key of Object.keys(spies)) spies[key].mockReset();
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
restore: () => {
|
|
60
|
+
for (const patch of lazyPatches ?? []) patch.lazy.importer = patch.originalImporter;
|
|
61
|
+
restoreRegistry(snapshot);
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//#endregion
|
|
67
|
+
export { createTestContainer, createToken };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"root":["../src/alloy-container.d.ts","../src/entry-points.test.ts","../src/rollup.ts","../src/runtime.ts","../src/test.ts","../src/vite.ts","../src/lib/container.identifiers.test.ts","../src/lib/container.internals.test.ts","../src/lib/container.test.ts","../src/lib/container.testing-features.test.ts","../src/lib/container.ts","../src/lib/decorators.runtime.test.ts","../src/lib/decorators.test-d.ts","../src/lib/decorators.ts","../src/lib/dependency-error.ts","../src/lib/env-detection.test.ts","../src/lib/env-detection.ts","../src/lib/lazy-retry.test.ts","../src/lib/lazy.ts","../src/lib/providers.test.ts","../src/lib/providers.ts","../src/lib/scope.ts","../src/lib/service-identifiers.test.ts","../src/lib/service-identifiers.ts","../src/lib/tokens.test.ts","../src/lib/types.ts","../src/lib/testing/mocking.test.ts","../src/lib/testing/mocking.ts","../src/lib/testing/registry.test.ts","../src/lib/testing/registry.ts","../src/plugins/core/codegen.test.ts","../src/plugins/core/codegen.ts","../src/plugins/core/decorators.helpers.test.ts","../src/plugins/core/decorators.ts","../src/plugins/core/discovery-store.ts","../src/plugins/core/identifier-resolver.test.ts","../src/plugins/core/identifier-resolver.ts","../src/plugins/core/lazy-utils.ts","../src/plugins/core/lazy.helpers.test.ts","../src/plugins/core/lazy.ts","../src/plugins/core/scanner.test.ts","../src/plugins/core/scanner.ts","../src/plugins/core/types.ts","../src/plugins/core/utils.ts","../src/plugins/rollup-plugin/build-utils.ts","../src/plugins/rollup-plugin/index.ts","../src/plugins/rollup-plugin/rollup-plugin.test.ts","../src/plugins/vite-plugin/codegen.specifier.test.ts","../src/plugins/vite-plugin/duplicate-registrations.test.ts","../src/plugins/vite-plugin/fixture-subpaths.test.ts","../src/plugins/vite-plugin/index.ts","../src/plugins/vite-plugin/lazy-services.test.ts","../src/plugins/vite-plugin/lifecycle-and-hmr.test.ts","../src/plugins/vite-plugin/manifest-utils.ts","../src/plugins/vite-plugin/manifest-utils.validation.test.ts","../src/plugins/vite-plugin/module-generation.test.ts","../src/plugins/vite-plugin/transform-guards.test.ts"],"version":"5.9.3"}
|
package/dist/vite.d.ts
ADDED
package/dist/vite.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "alloy-di",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A compile-time dependency injection plugin for Vite",
|
|
5
|
+
"module": "./dist/index.mjs",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "rimraf dist && rolldown -c",
|
|
10
|
+
"dev": "rolldown -c rolldown.config.ts --watch",
|
|
11
|
+
"format": "prettier -u --write ./",
|
|
12
|
+
"format:check": "prettier -u --check ./",
|
|
13
|
+
"lint": "oxlint --fix --type-aware --config ./.oxlintrc.json .",
|
|
14
|
+
"lint:check": "oxlint --type-aware --config ./.oxlintrc.json .",
|
|
15
|
+
"test": "vitest --run",
|
|
16
|
+
"test:cover": "vitest --run --coverage",
|
|
17
|
+
"test:watch": "vitest",
|
|
18
|
+
"typecheck": "tsc -b . --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
"./runtime": {
|
|
22
|
+
"types": "./dist/runtime.d.ts",
|
|
23
|
+
"import": "./dist/runtime.js"
|
|
24
|
+
},
|
|
25
|
+
"./vite": {
|
|
26
|
+
"types": "./dist/vite.d.ts",
|
|
27
|
+
"import": "./dist/vite.js"
|
|
28
|
+
},
|
|
29
|
+
"./rollup": {
|
|
30
|
+
"types": "./dist/rollup.d.ts",
|
|
31
|
+
"import": "./dist/rollup.js"
|
|
32
|
+
},
|
|
33
|
+
"./test": {
|
|
34
|
+
"types": "./dist/test.d.ts",
|
|
35
|
+
"import": "./dist/test.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"keywords": [
|
|
39
|
+
"vite",
|
|
40
|
+
"vite-plugin",
|
|
41
|
+
"di",
|
|
42
|
+
"dependency-injection",
|
|
43
|
+
"typescript"
|
|
44
|
+
],
|
|
45
|
+
"author": "Mikael Selander",
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"README.md"
|
|
50
|
+
],
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@rollup/plugin-typescript": "^12.3.0",
|
|
53
|
+
"rimraf": "^6.1.0",
|
|
54
|
+
"rolldown": "1.0.0-beta.52",
|
|
55
|
+
"rolldown-plugin-dts": "^0.18.1"
|
|
56
|
+
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"tslib": "^2.8.1",
|
|
59
|
+
"zod": "^4.1.13"
|
|
60
|
+
},
|
|
61
|
+
"peerDependencies": {
|
|
62
|
+
"vitest": ">=4.0.14 <5.0.0"
|
|
63
|
+
},
|
|
64
|
+
"peerDependenciesMeta": {
|
|
65
|
+
"vitest": {
|
|
66
|
+
"optional": true
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|