Skip to content

Commit fc5871a

Browse files
committed
feat(ioc): add error trace
1 parent 9628a16 commit fc5871a

5 files changed

Lines changed: 99 additions & 24 deletions

File tree

.changeset/small-aliens-move.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@dreamkit/ioc": patch
3+
---
4+
5+
Add error trace

packages/ioc/src/context.ts

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,11 @@ const fallbackKey = Symbol("fallback");
3838
export const undefinedValueKey = Symbol("undefined");
3939
export const ignoreValueKey = Symbol("ignore");
4040

41-
type RequiredResolveOptions = { parent?: unknown; abstract?: boolean };
41+
type RequiredResolveOptions = {
42+
parent?: unknown;
43+
abstract?: boolean;
44+
path?: string[];
45+
};
4246
type OptionalResolveOptions = RequiredResolveOptions & { optional: true };
4347
type ResolveOptions = RequiredResolveOptions & { optional?: boolean };
4448

@@ -116,15 +120,20 @@ export class IocContext {
116120
parent?: unknown;
117121
context?: IocContext;
118122
async?: boolean;
123+
path?: string[];
119124
} = {},
120125
) {
121126
const { parent } = options;
122127
const context = options.context ?? this;
123128
return param["isConfigurable"]()
124-
? this.createConfigurableParam(param, { parent, context })
129+
? this.createConfigurableParam(param, {
130+
parent,
131+
context,
132+
path: options.path,
133+
})
125134
: context.dynamicResolve(
126135
param["getKey"](),
127-
{ parent, optional: param.options.optional },
136+
{ parent, optional: param.options.optional, path: options.path },
128137
{ async: options.async },
129138
);
130139
}
@@ -133,6 +142,7 @@ export class IocContext {
133142
options: {
134143
context: IocContext;
135144
parent?: unknown;
145+
path?: string[];
136146
},
137147
) {
138148
const registerKey = param["getKey"]();
@@ -152,8 +162,10 @@ export class IocContext {
152162
return ensureSync(
153163
ctx.resolve(registerKey, {
154164
parent: options.parent,
165+
path: options.path,
155166
optional: param.options.optional as true,
156167
}),
168+
options.path,
157169
);
158170
};
159171
}
@@ -256,6 +268,7 @@ export class IocContext {
256268
options: {
257269
parent?: any;
258270
async?: boolean;
271+
path?: string[];
259272
},
260273
) {
261274
const { parent } = options;
@@ -271,7 +284,7 @@ export class IocContext {
271284
const ctx = this.fork().register(input as any, { value: ignoreValueKey });
272285
return ctx.dynamicResolve(
273286
data.useClass,
274-
{ parent },
287+
{ parent, path: options.path },
275288
{ async: options.async },
276289
);
277290
}
@@ -298,7 +311,7 @@ export class IocContext {
298311
):
299312
| {
300313
params: IocParamsConfig;
301-
paramOptions: { context: IocContext; parent: unknown };
314+
paramOptions: { context: IocContext; parent: unknown; path?: string[] };
302315
create: (params: Record<string, any>) => any;
303316
}
304317
| undefined {
@@ -312,7 +325,7 @@ export class IocContext {
312325
iocObject = input;
313326
create = (params: any) => input.bind(params);
314327
} else if (!options.optional) {
315-
throw createKeyError(input as IocRegistryKey);
328+
throw createKeyError(input as IocRegistryKey, options.path);
316329
} else {
317330
return;
318331
}
@@ -323,7 +336,7 @@ export class IocContext {
323336
: this;
324337
if (!onResolveIocObject || onResolveIocObject(input)) {
325338
return {
326-
paramOptions: { context, parent: input },
339+
paramOptions: { context, parent: input, path: options.path },
327340
params: iocObject.$ioc.params,
328341
create,
329342
};
@@ -346,16 +359,22 @@ export class IocContext {
346359
options: {
347360
parent?: unknown;
348361
context?: IocContext;
362+
path?: string[];
349363
} = {},
350364
): IocParams<T> {
351365
const config = normalizeIocParams(input, false);
366+
const prevPath = options.path ?? [];
352367
const params: Record<string, any> = {};
353368
for (const name in config) {
354369
const value = config[name];
370+
const path = [...prevPath, name];
355371
if (isPlainObject(value)) {
356-
params[name] = this.resolveParams(value, options);
372+
params[name] = this.resolveParams(value, { ...options, path });
357373
} else {
358-
params[name] = ensureSync(this.createParam(value, options));
374+
params[name] = ensureSync(
375+
this.createParam(value, { ...options, path }),
376+
path,
377+
);
359378
}
360379
}
361380
return params as any;
@@ -366,17 +385,24 @@ export class IocContext {
366385
options: {
367386
parent?: unknown;
368387
context?: IocContext;
388+
path?: string[];
369389
} = {},
370390
): Promise<IocParams<T>> {
371391
const config = normalizeIocParams(input, false);
392+
const prevPath = options.path ?? [];
372393
const params: Record<string, any> = {};
373394
for (const name in config) {
374395
const value = config[name];
396+
const path = [...prevPath, name];
375397
if (isPlainObject(value)) {
376-
params[name] = await this.resolveAsyncParams(value, options);
398+
params[name] = await this.resolveAsyncParams(value, {
399+
...options,
400+
path,
401+
});
377402
} else {
378403
params[name] = await this.createParam(value, {
379404
...options,
405+
path,
380406
async: true,
381407
});
382408
}
@@ -411,17 +437,26 @@ export class IocContext {
411437
input: Constructor | ((...args: any[]) => any) | IocRegistryKey,
412438
options: ResolveOptions = {},
413439
) {
440+
const path = options.path ?? [
441+
typeof input === "function" ? input.name : `${input}`,
442+
];
414443
const data = this.find(input as any);
415444

416445
if (data && this.shouldBeResolved(input, data)) {
417446
const value = ensureSync(
418-
this.resolveWithRegistryData(input, data, { parent: options.parent }),
447+
this.resolveWithRegistryData(input, data, {
448+
parent: options.parent,
449+
path,
450+
}),
419451
);
420452
const result = this.tryParseResolvedValue(input, data, value);
421453
if (result) return result.value;
422454
}
423455

424-
const object = this.tryParseIocObject(input, options);
456+
const object = this.tryParseIocObject(input, {
457+
...options,
458+
path,
459+
});
425460

426461
if (object) {
427462
const params = this.resolveParams(object.params, object.paramOptions);
@@ -456,18 +491,26 @@ export class IocContext {
456491
input: Constructor | ((...args: any[]) => any) | IocRegistryKey,
457492
options: ResolveOptions = {},
458493
) {
494+
const path = [
495+
...(options.path ?? []),
496+
typeof input === "function" ? input.name : `${input}`,
497+
];
459498
const data = this.find(input as any);
460499

461500
if (data && this.shouldBeResolved(input, data)) {
462501
const value = await this.resolveWithRegistryData(input, data, {
463502
parent: options.parent,
503+
path,
464504
async: true,
465505
});
466506
const result = this.tryParseResolvedValue(input, data, value);
467507
if (result) return result.value;
468508
}
469509

470-
const object = this.tryParseIocObject(input, options);
510+
const object = this.tryParseIocObject(input, {
511+
...options,
512+
path,
513+
});
471514

472515
if (object) {
473516
const params = await this.resolveAsyncParams(

packages/ioc/src/error.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,15 @@ import type { IocRegistryKey } from "./registry.js";
22

33
export class IocError extends Error {}
44

5-
export function createKeyError(input: IocRegistryKey) {
6-
if (typeof input === "symbol") {
7-
return new IocError(`Symbol is not registered: ${input.toString()}`);
8-
} else if (typeof input === "function") {
9-
return new IocError(`Class is not registered: ${input.name}`);
10-
} else {
11-
return new IocError(`Unknown object is not registered: ${input}`);
12-
}
5+
export function createKeyError(input: IocRegistryKey, path?: string[]) {
6+
const type =
7+
typeof input === "symbol"
8+
? `Symbol '${input.toString()}'`
9+
: typeof input === "function"
10+
? Function.prototype.toString.call(input).startsWith("class")
11+
? `Class '${input.name}'`
12+
: `Function '${input.name}'`
13+
: `Object '${input}'`;
14+
const trace = path?.length ? ` (${path?.join(".")})` : "";
15+
return new IocError(`${type} is not registered${trace}`);
1316
}

packages/ioc/src/utils/async.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { IocError } from "../error.js";
22

3-
export function ensureSync<T>(value: T): T {
4-
if (value instanceof Promise)
5-
throw new IocError("Synchronous resolutions can't return promises");
3+
export function ensureSync<T>(value: T, path?: string[]): T {
4+
if (value instanceof Promise) {
5+
const trace = path?.length ? ` (${path?.join(".")})` : "";
6+
throw new IocError(`Synchronous resolutions can't return promises${trace}`);
7+
}
68
return value;
79
}

packages/ioc/test/context.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,28 @@ describe("IocContext.resolve", () => {
314314
.resolve(Client),
315315
).toBeInstanceOf(NodeClient);
316316
});
317+
it("report trace", () => {
318+
class X {}
319+
class A extends IocClass({ X }) {}
320+
class B extends IocClass({ A }) {}
321+
const c = IocFunc({ B })(function c() {});
322+
class D extends IocClass({ c }) {}
323+
expect(() => context.resolve(D)).toThrowError(
324+
`Class 'X' is not registered (D.c.b.a.x)`,
325+
);
326+
expect(() => context.resolve(c)).toThrowError(
327+
`Class 'X' is not registered (c2.b.a.x)`,
328+
);
329+
expect(() => context.resolve(B)).toThrowError(
330+
`Class 'X' is not registered (B.a.x)`,
331+
);
332+
expect(() => context.resolve(A)).toThrowError(
333+
`Class 'X' is not registered (A.x)`,
334+
);
335+
expect(() => context.resolve(X)).toThrowError(
336+
`Class 'X' is not registered`,
337+
);
338+
});
317339
});
318340

319341
describe("IocContext.resolveParams", () => {

0 commit comments

Comments
 (0)