|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.dev/license |
| 7 | + */ |
| 8 | + |
| 9 | +import {getLContext} from '../../render3/context_discovery'; |
| 10 | +import {NodeInjector} from '../../render3/di'; |
| 11 | +import {TDirectiveHostNode, TNode} from '../../render3/interfaces/node'; |
| 12 | +import {INJECTOR, LView, T_HOST, TVIEW, TViewType} from '../../render3/interfaces/view'; |
| 13 | +import {DiGraph, SerializedInjector, serializeInjector} from './tree'; |
| 14 | +import {ChainedInjector} from '../../render3/chained_injector'; |
| 15 | +import {Injector} from '../../di/injector'; |
| 16 | +import {ToolDefinition} from './tool_definitions'; |
| 17 | +import {getLViewParent} from '../../render3/util/view_utils'; |
| 18 | +import {R3Injector} from '../../di/r3_injector'; |
| 19 | +import {NullInjector} from '../../di/null_injector'; |
| 20 | +import {walkLViewDirectives} from '../../render3/util/view_traversal_utils'; |
| 21 | + |
| 22 | +/** Tool that exposes Angular's DI graph to AI agents. */ |
| 23 | +export const diGraphTool: ToolDefinition<{}, DiGraph> = { |
| 24 | + name: 'angular:di_graph', |
| 25 | + // tslint:disable-next-line:no-toplevel-property-access |
| 26 | + description: ` |
| 27 | +Exposes the Angular Dependency Injection (DI) graph of the application. |
| 28 | +
|
| 29 | +This tool extracts both the element injector tree (associated with DOM elements and components) |
| 30 | +and the environment injector tree (associated with modules and standalone application roots). |
| 31 | +It captures the relationship structure and the providers resolved at each level. |
| 32 | +
|
| 33 | +Returns: |
| 34 | +- \`elementInjectorRoots\`: An array of root element injectors (one for each Angular application |
| 35 | + root found). Each node forms a tree hierarchy: |
| 36 | + - \`name\`: The constructor name of the injector. |
| 37 | + - \`type\`: 'element'. |
| 38 | + - \`providers\`: Array of providers configured on that specific node. |
| 39 | + - \`token\`: The DI token requested. |
| 40 | + - \`value\`: The resolved value of that provider if it was instantiated. |
| 41 | + - \`hostElement\`: The DOM element that this injector is associated with. |
| 42 | + - \`children\`: Array of child element injectors. |
| 43 | +- \`environmentInjectorRoot\`: The root environment injector. It forms a tree hierarchy of nodes |
| 44 | + representing all environment injectors: |
| 45 | + - \`name\`: The identifier for the environment injector. |
| 46 | + - \`type\`: 'environment' or 'null'. |
| 47 | + - \`providers\`: Array of providers configured on that injector. |
| 48 | + - \`children\`: Array of child environment injectors. |
| 49 | + `.trim(), |
| 50 | + inputSchema: { |
| 51 | + type: 'object', |
| 52 | + properties: {}, |
| 53 | + }, |
| 54 | + execute: async () => { |
| 55 | + const roots = Array.from(document.querySelectorAll('[ng-version]')) as HTMLElement[]; |
| 56 | + if (roots.length === 0) { |
| 57 | + throw new Error('Could not find Angular root element ([ng-version]) on the page.'); |
| 58 | + } |
| 59 | + return discoverDiGraph(roots); |
| 60 | + }, |
| 61 | +}; |
| 62 | + |
| 63 | +/** |
| 64 | + * Traverses the Angular internal tree from the root to discover element and environment injectors. |
| 65 | + */ |
| 66 | +function discoverDiGraph(roots: HTMLElement[]): DiGraph { |
| 67 | + const rootLViews = roots.map((root) => { |
| 68 | + const lContext = getLContext(root); |
| 69 | + if (!lContext?.lView) { |
| 70 | + throw new Error( |
| 71 | + `Could not find an \`LView\` for root \`<${root.tagName.toLowerCase()}>\`, is it an Angular component?`, |
| 72 | + ); |
| 73 | + } |
| 74 | + return lContext.lView; |
| 75 | + }); |
| 76 | + |
| 77 | + return { |
| 78 | + elementInjectorRoots: rootLViews.map((rootLView) => walkElementInjectors(rootLView)), |
| 79 | + environmentInjectorRoot: collectEnvInjectors(rootLViews), |
| 80 | + }; |
| 81 | +} |
| 82 | + |
| 83 | +/** |
| 84 | + * Traverses all directive-hosting nodes in the `rootLView` hierarchy and builds a tree of |
| 85 | + * serialized element injectors. |
| 86 | + * |
| 87 | + * This function uses `walkLViewDirectives` to visit nodes in depth-first order and a stack |
| 88 | + * to reconstruct the hierarchical tree of injectors, handling both same-view and cross-view |
| 89 | + * relationships. |
| 90 | + * |
| 91 | + * @param rootLView The root view to start traversal from. |
| 92 | + * @returns The root {@link SerializedInjector} object. |
| 93 | + */ |
| 94 | +function walkElementInjectors(rootLView: LView): SerializedInjector { |
| 95 | + // Assert that we were given a root `LView` rather than a random component. |
| 96 | + // A root component actually gets two `LView` objects, the "root `LView`" with |
| 97 | + // `type === TViewType.Root` and then an `LView` for the component itself as a child. |
| 98 | + if (rootLView[TVIEW].type !== TViewType.Root) { |
| 99 | + throw new Error(`Expected a root LView but got type: \`${rootLView[TVIEW].type}\`.`); |
| 100 | + } |
| 101 | + |
| 102 | + // Track the injectors we're currently processing. |
| 103 | + const stack: Array<[TNode, LView, SerializedInjector]> = []; |
| 104 | + |
| 105 | + // By constraining `rootLView` to only accepting root `LView` objects, we don't have to |
| 106 | + // process `rootLView` itself, knowing that it won't be a component or directive. |
| 107 | + // We can just check its descendants. |
| 108 | + for (const [tNode, lView] of walkLViewDirectives(rootLView)) { |
| 109 | + const injector = new NodeInjector(tNode as TDirectiveHostNode, lView); |
| 110 | + const serialized = serializeInjector(injector); |
| 111 | + |
| 112 | + // Look for our nearest ancestor in the stack. |
| 113 | + while (stack.length > 0) { |
| 114 | + const [lastTNode, lastLView, lastInjector] = stack[stack.length - 1]; |
| 115 | + |
| 116 | + const isDescendantInSameView = isTNodeDescendant(tNode, lastTNode); |
| 117 | + const isDescendantInDifferentView = isLViewDescendantOfTNode(lView, lastLView, lastTNode); |
| 118 | + if (isDescendantInSameView || isDescendantInDifferentView) { |
| 119 | + // This injector is a child of the current last injector in the stack. |
| 120 | + lastInjector.children.push(serialized); |
| 121 | + break; |
| 122 | + } else { |
| 123 | + stack.pop(); |
| 124 | + } |
| 125 | + } |
| 126 | + |
| 127 | + // Future injectors might be children of this one. |
| 128 | + stack.push([tNode, lView, serialized]); |
| 129 | + } |
| 130 | + |
| 131 | + // Since all component/directive LViews are descendants of the root LView, the first |
| 132 | + // item on the stack must still remain and will be the root injector. |
| 133 | + if (stack.length === 0) { |
| 134 | + throw new Error(`Expected at least one component/directive in the root \`LView\`.`); |
| 135 | + } |
| 136 | + const [, , rootInjector] = stack[0]; |
| 137 | + return rootInjector; |
| 138 | +} |
| 139 | + |
| 140 | +/** |
| 141 | + * Collects and serializes all environment injectors found in the hierarchy of the given |
| 142 | + * `rootLViews`. |
| 143 | + * |
| 144 | + * Injectors have pointers to their parents, but not their children, so walking "down" the |
| 145 | + * hierarchy is not a generally supported operation. |
| 146 | + * |
| 147 | + * The function walks down the `LView` hierarchy to find all the component/directive descendants. |
| 148 | + * For each one, it then walks back up the injector hierarchy to find the full set of environment |
| 149 | + * injectors. |
| 150 | + * |
| 151 | + * @param rootLViews The root views to start traversal from. |
| 152 | + * @returns The root {@link SerializedInjector} object containing the entire environment |
| 153 | + * injector tree. |
| 154 | + */ |
| 155 | +function collectEnvInjectors(rootLViews: LView[]): SerializedInjector { |
| 156 | + const serializedEnvInjectorMap = new Map<Injector, SerializedInjector>(); |
| 157 | + let rootEnvInjector: SerializedInjector | undefined = undefined; |
| 158 | + |
| 159 | + /** |
| 160 | + * Serialize all the ancestors of the given injector and return |
| 161 | + * its serialized version. |
| 162 | + * |
| 163 | + * @param injector The environment injector to start from. |
| 164 | + * @returns The serialized form of the input {@link Injector}. |
| 165 | + */ |
| 166 | + function serializeAncestors(injector: Injector): SerializedInjector { |
| 167 | + const existing = serializedEnvInjectorMap.get(injector); |
| 168 | + if (existing) return existing; |
| 169 | + |
| 170 | + const serialized = serializeInjector(injector); |
| 171 | + serializedEnvInjectorMap.set(injector, serialized); |
| 172 | + |
| 173 | + const parentInjector = getParentEnvInjector(injector); |
| 174 | + if (parentInjector) { |
| 175 | + // Recursively process the parent and attach ourselves as a child. |
| 176 | + const parentSerialized = serializeAncestors(parentInjector); |
| 177 | + parentSerialized.children.push(serialized); |
| 178 | + } else { |
| 179 | + // If there is no parent, this is a root environment injector. |
| 180 | + if (!rootEnvInjector) { |
| 181 | + rootEnvInjector = serialized; |
| 182 | + } else if (rootEnvInjector !== serialized) { |
| 183 | + throw new Error('Expected only one root environment injector, but found multiple.', { |
| 184 | + cause: {firstRoot: rootEnvInjector, secondRoot: serialized}, |
| 185 | + }); |
| 186 | + } |
| 187 | + } |
| 188 | + |
| 189 | + return serialized; |
| 190 | + } |
| 191 | + |
| 192 | + // Process all descendant environment injectors. |
| 193 | + for (const rootLView of rootLViews) { |
| 194 | + for (const [, lView] of walkLViewDirectives(rootLView)) { |
| 195 | + serializeAncestors(lView[INJECTOR]); |
| 196 | + } |
| 197 | + } |
| 198 | + |
| 199 | + if (!rootEnvInjector) { |
| 200 | + throw new Error('Expected a root environment injector but did not find one.'); |
| 201 | + } |
| 202 | + |
| 203 | + return rootEnvInjector; |
| 204 | +} |
| 205 | + |
| 206 | +/** |
| 207 | + * Checks if `node` is a descendant of `ancestor` within the SAME view. |
| 208 | + * |
| 209 | + * Since we are in the same view, we can safely use `tNode.parent` to determine |
| 210 | + * if `ancestor` is an ancestor of the current `node`. |
| 211 | + */ |
| 212 | +function isTNodeDescendant(node: TNode, ancestor: TNode): boolean { |
| 213 | + let curr: TNode | null = node; |
| 214 | + while (curr) { |
| 215 | + if (curr === ancestor) return true; |
| 216 | + curr = curr.parent; |
| 217 | + } |
| 218 | + return false; |
| 219 | +} |
| 220 | + |
| 221 | +/** |
| 222 | + * Checks if `lView` is a descendant of `parentTNode` in `parentLView` (crossing view boundaries). |
| 223 | + * |
| 224 | + * `tNode.parent` is restricted to referring to nodes within the SAME view. When we cross |
| 225 | + * view boundaries (e.g., entering a component's internal view or an embedded view like `@if`), |
| 226 | + * `tNode.parent` becomes `null` or points to something inside that view, breaking the chain to the |
| 227 | + * outside. |
| 228 | + * |
| 229 | + * To solve this, we use the `LView` hierarchy to find if the current view is a descendant of the |
| 230 | + * `parentLView`. |
| 231 | + */ |
| 232 | +function isLViewDescendantOfTNode(lView: LView, parentLView: LView, parentTNode: TNode): boolean { |
| 233 | + let currentLView: LView | null = lView; |
| 234 | + let hostTNode: TNode | null = null; |
| 235 | + |
| 236 | + while (currentLView && currentLView !== parentLView) { |
| 237 | + hostTNode = currentLView[T_HOST]; |
| 238 | + currentLView = getLViewParent(currentLView); |
| 239 | + } |
| 240 | + |
| 241 | + return ( |
| 242 | + currentLView === parentLView && hostTNode !== null && isTNodeDescendant(hostTNode, parentTNode) |
| 243 | + ); |
| 244 | +} |
| 245 | + |
| 246 | +/** Find the parent environment injector of the given injector. */ |
| 247 | +function getParentEnvInjector(injector: Injector): Injector | undefined { |
| 248 | + if (injector instanceof ChainedInjector) { |
| 249 | + // We skip `chainedInjector.injector` because that points at the parent element injector |
| 250 | + // which is handled by `walkElementInjectors`. |
| 251 | + const chainedInjector = injector; |
| 252 | + return chainedInjector.parentInjector; |
| 253 | + } else if (injector instanceof R3Injector) { |
| 254 | + return injector.parent; |
| 255 | + } else if (injector instanceof NullInjector) { |
| 256 | + return undefined; |
| 257 | + } else { |
| 258 | + throw new Error(`Unknown injector type: "${injector.constructor.name}".`); |
| 259 | + } |
| 260 | +} |
0 commit comments