@codefast/di 0.8.1 → 0.10.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.
Files changed (56) hide show
  1. package/CHANGELOG.md +164 -0
  2. package/LICENSE +1 -1
  3. package/README.md +29 -20
  4. package/dist/ambient/active-container.d.ts +13 -0
  5. package/dist/ambient/active-container.js +24 -0
  6. package/dist/container/binding-builders.d.ts +42 -20
  7. package/dist/container/binding-builders.js +155 -87
  8. package/dist/container/container.d.ts +10 -10
  9. package/dist/container/container.js +55 -23
  10. package/dist/core/binding.d.ts +55 -57
  11. package/dist/core/binding.js +11 -37
  12. package/dist/core/constraint-requirement.d.ts +19 -4
  13. package/dist/core/constraint-requirement.js +19 -9
  14. package/dist/core/registry.d.ts +47 -4
  15. package/dist/core/registry.js +361 -159
  16. package/dist/core/state-epoch.d.ts +16 -0
  17. package/dist/core/state-epoch.js +21 -0
  18. package/dist/core/tag.d.ts +2 -2
  19. package/dist/core/tag.js +2 -2
  20. package/dist/core/token.d.ts +24 -4
  21. package/dist/core/token.js +1 -1
  22. package/dist/core/types.d.ts +15 -6
  23. package/dist/decorators/inject.d.ts +1 -1
  24. package/dist/decorators/inject.js +6 -4
  25. package/dist/errors/diagnostics.d.ts +2 -0
  26. package/dist/errors/errors.d.ts +33 -1
  27. package/dist/errors/errors.js +44 -3
  28. package/dist/index.d.ts +2 -2
  29. package/dist/index.js +1 -1
  30. package/dist/injection/descriptor.d.ts +16 -8
  31. package/dist/injection/descriptor.js +3 -1
  32. package/dist/injection/resolve-options.d.ts +6 -0
  33. package/dist/injection/resolve-options.js +16 -0
  34. package/dist/introspection/dependency-graph.js +10 -5
  35. package/dist/introspection/inspector.d.ts +3 -1
  36. package/dist/introspection/inspector.js +10 -24
  37. package/dist/lifecycle/lifecycle-manager.js +10 -8
  38. package/dist/lifecycle/scope-manager.js +1 -1
  39. package/dist/metadata/metadata-reader-token.js +1 -1
  40. package/dist/resolution/cache/activation-need.d.ts +2 -0
  41. package/dist/resolution/cache/activation-need.js +13 -6
  42. package/dist/resolution/cache/binding-lookup-cache.d.ts +34 -1
  43. package/dist/resolution/cache/binding-lookup-cache.js +96 -12
  44. package/dist/resolution/cache/class-introspector.d.ts +1 -1
  45. package/dist/resolution/cache/class-introspector.js +28 -14
  46. package/dist/resolution/context.d.ts +8 -8
  47. package/dist/resolution/plan/instantiation-plan.d.ts +12 -0
  48. package/dist/resolution/plan/instantiation-plan.js +156 -65
  49. package/dist/resolution/plan/plan-codegen.d.ts +100 -0
  50. package/dist/resolution/plan/plan-codegen.js +185 -0
  51. package/dist/resolution/resolver.d.ts +14 -4
  52. package/dist/resolution/resolver.js +375 -134
  53. package/dist/resolution/select/binding-select.js +12 -7
  54. package/dist/resolution/select/constraints.d.ts +7 -4
  55. package/dist/resolution/select/constraints.js +34 -13
  56. package/package.json +14 -40
@@ -7,7 +7,7 @@ import { AmbiguousBindingError } from "#/errors/errors";
7
7
  * @since 0.3.16-canary.0
8
8
  */
9
9
  export function selectBinding(bindings, options, ctx, tokenDisplayName) {
10
- const candidates = filterBindings(bindings, options, ctx, true);
10
+ const candidates = filterBindings(bindings, options, ctx, true, false);
11
11
  if (candidates.length === 0) {
12
12
  return undefined;
13
13
  }
@@ -36,7 +36,7 @@ export function selectBinding(bindings, options, ctx, tokenDisplayName) {
36
36
  if (mostSpecific !== undefined) {
37
37
  return mostSpecific;
38
38
  }
39
- throw new AmbiguousBindingError(tokenDisplayName, candidates.map((c) => c.id));
39
+ throw new AmbiguousBindingError(tokenDisplayName, candidates.map((c) => c.identifier));
40
40
  }
41
41
  /** The lone candidate declaring more criteria than every other, or `undefined` when that is a tie. */
42
42
  function mostSpecificByCriterionCount(candidates) {
@@ -64,7 +64,7 @@ function mostSpecificByCriterionCount(candidates) {
64
64
  export function selectAllBindings(bindings, options, ctx) {
65
65
  // `resolveAll` matches the slot only when the request carries a criterion; with none it takes
66
66
  // every binding, where `resolve` would read an absent criterion as "the default slot".
67
- return filterBindings(bindings, options, ctx, options !== undefined && hasSlotCriterion(options));
67
+ return filterBindings(bindings, options, ctx, options !== undefined && hasSlotCriterion(options), true);
68
68
  }
69
69
  /**
70
70
  * @param bindings - the token's candidates, in registration order
@@ -72,13 +72,18 @@ export function selectAllBindings(bindings, options, ctx) {
72
72
  * @param ctx - what the constraint predicates read
73
73
  * @param requiresSlotMatch - `resolve` always matches the slot, where an absent criterion means
74
74
  * "the default slot"; `resolveAll` matches only when the request carries one
75
+ * @param includesMembers - `resolveAll` takes collection members; a single `resolve` never does
75
76
  */
76
- function filterBindings(bindings, options, ctx, requiresSlotMatch) {
77
+ function filterBindings(bindings, options, ctx, requiresSlotMatch, includesMembers) {
77
78
  const result = [];
78
- // A predicate is user code that may rebind the token mid-walk, but the registry replaces a
79
- // token's list on mutation instead of splicing it, so this walk keeps its own list — no copy.
80
- for (let index = 0; index < bindings.length; index += 1) {
79
+ // A predicate is user code that may rebind the token mid-walk. A removal replaces the list, and an
80
+ // append lands past the length read here, so the walk offers exactly the candidates it started with.
81
+ const length = bindings.length;
82
+ for (let index = 0; index < length; index += 1) {
81
83
  const binding = bindings[index];
84
+ if (!includesMembers && binding.isMany) {
85
+ continue;
86
+ }
82
87
  if (requiresSlotMatch && !matchesSlot(binding.slot, options)) {
83
88
  continue;
84
89
  }
@@ -26,17 +26,20 @@ export declare function whenAnyAncestorIs(token: Token<unknown> | Constructor):
26
26
  */
27
27
  export declare function whenNoAncestorIs(token: Token<unknown> | Constructor): BindingConstraint;
28
28
  /**
29
- * Matches when the direct parent slot carries the given name.
29
+ * Matches when the direct parent resolves the given token at the slot carrying the given name.
30
+ *
31
+ * @remarks A slot name is a label on one token's bindings, so the token is part of the question — and
32
+ * what types `name` to the names that token declares.
30
33
  *
31
34
  * @since 0.3.16-canary.0
32
35
  */
33
- export declare function whenParentNamed(name: string): BindingConstraint;
36
+ export declare function whenParentNamed<Names extends string>(token: Token<unknown, Names> | Constructor, name: NoInfer<Names>): BindingConstraint;
34
37
  /**
35
- * Matches when at least one ancestor slot carries the given name.
38
+ * Matches when at least one ancestor resolves the given token at the slot carrying the given name.
36
39
  *
37
40
  * @since 0.3.16-canary.0
38
41
  */
39
- export declare function whenAnyAncestorNamed(name: string): BindingConstraint;
42
+ export declare function whenAnyAncestorNamed<Names extends string>(token: Token<unknown, Names> | Constructor, name: NoInfer<Names>): BindingConstraint;
40
43
  /**
41
44
  * Matches when the direct parent slot carries the given tag pair.
42
45
  *
@@ -1,5 +1,5 @@
1
- import { requiringAncestorSlotName } from "#/core/constraint-requirement";
2
- import { coversTagKeys, tagKeyMaskOf } from "#/core/tag";
1
+ import { requiringAncestorSlotName, requiringAncestorSlotNames } from "#/core/constraint-requirement";
2
+ import { coversTagKeys, slotName, tagKeyMaskOf } from "#/core/tag";
3
3
  import { tokenName } from "#/core/token";
4
4
  import { EmptyTagCriteriaError } from "#/errors/errors";
5
5
  /**
@@ -39,20 +39,27 @@ export function whenNoAncestorIs(token) {
39
39
  return (constraintContext) => constraintContext.ancestors.every((ancestorFrame) => ancestorFrame.tokenName !== tokenDisplayName);
40
40
  }
41
41
  /**
42
- * Matches when the direct parent slot carries the given name.
42
+ * Matches when the direct parent resolves the given token at the slot carrying the given name.
43
+ *
44
+ * @remarks A slot name is a label on one token's bindings, so the token is part of the question — and
45
+ * what types `name` to the names that token declares.
43
46
  *
44
47
  * @since 0.3.16-canary.0
45
48
  */
46
- export function whenParentNamed(name) {
47
- return requiringAncestorSlotName((constraintContext) => constraintContext.parent !== undefined && constraintContext.parent.slot.name === name, name, "whenParentNamed");
49
+ export function whenParentNamed(token, name) {
50
+ const parentTokenName = tokenName(token);
51
+ return requiringAncestorSlotName((constraintContext) => constraintContext.parent !== undefined &&
52
+ constraintContext.parent.tokenName === parentTokenName &&
53
+ constraintContext.parent.slot.name === name, { tokenName: parentTokenName, name, helperName: "whenParentNamed" });
48
54
  }
49
55
  /**
50
- * Matches when at least one ancestor slot carries the given name.
56
+ * Matches when at least one ancestor resolves the given token at the slot carrying the given name.
51
57
  *
52
58
  * @since 0.3.16-canary.0
53
59
  */
54
- export function whenAnyAncestorNamed(name) {
55
- return requiringAncestorSlotName((constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.slot.name === name), name, "whenAnyAncestorNamed");
60
+ export function whenAnyAncestorNamed(token, name) {
61
+ const ancestorTokenName = tokenName(token);
62
+ return requiringAncestorSlotName((constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.tokenName === ancestorTokenName && ancestorFrame.slot.name === name), { tokenName: ancestorTokenName, name, helperName: "whenAnyAncestorNamed" });
56
63
  }
57
64
  /**
58
65
  * Matches when the direct parent slot carries the given tag pair.
@@ -60,7 +67,7 @@ export function whenAnyAncestorNamed(name) {
60
67
  * @since 0.3.16-canary.0
61
68
  */
62
69
  export function whenParentTagged(criterion) {
63
- return (constraintContext) => constraintContext.parent !== undefined && constraintContext.parent.slot.tags.includes(criterion);
70
+ return requiringReservedNamesAmong((constraintContext) => constraintContext.parent !== undefined && constraintContext.parent.slot.tags.includes(criterion), [criterion], "whenParentTagged");
64
71
  }
65
72
  /**
66
73
  * Matches when at least one ancestor slot carries the given tag pair.
@@ -68,7 +75,7 @@ export function whenParentTagged(criterion) {
68
75
  * @since 0.3.16-canary.0
69
76
  */
70
77
  export function whenAnyAncestorTagged(criterion) {
71
- return (constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.slot.tags.includes(criterion));
78
+ return requiringReservedNamesAmong((constraintContext) => constraintContext.ancestors.some((ancestorFrame) => ancestorFrame.slot.tags.includes(criterion)), [criterion], "whenAnyAncestorTagged");
72
79
  }
73
80
  /**
74
81
  * Matches when the direct parent slot carries **all** of the given tag pairs.
@@ -80,13 +87,13 @@ export function whenAnyAncestorTagged(criterion) {
80
87
  export function whenParentTaggedAll(tags) {
81
88
  assertHasCriteria(tags, "whenParentTaggedAll");
82
89
  const wanted = tagKeyMaskOf(tags);
83
- return (constraintContext) => {
90
+ return requiringReservedNamesAmong((constraintContext) => {
84
91
  const { parent } = constraintContext;
85
92
  if (parent === undefined || !coversTagKeys(parent.slot.keyMask, wanted)) {
86
93
  return false;
87
94
  }
88
95
  return tags.every((criterion) => parent.slot.tags.includes(criterion));
89
- };
96
+ }, tags, "whenParentTaggedAll");
90
97
  }
91
98
  /**
92
99
  * Matches when at least one ancestor slot carries **all** of the given tag pairs.
@@ -98,7 +105,21 @@ export function whenParentTaggedAll(tags) {
98
105
  export function whenAnyAncestorTaggedAll(tags) {
99
106
  assertHasCriteria(tags, "whenAnyAncestorTaggedAll");
100
107
  const wanted = tagKeyMaskOf(tags);
101
- return (constraintContext) => constraintContext.ancestors.some((frame) => coversTagKeys(frame.slot.keyMask, wanted) && tags.every((criterion) => frame.slot.tags.includes(criterion)));
108
+ return requiringReservedNamesAmong((constraintContext) => constraintContext.ancestors.some((frame) => coversTagKeys(frame.slot.keyMask, wanted) && tags.every((criterion) => frame.slot.tags.includes(criterion))), tags, "whenAnyAncestorTaggedAll");
109
+ }
110
+ /**
111
+ * Records a requirement for every reserved-key criterion in a list, so a name spelled through the tag
112
+ * lane is validated exactly as one spelled through `whenNamed`.
113
+ */
114
+ function requiringReservedNamesAmong(predicate, tags, helperName) {
115
+ let requirements;
116
+ for (const criterion of tags) {
117
+ if (criterion.key === slotName) {
118
+ requirements ??= [];
119
+ requirements.push({ tokenName: undefined, name: criterion.value, helperName });
120
+ }
121
+ }
122
+ return requirements === undefined ? predicate : requiringAncestorSlotNames(predicate, requirements);
102
123
  }
103
124
  /**
104
125
  * Refuses a criteria list with nothing in it.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codefast/di",
3
- "version": "0.8.1",
3
+ "version": "0.10.0",
4
4
  "description": "Lightweight dependency injection primitives for Codefast",
5
5
  "keywords": [
6
6
  "codefast",
@@ -9,8 +9,12 @@
9
9
  "inversion-of-control",
10
10
  "typescript"
11
11
  ],
12
+ "homepage": "https://github.com/codefastlabs/codefast/tree/main/packages/di#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/codefastlabs/codefast/issues"
15
+ },
12
16
  "license": "MIT",
13
- "author": "Vuong Phan <mr.thevuong@gmail.com>",
17
+ "author": "Vuong Phan <mr.thevuong@gmail.com> (https://github.com/thevuong)",
14
18
  "repository": {
15
19
  "type": "git",
16
20
  "url": "git+https://github.com/codefastlabs/codefast.git",
@@ -28,20 +32,6 @@
28
32
  "module": "./dist/index.js",
29
33
  "types": "./dist/index.d.ts",
30
34
  "imports": {
31
- "#/tests/*": [
32
- "./tests/*",
33
- "./tests/*.ts",
34
- "./tests/*.tsx",
35
- "./tests/*/index.ts",
36
- "./tests/*/index.tsx"
37
- ],
38
- "#/examples/*": [
39
- "./examples/*",
40
- "./examples/*.ts",
41
- "./examples/*.tsx",
42
- "./examples/*/index.ts",
43
- "./examples/*/index.tsx"
44
- ],
45
35
  "#/*": {
46
36
  "types": "./dist/*.d.ts",
47
37
  "default": "./dist/*.js"
@@ -92,6 +82,10 @@
92
82
  "types": "./dist/core/registry.d.ts",
93
83
  "import": "./dist/core/registry.js"
94
84
  },
85
+ "./core/state-epoch": {
86
+ "types": "./dist/core/state-epoch.d.ts",
87
+ "import": "./dist/core/state-epoch.js"
88
+ },
95
89
  "./core/tag": {
96
90
  "types": "./dist/core/tag.d.ts",
97
91
  "import": "./dist/core/tag.js"
@@ -208,6 +202,10 @@
208
202
  "types": "./dist/resolution/plan/instantiation-plan.d.ts",
209
203
  "import": "./dist/resolution/plan/instantiation-plan.js"
210
204
  },
205
+ "./resolution/plan/plan-codegen": {
206
+ "types": "./dist/resolution/plan/plan-codegen.d.ts",
207
+ "import": "./dist/resolution/plan/plan-codegen.js"
208
+ },
211
209
  "./resolution/resolver": {
212
210
  "types": "./dist/resolution/resolver.d.ts",
213
211
  "import": "./dist/resolution/resolver.js"
@@ -225,31 +223,7 @@
225
223
  "publishConfig": {
226
224
  "access": "public"
227
225
  },
228
- "devDependencies": {
229
- "@babel/core": "^8.0.1",
230
- "@babel/plugin-proposal-decorators": "8.0.2",
231
- "@rolldown/plugin-babel": "^0.2.3",
232
- "@types/node": "^26.4.1",
233
- "@vitest/coverage-v8": "^5.0.0",
234
- "tsx": "^4.23.13",
235
- "typescript": "^7.0.2",
236
- "vitest": "^5.0.0",
237
- "@codefast/typescript-config": "0.9.0"
238
- },
239
226
  "engines": {
240
227
  "node": ">=24.0.0"
241
- },
242
- "scripts": {
243
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
244
- "check-types": "tsc --noEmit",
245
- "clean": "rm -rf dist",
246
- "examples": "node examples/run.mjs",
247
- "test": "vitest run",
248
- "test:coverage": "vitest run --coverage",
249
- "test:e2e": "vitest run tests/e2e",
250
- "test:integration": "vitest run tests/integration",
251
- "test:type": "vitest run tests/types",
252
- "test:unit": "vitest run tests/unit",
253
- "test:watch": "vitest"
254
228
  }
255
229
  }