@onebun/core 0.1.14 → 0.1.15
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/package.json +1 -1
- package/src/module/module.test.ts +88 -0
- package/src/module/module.ts +77 -3
package/package.json
CHANGED
|
@@ -154,6 +154,94 @@ describe('OneBunModule', () => {
|
|
|
154
154
|
|
|
155
155
|
expect(() => new OneBunModule(TestClass, mockLoggerLayer)).toThrow();
|
|
156
156
|
});
|
|
157
|
+
|
|
158
|
+
test('should detect circular dependencies and provide detailed error message', () => {
|
|
159
|
+
const { registerDependencies } = require('../decorators/decorators');
|
|
160
|
+
const { Effect, Layer } = require('effect');
|
|
161
|
+
const { LoggerService } = require('@onebun/logger');
|
|
162
|
+
|
|
163
|
+
// Collect error messages
|
|
164
|
+
const errorMessages: string[] = [];
|
|
165
|
+
|
|
166
|
+
// Create mock logger that captures error messages
|
|
167
|
+
// Using Effect.sync to ensure the message is captured synchronously when Effect.runSync is called
|
|
168
|
+
const captureLogger = {
|
|
169
|
+
trace: () => Effect.sync(() => undefined),
|
|
170
|
+
debug: () => Effect.sync(() => undefined),
|
|
171
|
+
info: () => Effect.sync(() => undefined),
|
|
172
|
+
warn: () => Effect.sync(() => undefined),
|
|
173
|
+
error: (msg: string) =>
|
|
174
|
+
Effect.sync(() => {
|
|
175
|
+
errorMessages.push(msg);
|
|
176
|
+
}),
|
|
177
|
+
fatal: () => Effect.sync(() => undefined),
|
|
178
|
+
child: () => captureLogger,
|
|
179
|
+
};
|
|
180
|
+
const captureLoggerLayer = Layer.succeed(LoggerService, captureLogger);
|
|
181
|
+
|
|
182
|
+
// Create services - define all classes first
|
|
183
|
+
@Service()
|
|
184
|
+
class CircularServiceA {
|
|
185
|
+
getValue() {
|
|
186
|
+
return 'A';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@Service()
|
|
191
|
+
class CircularServiceB {
|
|
192
|
+
getValue() {
|
|
193
|
+
return 'B';
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
@Service()
|
|
198
|
+
class CircularServiceC {
|
|
199
|
+
getValue() {
|
|
200
|
+
return 'C';
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Now register circular dependencies manually: A -> B -> C -> A
|
|
205
|
+
registerDependencies(CircularServiceA, [CircularServiceC]);
|
|
206
|
+
registerDependencies(CircularServiceB, [CircularServiceA]);
|
|
207
|
+
registerDependencies(CircularServiceC, [CircularServiceB]);
|
|
208
|
+
|
|
209
|
+
@Module({
|
|
210
|
+
providers: [CircularServiceA, CircularServiceB, CircularServiceC],
|
|
211
|
+
})
|
|
212
|
+
class CircularModule {}
|
|
213
|
+
|
|
214
|
+
// Initialize module - should detect circular dependency
|
|
215
|
+
new OneBunModule(CircularModule, captureLoggerLayer);
|
|
216
|
+
|
|
217
|
+
// Verify error message was logged
|
|
218
|
+
expect(errorMessages.length).toBeGreaterThan(0);
|
|
219
|
+
|
|
220
|
+
// Find the circular dependency error message
|
|
221
|
+
const circularError = errorMessages.find((msg) =>
|
|
222
|
+
msg.includes('Circular dependency detected'),
|
|
223
|
+
);
|
|
224
|
+
expect(circularError).toBeDefined();
|
|
225
|
+
|
|
226
|
+
// Should contain module name
|
|
227
|
+
expect(circularError).toContain('CircularModule');
|
|
228
|
+
|
|
229
|
+
// Should contain "Unresolved services" section
|
|
230
|
+
expect(circularError).toContain('Unresolved services');
|
|
231
|
+
|
|
232
|
+
// Should contain at least one of the service names with their dependencies
|
|
233
|
+
const hasServiceInfo =
|
|
234
|
+
circularError!.includes('CircularServiceA') ||
|
|
235
|
+
circularError!.includes('CircularServiceB') ||
|
|
236
|
+
circularError!.includes('CircularServiceC');
|
|
237
|
+
expect(hasServiceInfo).toBe(true);
|
|
238
|
+
|
|
239
|
+
// Should contain "needs:" showing what dependencies are required
|
|
240
|
+
expect(circularError).toContain('needs:');
|
|
241
|
+
|
|
242
|
+
// Should contain "Dependency chain" showing the cycle
|
|
243
|
+
expect(circularError).toContain('Dependency chain');
|
|
244
|
+
});
|
|
157
245
|
});
|
|
158
246
|
|
|
159
247
|
describe('Module instance methods', () => {
|
package/src/module/module.ts
CHANGED
|
@@ -229,6 +229,7 @@ export class OneBunModule implements ModuleInstance {
|
|
|
229
229
|
// Create services in dependency order
|
|
230
230
|
const pendingProviders = [...metadata.providers.filter((p) => typeof p === 'function')];
|
|
231
231
|
const createdServices = new Set<string>();
|
|
232
|
+
const unresolvedDeps = new Map<string, string[]>(); // Track unresolved dependencies for error reporting
|
|
232
233
|
let iterations = 0;
|
|
233
234
|
const maxIterations = pendingProviders.length * 2; // Prevent infinite loops
|
|
234
235
|
|
|
@@ -245,8 +246,11 @@ export class OneBunModule implements ModuleInstance {
|
|
|
245
246
|
continue;
|
|
246
247
|
}
|
|
247
248
|
|
|
248
|
-
// Use
|
|
249
|
-
|
|
249
|
+
// Use getConstructorParamTypes first (for @Inject and TypeScript metadata),
|
|
250
|
+
// then fallback to autoDetectDependencies (for constructor source analysis)
|
|
251
|
+
const detectedDeps =
|
|
252
|
+
getConstructorParamTypes(provider) ||
|
|
253
|
+
autoDetectDependencies(provider, availableServiceClasses);
|
|
250
254
|
const dependencies: unknown[] = [];
|
|
251
255
|
let allDependenciesResolved = true;
|
|
252
256
|
|
|
@@ -259,6 +263,12 @@ export class OneBunModule implements ModuleInstance {
|
|
|
259
263
|
// Check if it's a service that hasn't been created yet
|
|
260
264
|
const isServiceInModule = availableServiceClasses.has(depType.name);
|
|
261
265
|
if (isServiceInModule && !createdServices.has(depType.name)) {
|
|
266
|
+
// Track unresolved dependency for error reporting
|
|
267
|
+
const deps = unresolvedDeps.get(provider.name) || [];
|
|
268
|
+
if (!deps.includes(depType.name)) {
|
|
269
|
+
deps.push(depType.name);
|
|
270
|
+
unresolvedDeps.set(provider.name, deps);
|
|
271
|
+
}
|
|
262
272
|
// This dependency will be created later, defer this service
|
|
263
273
|
allDependenciesResolved = false;
|
|
264
274
|
pendingProviders.push(provider);
|
|
@@ -317,7 +327,25 @@ export class OneBunModule implements ModuleInstance {
|
|
|
317
327
|
}
|
|
318
328
|
|
|
319
329
|
if (iterations >= maxIterations) {
|
|
320
|
-
|
|
330
|
+
const unresolvedServices = pendingProviders
|
|
331
|
+
.filter((p) => typeof p === 'function')
|
|
332
|
+
.map((p) => p.name);
|
|
333
|
+
|
|
334
|
+
const details = unresolvedServices
|
|
335
|
+
.map((serviceName) => {
|
|
336
|
+
const deps = unresolvedDeps.get(serviceName) || [];
|
|
337
|
+
|
|
338
|
+
return ` - ${serviceName} -> needs: [${deps.join(', ')}]`;
|
|
339
|
+
})
|
|
340
|
+
.join('\n');
|
|
341
|
+
|
|
342
|
+
const dependencyChain = this.buildDependencyChain(unresolvedDeps, unresolvedServices);
|
|
343
|
+
|
|
344
|
+
this.logger.error(
|
|
345
|
+
`Circular dependency detected in module ${this.moduleClass.name}!\n` +
|
|
346
|
+
`Unresolved services:\n${details}\n` +
|
|
347
|
+
`Dependency chain: ${dependencyChain}`,
|
|
348
|
+
);
|
|
321
349
|
}
|
|
322
350
|
}
|
|
323
351
|
|
|
@@ -479,6 +507,52 @@ export class OneBunModule implements ModuleInstance {
|
|
|
479
507
|
return serviceInstance;
|
|
480
508
|
}
|
|
481
509
|
|
|
510
|
+
/**
|
|
511
|
+
* Build a human-readable dependency chain for circular dependency error reporting
|
|
512
|
+
* Traverses the dependency graph to find and display the cycle
|
|
513
|
+
*/
|
|
514
|
+
private buildDependencyChain(
|
|
515
|
+
unresolvedDeps: Map<string, string[]>,
|
|
516
|
+
unresolvedServices: string[],
|
|
517
|
+
): string {
|
|
518
|
+
// Find cycle by traversing dependencies
|
|
519
|
+
const visited = new Set<string>();
|
|
520
|
+
const chain: string[] = [];
|
|
521
|
+
|
|
522
|
+
const findCycle = (service: string): boolean => {
|
|
523
|
+
if (visited.has(service)) {
|
|
524
|
+
chain.push(service);
|
|
525
|
+
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
visited.add(service);
|
|
529
|
+
chain.push(service);
|
|
530
|
+
|
|
531
|
+
const deps = unresolvedDeps.get(service) || [];
|
|
532
|
+
for (const dep of deps) {
|
|
533
|
+
if (unresolvedServices.includes(dep)) {
|
|
534
|
+
if (findCycle(dep)) {
|
|
535
|
+
return true;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
chain.pop();
|
|
540
|
+
|
|
541
|
+
return false;
|
|
542
|
+
};
|
|
543
|
+
|
|
544
|
+
for (const service of unresolvedServices) {
|
|
545
|
+
visited.clear();
|
|
546
|
+
chain.length = 0;
|
|
547
|
+
if (findCycle(service)) {
|
|
548
|
+
return chain.join(' -> ');
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// If no cycle found, just show all unresolved services
|
|
553
|
+
return unresolvedServices.join(' <-> ');
|
|
554
|
+
}
|
|
555
|
+
|
|
482
556
|
/**
|
|
483
557
|
* Setup the module and its dependencies
|
|
484
558
|
*/
|