@nexus-ioc/core 0.6.2 → 0.7.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 (220) hide show
  1. package/README.md +731 -1
  2. package/dist/cjs/browser.js +2 -2
  3. package/dist/cjs/browser.js.map +1 -1
  4. package/dist/cjs/core/child-module.js +32 -0
  5. package/dist/cjs/core/child-module.js.map +1 -0
  6. package/dist/cjs/core/graph/analyze-lazy-module.js +54 -0
  7. package/dist/cjs/core/graph/analyze-lazy-module.js.map +1 -0
  8. package/dist/cjs/core/graph/analyze-module.js +11 -0
  9. package/dist/cjs/core/graph/analyze-module.js.map +1 -1
  10. package/dist/cjs/core/graph/module-graph.js +685 -70
  11. package/dist/cjs/core/graph/module-graph.js.map +1 -1
  12. package/dist/cjs/core/injection/injection-context.js +29 -0
  13. package/dist/cjs/core/injection/injection-context.js.map +1 -0
  14. package/dist/cjs/core/lazy-module-loader.js +41 -0
  15. package/dist/cjs/core/lazy-module-loader.js.map +1 -0
  16. package/dist/cjs/core/module-ref.js +84 -0
  17. package/dist/cjs/core/module-ref.js.map +1 -0
  18. package/dist/cjs/core/modules/container.js +305 -17
  19. package/dist/cjs/core/modules/container.js.map +1 -1
  20. package/dist/cjs/core/modules/module-container-factory.js +2 -2
  21. package/dist/cjs/core/modules/module-container-factory.js.map +1 -1
  22. package/dist/cjs/core/modules/module-container.js +54 -16
  23. package/dist/cjs/core/modules/module-container.js.map +1 -1
  24. package/dist/cjs/core/modules/module-token-factory.js.map +1 -1
  25. package/dist/cjs/core/modules/modules-container.js +43 -3
  26. package/dist/cjs/core/modules/modules-container.js.map +1 -1
  27. package/dist/cjs/core/nexus-applications.browser.js +57 -15
  28. package/dist/cjs/core/nexus-applications.browser.js.map +1 -1
  29. package/dist/cjs/core/nexus-applications.js +440 -82
  30. package/dist/cjs/core/nexus-applications.js.map +1 -1
  31. package/dist/cjs/core/nexus-applications.server.js +57 -15
  32. package/dist/cjs/core/nexus-applications.server.js.map +1 -1
  33. package/dist/cjs/core/resolver/providers-container.js.map +1 -1
  34. package/dist/cjs/core/resolver/resolution-context.js +62 -0
  35. package/dist/cjs/core/resolver/resolution-context.js.map +1 -0
  36. package/dist/cjs/core/resolver/resolver.js +523 -99
  37. package/dist/cjs/core/resolver/resolver.js.map +1 -1
  38. package/dist/cjs/decorators/inject.js +6 -3
  39. package/dist/cjs/decorators/inject.js.map +1 -1
  40. package/dist/cjs/decorators/injectable.js +0 -7
  41. package/dist/cjs/decorators/injectable.js.map +1 -1
  42. package/dist/cjs/decorators/{nsModule.js → module.js} +9 -7
  43. package/dist/cjs/decorators/module.js.map +1 -0
  44. package/dist/cjs/errors/bootstrap-error.js +47 -0
  45. package/dist/cjs/errors/bootstrap-error.js.map +1 -0
  46. package/dist/cjs/errors/child-errors.js +21 -0
  47. package/dist/cjs/errors/child-errors.js.map +1 -0
  48. package/dist/cjs/errors/index.js +24 -0
  49. package/dist/cjs/errors/index.js.map +1 -0
  50. package/dist/cjs/errors/injection-errors.js +25 -0
  51. package/dist/cjs/errors/injection-errors.js.map +1 -0
  52. package/dist/cjs/errors/lazy-module-errors.js +26 -0
  53. package/dist/cjs/errors/lazy-module-errors.js.map +1 -0
  54. package/dist/cjs/errors/provider-errors.js +95 -0
  55. package/dist/cjs/errors/provider-errors.js.map +1 -0
  56. package/dist/cjs/index.js +30 -6
  57. package/dist/cjs/index.js.map +1 -1
  58. package/dist/cjs/interfaces/index.js +1 -0
  59. package/dist/cjs/interfaces/index.js.map +1 -1
  60. package/dist/cjs/interfaces/modules/index.js +1 -0
  61. package/dist/cjs/interfaces/modules/index.js.map +1 -1
  62. package/dist/cjs/interfaces/modules/module-graph.interface.js +2 -0
  63. package/dist/cjs/interfaces/modules/module-graph.interface.js.map +1 -1
  64. package/dist/cjs/interfaces/modules/parent-link.interface.js +3 -0
  65. package/dist/cjs/interfaces/modules/parent-link.interface.js.map +1 -0
  66. package/dist/cjs/interfaces/on-module-destroy.interface.js +3 -0
  67. package/dist/cjs/interfaces/on-module-destroy.interface.js.map +1 -0
  68. package/dist/cjs/interfaces/scope.interface.js +2 -55
  69. package/dist/cjs/interfaces/scope.interface.js.map +1 -1
  70. package/dist/cjs/interfaces/validate-module-keys.js +2 -0
  71. package/dist/cjs/interfaces/validate-module-keys.js.map +1 -1
  72. package/dist/cjs/internal.js +9 -0
  73. package/dist/cjs/internal.js.map +1 -0
  74. package/dist/cjs/server.js +2 -2
  75. package/dist/cjs/server.js.map +1 -1
  76. package/dist/cjs/utils/forward-ref.js +14 -0
  77. package/dist/cjs/utils/forward-ref.js.map +1 -0
  78. package/dist/cjs/utils/helpers.js +14 -4
  79. package/dist/cjs/utils/helpers.js.map +1 -1
  80. package/dist/cjs/utils/lazy-module.js +33 -0
  81. package/dist/cjs/utils/lazy-module.js.map +1 -0
  82. package/dist/esm/browser.js +1 -1
  83. package/dist/esm/browser.js.map +1 -1
  84. package/dist/esm/core/child-module.js +29 -0
  85. package/dist/esm/core/child-module.js.map +1 -0
  86. package/dist/esm/core/graph/analyze-lazy-module.js +50 -0
  87. package/dist/esm/core/graph/analyze-lazy-module.js.map +1 -0
  88. package/dist/esm/core/graph/analyze-module.js +11 -0
  89. package/dist/esm/core/graph/analyze-module.js.map +1 -1
  90. package/dist/esm/core/graph/module-graph.js +686 -71
  91. package/dist/esm/core/graph/module-graph.js.map +1 -1
  92. package/dist/esm/core/injection/injection-context.js +25 -0
  93. package/dist/esm/core/injection/injection-context.js.map +1 -0
  94. package/dist/esm/core/lazy-module-loader.js +36 -0
  95. package/dist/esm/core/lazy-module-loader.js.map +1 -0
  96. package/dist/esm/core/module-ref.js +80 -0
  97. package/dist/esm/core/module-ref.js.map +1 -0
  98. package/dist/esm/core/modules/container.js +305 -17
  99. package/dist/esm/core/modules/container.js.map +1 -1
  100. package/dist/esm/core/modules/module-container-factory.js +2 -2
  101. package/dist/esm/core/modules/module-container-factory.js.map +1 -1
  102. package/dist/esm/core/modules/module-container.js +55 -17
  103. package/dist/esm/core/modules/module-container.js.map +1 -1
  104. package/dist/esm/core/modules/module-token-factory.js.map +1 -1
  105. package/dist/esm/core/modules/modules-container.js +43 -3
  106. package/dist/esm/core/modules/modules-container.js.map +1 -1
  107. package/dist/esm/core/nexus-applications.browser.js +55 -13
  108. package/dist/esm/core/nexus-applications.browser.js.map +1 -1
  109. package/dist/esm/core/nexus-applications.js +439 -81
  110. package/dist/esm/core/nexus-applications.js.map +1 -1
  111. package/dist/esm/core/nexus-applications.server.js +55 -13
  112. package/dist/esm/core/nexus-applications.server.js.map +1 -1
  113. package/dist/esm/core/resolver/providers-container.js.map +1 -1
  114. package/dist/esm/core/resolver/resolution-context.js +58 -0
  115. package/dist/esm/core/resolver/resolution-context.js.map +1 -0
  116. package/dist/esm/core/resolver/resolver.js +524 -100
  117. package/dist/esm/core/resolver/resolver.js.map +1 -1
  118. package/dist/esm/decorators/inject.js +6 -3
  119. package/dist/esm/decorators/inject.js.map +1 -1
  120. package/dist/esm/decorators/injectable.js +0 -7
  121. package/dist/esm/decorators/injectable.js.map +1 -1
  122. package/dist/esm/decorators/{nsModule.js → module.js} +8 -6
  123. package/dist/esm/decorators/module.js.map +1 -0
  124. package/dist/esm/errors/bootstrap-error.js +42 -0
  125. package/dist/esm/errors/bootstrap-error.js.map +1 -0
  126. package/dist/esm/errors/child-errors.js +17 -0
  127. package/dist/esm/errors/child-errors.js.map +1 -0
  128. package/dist/esm/errors/index.js +6 -0
  129. package/dist/esm/errors/index.js.map +1 -0
  130. package/dist/esm/errors/injection-errors.js +20 -0
  131. package/dist/esm/errors/injection-errors.js.map +1 -0
  132. package/dist/esm/errors/lazy-module-errors.js +21 -0
  133. package/dist/esm/errors/lazy-module-errors.js.map +1 -0
  134. package/dist/esm/errors/provider-errors.js +84 -0
  135. package/dist/esm/errors/provider-errors.js.map +1 -0
  136. package/dist/esm/index.js +9 -4
  137. package/dist/esm/index.js.map +1 -1
  138. package/dist/esm/interfaces/index.js +1 -0
  139. package/dist/esm/interfaces/index.js.map +1 -1
  140. package/dist/esm/interfaces/modules/index.js +1 -0
  141. package/dist/esm/interfaces/modules/index.js.map +1 -1
  142. package/dist/esm/interfaces/modules/module-graph.interface.js +2 -0
  143. package/dist/esm/interfaces/modules/module-graph.interface.js.map +1 -1
  144. package/dist/esm/interfaces/modules/parent-link.interface.js +2 -0
  145. package/dist/esm/interfaces/modules/parent-link.interface.js.map +1 -0
  146. package/dist/esm/interfaces/on-module-destroy.interface.js +2 -0
  147. package/dist/esm/interfaces/on-module-destroy.interface.js.map +1 -0
  148. package/dist/esm/interfaces/scope.interface.js +1 -55
  149. package/dist/esm/interfaces/scope.interface.js.map +1 -1
  150. package/dist/esm/interfaces/validate-module-keys.js +2 -0
  151. package/dist/esm/interfaces/validate-module-keys.js.map +1 -1
  152. package/dist/esm/internal.js +3 -0
  153. package/dist/esm/internal.js.map +1 -0
  154. package/dist/esm/server.js +1 -1
  155. package/dist/esm/server.js.map +1 -1
  156. package/dist/esm/utils/forward-ref.js +10 -0
  157. package/dist/esm/utils/forward-ref.js.map +1 -0
  158. package/dist/esm/utils/helpers.js +14 -4
  159. package/dist/esm/utils/helpers.js.map +1 -1
  160. package/dist/esm/utils/lazy-module.js +29 -0
  161. package/dist/esm/utils/lazy-module.js.map +1 -0
  162. package/dist/types/browser.d.ts +1 -1
  163. package/dist/types/core/child-module.d.ts +8 -0
  164. package/dist/types/core/graph/analyze-lazy-module.d.ts +28 -0
  165. package/dist/types/core/graph/analyze-module.d.ts +4 -2
  166. package/dist/types/core/graph/analyze-provider.d.ts +4 -4
  167. package/dist/types/core/graph/module-graph.d.ts +200 -7
  168. package/dist/types/core/graph/providers/analyze-class-provider.d.ts +1 -1
  169. package/dist/types/core/graph/providers/analyze-factory-provider.d.ts +2 -2
  170. package/dist/types/core/graph/providers/analyze-value-provider.d.ts +1 -1
  171. package/dist/types/core/injection/injection-context.d.ts +29 -0
  172. package/dist/types/core/lazy-module-loader.d.ts +17 -0
  173. package/dist/types/core/module-ref.d.ts +34 -0
  174. package/dist/types/core/modules/container.d.ts +134 -18
  175. package/dist/types/core/modules/module-container-factory.d.ts +3 -3
  176. package/dist/types/core/modules/module-container.d.ts +44 -5
  177. package/dist/types/core/modules/module-token-factory.d.ts +2 -2
  178. package/dist/types/core/modules/modules-container.d.ts +23 -4
  179. package/dist/types/core/nexus-applications.browser.d.ts +30 -8
  180. package/dist/types/core/nexus-applications.d.ts +219 -55
  181. package/dist/types/core/nexus-applications.server.d.ts +30 -8
  182. package/dist/types/core/resolver/providers-container.d.ts +2 -2
  183. package/dist/types/core/resolver/resolution-context.d.ts +22 -0
  184. package/dist/types/core/resolver/resolver.d.ts +122 -4
  185. package/dist/types/decorators/inject.d.ts +9 -5
  186. package/dist/types/decorators/injectable.d.ts +0 -7
  187. package/dist/types/decorators/{nsModule.d.ts → module.d.ts} +6 -5
  188. package/dist/types/errors/bootstrap-error.d.ts +6 -0
  189. package/dist/types/errors/child-errors.d.ts +10 -0
  190. package/dist/types/errors/index.d.ts +5 -0
  191. package/dist/types/errors/injection-errors.d.ts +11 -0
  192. package/dist/types/errors/lazy-module-errors.d.ts +12 -0
  193. package/dist/types/errors/provider-errors.d.ts +47 -0
  194. package/dist/types/index.d.ts +13 -4
  195. package/dist/types/interfaces/abstract.interface.d.ts +1 -3
  196. package/dist/types/interfaces/dynamic-module.interface.d.ts +7 -5
  197. package/dist/types/interfaces/index.d.ts +1 -0
  198. package/dist/types/interfaces/injection-token.interface.d.ts +1 -3
  199. package/dist/types/interfaces/module-metadata.interface.d.ts +12 -4
  200. package/dist/types/interfaces/module-types.interface.d.ts +21 -24
  201. package/dist/types/interfaces/modules/container.interface.d.ts +10 -4
  202. package/dist/types/interfaces/modules/index.d.ts +1 -0
  203. package/dist/types/interfaces/modules/module-container-factory.interface.d.ts +2 -2
  204. package/dist/types/interfaces/modules/module-graph.interface.d.ts +85 -12
  205. package/dist/types/interfaces/modules/module-token-factory.interface.d.ts +2 -2
  206. package/dist/types/interfaces/modules/modules-container.interface.d.ts +5 -4
  207. package/dist/types/interfaces/modules/parent-link.interface.d.ts +12 -0
  208. package/dist/types/interfaces/nexus-application.interface.d.ts +49 -6
  209. package/dist/types/interfaces/on-module-destroy.interface.d.ts +3 -0
  210. package/dist/types/interfaces/plugins/scanner-plugin.interface.d.ts +1 -1
  211. package/dist/types/interfaces/scope.interface.d.ts +1 -54
  212. package/dist/types/interfaces/validate-module-keys.d.ts +1 -0
  213. package/dist/types/internal.d.ts +3 -0
  214. package/dist/types/server.d.ts +1 -1
  215. package/dist/types/utils/forward-ref.d.ts +5 -0
  216. package/dist/types/utils/helpers.d.ts +8 -8
  217. package/dist/types/utils/lazy-module.d.ts +20 -0
  218. package/package.json +21 -4
  219. package/dist/cjs/decorators/nsModule.js.map +0 -1
  220. package/dist/esm/decorators/nsModule.js.map +0 -1
@@ -1,27 +1,60 @@
1
1
  import { EdgeTypeEnum, MODULE_TOKEN_WATERMARK, MODULE_WATERMARK, NodeTypeEnum, PROPERTY_DEPS_METADATA, PROPERTY_OPTIONAL_DEPS_METADATA, SELF_DECLARED_DEPS_METADATA, SELF_DECLARED_OPTIONAL_DEPS_METADATA, } from "../../interfaces/index.js";
2
- import { getDependencyToken, getProviderToken, isModule, } from "../../utils/helpers.js";
2
+ import { isForwardRef } from "../../utils/forward-ref.js";
3
+ import { getDependencyToken, getModuleLabel, getProviderToken, isModule, } from "../../utils/helpers.js";
4
+ import { isLazyModule } from "../../utils/lazy-module.js";
5
+ import { AnalyzeLazyModule } from "./analyze-lazy-module.js";
3
6
  import { AnalyzeModule } from "./analyze-module.js";
4
7
  import { isAnalyzeClassProvider, isAnalyzeFactoryProvider, isAnalyzeFunctionProvider, } from "./helpers/index.js";
5
8
  import { ProviderFactory } from "./providers/provider-factory.js";
6
9
  /**
7
- * Helper function to safely convert InjectionToken to string for error messages
10
+ * Helper function to safely convert ProviderToken to string for error messages
8
11
  */
9
12
  function tokenToString(token) {
10
13
  return typeof token === "function" ? token.name : String(token);
11
14
  }
12
- /**
13
- * Type guard to check if a node is an AnalyzeProvider
14
- */
15
- function isProviderNode(entry) {
16
- return entry[1].type === NodeTypeEnum.PROVIDER;
17
- }
18
15
  export class ModuleGraph {
19
- constructor(_root) {
16
+ constructor(_root, _internalRoots = [], options = {}) {
20
17
  this._root = _root;
18
+ this._internalRoots = _internalRoots;
19
+ this.options = options;
21
20
  this._nodes = new Map();
22
21
  this._edges = new Map();
23
22
  this._globalModules = new Map();
23
+ /** Module token -> its own declared provider tokens. */
24
+ this._moduleProviders = new Map();
25
+ /**
26
+ * Module token -> the other module tokens that need it alive: every
27
+ * module that imports it, plus every module owning a provider that
28
+ * depends (via a resolved `DEPENDENCY` edge, including one resolved
29
+ * through a `@Global()` module) on one of its providers.
30
+ */
31
+ this._moduleReferrers = new Map();
32
+ /**
33
+ * Late-bound edge index: target token -> source provider token -> whether
34
+ * the edge is bound. A `lateBound` edge is an `@Optional()` static edge
35
+ * whose target was not visible from the source's module when it was
36
+ * compiled (it may not have existed yet). The resolver injects its target
37
+ * only while the target exists and is visible. "Bound" means that held
38
+ * when a segment added the target (or a global module exposing it), so
39
+ * the source's module was recorded as a referrer of the target's module;
40
+ * only a bound edge counts in `moduleDependencies`, which keeps the
41
+ * referrer purge symmetric with the recording.
42
+ */
43
+ this._lateBound = new Map();
44
+ /**
45
+ * Child declaration index: the child's module class (or the `LazyModule.id`
46
+ * symbol of a lazy ref) -> the token of the one module declaring it.
47
+ */
48
+ this._childDeclarations = new Map();
24
49
  this._errors = [];
50
+ /**
51
+ * While a segment compiles, its errors are collected here instead of in
52
+ * `_errors`, so they are never mixed with (or mistaken for) errors of the
53
+ * eager graph or of another segment.
54
+ */
55
+ this._errorSink = null;
56
+ /** See `version`. */
57
+ this._version = 0;
25
58
  }
26
59
  get nodes() {
27
60
  return this._nodes;
@@ -32,11 +65,389 @@ export class ModuleGraph {
32
65
  get errors() {
33
66
  return this._errors;
34
67
  }
68
+ get rootToken() {
69
+ return this._root.token;
70
+ }
71
+ get internalRootTokens() {
72
+ return this._internalRoots.map((root) => root.token);
73
+ }
74
+ /**
75
+ * Bumped whenever `compile`, `compileSegment` or `unloadSegment` may have
76
+ * changed which providers exist or which modules see them. A cache of
77
+ * `isProviderVisible` results is valid while this number is unchanged.
78
+ */
79
+ get version() {
80
+ return this._version;
81
+ }
82
+ /** Records a graph error in the active sink, or in the shared list. */
83
+ pushError(error) {
84
+ (this._errorSink ?? this._errors).push(error);
85
+ }
86
+ /**
87
+ * Compiles the eager graph. Internal roots are added before the user root so
88
+ * `_globalModules` already contains the internal modules when user providers
89
+ * are checked by `isProviderExported`.
90
+ */
35
91
  async compile() {
36
- await this.addModules();
37
- await this.addDependencies();
38
- await this.detectCircularDependencies();
39
- await this.detectCircularImports();
92
+ const moduleTokens = [];
93
+ const providerTokens = [];
94
+ for (const root of [...this._internalRoots, this._root]) {
95
+ const added = await this.addModules(root, false);
96
+ moduleTokens.push(...added.moduleTokens);
97
+ providerTokens.push(...added.providerTokens);
98
+ }
99
+ await this.addDependencies(providerTokens);
100
+ await this.detectCircularDependencies(providerTokens);
101
+ await this.detectCircularImports(moduleTokens);
102
+ this._version++;
103
+ }
104
+ /**
105
+ * Compiles the part of the graph reachable from `root` that is not already
106
+ * registered, attributing it to the `lazyModule` placeholder node.
107
+ *
108
+ * The pass is atomic: if it produces any error the segment is rolled back
109
+ * (every node, edge, global registration and LAZY placeholder it created is
110
+ * removed) and the placeholder stays unloaded. On success the placeholder is
111
+ * marked loaded. A placeholder that already existed before the segment
112
+ * started is never removed.
113
+ *
114
+ * The segment's errors are collected in a segment-local sink and never
115
+ * reach `this.errors`, so they cannot be attributed to the eager graph or
116
+ * to another segment. The caller (`Container.load`) runs one segment at a
117
+ * time, which is what makes the sink and the graph mutations safe under
118
+ * concurrent loads of different refs.
119
+ */
120
+ async compileSegment(root, lazyModule) {
121
+ const createdPlaceholder = !this._nodes.has(lazyModule.id);
122
+ if (createdPlaceholder) {
123
+ this.addNode(lazyModule.id, new AnalyzeLazyModule(lazyModule));
124
+ }
125
+ const errors = [];
126
+ const previousSink = this._errorSink;
127
+ this._errorSink = errors;
128
+ try {
129
+ const added = await this.addModules(root, true);
130
+ await this.addDependencies(added.providerTokens);
131
+ await this.detectCircularDependencies(added.providerTokens);
132
+ await this.detectCircularImports(added.moduleTokens);
133
+ if (errors.length > 0) {
134
+ this.removeTokens([
135
+ ...added.moduleTokens,
136
+ ...added.providerTokens,
137
+ ...added.lazyTokens,
138
+ ...(createdPlaceholder ? [lazyModule.id] : []),
139
+ ]);
140
+ }
141
+ else {
142
+ this.bindLateEdges(added.moduleTokens, added.providerTokens);
143
+ this._nodes.get(lazyModule.id).markLoaded(root.token);
144
+ }
145
+ return {
146
+ lazyModule,
147
+ moduleContainer: root,
148
+ moduleTokens: added.moduleTokens,
149
+ providerTokens: added.providerTokens,
150
+ errors,
151
+ };
152
+ }
153
+ finally {
154
+ this._errorSink = previousSink;
155
+ // Even a rolled-back segment was briefly part of the graph.
156
+ this._version++;
157
+ }
158
+ }
159
+ /**
160
+ * Binds the late-bound edges (see `_lateBound`) whose target this
161
+ * segment made visible to the edge's source, recording the referrer the
162
+ * resolver's injection now implies. The same visibility check as the
163
+ * resolver's, so bookkeeping and injection always agree.
164
+ *
165
+ * Only two things can make a late-bound target visible to an existing
166
+ * source: the target's node being added (looked up per added provider)
167
+ * or a new `@Global()` module exposing it (compiled modules' imports
168
+ * never change). Only a segment adding a global module rechecks every
169
+ * indexed target; the index holds only late-bound edges, so it is small.
170
+ *
171
+ * Once bound, an edge keeps the target's module pinned for the consumer
172
+ * module's lifetime, even after the global module that exposed it
173
+ * unloads. This is deliberately safe: instances created meanwhile may
174
+ * still hold the target.
175
+ */
176
+ bindLateEdges(addedModuleTokens, addedProviderTokens) {
177
+ if (this._lateBound.size === 0) {
178
+ return;
179
+ }
180
+ const addsGlobal = addedModuleTokens.some((token) => this._globalModules.has(token));
181
+ const targets = addsGlobal
182
+ ? [...this._lateBound.keys()]
183
+ : addedProviderTokens.filter((token) => this._lateBound.has(token));
184
+ for (const target of targets) {
185
+ if (this._nodes.get(target)?.type !== NodeTypeEnum.PROVIDER) {
186
+ continue;
187
+ }
188
+ const sources = this._lateBound.get(target);
189
+ for (const [source, bound] of sources) {
190
+ const consumer = this._nodes.get(source);
191
+ if (bound || consumer?.type !== NodeTypeEnum.PROVIDER) {
192
+ continue;
193
+ }
194
+ if (this.isProviderVisible(consumer.moduleContainer, target)) {
195
+ sources.set(source, true);
196
+ this.recordCrossModuleReferrer(source, target, true);
197
+ }
198
+ }
199
+ }
200
+ }
201
+ /**
202
+ * Drops removed sources from the late-bound index and unbinds edges
203
+ * whose target was removed (a later load of the target binds them
204
+ * again). Runs after `purgeModuleIndexes`, which reads the bound state.
205
+ */
206
+ purgeLateBound(tokens) {
207
+ if (this._lateBound.size === 0) {
208
+ return;
209
+ }
210
+ const removed = new Set(tokens);
211
+ for (const [target, sources] of this._lateBound) {
212
+ for (const [source, bound] of sources) {
213
+ if (removed.has(source)) {
214
+ sources.delete(source);
215
+ }
216
+ else if (bound && removed.has(target)) {
217
+ sources.set(source, false);
218
+ }
219
+ }
220
+ if (sources.size === 0) {
221
+ this._lateBound.delete(target);
222
+ }
223
+ }
224
+ }
225
+ /**
226
+ * Rolls back a segment that failed validation: removes every node, edge
227
+ * and global registration it created (module, provider and LAZY
228
+ * placeholder tokens are all passed in together). Also purges
229
+ * `_moduleProviders`/`_moduleReferrers` index entries for `tokens` via
230
+ * `purgeModuleIndexes` — without this, a referrer entry recorded by
231
+ * `addModuleImports` before the rollback (pointing at a module token
232
+ * that no longer exists) would survive and could later be mistaken by
233
+ * `rescueCandidates` for a live outside referrer of a module reused by a
234
+ * different, successful segment.
235
+ */
236
+ removeTokens(tokens) {
237
+ this.purgeModuleIndexes(tokens);
238
+ this.purgeLateBound(tokens);
239
+ for (const token of tokens) {
240
+ this._nodes.delete(token);
241
+ this._edges.delete(token);
242
+ this._globalModules.delete(token);
243
+ }
244
+ }
245
+ /**
246
+ * Unloads a loaded lazy segment. Scoped to the segment's own subgraph,
247
+ * not the whole live graph: collects everything reachable from the
248
+ * segment's root (pruned at any module in `protectedRoots` — the eager
249
+ * root, internal roots, and every *other* currently loaded segment's
250
+ * root), then rescues back anything a survivor still needs. Returns the
251
+ * modules and providers that were actually destroyed; a no-op (empty
252
+ * result) if the ref is not currently loaded.
253
+ */
254
+ unloadSegment(lazyModule, protectedRoots) {
255
+ const placeholder = this._nodes.get(lazyModule.id);
256
+ if (!placeholder ||
257
+ !placeholder.loaded ||
258
+ placeholder.moduleToken === null) {
259
+ return { destroyedModules: [], destroyedProviders: [] };
260
+ }
261
+ const candidates = this.collectCandidates(placeholder.moduleToken, protectedRoots);
262
+ const alive = this.rescueCandidates(candidates);
263
+ const destroyedModules = [];
264
+ const destroyedProviders = [];
265
+ for (const moduleToken of candidates) {
266
+ if (alive.has(moduleToken)) {
267
+ continue;
268
+ }
269
+ destroyedModules.push(moduleToken);
270
+ for (const providerToken of this._moduleProviders.get(moduleToken) ??
271
+ []) {
272
+ destroyedProviders.push(providerToken);
273
+ }
274
+ }
275
+ this.removeModuleTokens(destroyedModules, destroyedProviders);
276
+ placeholder.markUnloaded();
277
+ this._version++;
278
+ return { destroyedModules, destroyedProviders };
279
+ }
280
+ /** A module's own `IMPORT` targets, plus the declaring modules of
281
+ * whatever its own providers depend on in another module. Shared by
282
+ * candidate collection, rescue propagation and teardown cleanup. */
283
+ moduleDependencies(moduleToken) {
284
+ const dependencies = [];
285
+ for (const edge of this.getEdge(moduleToken)) {
286
+ if (edge.type === EdgeTypeEnum.IMPORT) {
287
+ dependencies.push(edge.source);
288
+ }
289
+ }
290
+ for (const providerToken of this._moduleProviders.get(moduleToken) ?? []) {
291
+ for (const edge of this.getEdge(providerToken)) {
292
+ if (edge.type !== EdgeTypeEnum.DEPENDENCY ||
293
+ edge.metadata.unreached ||
294
+ edge.metadata.external ||
295
+ (edge.metadata.lateBound &&
296
+ this._lateBound.get(edge.target)?.get(providerToken) !== true)) {
297
+ continue;
298
+ }
299
+ const dependency = this._nodes.get(edge.target);
300
+ if (dependency?.type === NodeTypeEnum.PROVIDER) {
301
+ dependencies.push(dependency.moduleContainer.token);
302
+ }
303
+ }
304
+ }
305
+ return dependencies;
306
+ }
307
+ collectCandidates(rootToken, protectedRoots) {
308
+ const candidates = new Set();
309
+ const queue = [rootToken];
310
+ while (queue.length) {
311
+ const moduleToken = queue.shift();
312
+ if (protectedRoots.has(moduleToken) || candidates.has(moduleToken)) {
313
+ continue;
314
+ }
315
+ candidates.add(moduleToken);
316
+ queue.push(...this.moduleDependencies(moduleToken));
317
+ }
318
+ return candidates;
319
+ }
320
+ /**
321
+ * A candidate survives if something outside the candidate set still
322
+ * needs it (a "seed"), or if it is only reachable through a candidate
323
+ * that survives. A plain per-node check for the first case alone is not
324
+ * enough — see the "rescues a module only reachable through a surviving
325
+ * sibling candidate" test.
326
+ */
327
+ rescueCandidates(candidates) {
328
+ const alive = new Set();
329
+ const queue = [];
330
+ for (const moduleToken of candidates) {
331
+ const referrers = this._moduleReferrers.get(moduleToken);
332
+ const hasOutsideReferrer = referrers &&
333
+ [...referrers].some((referrer) => !candidates.has(referrer));
334
+ if (hasOutsideReferrer) {
335
+ queue.push(moduleToken);
336
+ }
337
+ }
338
+ while (queue.length) {
339
+ const moduleToken = queue.shift();
340
+ if (alive.has(moduleToken)) {
341
+ continue;
342
+ }
343
+ alive.add(moduleToken);
344
+ queue.push(...this.moduleDependencies(moduleToken).filter((token) => candidates.has(token)));
345
+ }
346
+ return alive;
347
+ }
348
+ removeModuleTokens(moduleTokens, providerTokens) {
349
+ // Drop stale referrer entries, and the modules' own index entries,
350
+ // before deleting the edges they were derived from, so a module that
351
+ // stays alive never keeps a phantom referrer from a module that no
352
+ // longer exists.
353
+ //
354
+ // Note: this only removes edges from the maps keyed by the destroyed
355
+ // tokens themselves, not from every place a destroyed token might
356
+ // still appear as an edge *target* elsewhere — e.g. an
357
+ // `unreached: true` DEPENDENCY edge left on a surviving provider, or
358
+ // a LAZY placeholder edge whose declaring module was destroyed.
359
+ // That's safe for resolution (`Resolver.resolveProvider` already
360
+ // returns `undefined` for a missing node), but graph-introspection
361
+ // consumers (like `graph-analyzer`) can still see these as orphaned
362
+ // edge references.
363
+ this.purgeModuleIndexes(moduleTokens);
364
+ this.purgeLateBound(providerTokens);
365
+ // Defensive: a runtime (inject()) edge also records its source's module
366
+ // as a referrer of the target's module, so a provider that survives
367
+ // this unload should never hold one to a destroyed provider. Should it
368
+ // happen anyway, drop the edge; it is re-discovered on the next creation.
369
+ const destroyedProviders = new Set(providerTokens);
370
+ for (const [source, edges] of this._edges) {
371
+ if (destroyedProviders.has(source)) {
372
+ continue;
373
+ }
374
+ if (edges.some((e) => e.metadata.inject === "runtime" && destroyedProviders.has(e.target))) {
375
+ this._edges.set(source, edges.filter((e) => !(e.metadata.inject === "runtime" &&
376
+ destroyedProviders.has(e.target))));
377
+ }
378
+ }
379
+ for (const providerToken of providerTokens) {
380
+ this._nodes.delete(providerToken);
381
+ this._edges.delete(providerToken);
382
+ }
383
+ for (const moduleToken of moduleTokens) {
384
+ this._nodes.delete(moduleToken);
385
+ this._edges.delete(moduleToken);
386
+ this._globalModules.delete(moduleToken);
387
+ }
388
+ }
389
+ /**
390
+ * Purges `_moduleProviders`/`_moduleReferrers` index entries for
391
+ * `tokens`, and removes each of them from any surviving module's
392
+ * referrer set. Shared by `removeModuleTokens` (destroying a live
393
+ * segment) and `removeTokens` (rolling back a failed one).
394
+ *
395
+ * `tokens` may freely mix module tokens with provider or LAZY
396
+ * placeholder tokens: both indexes are keyed by module tokens only, so a
397
+ * lookup or delete with a non-module key is a harmless no-op.
398
+ *
399
+ * Must run before the tokens' own `_edges` entries (and, for a removed
400
+ * module, its `_moduleProviders` entry) are deleted — `moduleDependencies`
401
+ * reads both to determine what each token itself depended on.
402
+ */
403
+ purgeModuleIndexes(tokens) {
404
+ for (const token of tokens) {
405
+ for (const dependencyModule of this.moduleDependencies(token)) {
406
+ this._moduleReferrers.get(dependencyModule)?.delete(token);
407
+ }
408
+ }
409
+ const purged = new Set(tokens);
410
+ for (const [child, declarer] of this._childDeclarations) {
411
+ if (purged.has(declarer)) {
412
+ this._childDeclarations.delete(child);
413
+ }
414
+ }
415
+ for (const token of tokens) {
416
+ this._moduleProviders.delete(token);
417
+ this._moduleReferrers.delete(token);
418
+ }
419
+ }
420
+ addRuntimeDependency(source, target) {
421
+ // inject() only gets here for a visible target. Recorded even when an
422
+ // edge already exists: a static late-bound edge to the same target
423
+ // (an @Optional one compiled while the target was not visible) may
424
+ // not be bound yet, and then recorded no referrer. Idempotent.
425
+ this.recordCrossModuleReferrer(source, target, true);
426
+ const exists = this.getEdge(source).some((edge) => edge.type === EdgeTypeEnum.DEPENDENCY && edge.target === target);
427
+ if (exists) {
428
+ // Bind it, so moduleDependencies sees it and the purge drops the
429
+ // referrer just recorded when either side unloads.
430
+ const sources = this._lateBound.get(target);
431
+ if (sources?.has(source)) {
432
+ sources.set(source, true);
433
+ }
434
+ return;
435
+ }
436
+ this.addEdge(source, {
437
+ type: EdgeTypeEnum.DEPENDENCY,
438
+ source,
439
+ target,
440
+ metadata: { inject: "runtime", unreached: false, isCircular: false },
441
+ });
442
+ }
443
+ findChildDeclaration(child) {
444
+ const key = isLazyModule(child) ? child.id : child;
445
+ const declarer = this._childDeclarations.get(key);
446
+ if (declarer === undefined) {
447
+ return undefined;
448
+ }
449
+ const node = this._nodes.get(declarer);
450
+ return node instanceof AnalyzeModule ? node.moduleContainer : undefined;
40
451
  }
41
452
  getNode(token) {
42
453
  return this._nodes.get(token);
@@ -51,43 +462,121 @@ export class ModuleGraph {
51
462
  return [...this._nodes.values()];
52
463
  }
53
464
  // modules analyze
54
- async addModules() {
465
+ async addModules(root, strictTokens) {
466
+ const moduleTokens = [];
467
+ const providerTokens = [];
468
+ const lazyTokens = [];
55
469
  const visited = new Set();
56
- const imports = [this._root];
470
+ const imports = [root];
57
471
  while (imports.length) {
58
472
  const importModule = imports.shift();
59
473
  if (!importModule || visited.has(importModule.token)) {
60
474
  continue;
61
475
  }
476
+ visited.add(importModule.token);
477
+ if (this._nodes.has(importModule.token)) {
478
+ // Already part of the graph (eager module or an earlier segment).
479
+ continue;
480
+ }
62
481
  const analyzeModule = new AnalyzeModule(importModule);
63
482
  await this.addModule(analyzeModule);
64
- await this.addModuleImports(analyzeModule);
65
- await this.addModuleProviders(analyzeModule);
483
+ moduleTokens.push(analyzeModule.id);
484
+ lazyTokens.push(...(await this.addModuleImports(analyzeModule)));
485
+ providerTokens.push(...(await this.addModuleProviders(analyzeModule, strictTokens)));
66
486
  imports.push(...(await analyzeModule.imports));
67
- visited.add(analyzeModule.id);
68
487
  }
488
+ return { moduleTokens, providerTokens, lazyTokens };
69
489
  }
70
490
  async addModule(analyzeModule) {
71
491
  this.addNode(analyzeModule.id, analyzeModule);
492
+ this.indexChildren(analyzeModule);
72
493
  if (analyzeModule.isGlobal) {
73
494
  this._globalModules.set(analyzeModule.id, analyzeModule.moduleContainer);
74
495
  }
75
496
  }
497
+ /**
498
+ * Records the module's declared children. Children are never compiled into
499
+ * this graph; only the declaration is indexed. A child declared by a second
500
+ * module is a graph error and keeps its first declarer.
501
+ */
502
+ indexChildren(analyzeModule) {
503
+ for (const child of analyzeModule.moduleContainer.children) {
504
+ const key = isLazyModule(child) ? child.id : child;
505
+ const existing = this._childDeclarations.get(key);
506
+ if (existing === undefined) {
507
+ this._childDeclarations.set(key, analyzeModule.id);
508
+ }
509
+ else if (existing !== analyzeModule.id) {
510
+ // A second declarer; a module meeting its own entry is only
511
+ // listing the child twice, which is not an error.
512
+ this.pushError({
513
+ type: "CHILD_DECLARED_TWICE",
514
+ child: child.name,
515
+ modules: [
516
+ this.moduleLabel(existing),
517
+ this.moduleLabel(analyzeModule.id),
518
+ ],
519
+ });
520
+ }
521
+ }
522
+ }
523
+ moduleLabel(moduleToken) {
524
+ const node = this._nodes.get(moduleToken);
525
+ if (node instanceof AnalyzeModule) {
526
+ return getModuleLabel(node.moduleContainer.metatype);
527
+ }
528
+ return moduleToken;
529
+ }
530
+ /**
531
+ * @returns the ids of the LAZY placeholder nodes this call created, so a
532
+ * rolled back segment can remove them again. Placeholders that already
533
+ * existed are not reported and must never be removed.
534
+ */
76
535
  async addModuleImports(analyzeModule) {
536
+ const createdLazyIds = [];
77
537
  const imports = await analyzeModule.edges;
78
538
  for (const importEdge of imports) {
79
539
  this.addEdge(analyzeModule.id, importEdge);
540
+ this.addModuleReferrer(importEdge.source, analyzeModule.id);
541
+ }
542
+ for (const lazyModule of analyzeModule.lazyImports) {
543
+ if (!this._nodes.has(lazyModule.id)) {
544
+ this.addNode(lazyModule.id, new AnalyzeLazyModule(lazyModule));
545
+ createdLazyIds.push(lazyModule.id);
546
+ }
547
+ }
548
+ for (const lazyEdge of analyzeModule.lazyEdges) {
549
+ this.addEdge(analyzeModule.id, lazyEdge);
80
550
  }
551
+ return createdLazyIds;
81
552
  }
82
- async addModuleProviders(analyzeModule) {
553
+ async addModuleProviders(analyzeModule, strictTokens) {
554
+ const added = [];
83
555
  for (const provider of analyzeModule.providers) {
84
556
  const analyzeProvider = ProviderFactory(provider, analyzeModule.moduleContainer);
85
557
  if (analyzeProvider === null) {
86
558
  continue;
87
559
  }
560
+ const existing = this._nodes.get(analyzeProvider.id);
561
+ if (strictTokens &&
562
+ existing &&
563
+ existing.type === NodeTypeEnum.PROVIDER &&
564
+ existing.moduleContainer.token !==
565
+ analyzeModule.moduleContainer.token) {
566
+ this.pushError({
567
+ type: "PROVIDER_TOKEN_CONFLICT",
568
+ token: analyzeProvider.label,
569
+ module: analyzeModule.label,
570
+ existingModule: getModuleLabel(existing.moduleContainer.metatype),
571
+ });
572
+ continue;
573
+ }
88
574
  this.addNode(analyzeProvider.id, analyzeProvider);
89
575
  this.addEdge(analyzeModule.id, analyzeProvider.edge);
576
+ this.addModuleProvider(analyzeModule.id, analyzeProvider.id);
577
+ added.push(analyzeProvider.id);
90
578
  }
579
+ return added;
91
580
  }
92
581
  // modules analyze
93
582
  // graph helpers
@@ -99,13 +588,68 @@ export class ModuleGraph {
99
588
  addNode(token, node) {
100
589
  this._nodes.set(token, node);
101
590
  }
591
+ addModuleProvider(moduleToken, providerToken) {
592
+ const providers = this._moduleProviders.get(moduleToken) ?? new Set();
593
+ providers.add(providerToken);
594
+ this._moduleProviders.set(moduleToken, providers);
595
+ }
596
+ addModuleReferrer(dependencyModule, referrerModule) {
597
+ const referrers = this._moduleReferrers.get(dependencyModule) ?? new Set();
598
+ referrers.add(referrerModule);
599
+ this._moduleReferrers.set(dependencyModule, referrers);
600
+ }
601
+ /**
602
+ * Bookkeeping for a static `DEPENDENCY` edge just added: a late-bound
603
+ * edge is indexed (its referrer is recorded by `bindLateEdges` once the
604
+ * target is visible), any other edge records its referrer now.
605
+ */
606
+ recordStaticDependency(source, target, countsAsReferrer, lateBound) {
607
+ if (!lateBound) {
608
+ this.recordCrossModuleReferrer(source, target, countsAsReferrer);
609
+ return;
610
+ }
611
+ const sources = this._lateBound.get(target) ?? new Map();
612
+ if (!sources.has(source)) {
613
+ sources.set(source, false);
614
+ }
615
+ this._lateBound.set(target, sources);
616
+ }
617
+ /**
618
+ * Records that `consumerToken` (a provider) keeps `dependencyToken`'s
619
+ * declaring module alive, when the two providers are declared in
620
+ * different modules. Called after every `DEPENDENCY` edge is added,
621
+ * regardless of how the dependency was resolved (own module, import, or
622
+ * `@Global()` fallback) — the edge already reflects that, so this needs
623
+ * no help from `isProviderExported`.
624
+ *
625
+ * @param countsAsReferrer - False for an edge that keeps nothing in this
626
+ * graph alive: an unreached dependency, or an external one (resolved
627
+ * through the parent link, so its target lives in another container).
628
+ */
629
+ recordCrossModuleReferrer(consumerToken, dependencyToken, countsAsReferrer) {
630
+ if (!countsAsReferrer) {
631
+ return;
632
+ }
633
+ const consumer = this._nodes.get(consumerToken);
634
+ const dependency = this._nodes.get(dependencyToken);
635
+ if (consumer?.type !== NodeTypeEnum.PROVIDER ||
636
+ dependency?.type !== NodeTypeEnum.PROVIDER) {
637
+ return;
638
+ }
639
+ const consumerModule = consumer.moduleContainer.token;
640
+ const dependencyModule = dependency.moduleContainer
641
+ .token;
642
+ if (consumerModule !== dependencyModule) {
643
+ this.addModuleReferrer(dependencyModule, consumerModule);
644
+ }
645
+ }
102
646
  // graph helpers
103
647
  // providers dependencies
104
- async addDependencies() {
648
+ async addDependencies(providerTokens) {
105
649
  const visited = new Set();
106
- const providerNodes = [...this.nodes].filter(isProviderNode);
107
- for (const [token, node] of providerNodes) {
108
- if (visited.has(token)) {
650
+ for (const token of providerTokens) {
651
+ const node = this._nodes.get(token);
652
+ if (!node || node.type !== NodeTypeEnum.PROVIDER || visited.has(token)) {
109
653
  continue;
110
654
  }
111
655
  if (isAnalyzeFactoryProvider(node)) {
@@ -124,11 +668,13 @@ export class ModuleGraph {
124
668
  const constructorDependencies = this.getConstructorDependencies(node.metatype);
125
669
  const optionalDependency = this.getOptionalConstructorDependencies(node.metatype);
126
670
  for (const [index, dependency] of constructorDependencies.entries()) {
127
- const dependencyToken = getDependencyToken(dependency.param);
128
- const isExported = optionalDependency.includes(index) ||
129
- (await this.isProviderExported(node.moduleContainer, dependencyToken));
130
- if (!isExported) {
131
- this.errors.push({
671
+ const isLazy = isForwardRef(dependency.param);
672
+ const dependencyToken = isLazy
673
+ ? dependency.param.forwardRef()
674
+ : getDependencyToken(dependency.param);
675
+ const { reachable, external, lateBound } = await this.reachDependency(node.moduleContainer, dependencyToken, optionalDependency.includes(index));
676
+ if (!reachable) {
677
+ this.pushError({
132
678
  type: "UNREACHED_DEP_CONSTRUCTOR",
133
679
  token: node.label,
134
680
  dependency: tokenToString(dependencyToken),
@@ -140,22 +686,25 @@ export class ModuleGraph {
140
686
  source: token,
141
687
  target: dependencyToken,
142
688
  metadata: {
143
- unreached: !isExported,
689
+ unreached: !reachable,
690
+ ...(external && { external: true }),
691
+ ...(lateBound && { lateBound: true }),
144
692
  isCircular: false,
693
+ lazy: isLazy,
145
694
  index: dependency.index,
146
695
  inject: "constructor",
147
696
  },
148
697
  };
149
698
  this.addEdge(token, newEdge);
699
+ this.recordStaticDependency(token, dependencyToken, reachable && !external, lateBound);
150
700
  }
151
701
  const propertiesDependencies = this.getPropertiesDependencies(node.metatype);
152
702
  const optionalProperties = this.getOptionalPropertyDependencies(node.metatype);
153
703
  for (const dependency of propertiesDependencies) {
154
704
  const dependencyToken = getDependencyToken(dependency.type);
155
- const isExported = optionalProperties.includes(dependency.key) ||
156
- (await this.isProviderExported(node.moduleContainer, dependencyToken));
157
- if (!isExported) {
158
- this.errors.push({
705
+ const { reachable, external, lateBound } = await this.reachDependency(node.moduleContainer, dependencyToken, optionalProperties.includes(dependency.key));
706
+ if (!reachable) {
707
+ this.pushError({
159
708
  type: "UNREACHED_DEP_PROPERTY",
160
709
  token: node.label,
161
710
  dependency: tokenToString(dependencyToken),
@@ -167,13 +716,16 @@ export class ModuleGraph {
167
716
  source: token,
168
717
  target: dependencyToken,
169
718
  metadata: {
170
- unreached: !isExported,
719
+ unreached: !reachable,
720
+ ...(external && { external: true }),
721
+ ...(lateBound && { lateBound: true }),
171
722
  isCircular: false,
172
723
  key: dependency.key,
173
724
  inject: "property",
174
725
  },
175
726
  };
176
727
  this.addEdge(token, newEdge);
728
+ this.recordStaticDependency(token, dependencyToken, reachable && !external, lateBound);
177
729
  }
178
730
  }
179
731
  async addClassProviderDependency(token, node) {
@@ -181,11 +733,13 @@ export class ModuleGraph {
181
733
  const constructorDependencies = this.getConstructorDependencies(Class);
182
734
  const optionalDependency = this.getOptionalConstructorDependencies(Class);
183
735
  for (const [index, dependency] of constructorDependencies.entries()) {
184
- const dependencyToken = getDependencyToken(dependency.param);
185
- const isExported = optionalDependency.includes(index) ||
186
- (await this.isProviderExported(node.moduleContainer, dependencyToken));
187
- if (!isExported) {
188
- this.errors.push({
736
+ const isLazy = isForwardRef(dependency.param);
737
+ const dependencyToken = isLazy
738
+ ? dependency.param.forwardRef()
739
+ : getDependencyToken(dependency.param);
740
+ const { reachable, external, lateBound } = await this.reachDependency(node.moduleContainer, dependencyToken, optionalDependency.includes(index));
741
+ if (!reachable) {
742
+ this.pushError({
189
743
  type: "UNREACHED_DEP_CONSTRUCTOR",
190
744
  token: node.label,
191
745
  dependency: tokenToString(dependencyToken),
@@ -197,22 +751,25 @@ export class ModuleGraph {
197
751
  source: token,
198
752
  target: dependencyToken,
199
753
  metadata: {
200
- unreached: !isExported,
754
+ unreached: !reachable,
755
+ ...(external && { external: true }),
756
+ ...(lateBound && { lateBound: true }),
201
757
  isCircular: false,
758
+ lazy: isLazy,
202
759
  index: dependency.index,
203
760
  inject: "constructor",
204
761
  },
205
762
  };
206
763
  this.addEdge(token, newEdge);
764
+ this.recordStaticDependency(token, dependencyToken, reachable && !external, lateBound);
207
765
  }
208
766
  const propertiesDependencies = this.getPropertiesDependencies(Class);
209
767
  const optionalProperty = this.getOptionalPropertyDependencies(Class);
210
768
  for (const dependency of propertiesDependencies) {
211
769
  const dependencyToken = getDependencyToken(dependency.type);
212
- const isExported = optionalProperty.includes(dependency.key) ||
213
- (await this.isProviderExported(node.moduleContainer, dependencyToken));
214
- if (!isExported) {
215
- this.errors.push({
770
+ const { reachable, external, lateBound } = await this.reachDependency(node.moduleContainer, dependencyToken, optionalProperty.includes(dependency.key));
771
+ if (!reachable) {
772
+ this.pushError({
216
773
  type: "UNREACHED_DEP_PROPERTY",
217
774
  token: node.label,
218
775
  dependency: tokenToString(dependencyToken),
@@ -224,58 +781,101 @@ export class ModuleGraph {
224
781
  source: token,
225
782
  target: dependencyToken,
226
783
  metadata: {
227
- unreached: !isExported,
784
+ unreached: !reachable,
785
+ ...(external && { external: true }),
786
+ ...(lateBound && { lateBound: true }),
228
787
  isCircular: false,
229
788
  key: dependency.key,
230
789
  inject: "property",
231
790
  },
232
791
  });
792
+ this.recordStaticDependency(token, dependencyToken, reachable && !external, lateBound);
233
793
  }
234
794
  }
235
795
  async addFactoryProviderDependency(token, node) {
236
796
  const dependencies = node.inject || [];
237
797
  let index = 0;
238
798
  for (const dependency of dependencies) {
239
- const isExported = await this.isProviderExported(node.moduleContainer, dependency);
240
- if (!isExported) {
241
- this.errors.push({
799
+ const isLazy = isForwardRef(dependency);
800
+ const dependencyToken = isLazy
801
+ ? dependency.forwardRef()
802
+ : dependency;
803
+ const { reachable, external, lateBound } = await this.reachDependency(node.moduleContainer, dependencyToken, false);
804
+ if (!reachable) {
805
+ this.pushError({
242
806
  type: "UNREACHED_DEP_FACTORY",
243
807
  token: node.label,
244
- dependency: tokenToString(dependency),
808
+ dependency: tokenToString(dependencyToken),
245
809
  key: index,
246
810
  });
247
811
  }
248
812
  const factoryDependency = {
249
813
  type: EdgeTypeEnum.DEPENDENCY,
250
814
  source: token,
251
- target: dependency,
815
+ target: dependencyToken,
252
816
  metadata: {
253
- unreached: !isExported,
817
+ unreached: !reachable,
818
+ ...(external && { external: true }),
819
+ ...(lateBound && { lateBound: true }),
254
820
  isCircular: false,
821
+ lazy: isLazy,
255
822
  index: index++,
256
823
  inject: "constructor",
257
824
  },
258
825
  };
259
826
  this.addEdge(token, factoryDependency);
827
+ this.recordStaticDependency(token, dependencyToken, reachable && !external, lateBound);
260
828
  }
261
829
  }
830
+ /**
831
+ * How a dependency of a provider declared in `moduleContainer` is
832
+ * satisfied: locally (own module, imports, globals), by the parent
833
+ * container chain, or not at all. Optional dependencies count as
834
+ * satisfied; a parent that can provide one is still preferred. An
835
+ * optional dependency satisfied only by being optional is `lateBound`:
836
+ * the resolver injects it only once it exists and is visible.
837
+ */
838
+ async reachDependency(moduleContainer, token, optional) {
839
+ if (await this.isProviderExported(moduleContainer, token)) {
840
+ return { reachable: true, external: false, lateBound: false };
841
+ }
842
+ if (this.options.externalLookup?.(token)) {
843
+ return { reachable: true, external: true, lateBound: false };
844
+ }
845
+ return { reachable: optional, external: false, lateBound: optional };
846
+ }
262
847
  // providers dependencies
263
848
  async isProviderExported(moduleContainer, dependencyToken) {
264
- // check globals
849
+ await moduleContainer.imports;
850
+ return this.isProviderVisible(moduleContainer, dependencyToken);
851
+ }
852
+ isProviderVisible(moduleContainer, dependencyToken) {
853
+ // globals, including what they re-export through a module class
854
+ const globalExports = [];
265
855
  for (const globalModule of this._globalModules.values()) {
266
- if (globalModule.exports.some((provider) => provider === dependencyToken)) {
267
- return true;
268
- }
856
+ globalExports.push(...globalModule.exports);
857
+ }
858
+ if (this.exportsReach(globalExports, dependencyToken)) {
859
+ return true;
269
860
  }
270
- // check globals
271
- // check internals
861
+ // own providers
272
862
  if (moduleContainer.providers.some((provider) => getProviderToken(provider) === dependencyToken)) {
273
863
  return true;
274
864
  }
275
- // externals
276
- const containerImports = await moduleContainer.imports;
865
+ // imports
866
+ const containerImports = moduleContainer.resolvedImports;
867
+ if (!containerImports) {
868
+ throw new Error("Module imports are not resolved yet");
869
+ }
870
+ return this.exportsReach(containerImports.flatMap((imported) => imported.exports), dependencyToken);
871
+ }
872
+ /**
873
+ * Whether `dependencyToken` is among `exports`, following module-class
874
+ * re-exports through the modules' own exports.
875
+ */
876
+ exportsReach(exports, dependencyToken) {
277
877
  const visitedModules = new Set();
278
- const queue = containerImports.flatMap((firstLevelContainer) => firstLevelContainer.exports);
878
+ const queue = [...exports];
279
879
  while (queue.length) {
280
880
  const currentExportToken = queue.shift();
281
881
  if (!currentExportToken || visitedModules.has(currentExportToken)) {
@@ -298,14 +898,13 @@ export class ModuleGraph {
298
898
  }
299
899
  return false;
300
900
  }
301
- async detectCircularDependencies() {
901
+ async detectCircularDependencies(startTokens) {
302
902
  const visit = (nodeId, path, visited, stack) => {
303
903
  if (stack.has(nodeId)) {
304
904
  const cyclePath = path.map((node, index) => [node, path[index + 1] || nodeId]);
305
905
  const from = cyclePath[0];
306
906
  const to = cyclePath[cyclePath.length - 1];
307
907
  if (from[0] === to[1]) {
308
- const _edges = this._edges.get(nodeId);
309
908
  for (const [from, to] of cyclePath) {
310
909
  const edges = this._edges.get(from);
311
910
  if (edges) {
@@ -315,10 +914,20 @@ export class ModuleGraph {
315
914
  }
316
915
  }
317
916
  }
318
- this.errors.push({
319
- type: "CD_PROVIDERS",
320
- path: cyclePath,
917
+ // Only push CD_PROVIDERS if NO edge in cycle has forwardRef (lazy)
918
+ const hasForwardRef = cyclePath.some(([from, to]) => {
919
+ const edges = this._edges.get(from);
920
+ if (!edges)
921
+ return false;
922
+ const edge = edges.find((e) => e.target === to && e.type === EdgeTypeEnum.DEPENDENCY);
923
+ return edge?.metadata.lazy === true;
321
924
  });
925
+ if (!hasForwardRef) {
926
+ this.pushError({
927
+ type: "CD_PROVIDERS",
928
+ path: cyclePath,
929
+ });
930
+ }
322
931
  }
323
932
  return true;
324
933
  }
@@ -328,20 +937,27 @@ export class ModuleGraph {
328
937
  visited.add(nodeId);
329
938
  stack.add(nodeId);
330
939
  path.push(nodeId);
940
+ // Runtime (inject()) edges are followed too. That is safe: one is
941
+ // recorded only after inject() returned its target, and a cycle
942
+ // through inject() throws CircularInjectionError during creation
943
+ // before the edge is recorded. So runtime edges only mirror
944
+ // dependencies that were actually created without a cycle.
331
945
  const edges = this.getEdge(nodeId) || [];
332
946
  for (const edge of edges) {
947
+ // An external edge ends here: its target lives in the parent chain.
948
+ if (edge.metadata.external)
949
+ continue;
333
950
  visit(edge.target, path, visited, stack);
334
951
  }
335
952
  stack.delete(nodeId);
336
953
  path.pop();
337
954
  return false;
338
955
  };
339
- const providers = [...this._nodes].filter(([_, node]) => node.type === NodeTypeEnum.PROVIDER);
340
- for (const [nodeId, _] of providers) {
956
+ for (const nodeId of new Set(startTokens)) {
341
957
  visit(nodeId, [], new Set(), new Set());
342
958
  }
343
959
  }
344
- async detectCircularImports() {
960
+ async detectCircularImports(startTokens) {
345
961
  const visit = (nodeId, path, visited, stack) => {
346
962
  if (stack.has(nodeId)) {
347
963
  const cycleStartIndex = path.indexOf(nodeId);
@@ -353,7 +969,7 @@ export class ModuleGraph {
353
969
  if (edges) {
354
970
  const edge = edges.find((e) => e.source === to);
355
971
  if (edge && edge.type === EdgeTypeEnum.IMPORT) {
356
- this.errors.push({
972
+ this.pushError({
357
973
  type: "CD_IMPORTS",
358
974
  path: cyclePath
359
975
  .map((token) => this.getNode(token))
@@ -381,8 +997,7 @@ export class ModuleGraph {
381
997
  stack.delete(nodeId);
382
998
  path.pop();
383
999
  };
384
- const modules = [...this._nodes].filter(([_, node]) => node.type === NodeTypeEnum.MODULE);
385
- for (const [nodeId, _] of modules) {
1000
+ for (const nodeId of new Set(startTokens)) {
386
1001
  visit(nodeId, [], new Set(), new Set());
387
1002
  }
388
1003
  }