@djodjonx/neosyringe-core 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.
@@ -0,0 +1,431 @@
1
+ import * as ts from "typescript";
2
+ import * as crypto from "node:crypto";
3
+ import * as path from "node:path";
4
+
5
+ //#region src/analyzer/Analyzer.ts
6
+ /**
7
+ * Generates a unique, deterministic Token ID for a symbol.
8
+ * Uses the symbol name and a short hash of its relative file path.
9
+ */
10
+ function generateTokenId(symbol, sourceFile) {
11
+ const name = symbol.getName();
12
+ let relativePath = path.relative(process.cwd(), sourceFile.fileName);
13
+ relativePath = relativePath.split(path.sep).join("/");
14
+ return `${name}_${crypto.createHash("md5").update(relativePath).digest("hex").substring(0, 8)}`;
15
+ }
16
+ /**
17
+ * Analyzes TypeScript source code to extract the dependency injection graph.
18
+ *
19
+ * This class uses the TypeScript Compiler API to:
20
+ * 1. Locate `createContainer` calls.
21
+ * 2. Parse the fluent chain of `bind` and `register` calls.
22
+ * 3. Resolve symbols and types for services and their dependencies.
23
+ * 4. Build a `DependencyGraph` representing the system.
24
+ */
25
+ var Analyzer = class {
26
+ program;
27
+ checker;
28
+ /** Set of variable names that are parent containers (should not be added to main graph) */
29
+ parentContainerNames = /* @__PURE__ */ new Set();
30
+ /**
31
+ * Creates a new Analyzer instance.
32
+ * @param program - The TypeScript Program instance containing the source files to analyze.
33
+ */
34
+ constructor(program) {
35
+ this.program = program;
36
+ this.checker = program.getTypeChecker();
37
+ }
38
+ /**
39
+ * Extracts the dependency graph from the program's source files.
40
+ *
41
+ * It scans all non-declaration source files for container configurations.
42
+ *
43
+ * @returns A `DependencyGraph` containing all registered services and their dependencies.
44
+ */
45
+ extract() {
46
+ const graph = {
47
+ nodes: /* @__PURE__ */ new Map(),
48
+ roots: [],
49
+ buildArguments: []
50
+ };
51
+ for (const sourceFile of this.program.getSourceFiles()) {
52
+ if (sourceFile.isDeclarationFile) continue;
53
+ this.identifyParentContainers(sourceFile);
54
+ }
55
+ for (const sourceFile of this.program.getSourceFiles()) {
56
+ if (sourceFile.isDeclarationFile) continue;
57
+ this.visitNode(sourceFile, graph);
58
+ }
59
+ this.resolveAllDependencies(graph);
60
+ return graph;
61
+ }
62
+ /**
63
+ * First pass: identify containers used as parents so we can skip them in visitNode.
64
+ */
65
+ identifyParentContainers(node) {
66
+ if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "useContainer" && ts.isIdentifier(node.initializer)) this.parentContainerNames.add(node.initializer.text);
67
+ ts.forEachChild(node, (child) => this.identifyParentContainers(child));
68
+ }
69
+ /**
70
+ * Visits an AST node to find container calls.
71
+ * @param node - The AST node to visit.
72
+ * @param graph - The graph to populate.
73
+ */
74
+ visitNode(node, graph) {
75
+ if (ts.isCallExpression(node)) {
76
+ if (this.isDefineBuilderConfigCall(node)) {
77
+ const parent = node.parent;
78
+ if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
79
+ if (this.parentContainerNames.has(parent.name.text)) return;
80
+ graph.exportedVariableName = parent.name.text;
81
+ let current = parent;
82
+ while (current && !ts.isVariableStatement(current)) current = current.parent;
83
+ if (current && ts.isVariableStatement(current)) graph.variableStatementStart = current.getStart();
84
+ }
85
+ graph.defineBuilderConfigStart = node.getStart();
86
+ graph.defineBuilderConfigEnd = node.getEnd();
87
+ this.parseBuilderConfig(node, graph);
88
+ }
89
+ }
90
+ ts.forEachChild(node, (child) => this.visitNode(child, graph));
91
+ }
92
+ isDefineBuilderConfigCall(node) {
93
+ const expression = node.expression;
94
+ if (ts.isIdentifier(expression)) return expression.text === "defineBuilderConfig";
95
+ return false;
96
+ }
97
+ parseBuilderConfig(node, graph) {
98
+ const args = node.arguments;
99
+ if (args.length < 1) return;
100
+ const configObj = args[0];
101
+ if (!ts.isObjectLiteralExpression(configObj)) return;
102
+ const nameProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "name");
103
+ if (nameProp && ts.isPropertyAssignment(nameProp) && ts.isStringLiteral(nameProp.initializer)) graph.containerName = nameProp.initializer.text;
104
+ const injectionsProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "injections");
105
+ if (injectionsProp && ts.isPropertyAssignment(injectionsProp) && ts.isArrayLiteralExpression(injectionsProp.initializer)) this.parseInjectionsArray(injectionsProp.initializer, graph);
106
+ const extendsProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "extends");
107
+ if (extendsProp && ts.isPropertyAssignment(extendsProp) && ts.isArrayLiteralExpression(extendsProp.initializer)) this.parseExtendsArray(extendsProp.initializer, graph);
108
+ const useContainerProp = configObj.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === "useContainer");
109
+ if (useContainerProp && ts.isPropertyAssignment(useContainerProp)) {
110
+ if (!graph.legacyContainers) graph.legacyContainers = [];
111
+ if (!graph.parentProvidedTokens) graph.parentProvidedTokens = /* @__PURE__ */ new Set();
112
+ const containerExpr = useContainerProp.initializer;
113
+ if (ts.isIdentifier(containerExpr)) {
114
+ graph.legacyContainers.push(containerExpr.text);
115
+ this.extractParentContainerTokens(containerExpr, graph);
116
+ }
117
+ }
118
+ }
119
+ /**
120
+ * Extracts tokens provided by a parent container.
121
+ * Handles both NeoSyringe containers (defineBuilderConfig) and
122
+ * declared legacy containers (declareContainerTokens).
123
+ */
124
+ extractParentContainerTokens(containerIdentifier, graph) {
125
+ const symbol = this.checker.getSymbolAtLocation(containerIdentifier);
126
+ if (!symbol) return;
127
+ this.parentContainerNames.add(containerIdentifier.text);
128
+ const resolvedSymbol = this.resolveSymbol(symbol);
129
+ const declaration = resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0];
130
+ if (!declaration) return;
131
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
132
+ const init = declaration.initializer;
133
+ if (ts.isCallExpression(init)) {
134
+ if (this.isDefineBuilderConfigCall(init) || this.isDefinePartialConfigCall(init)) {
135
+ const parentGraph = {
136
+ nodes: /* @__PURE__ */ new Map(),
137
+ roots: []
138
+ };
139
+ this.parseBuilderConfig(init, parentGraph);
140
+ for (const tokenId of parentGraph.nodes.keys()) graph.parentProvidedTokens.add(tokenId);
141
+ if (parentGraph.parentProvidedTokens) for (const tokenId of parentGraph.parentProvidedTokens) graph.parentProvidedTokens.add(tokenId);
142
+ return;
143
+ }
144
+ if (this.isDeclareContainerTokensCall(init)) {
145
+ this.extractDeclaredTokens(init, graph);
146
+ return;
147
+ }
148
+ }
149
+ }
150
+ }
151
+ /**
152
+ * Checks if a call expression is declareContainerTokens<T>().
153
+ */
154
+ isDeclareContainerTokensCall(node) {
155
+ if (ts.isIdentifier(node.expression)) return node.expression.text === "declareContainerTokens";
156
+ return false;
157
+ }
158
+ /**
159
+ * Extracts tokens from declareContainerTokens<{ Token: Type }>().
160
+ * The type argument contains the token names.
161
+ */
162
+ extractDeclaredTokens(node, graph) {
163
+ if (!node.typeArguments || node.typeArguments.length === 0) return;
164
+ const typeArg = node.typeArguments[0];
165
+ const properties = this.checker.getTypeFromTypeNode(typeArg).getProperties();
166
+ for (const prop of properties) {
167
+ const propType = this.checker.getTypeOfSymbol(prop);
168
+ if (propType) {
169
+ const tokenId = this.getTypeId(propType);
170
+ graph.parentProvidedTokens.add(tokenId);
171
+ } else graph.parentProvidedTokens.add(prop.getName());
172
+ }
173
+ }
174
+ parseExtendsArray(arrayLiteral, graph) {
175
+ for (const element of arrayLiteral.elements) if (ts.isIdentifier(element)) this.parsePartialConfig(element, graph);
176
+ }
177
+ parsePartialConfig(identifier, graph) {
178
+ const symbol = this.checker.getSymbolAtLocation(identifier);
179
+ if (!symbol) return;
180
+ const resolvedSymbol = this.resolveSymbol(symbol);
181
+ const declaration = resolvedSymbol.valueDeclaration ?? resolvedSymbol.declarations?.[0];
182
+ if (!declaration) return;
183
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer && ts.isCallExpression(declaration.initializer)) {
184
+ const callExpr = declaration.initializer;
185
+ if (this.isDefinePartialConfigCall(callExpr)) this.parseBuilderConfig(callExpr, graph);
186
+ }
187
+ if (ts.isExportSpecifier(declaration)) {}
188
+ }
189
+ isDefinePartialConfigCall(node) {
190
+ const expression = node.expression;
191
+ if (ts.isIdentifier(expression)) return expression.text === "definePartialConfig";
192
+ return false;
193
+ }
194
+ parseInjectionsArray(arrayLiteral, graph) {
195
+ for (const element of arrayLiteral.elements) if (ts.isObjectLiteralExpression(element)) this.parseInjectionObject(element, graph);
196
+ }
197
+ parseInjectionObject(obj, graph) {
198
+ let tokenNode;
199
+ let providerNode;
200
+ let lifecycle = "singleton";
201
+ let useFactory = false;
202
+ let isScoped = false;
203
+ for (const prop of obj.properties) {
204
+ if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) continue;
205
+ if (prop.name.text === "token") tokenNode = prop.initializer;
206
+ else if (prop.name.text === "provider") providerNode = prop.initializer;
207
+ else if (prop.name.text === "lifecycle" && ts.isStringLiteral(prop.initializer)) {
208
+ if (prop.initializer.text === "transient") lifecycle = "transient";
209
+ } else if (prop.name.text === "useFactory") {
210
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) useFactory = true;
211
+ } else if (prop.name.text === "scoped") {
212
+ if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword) isScoped = true;
213
+ }
214
+ }
215
+ if (!tokenNode) return;
216
+ if (providerNode && (ts.isArrowFunction(providerNode) || ts.isFunctionExpression(providerNode))) useFactory = true;
217
+ let tokenId;
218
+ let implementationSymbol;
219
+ let tokenSymbol;
220
+ let type = "autowire";
221
+ let isInterfaceToken = false;
222
+ let isValueToken = false;
223
+ let factorySource;
224
+ let resolvedTokenNode = tokenNode;
225
+ const resolved = this.resolveToInitializer(tokenNode);
226
+ if (resolved) resolvedTokenNode = resolved;
227
+ if (ts.isCallExpression(resolvedTokenNode) && this.isUseInterfaceCall(resolvedTokenNode)) {
228
+ tokenId = this.extractInterfaceTokenId(resolvedTokenNode);
229
+ type = "explicit";
230
+ isInterfaceToken = true;
231
+ } else if (ts.isCallExpression(resolvedTokenNode) && this.isUsePropertyCall(resolvedTokenNode)) {
232
+ const propertyInfo = this.extractPropertyTokenId(resolvedTokenNode);
233
+ tokenId = propertyInfo.tokenId;
234
+ type = "explicit";
235
+ isValueToken = true;
236
+ if (!providerNode) throw new Error(`useProperty(${propertyInfo.className}, '${propertyInfo.paramName}') requires a provider (factory).`);
237
+ useFactory = true;
238
+ } else {
239
+ const tokenType = this.checker.getTypeAtLocation(tokenNode);
240
+ tokenId = this.getTypeIdFromConstructor(tokenType);
241
+ if (ts.isIdentifier(tokenNode)) tokenSymbol = this.checker.getSymbolAtLocation(tokenNode);
242
+ }
243
+ if (useFactory && providerNode) {
244
+ factorySource = providerNode.getText();
245
+ type = "factory";
246
+ if (tokenId) {
247
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
248
+ const definition = {
249
+ tokenId,
250
+ tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
251
+ registrationNode: obj,
252
+ type: "factory",
253
+ lifecycle,
254
+ isInterfaceToken,
255
+ isValueToken,
256
+ isFactory: true,
257
+ factorySource,
258
+ isScoped
259
+ };
260
+ graph.nodes.set(tokenId, {
261
+ service: definition,
262
+ dependencies: []
263
+ });
264
+ }
265
+ return;
266
+ }
267
+ if (providerNode) {
268
+ implementationSymbol = this.checker.getSymbolAtLocation(providerNode);
269
+ type = "explicit";
270
+ } else if (type === "explicit" && !providerNode) {
271
+ if (ts.isIdentifier(tokenNode)) {
272
+ implementationSymbol = this.checker.getSymbolAtLocation(tokenNode);
273
+ type = "autowire";
274
+ }
275
+ } else if (ts.isIdentifier(tokenNode)) {
276
+ implementationSymbol = this.checker.getSymbolAtLocation(tokenNode);
277
+ type = "autowire";
278
+ }
279
+ if (tokenId && implementationSymbol) {
280
+ if (graph.nodes.has(tokenId) && !isScoped) throw new Error(`Duplicate registration: '${tokenId}' is already registered.`);
281
+ const definition = {
282
+ tokenId,
283
+ implementationSymbol: this.resolveSymbol(implementationSymbol),
284
+ tokenSymbol: tokenSymbol ? this.resolveSymbol(tokenSymbol) : void 0,
285
+ registrationNode: obj,
286
+ type,
287
+ lifecycle,
288
+ isInterfaceToken: isInterfaceToken || ts.isCallExpression(tokenNode) && this.isUseInterfaceCall(tokenNode),
289
+ isScoped
290
+ };
291
+ graph.nodes.set(tokenId, {
292
+ service: definition,
293
+ dependencies: []
294
+ });
295
+ }
296
+ }
297
+ isUseInterfaceCall(node) {
298
+ if (ts.isIdentifier(node.expression)) return node.expression.text === "useInterface";
299
+ return false;
300
+ }
301
+ isUsePropertyCall(node) {
302
+ if (ts.isIdentifier(node.expression)) return node.expression.text === "useProperty";
303
+ return false;
304
+ }
305
+ /**
306
+ * Resolves a node to its original initializer expression.
307
+ * Handles identifiers, property access, imports, and unwraps type assertions/casts.
308
+ */
309
+ resolveToInitializer(node) {
310
+ let expr = node;
311
+ while (ts.isParenthesizedExpression(expr) || ts.isAsExpression(expr) || ts.isTypeAssertionExpression(expr) || ts.isSatisfiesExpression && ts.isSatisfiesExpression(expr)) expr = expr.expression;
312
+ if (ts.isCallExpression(expr)) return expr;
313
+ if (ts.isIdentifier(expr)) return this.resolveIdentifierToInitializer(expr);
314
+ if (ts.isPropertyAccessExpression(expr)) return this.resolvePropertyAccessToInitializer(expr);
315
+ }
316
+ resolveIdentifierToInitializer(identifier) {
317
+ const symbol = this.checker.getSymbolAtLocation(identifier);
318
+ if (!symbol) return void 0;
319
+ const declarations = this.resolveSymbol(symbol).getDeclarations();
320
+ if (!declarations || declarations.length === 0) return void 0;
321
+ for (const decl of declarations) if (ts.isVariableDeclaration(decl) && decl.initializer) return this.resolveToInitializer(decl.initializer) ?? decl.initializer;
322
+ }
323
+ resolvePropertyAccessToInitializer(node) {
324
+ const objectInitializer = this.resolveToInitializer(node.expression);
325
+ if (objectInitializer && ts.isObjectLiteralExpression(objectInitializer)) {
326
+ const propName = node.name.text;
327
+ const prop = objectInitializer.properties.find((p) => p.name && ts.isIdentifier(p.name) && p.name.text === propName);
328
+ if (prop && ts.isPropertyAssignment(prop)) return this.resolveToInitializer(prop.initializer) ?? prop.initializer;
329
+ }
330
+ }
331
+ extractPropertyTokenId(node) {
332
+ if (node.arguments.length < 2) throw new Error("useProperty requires two arguments: (Class, paramName)");
333
+ const classArg = node.arguments[0];
334
+ const nameArg = node.arguments[1];
335
+ if (!ts.isIdentifier(classArg)) throw new Error("useProperty first argument must be a class identifier.");
336
+ if (!ts.isStringLiteral(nameArg)) throw new Error("useProperty second argument must be a string literal.");
337
+ const className = classArg.text;
338
+ const paramName = nameArg.text;
339
+ return {
340
+ tokenId: `PropertyToken:${className}.${paramName}`,
341
+ className,
342
+ paramName
343
+ };
344
+ }
345
+ extractInterfaceTokenId(node) {
346
+ if (!node.typeArguments || node.typeArguments.length === 0) throw new Error("useInterface must have a type argument.");
347
+ const typeNode = node.typeArguments[0];
348
+ const symbol = this.checker.getTypeFromTypeNode(typeNode).getSymbol();
349
+ if (!symbol) return "AnonymousInterface";
350
+ const declarations = symbol.getDeclarations();
351
+ if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
352
+ return symbol.getName();
353
+ }
354
+ getTypeIdFromConstructor(type) {
355
+ const constructSignatures = type.getConstructSignatures();
356
+ let instanceType;
357
+ if (constructSignatures.length > 0) instanceType = constructSignatures[0].getReturnType();
358
+ else instanceType = type;
359
+ return this.getTypeId(instanceType);
360
+ }
361
+ extractScope(optionsNode) {
362
+ for (const prop of optionsNode.properties) if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name) && prop.name.text === "scope" && ts.isStringLiteral(prop.initializer)) {
363
+ if (prop.initializer.text === "transient") return "transient";
364
+ }
365
+ return "singleton";
366
+ }
367
+ /**
368
+ * Resolves dependencies for all nodes in the graph.
369
+ * @param graph - The dependency graph.
370
+ */
371
+ resolveAllDependencies(graph) {
372
+ for (const node of graph.nodes.values()) this.resolveDependencies(node, graph);
373
+ }
374
+ /**
375
+ * Resolves dependencies for a single service node by inspecting its constructor.
376
+ * @param node - The dependency node to resolve.
377
+ */
378
+ resolveDependencies(node, graph) {
379
+ if (node.service.isFactory || node.service.type === "factory") return;
380
+ const symbol = node.service.implementationSymbol;
381
+ if (!symbol) return;
382
+ const declarations = symbol.getDeclarations();
383
+ if (!declarations || declarations.length === 0) return;
384
+ const classDecl = declarations.find((d) => ts.isClassDeclaration(d));
385
+ if (!classDecl) return;
386
+ const className = classDecl.name?.getText() ?? "Anonymous";
387
+ const constructor = classDecl.members.find((m) => ts.isConstructorDeclaration(m));
388
+ if (!constructor) return;
389
+ for (const param of constructor.parameters) {
390
+ const paramName = param.name.getText();
391
+ const typeNode = param.type;
392
+ if (!typeNode) continue;
393
+ const type = this.checker.getTypeFromTypeNode(typeNode);
394
+ const propertyTokenId = `PropertyToken:${className}.${paramName}`;
395
+ if (graph.nodes.has(propertyTokenId)) {
396
+ node.dependencies.push(propertyTokenId);
397
+ continue;
398
+ }
399
+ const depTokenId = this.getTypeId(type);
400
+ node.dependencies.push(depTokenId);
401
+ }
402
+ }
403
+ /**
404
+ * Generates a unique Token ID for a given Type.
405
+ * Uses file path hash + name for consistency and collision avoidance.
406
+ *
407
+ * @param type - The TypeScript Type.
408
+ * @returns A string identifier for the token.
409
+ */
410
+ getTypeId(type) {
411
+ const symbol = type.getSymbol();
412
+ if (!symbol) return this.checker.typeToString(type);
413
+ const name = symbol.getName();
414
+ if (name === "__type" || name === "InterfaceToken" || name === "__brand") return this.checker.typeToString(type);
415
+ const declarations = symbol.getDeclarations();
416
+ if (declarations && declarations.length > 0) return generateTokenId(symbol, declarations[0].getSourceFile());
417
+ return symbol.getName();
418
+ }
419
+ /**
420
+ * Resolves a symbol, following aliases if necessary.
421
+ * @param symbol - The symbol to resolve.
422
+ * @returns The resolved symbol.
423
+ */
424
+ resolveSymbol(symbol) {
425
+ if (symbol.flags & ts.SymbolFlags.Alias) return this.resolveSymbol(this.checker.getAliasedSymbol(symbol));
426
+ return symbol;
427
+ }
428
+ };
429
+
430
+ //#endregion
431
+ export { Analyzer, generateTokenId };
@@ -0,0 +1,249 @@
1
+
2
+ //#region src/generator/Generator.ts
3
+ /**
4
+ * Generates TypeScript code for the dependency injection container.
5
+ *
6
+ * Takes a validated dependency graph and produces:
7
+ * - Import statements for all dependencies
8
+ * - Factory functions for each service
9
+ * - A NeoContainer class with resolve logic
10
+ */
11
+ var Generator = class {
12
+ /**
13
+ * Creates a new Generator.
14
+ * @param graph - The validated dependency graph to generate code from.
15
+ * @param useDirectSymbolNames - If true, uses symbol names directly without import prefixes.
16
+ */
17
+ constructor(graph, useDirectSymbolNames = false) {
18
+ this.graph = graph;
19
+ this.useDirectSymbolNames = useDirectSymbolNames;
20
+ }
21
+ /**
22
+ * Generates the complete container code as a string.
23
+ * @returns TypeScript source code for the generated container.
24
+ */
25
+ generate() {
26
+ const sorted = this.topologicalSort();
27
+ const imports = /* @__PURE__ */ new Map();
28
+ const factories = [];
29
+ const resolveCases = [];
30
+ const getImport = (symbol) => {
31
+ if (this.useDirectSymbolNames) return symbol.getName();
32
+ const decl = symbol.declarations?.[0];
33
+ if (!decl) return "UNKNOWN";
34
+ const filePath = decl.getSourceFile().fileName;
35
+ if (!imports.has(filePath)) {
36
+ const alias = `Import_${imports.size}`;
37
+ imports.set(filePath, alias);
38
+ }
39
+ return `${imports.get(filePath)}.${symbol.getName()}`;
40
+ };
41
+ for (const tokenId of sorted) {
42
+ const node = this.graph.nodes.get(tokenId);
43
+ if (!node) continue;
44
+ if (node.service.type === "parent") continue;
45
+ const factoryId = this.getFactoryName(tokenId);
46
+ if (node.service.isFactory && node.service.factorySource) {
47
+ const userFactory = node.service.factorySource;
48
+ factories.push(`
49
+ private ${factoryId}(): any {
50
+ const userFactory = ${userFactory};
51
+ return userFactory(this);
52
+ }`);
53
+ } else {
54
+ const className = node.service.implementationSymbol ? getImport(node.service.implementationSymbol) : "undefined";
55
+ const args = node.dependencies.map((depId) => {
56
+ const depNode = this.graph.nodes.get(depId);
57
+ if (!depNode) return "undefined";
58
+ if (depNode.service.isInterfaceToken) return `this.resolve("${depNode.service.tokenId}")`;
59
+ else if (depNode.service.tokenSymbol) return `this.resolve(${getImport(depNode.service.tokenSymbol)})`;
60
+ else if (depNode.service.implementationSymbol) return `this.resolve(${getImport(depNode.service.implementationSymbol)})`;
61
+ return "undefined";
62
+ }).join(", ");
63
+ factories.push(`
64
+ private ${factoryId}(): any {
65
+ return new ${className}(${args});
66
+ }`);
67
+ }
68
+ const isTransient = node.service.lifecycle === "transient";
69
+ let tokenKey;
70
+ let tokenCheck;
71
+ if (node.service.isInterfaceToken) {
72
+ tokenKey = `"${node.service.tokenId}"`;
73
+ tokenCheck = `if (token === "${node.service.tokenId}")`;
74
+ } else if (node.service.tokenSymbol) {
75
+ const tokenClass = getImport(node.service.tokenSymbol);
76
+ tokenKey = tokenClass;
77
+ tokenCheck = `if (token === ${tokenClass})`;
78
+ } else if (node.service.implementationSymbol) {
79
+ const className = getImport(node.service.implementationSymbol);
80
+ tokenKey = className;
81
+ tokenCheck = `if (token === ${className})`;
82
+ } else {
83
+ tokenKey = `"${node.service.tokenId}"`;
84
+ tokenCheck = `if (token === "${node.service.tokenId}")`;
85
+ }
86
+ const creationLogic = isTransient ? `return this.${factoryId}();` : `
87
+ if (!this.instances.has(${tokenKey})) {
88
+ const instance = this.${factoryId}();
89
+ this.instances.set(${tokenKey}, instance);
90
+ return instance;
91
+ }
92
+ return this.instances.get(${tokenKey});
93
+ `;
94
+ resolveCases.push(`${tokenCheck} { ${creationLogic} }`);
95
+ }
96
+ const importLines = [];
97
+ if (!this.useDirectSymbolNames) for (const [filePath, alias] of imports) importLines.push(`import * as ${alias} from '${filePath}';`);
98
+ return `
99
+ ${importLines.join("\n")}
100
+
101
+ // -- Container --
102
+ export class NeoContainer {
103
+ private instances = new Map<any, any>();
104
+
105
+ // -- Factories --
106
+ ${factories.join("\n ")}
107
+
108
+ constructor(
109
+ private parent?: any,
110
+ private legacy?: any[],
111
+ private name: string = 'NeoContainer'
112
+ ) {}
113
+
114
+ public resolve(token: any): any {
115
+ // 1. Try to resolve locally (or create if singleton)
116
+ const result = this.resolveLocal(token);
117
+ if (result !== undefined) return result;
118
+
119
+ // 2. Delegate to parent
120
+ if (this.parent) {
121
+ try {
122
+ return this.parent.resolve(token);
123
+ } catch (e) {
124
+ // Ignore error, try legacy
125
+ }
126
+ }
127
+
128
+ // 3. Delegate to legacy
129
+ if (this.legacy) {
130
+ for (const legacyContainer of this.legacy) {
131
+ // Assume legacy container has resolve()
132
+ try {
133
+ if (legacyContainer.resolve) return legacyContainer.resolve(token);
134
+ } catch (e) {
135
+ // Ignore
136
+ }
137
+ }
138
+ }
139
+
140
+ throw new Error(\`[\${this.name}] Service not found or token not registered: \${token}\`);
141
+ }
142
+
143
+ private resolveLocal(token: any): any {
144
+ ${resolveCases.join("\n ")}
145
+ return undefined;
146
+ }
147
+
148
+ // For debugging/inspection
149
+ public get _graph() {
150
+ return ${JSON.stringify(Array.from(this.graph.nodes.keys()))};
151
+ }
152
+ }
153
+ ${this.useDirectSymbolNames ? "" : `
154
+ // -- Container Instance --
155
+ export const ${this.graph.exportedVariableName || "container"} = ${this.generateInstantiation()};
156
+ `}`;
157
+ }
158
+ /**
159
+ * Generates only the instantiation expression: new NeoContainer(...)
160
+ * This is used to replace defineBuilderConfig(...) in the source.
161
+ */
162
+ generateInstantiation() {
163
+ return `new NeoContainer(${this.graph.buildArguments && this.graph.buildArguments.length > 0 ? this.graph.buildArguments[0] : "undefined"}, ${this.graph.legacyContainers ? `[${this.graph.legacyContainers.join(", ")}]` : "undefined"}, ${this.graph.containerName ? `'${this.graph.containerName}'` : "undefined"})`;
164
+ }
165
+ /**
166
+ * Sorts services in topological order (dependencies before dependents).
167
+ * @returns Array of TokenIds in dependency order.
168
+ */
169
+ topologicalSort() {
170
+ const visited = /* @__PURE__ */ new Set();
171
+ const sorted = [];
172
+ const visit = (id) => {
173
+ if (visited.has(id)) return;
174
+ visited.add(id);
175
+ const node = this.graph.nodes.get(id);
176
+ if (node) {
177
+ for (const depId of node.dependencies) visit(depId);
178
+ sorted.push(id);
179
+ }
180
+ };
181
+ for (const id of this.graph.nodes.keys()) visit(id);
182
+ return sorted;
183
+ }
184
+ /**
185
+ * Creates a valid JavaScript function name from a token ID.
186
+ * @param tokenId - The token identifier.
187
+ * @returns A sanitized factory function name.
188
+ */
189
+ getFactoryName(tokenId) {
190
+ return `create_${tokenId.replace(/[^a-zA-Z0-9]/g, "_")}`;
191
+ }
192
+ };
193
+
194
+ //#endregion
195
+ //#region src/generator/GraphValidator.ts
196
+ /**
197
+ * Validates the dependency graph for correctness.
198
+ * Detects circular dependencies, missing bindings, and duplicates.
199
+ */
200
+ var GraphValidator = class {
201
+ /**
202
+ * Validates the graph structure.
203
+ *
204
+ * @param graph - The dependency graph to validate.
205
+ * @throws {Error} If a circular dependency or missing binding is detected.
206
+ */
207
+ validate(graph) {
208
+ const visited = /* @__PURE__ */ new Set();
209
+ const recursionStack = /* @__PURE__ */ new Set();
210
+ const parentTokens = graph.parentProvidedTokens ?? /* @__PURE__ */ new Set();
211
+ for (const [nodeId, node] of graph.nodes) if (parentTokens.has(nodeId)) {
212
+ if (node.service.isScoped) continue;
213
+ throw new Error(`Duplicate registration: '${nodeId}' is already registered in the parent container. Use 'scoped: true' to override the parent's registration intentionally.`);
214
+ }
215
+ for (const [nodeId, node] of graph.nodes) for (const depId of node.dependencies) {
216
+ const isProvidedLocally = graph.nodes.has(depId);
217
+ const isProvidedByParent = parentTokens.has(depId);
218
+ if (!isProvidedLocally && !isProvidedByParent) {
219
+ const serviceName = nodeId;
220
+ throw new Error(`Missing binding: Service '${serviceName}' depends on '${depId}', but no provider was registered.`);
221
+ }
222
+ }
223
+ for (const nodeId of graph.nodes.keys()) if (!visited.has(nodeId)) this.detectCycle(nodeId, graph, visited, recursionStack, parentTokens);
224
+ }
225
+ /**
226
+ * Recursive Helper for Cycle Detection (DFS).
227
+ *
228
+ * @param nodeId - The current node ID.
229
+ * @param graph - The graph.
230
+ * @param visited - Set of all visited nodes.
231
+ * @param stack - Set of nodes in the current recursion stack (path).
232
+ * @param parentTokens - Tokens from parent container (skip traversal).
233
+ */
234
+ detectCycle(nodeId, graph, visited, stack, parentTokens) {
235
+ visited.add(nodeId);
236
+ stack.add(nodeId);
237
+ const node = graph.nodes.get(nodeId);
238
+ if (node) for (const depId of node.dependencies) {
239
+ if (parentTokens.has(depId)) continue;
240
+ if (!visited.has(depId)) this.detectCycle(depId, graph, visited, stack, parentTokens);
241
+ else if (stack.has(depId)) throw new Error(`Circular dependency detected: ${[...stack, depId].join(" -> ")}`);
242
+ }
243
+ stack.delete(nodeId);
244
+ }
245
+ };
246
+
247
+ //#endregion
248
+ exports.Generator = Generator;
249
+ exports.GraphValidator = GraphValidator;