@devcraft-ts/diadem 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/CHANGELOG.md +22 -0
- package/LICENSE +21 -0
- package/README.md +306 -0
- package/dist/auto-discovery-5IV22D5D.cjs +73 -0
- package/dist/auto-discovery-5IV22D5D.cjs.map +1 -0
- package/dist/auto-discovery-RPCKK3PB.js +68 -0
- package/dist/auto-discovery-RPCKK3PB.js.map +1 -0
- package/dist/chunk-72YY5X6T.cjs +683 -0
- package/dist/chunk-72YY5X6T.cjs.map +1 -0
- package/dist/chunk-FHQRDO5C.cjs +187 -0
- package/dist/chunk-FHQRDO5C.cjs.map +1 -0
- package/dist/chunk-RTX6B4YY.js +681 -0
- package/dist/chunk-RTX6B4YY.js.map +1 -0
- package/dist/chunk-W7NA3ZZF.js +169 -0
- package/dist/chunk-W7NA3ZZF.js.map +1 -0
- package/dist/cli.cjs +821 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.js +815 -0
- package/dist/cli.js.map +1 -0
- package/dist/container-C1FFn9_4.d.cts +249 -0
- package/dist/container-C1FFn9_4.d.ts +249 -0
- package/dist/index.cjs +145 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +304 -0
- package/dist/index.d.ts +304 -0
- package/dist/index.js +71 -0
- package/dist/index.js.map +1 -0
- package/dist/setup/index.cjs +87 -0
- package/dist/setup/index.cjs.map +1 -0
- package/dist/setup/index.d.cts +81 -0
- package/dist/setup/index.d.ts +81 -0
- package/dist/setup/index.js +75 -0
- package/dist/setup/index.js.map +1 -0
- package/package.json +92 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,815 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, readdirSync } from 'fs';
|
|
3
|
+
import { resolve, dirname, join, relative } from 'path';
|
|
4
|
+
import ts from 'typescript';
|
|
5
|
+
|
|
6
|
+
var DEFAULTS = {
|
|
7
|
+
scanDirs: ["src"],
|
|
8
|
+
// Default: every .ts file. The AST pass keeps only DI-decorated classes, so
|
|
9
|
+
// `include` is purely an optional performance narrowing (e.g. ['Service\\.ts$']).
|
|
10
|
+
include: ["\\.ts$"],
|
|
11
|
+
exclude: [
|
|
12
|
+
"\\.test\\.ts$",
|
|
13
|
+
"\\.spec\\.ts$",
|
|
14
|
+
"\\.d\\.ts$",
|
|
15
|
+
"node_modules",
|
|
16
|
+
"/dist/",
|
|
17
|
+
"/build/",
|
|
18
|
+
"/generated/"
|
|
19
|
+
],
|
|
20
|
+
outFile: "src/generated/service-manifest.ts",
|
|
21
|
+
environments: ["development", "production", "test"],
|
|
22
|
+
emit: "manifest",
|
|
23
|
+
targetEnv: void 0
|
|
24
|
+
};
|
|
25
|
+
function stripUndefined(obj) {
|
|
26
|
+
return Object.fromEntries(
|
|
27
|
+
Object.entries(obj).filter(([, v]) => v !== void 0)
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
function loadConfig(rootDir, overrides = {}) {
|
|
31
|
+
let fileConfig = {};
|
|
32
|
+
const configPath = resolve(rootDir, "diadem.config.json");
|
|
33
|
+
if (existsSync(configPath)) {
|
|
34
|
+
fileConfig = JSON.parse(
|
|
35
|
+
readFileSync(configPath, "utf8")
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
const merged = {
|
|
39
|
+
...DEFAULTS,
|
|
40
|
+
...stripUndefined(fileConfig),
|
|
41
|
+
...stripUndefined(overrides)
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
rootDir,
|
|
45
|
+
scanDirs: merged.scanDirs,
|
|
46
|
+
include: merged.include.map((p) => new RegExp(p)),
|
|
47
|
+
exclude: merged.exclude.map((p) => new RegExp(p)),
|
|
48
|
+
outFile: merged.outFile,
|
|
49
|
+
environments: merged.environments,
|
|
50
|
+
emit: merged.emit,
|
|
51
|
+
targetEnv: merged.targetEnv
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
var PACKAGE_NAME = "@devcraft-ts/diadem";
|
|
55
|
+
var LIFECYCLE_BY_DECORATOR = {
|
|
56
|
+
singleton: "singleton",
|
|
57
|
+
factory: "factory",
|
|
58
|
+
lazy: "lazy",
|
|
59
|
+
lazySingleton: "lazySingleton",
|
|
60
|
+
injectable: "dependency"
|
|
61
|
+
};
|
|
62
|
+
var PRIMITIVE_TYPES = /* @__PURE__ */ new Set([
|
|
63
|
+
"string",
|
|
64
|
+
"number",
|
|
65
|
+
"boolean",
|
|
66
|
+
"Date",
|
|
67
|
+
"Array",
|
|
68
|
+
"Object",
|
|
69
|
+
"any",
|
|
70
|
+
"unknown",
|
|
71
|
+
"void",
|
|
72
|
+
"null",
|
|
73
|
+
"undefined",
|
|
74
|
+
"Promise",
|
|
75
|
+
"RegExp",
|
|
76
|
+
"Error",
|
|
77
|
+
"Map",
|
|
78
|
+
"Set"
|
|
79
|
+
]);
|
|
80
|
+
function generateManifest(config) {
|
|
81
|
+
const files = collectFiles(config);
|
|
82
|
+
const services = [];
|
|
83
|
+
for (const file of files) {
|
|
84
|
+
services.push(...analyzeFile(file.fullPath, file.relPath));
|
|
85
|
+
}
|
|
86
|
+
const { sorted, cycles, duplicateTokens } = resolveAndSort(services);
|
|
87
|
+
sorted.forEach((service, index) => {
|
|
88
|
+
service.registrationOrder = index;
|
|
89
|
+
});
|
|
90
|
+
const outFile = resolve(config.rootDir, config.outFile);
|
|
91
|
+
const content = config.emit === "compiled" ? renderCompiled(sorted, config, outFile) : renderManifest(sorted, config, outFile);
|
|
92
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
93
|
+
writeFileSync(outFile, content, "utf8");
|
|
94
|
+
const externalDependencies = sorted.reduce(
|
|
95
|
+
(sum, s) => sum + s.resolvedDependencies.filter((d) => d.external).length,
|
|
96
|
+
0
|
|
97
|
+
);
|
|
98
|
+
const unresolved = [];
|
|
99
|
+
for (const service of sorted) {
|
|
100
|
+
for (const dep of service.resolvedDependencies) {
|
|
101
|
+
if (dep.external && !dep.isOptional) {
|
|
102
|
+
unresolved.push({
|
|
103
|
+
service: service.className,
|
|
104
|
+
paramName: dep.paramName,
|
|
105
|
+
typeName: dep.typeName
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return {
|
|
111
|
+
outFile,
|
|
112
|
+
serviceCount: sorted.length,
|
|
113
|
+
cycles,
|
|
114
|
+
externalDependencies,
|
|
115
|
+
unresolved,
|
|
116
|
+
duplicateTokens
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
function collectFiles(config) {
|
|
120
|
+
const files = [];
|
|
121
|
+
for (const dir of config.scanDirs) {
|
|
122
|
+
const abs = resolve(config.rootDir, dir);
|
|
123
|
+
if (!existsSync(abs)) {
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
walk(abs, dir, config, files);
|
|
127
|
+
}
|
|
128
|
+
return files;
|
|
129
|
+
}
|
|
130
|
+
function walk(absDir, relDir, config, out) {
|
|
131
|
+
for (const entry of readdirSync(absDir, { withFileTypes: true })) {
|
|
132
|
+
const fullPath = join(absDir, entry.name);
|
|
133
|
+
const relPath = join(relDir, entry.name);
|
|
134
|
+
if (entry.isDirectory()) {
|
|
135
|
+
walk(fullPath, relPath, config, out);
|
|
136
|
+
} else if (entry.isFile() && shouldScan(relPath, config)) {
|
|
137
|
+
out.push({ fullPath, relPath });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
function shouldScan(relPath, config) {
|
|
142
|
+
const normalized = relPath.replace(/\\/g, "/");
|
|
143
|
+
if (config.exclude.some((re) => re.test(normalized))) {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
return config.include.some((re) => re.test(normalized));
|
|
147
|
+
}
|
|
148
|
+
function analyzeFile(fullPath, relPath) {
|
|
149
|
+
const source = ts.createSourceFile(
|
|
150
|
+
fullPath,
|
|
151
|
+
readFileSync(fullPath, "utf8"),
|
|
152
|
+
ts.ScriptTarget.Latest,
|
|
153
|
+
/* setParentNodes */
|
|
154
|
+
true
|
|
155
|
+
);
|
|
156
|
+
const tokenSources = /* @__PURE__ */ new Map();
|
|
157
|
+
const fileDir = dirname(fullPath);
|
|
158
|
+
const exportedClasses = /* @__PURE__ */ new Set();
|
|
159
|
+
ts.forEachChild(source, function collect(node) {
|
|
160
|
+
if (ts.isImportDeclaration(node) && node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) && ts.isStringLiteral(node.moduleSpecifier)) {
|
|
161
|
+
const specifier = node.moduleSpecifier.text;
|
|
162
|
+
const module = specifier.startsWith(".") ? { kind: "file", fullPath: resolve(fileDir, specifier) } : { kind: "bare", specifier };
|
|
163
|
+
for (const element of node.importClause.namedBindings.elements) {
|
|
164
|
+
tokenSources.set(element.name.text, module);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
if (ts.isClassDeclaration(node) && node.name && isExported(node)) {
|
|
168
|
+
exportedClasses.add(node.name.text);
|
|
169
|
+
tokenSources.set(node.name.text, { kind: "file", fullPath });
|
|
170
|
+
}
|
|
171
|
+
ts.forEachChild(node, collect);
|
|
172
|
+
});
|
|
173
|
+
const services = [];
|
|
174
|
+
ts.forEachChild(source, function visit(node) {
|
|
175
|
+
if (ts.isClassDeclaration(node)) {
|
|
176
|
+
const info = analyzeClass(node, fullPath, relPath, exportedClasses, tokenSources);
|
|
177
|
+
if (info) {
|
|
178
|
+
services.push(info);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
ts.forEachChild(node, visit);
|
|
182
|
+
});
|
|
183
|
+
return services;
|
|
184
|
+
}
|
|
185
|
+
function analyzeClass(node, fullPath, relPath, exportedClasses, tokenSources) {
|
|
186
|
+
if (!node.name) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
const decoratorInfo = findDIDecorator(node);
|
|
190
|
+
if (!decoratorInfo) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
const dependencies = analyzeConstructor(node);
|
|
194
|
+
const token = decoratorInfo.token;
|
|
195
|
+
return {
|
|
196
|
+
className: node.name.text,
|
|
197
|
+
lifecycle: decoratorInfo.lifecycle,
|
|
198
|
+
environment: decoratorInfo.environment,
|
|
199
|
+
token,
|
|
200
|
+
tokenExported: !!token && exportedClasses.has(token),
|
|
201
|
+
tokenModule: token ? tokenSources.get(token) : void 0,
|
|
202
|
+
fullPath,
|
|
203
|
+
filePath: relPath.replace(/\\/g, "/"),
|
|
204
|
+
exported: isExported(node),
|
|
205
|
+
dependencies,
|
|
206
|
+
resolvedDependencies: [],
|
|
207
|
+
registrationOrder: 0
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function findDIDecorator(node) {
|
|
211
|
+
const decorators = ts.canHaveDecorators(node) ? ts.getDecorators(node) ?? [] : [];
|
|
212
|
+
for (const decorator of decorators) {
|
|
213
|
+
const expr = decorator.expression;
|
|
214
|
+
let name = "";
|
|
215
|
+
let args;
|
|
216
|
+
if (ts.isCallExpression(expr)) {
|
|
217
|
+
name = expr.expression.getText();
|
|
218
|
+
args = expr.arguments;
|
|
219
|
+
} else if (ts.isIdentifier(expr)) {
|
|
220
|
+
name = expr.getText();
|
|
221
|
+
}
|
|
222
|
+
const lifecycle = LIFECYCLE_BY_DECORATOR[name];
|
|
223
|
+
if (!lifecycle) {
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
let token;
|
|
227
|
+
let environment;
|
|
228
|
+
if (args && args.length > 0 && ts.isIdentifier(args[0])) {
|
|
229
|
+
token = args[0].getText();
|
|
230
|
+
}
|
|
231
|
+
if (args && args.length > 1 && ts.isStringLiteral(args[1])) {
|
|
232
|
+
environment = args[1].text;
|
|
233
|
+
}
|
|
234
|
+
return { lifecycle, token, environment };
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
function analyzeConstructor(node) {
|
|
239
|
+
const ctor = node.members.find((m) => ts.isConstructorDeclaration(m));
|
|
240
|
+
if (!ctor) {
|
|
241
|
+
return [];
|
|
242
|
+
}
|
|
243
|
+
const deps = [];
|
|
244
|
+
ctor.parameters.forEach((param, index) => {
|
|
245
|
+
if (!param.type) {
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
const typeName = extractTypeName(param.type);
|
|
249
|
+
if (isPrimitive(typeName)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
const modifiers = ts.canHaveModifiers(param) ? ts.getModifiers(param) ?? [] : [];
|
|
253
|
+
deps.push({
|
|
254
|
+
paramName: param.name.getText(),
|
|
255
|
+
paramIndex: index,
|
|
256
|
+
typeName,
|
|
257
|
+
isOptional: !!param.questionToken || !!param.initializer,
|
|
258
|
+
isReadonly: modifiers.some(
|
|
259
|
+
(m) => m.kind === ts.SyntaxKind.ReadonlyKeyword
|
|
260
|
+
),
|
|
261
|
+
isPrivate: modifiers.some((m) => m.kind === ts.SyntaxKind.PrivateKeyword)
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
return deps;
|
|
265
|
+
}
|
|
266
|
+
function extractTypeName(typeNode) {
|
|
267
|
+
const raw = ts.isTypeReferenceNode(typeNode) ? typeNode.typeName.getText() : typeNode.getText();
|
|
268
|
+
return raw.replace(/<[^>]*>/g, "").replace(/\[\]/g, "").trim();
|
|
269
|
+
}
|
|
270
|
+
function isPrimitive(typeName) {
|
|
271
|
+
return PRIMITIVE_TYPES.has(typeName) || typeName.toLowerCase() === typeName || typeName.includes("<");
|
|
272
|
+
}
|
|
273
|
+
function isExported(node) {
|
|
274
|
+
const modifiers = ts.canHaveModifiers(node) ? ts.getModifiers(node) : void 0;
|
|
275
|
+
return modifiers?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword) ?? false;
|
|
276
|
+
}
|
|
277
|
+
function resolveAndSort(services) {
|
|
278
|
+
const serviceByName = /* @__PURE__ */ new Map();
|
|
279
|
+
const tokenToImpl = /* @__PURE__ */ new Map();
|
|
280
|
+
const duplicateTokens = /* @__PURE__ */ new Set();
|
|
281
|
+
for (const service of services) {
|
|
282
|
+
serviceByName.set(service.className, service);
|
|
283
|
+
if (service.token) {
|
|
284
|
+
if (tokenToImpl.has(service.token)) {
|
|
285
|
+
duplicateTokens.add(service.token);
|
|
286
|
+
} else {
|
|
287
|
+
tokenToImpl.set(service.token, service.className);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const resolveByHeuristic = (typeName) => {
|
|
292
|
+
const direct = serviceByName.get(typeName);
|
|
293
|
+
if (direct) {
|
|
294
|
+
return direct.className;
|
|
295
|
+
}
|
|
296
|
+
if (typeName.startsWith("I")) {
|
|
297
|
+
const stripped = typeName.slice(1);
|
|
298
|
+
if (serviceByName.has(stripped)) {
|
|
299
|
+
return stripped;
|
|
300
|
+
}
|
|
301
|
+
const suffixed = services.find(
|
|
302
|
+
(s) => (s.className.endsWith("Service") || s.className.endsWith("Repository")) && s.className.includes(stripped)
|
|
303
|
+
);
|
|
304
|
+
if (suffixed) {
|
|
305
|
+
return suffixed.className;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return void 0;
|
|
309
|
+
};
|
|
310
|
+
for (const service of services) {
|
|
311
|
+
service.resolvedDependencies = service.dependencies.map((dep) => {
|
|
312
|
+
const implementingService = tokenToImpl.get(dep.typeName) ?? resolveByHeuristic(dep.typeName);
|
|
313
|
+
return implementingService ? { ...dep, implementingService } : { ...dep, external: true };
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
const { sorted, cycles } = topologicalSort(services, serviceByName);
|
|
317
|
+
return { sorted, cycles, duplicateTokens: [...duplicateTokens] };
|
|
318
|
+
}
|
|
319
|
+
function topologicalSort(services, serviceByName) {
|
|
320
|
+
const visited = /* @__PURE__ */ new Set();
|
|
321
|
+
const visiting = /* @__PURE__ */ new Set();
|
|
322
|
+
const sorted = [];
|
|
323
|
+
const cycles = [];
|
|
324
|
+
const visit = (name) => {
|
|
325
|
+
if (visited.has(name)) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
if (visiting.has(name)) {
|
|
329
|
+
cycles.push(name);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
const service = serviceByName.get(name);
|
|
333
|
+
if (!service) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
visiting.add(name);
|
|
337
|
+
for (const dep of service.resolvedDependencies) {
|
|
338
|
+
if (dep.implementingService && !dep.external) {
|
|
339
|
+
visit(dep.implementingService);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
visiting.delete(name);
|
|
343
|
+
visited.add(name);
|
|
344
|
+
sorted.push(service);
|
|
345
|
+
};
|
|
346
|
+
for (const service of services) {
|
|
347
|
+
visit(service.className);
|
|
348
|
+
}
|
|
349
|
+
return { sorted, cycles };
|
|
350
|
+
}
|
|
351
|
+
function importPathFor(outFile, serviceFullPath) {
|
|
352
|
+
let rel = relative(dirname(outFile), serviceFullPath).replace(/\\/g, "/").replace(/\.ts$/, "");
|
|
353
|
+
if (!rel.startsWith(".")) {
|
|
354
|
+
rel = `./${rel}`;
|
|
355
|
+
}
|
|
356
|
+
return rel;
|
|
357
|
+
}
|
|
358
|
+
function groupByEnvironment(services, environments) {
|
|
359
|
+
const groups = { all: [] };
|
|
360
|
+
for (const env of environments) {
|
|
361
|
+
groups[env] = [];
|
|
362
|
+
}
|
|
363
|
+
for (const service of services) {
|
|
364
|
+
groups.all.push(service);
|
|
365
|
+
if (service.environment) {
|
|
366
|
+
groups[service.environment]?.push(service);
|
|
367
|
+
} else {
|
|
368
|
+
for (const env of environments) {
|
|
369
|
+
groups[env].push(service);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
return groups;
|
|
374
|
+
}
|
|
375
|
+
function toEntry(service, outFile) {
|
|
376
|
+
const entry = {
|
|
377
|
+
className: service.className,
|
|
378
|
+
importPath: importPathFor(outFile, service.fullPath),
|
|
379
|
+
lifecycle: service.lifecycle,
|
|
380
|
+
exported: service.exported,
|
|
381
|
+
filePath: service.filePath,
|
|
382
|
+
registrationOrder: service.registrationOrder,
|
|
383
|
+
dependencies: service.dependencies,
|
|
384
|
+
resolvedDependencies: service.resolvedDependencies
|
|
385
|
+
};
|
|
386
|
+
if (service.environment) {
|
|
387
|
+
entry.environment = service.environment;
|
|
388
|
+
}
|
|
389
|
+
return entry;
|
|
390
|
+
}
|
|
391
|
+
function renderManifest(services, config, outFile) {
|
|
392
|
+
const byEnv = groupByEnvironment(services, config.environments);
|
|
393
|
+
const importsByPath = /* @__PURE__ */ new Map();
|
|
394
|
+
for (const service of services) {
|
|
395
|
+
const path = importPathFor(outFile, service.fullPath);
|
|
396
|
+
const names = importsByPath.get(path) ?? /* @__PURE__ */ new Set();
|
|
397
|
+
names.add(service.className);
|
|
398
|
+
if (service.token && service.token !== service.className && service.tokenExported) {
|
|
399
|
+
names.add(service.token);
|
|
400
|
+
}
|
|
401
|
+
importsByPath.set(path, names);
|
|
402
|
+
}
|
|
403
|
+
const staticImports = [...importsByPath.keys()].sort().map((path) => {
|
|
404
|
+
const names = [...importsByPath.get(path) ?? []].sort();
|
|
405
|
+
return names.length === 1 ? `import { ${names[0]} } from '${path}'` : `import {
|
|
406
|
+
${names.join(",\n ")}
|
|
407
|
+
} from '${path}'`;
|
|
408
|
+
}).join("\n");
|
|
409
|
+
const serviceClassMapping = services.map((s) => ` ${s.className}`).join(",\n");
|
|
410
|
+
const envEntries = ["all", ...config.environments].map(
|
|
411
|
+
(env) => ` ${env}: ${JSON.stringify(byEnv[env].map((s) => toEntry(s, outFile)), null, 2)} as ServiceManifestEntry[]`
|
|
412
|
+
).join(",\n");
|
|
413
|
+
const lifecycleCounts = ["dependency", "singleton", "factory", "lazy", "lazySingleton"].map((lc) => ` ${lc}: ${services.filter((s) => s.lifecycle === lc).length}`).join(",\n");
|
|
414
|
+
const totalDependencies = services.reduce(
|
|
415
|
+
(sum, s) => sum + s.dependencies.length,
|
|
416
|
+
0
|
|
417
|
+
);
|
|
418
|
+
const externalDependencies = services.reduce(
|
|
419
|
+
(sum, s) => sum + s.resolvedDependencies.filter((d) => d.external).length,
|
|
420
|
+
0
|
|
421
|
+
);
|
|
422
|
+
const maxDepth = services.reduce(
|
|
423
|
+
(max, s) => Math.max(max, s.dependencies.length),
|
|
424
|
+
0
|
|
425
|
+
);
|
|
426
|
+
return `/**
|
|
427
|
+
* Auto-generated by \`diadem build\`. DO NOT EDIT MANUALLY.
|
|
428
|
+
*
|
|
429
|
+
* Total services: ${services.length}
|
|
430
|
+
*/
|
|
431
|
+
|
|
432
|
+
/* eslint-disable */
|
|
433
|
+
|
|
434
|
+
import type {
|
|
435
|
+
ImportedService,
|
|
436
|
+
ServiceManifestEntry
|
|
437
|
+
} from '${PACKAGE_NAME}'
|
|
438
|
+
|
|
439
|
+
${staticImports}
|
|
440
|
+
|
|
441
|
+
export const SERVICE_MANIFEST: ServiceManifestEntry[] = ${JSON.stringify(
|
|
442
|
+
services.map((s) => toEntry(s, outFile)),
|
|
443
|
+
null,
|
|
444
|
+
2
|
|
445
|
+
)} as ServiceManifestEntry[]
|
|
446
|
+
|
|
447
|
+
export const SERVICES_BY_ENVIRONMENT = {
|
|
448
|
+
${envEntries}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
export const SERVICE_CLASSES = {
|
|
452
|
+
${serviceClassMapping}
|
|
453
|
+
} as const
|
|
454
|
+
|
|
455
|
+
export function getServicesForEnvironment(
|
|
456
|
+
environment?: string
|
|
457
|
+
): ServiceManifestEntry[] {
|
|
458
|
+
if (!environment || environment === 'all') {
|
|
459
|
+
return SERVICE_MANIFEST
|
|
460
|
+
}
|
|
461
|
+
return (
|
|
462
|
+
SERVICES_BY_ENVIRONMENT[
|
|
463
|
+
environment as keyof typeof SERVICES_BY_ENVIRONMENT
|
|
464
|
+
] ?? []
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
export async function importService(entry: ServiceManifestEntry) {
|
|
469
|
+
const serviceClass =
|
|
470
|
+
SERVICE_CLASSES[entry.className as keyof typeof SERVICE_CLASSES]
|
|
471
|
+
if (!serviceClass) {
|
|
472
|
+
throw new Error(\`Service \${entry.className} not found in manifest.\`)
|
|
473
|
+
}
|
|
474
|
+
return serviceClass
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
export async function importAllServices(
|
|
478
|
+
entries: ServiceManifestEntry[]
|
|
479
|
+
): Promise<ImportedService[]> {
|
|
480
|
+
const ordered = [...entries].sort(
|
|
481
|
+
(a, b) => a.registrationOrder - b.registrationOrder
|
|
482
|
+
)
|
|
483
|
+
const results: ImportedService[] = []
|
|
484
|
+
for (const entry of ordered) {
|
|
485
|
+
const serviceClass =
|
|
486
|
+
SERVICE_CLASSES[entry.className as keyof typeof SERVICE_CLASSES]
|
|
487
|
+
if (serviceClass) {
|
|
488
|
+
results.push({ entry, serviceClass })
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return results
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
export const MANIFEST_STATS = {
|
|
495
|
+
totalServices: ${services.length},
|
|
496
|
+
environments: [${config.environments.map((e) => `'${e}'`).join(", ")}],
|
|
497
|
+
lifecycles: {
|
|
498
|
+
${lifecycleCounts}
|
|
499
|
+
},
|
|
500
|
+
dependencyAnalysis: {
|
|
501
|
+
servicesWithDependencies: ${services.filter((s) => s.dependencies.length > 0).length},
|
|
502
|
+
totalDependencies: ${totalDependencies},
|
|
503
|
+
externalDependencies: ${externalDependencies},
|
|
504
|
+
maxDependencyDepth: ${maxDepth}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
`;
|
|
508
|
+
}
|
|
509
|
+
var EAGER_LIFECYCLES = /* @__PURE__ */ new Set([
|
|
510
|
+
"singleton",
|
|
511
|
+
"dependency",
|
|
512
|
+
"lazySingleton"
|
|
513
|
+
]);
|
|
514
|
+
function localName(className) {
|
|
515
|
+
return `_${className}`;
|
|
516
|
+
}
|
|
517
|
+
function externalDefault(typeName) {
|
|
518
|
+
switch (typeName) {
|
|
519
|
+
case "string":
|
|
520
|
+
return "''";
|
|
521
|
+
case "number":
|
|
522
|
+
return "0";
|
|
523
|
+
case "boolean":
|
|
524
|
+
return "false";
|
|
525
|
+
default:
|
|
526
|
+
return "undefined";
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
function renderCompiled(allServices, config, outFile) {
|
|
530
|
+
const target = config.targetEnv;
|
|
531
|
+
const services = allServices.filter(
|
|
532
|
+
(s) => !target || !s.environment || s.environment === target
|
|
533
|
+
);
|
|
534
|
+
const selected = new Set(services.map((s) => s.className));
|
|
535
|
+
const eager = /* @__PURE__ */ new Map();
|
|
536
|
+
for (const s of services) {
|
|
537
|
+
eager.set(s.className, EAGER_LIFECYCLES.has(s.lifecycle));
|
|
538
|
+
}
|
|
539
|
+
const classNames = new Set(services.map((s) => s.className));
|
|
540
|
+
const importsByPath = /* @__PURE__ */ new Map();
|
|
541
|
+
const addImport = (path, name) => {
|
|
542
|
+
const names = importsByPath.get(path) ?? /* @__PURE__ */ new Set();
|
|
543
|
+
names.add(name);
|
|
544
|
+
importsByPath.set(path, names);
|
|
545
|
+
};
|
|
546
|
+
for (const service of services) {
|
|
547
|
+
addImport(importPathFor(outFile, service.fullPath), service.className);
|
|
548
|
+
}
|
|
549
|
+
const tokenCount = /* @__PURE__ */ new Map();
|
|
550
|
+
for (const s of services) {
|
|
551
|
+
if (s.token) {
|
|
552
|
+
tokenCount.set(s.token, (tokenCount.get(s.token) ?? 0) + 1);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
const tokenPath = (m) => m.kind === "file" ? importPathFor(outFile, m.fullPath) : m.specifier;
|
|
556
|
+
const typed = services.filter(
|
|
557
|
+
(s) => !!s.token && !!s.tokenModule && tokenCount.get(s.token) === 1 && (s.token === s.className || !classNames.has(s.token))
|
|
558
|
+
);
|
|
559
|
+
for (const s of typed) {
|
|
560
|
+
addImport(tokenPath(s.tokenModule), s.token);
|
|
561
|
+
}
|
|
562
|
+
const serviceImports = [...importsByPath.keys()].sort().map((path) => {
|
|
563
|
+
const names = [...importsByPath.get(path) ?? []].sort();
|
|
564
|
+
return names.length === 1 ? `import { ${names[0]} } from '${path}'` : `import {
|
|
565
|
+
${names.join(",\n ")}
|
|
566
|
+
} from '${path}'`;
|
|
567
|
+
}).join("\n");
|
|
568
|
+
const argExpr = (service) => {
|
|
569
|
+
const arity = service.dependencies.reduce(
|
|
570
|
+
(max, d) => Math.max(max, d.paramIndex + 1),
|
|
571
|
+
0
|
|
572
|
+
);
|
|
573
|
+
const args = Array.from({ length: arity }, () => "undefined");
|
|
574
|
+
for (const dep of service.resolvedDependencies) {
|
|
575
|
+
if (dep.external) {
|
|
576
|
+
args[dep.paramIndex] = dep.isOptional ? "undefined" : externalDefault(dep.typeName);
|
|
577
|
+
continue;
|
|
578
|
+
}
|
|
579
|
+
const impl = dep.implementingService;
|
|
580
|
+
if (impl && selected.has(impl)) {
|
|
581
|
+
args[dep.paramIndex] = eager.get(impl) ? localName(impl) : `c.resolve(token(${impl}))`;
|
|
582
|
+
} else {
|
|
583
|
+
args[dep.paramIndex] = "undefined";
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
return args.join(", ");
|
|
587
|
+
};
|
|
588
|
+
const lines = [];
|
|
589
|
+
for (const service of services) {
|
|
590
|
+
const cls = service.className;
|
|
591
|
+
if (eager.get(cls)) {
|
|
592
|
+
lines.push(` const ${localName(cls)} = new ${cls}(${argExpr(service)})`);
|
|
593
|
+
lines.push(` c.register(token(${cls}), ${localName(cls)})`);
|
|
594
|
+
} else {
|
|
595
|
+
lines.push(
|
|
596
|
+
` c.registerFactory(token(${cls}), () => new ${cls}(${argExpr(service)}))`
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const targetComment = target ? `environment: ${target}` : "environment: all";
|
|
601
|
+
const accessorBlock = typed.length === 0 ? "" : `
|
|
602
|
+
/**
|
|
603
|
+
* Type-safe accessor surface. Only registered tokens are present, each typed to
|
|
604
|
+
* its token \u2014 resolving an unregistered token is a compile error.
|
|
605
|
+
*/
|
|
606
|
+
export interface DiademServices {
|
|
607
|
+
${typed.map((s) => ` ${s.token}: ${s.token}`).join("\n")}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export function createServices(): DiademServices & {
|
|
611
|
+
readonly container: DiademContainer
|
|
612
|
+
dispose: () => Promise<void>
|
|
613
|
+
} {
|
|
614
|
+
const container = createContainer()
|
|
615
|
+
return {
|
|
616
|
+
container,
|
|
617
|
+
dispose: () => container.dispose(),
|
|
618
|
+
${typed.map(
|
|
619
|
+
(s) => ` get ${s.token}(): ${s.token} {
|
|
620
|
+
return container.resolve(${s.token})
|
|
621
|
+
}`
|
|
622
|
+
).join(",\n")}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
`;
|
|
626
|
+
return `/**
|
|
627
|
+
* Auto-generated by \`diadem build --emit=compiled\`. DO NOT EDIT MANUALLY.
|
|
628
|
+
*
|
|
629
|
+
* Straight-line wiring (${targetComment}). Total services: ${services.length}.
|
|
630
|
+
*/
|
|
631
|
+
|
|
632
|
+
/* eslint-disable */
|
|
633
|
+
|
|
634
|
+
import { DiademContainer, getDIMetadata } from '${PACKAGE_NAME}'
|
|
635
|
+
|
|
636
|
+
${serviceImports}
|
|
637
|
+
|
|
638
|
+
function token(cls: any): any {
|
|
639
|
+
const meta = getDIMetadata(cls)
|
|
640
|
+
if (!meta) {
|
|
641
|
+
throw new Error('diadem: missing DI metadata for ' + cls.name)
|
|
642
|
+
}
|
|
643
|
+
return meta.token
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/** Build a fully-wired, ready container. */
|
|
647
|
+
export function createContainer(): DiademContainer {
|
|
648
|
+
const c = new DiademContainer()
|
|
649
|
+
${lines.join("\n")}
|
|
650
|
+
c.setReady()
|
|
651
|
+
return c
|
|
652
|
+
}
|
|
653
|
+
${accessorBlock}`;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// src/cli/index.ts
|
|
657
|
+
function parseArgs(argv) {
|
|
658
|
+
const parsed = {
|
|
659
|
+
command: "build",
|
|
660
|
+
cwd: process.cwd(),
|
|
661
|
+
failOnCycle: false,
|
|
662
|
+
strict: false,
|
|
663
|
+
help: false,
|
|
664
|
+
overrides: {}
|
|
665
|
+
};
|
|
666
|
+
const scanDirs = [];
|
|
667
|
+
const include = [];
|
|
668
|
+
const exclude = [];
|
|
669
|
+
const environments = [];
|
|
670
|
+
let i = 0;
|
|
671
|
+
if (argv[i] && !argv[i].startsWith("-")) {
|
|
672
|
+
parsed.command = argv[i];
|
|
673
|
+
i++;
|
|
674
|
+
}
|
|
675
|
+
for (; i < argv.length; i++) {
|
|
676
|
+
const arg = argv[i];
|
|
677
|
+
const next = () => {
|
|
678
|
+
const value = argv[++i];
|
|
679
|
+
if (value === void 0) {
|
|
680
|
+
throw new Error(`Missing value for ${arg}`);
|
|
681
|
+
}
|
|
682
|
+
return value;
|
|
683
|
+
};
|
|
684
|
+
switch (arg) {
|
|
685
|
+
case "--scan-dir":
|
|
686
|
+
scanDirs.push(next());
|
|
687
|
+
break;
|
|
688
|
+
case "--out":
|
|
689
|
+
parsed.overrides.outFile = next();
|
|
690
|
+
break;
|
|
691
|
+
case "--include":
|
|
692
|
+
include.push(next());
|
|
693
|
+
break;
|
|
694
|
+
case "--exclude":
|
|
695
|
+
exclude.push(next());
|
|
696
|
+
break;
|
|
697
|
+
case "--env":
|
|
698
|
+
environments.push(next());
|
|
699
|
+
break;
|
|
700
|
+
case "--cwd":
|
|
701
|
+
parsed.cwd = next();
|
|
702
|
+
break;
|
|
703
|
+
case "--emit": {
|
|
704
|
+
const mode = next();
|
|
705
|
+
if (mode !== "manifest" && mode !== "compiled") {
|
|
706
|
+
throw new Error(`Invalid --emit value: ${mode} (expected manifest|compiled)`);
|
|
707
|
+
}
|
|
708
|
+
parsed.overrides.emit = mode;
|
|
709
|
+
break;
|
|
710
|
+
}
|
|
711
|
+
case "--target-env":
|
|
712
|
+
parsed.overrides.targetEnv = next();
|
|
713
|
+
break;
|
|
714
|
+
case "--fail-on-cycle":
|
|
715
|
+
parsed.failOnCycle = true;
|
|
716
|
+
break;
|
|
717
|
+
case "--strict":
|
|
718
|
+
parsed.strict = true;
|
|
719
|
+
break;
|
|
720
|
+
case "-h":
|
|
721
|
+
case "--help":
|
|
722
|
+
parsed.help = true;
|
|
723
|
+
break;
|
|
724
|
+
default:
|
|
725
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (scanDirs.length) parsed.overrides.scanDirs = scanDirs;
|
|
729
|
+
if (include.length) parsed.overrides.include = include;
|
|
730
|
+
if (exclude.length) parsed.overrides.exclude = exclude;
|
|
731
|
+
if (environments.length) parsed.overrides.environments = environments;
|
|
732
|
+
return parsed;
|
|
733
|
+
}
|
|
734
|
+
var HELP = `diadem \u2014 build-time DI manifest generator
|
|
735
|
+
|
|
736
|
+
Usage:
|
|
737
|
+
diadem build [options]
|
|
738
|
+
|
|
739
|
+
Options:
|
|
740
|
+
--scan-dir <dir> Directory to scan (repeatable). Default: src
|
|
741
|
+
--out <file> Output manifest path. Default: src/generated/service-manifest.ts
|
|
742
|
+
--include <regex> Filename include pattern (repeatable). Default: \\.ts$
|
|
743
|
+
--exclude <regex> Filename exclude pattern (repeatable).
|
|
744
|
+
--env <name> Environment to group by (repeatable). Default: development, production, test
|
|
745
|
+
--emit <mode> Output mode: manifest (default) or compiled (straight-line wiring)
|
|
746
|
+
--target-env <name> For --emit=compiled, bake in a single environment
|
|
747
|
+
--cwd <dir> Project root. Default: current directory
|
|
748
|
+
--fail-on-cycle Exit non-zero if a dependency cycle is detected
|
|
749
|
+
--strict Exit non-zero on cycles, ambiguous tokens, or required
|
|
750
|
+
dependencies with no implementing service
|
|
751
|
+
-h, --help Show this help
|
|
752
|
+
|
|
753
|
+
A diadem.config.json in the project root is merged under CLI flags.
|
|
754
|
+
`;
|
|
755
|
+
function main() {
|
|
756
|
+
const args = parseArgs(process.argv.slice(2));
|
|
757
|
+
if (args.help) {
|
|
758
|
+
process.stdout.write(HELP);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
if (args.command !== "build") {
|
|
762
|
+
process.stderr.write(`Unknown command: ${args.command}
|
|
763
|
+
|
|
764
|
+
${HELP}`);
|
|
765
|
+
process.exit(1);
|
|
766
|
+
}
|
|
767
|
+
const config = loadConfig(args.cwd, args.overrides);
|
|
768
|
+
const result = generateManifest(config);
|
|
769
|
+
process.stdout.write(
|
|
770
|
+
`diadem: wrote ${result.serviceCount} services to ${result.outFile}
|
|
771
|
+
`
|
|
772
|
+
);
|
|
773
|
+
if (result.externalDependencies > 0) {
|
|
774
|
+
process.stdout.write(
|
|
775
|
+
`diadem: ${result.externalDependencies} external dependencies (not container-managed)
|
|
776
|
+
`
|
|
777
|
+
);
|
|
778
|
+
}
|
|
779
|
+
for (const token of result.duplicateTokens) {
|
|
780
|
+
process.stderr.write(
|
|
781
|
+
`diadem: warning \u2014 token ${token} is declared by more than one service (ambiguous)
|
|
782
|
+
`
|
|
783
|
+
);
|
|
784
|
+
}
|
|
785
|
+
if (result.cycles.length > 0) {
|
|
786
|
+
process.stderr.write(
|
|
787
|
+
`diadem: warning \u2014 dependency cycle(s) detected: ${result.cycles.join(", ")}
|
|
788
|
+
`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
const cycleViolation = result.cycles.length > 0 && (args.strict || args.failOnCycle);
|
|
792
|
+
const strictViolation = args.strict && (result.unresolved.length > 0 || result.duplicateTokens.length > 0);
|
|
793
|
+
if (args.strict) {
|
|
794
|
+
for (const dep of result.unresolved) {
|
|
795
|
+
process.stderr.write(
|
|
796
|
+
`diadem: error \u2014 ${dep.service} requires ${dep.typeName} (${dep.paramName}), but no service implements it
|
|
797
|
+
`
|
|
798
|
+
);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
if (cycleViolation || strictViolation) {
|
|
802
|
+
process.exit(1);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
try {
|
|
806
|
+
main();
|
|
807
|
+
} catch (error) {
|
|
808
|
+
process.stderr.write(
|
|
809
|
+
`diadem: ${error instanceof Error ? error.message : String(error)}
|
|
810
|
+
`
|
|
811
|
+
);
|
|
812
|
+
process.exit(1);
|
|
813
|
+
}
|
|
814
|
+
//# sourceMappingURL=cli.js.map
|
|
815
|
+
//# sourceMappingURL=cli.js.map
|