(
name: string,
options: ResolveOptions = {},
)
| 112 | * parallel sibling fetches only if graph depth ever becomes a real cost. |
| 113 | */ |
| 114 | export async function resolveItemWithDependencies( |
| 115 | name: string, |
| 116 | options: ResolveOptions = {}, |
| 117 | ): Promise<RegistryItem[]> { |
| 118 | const entries = await listRegistryItems(undefined, options); |
| 119 | const entry = entries.find((e) => e.name === name); |
| 120 | if (!entry) { |
| 121 | const available = entries.map((e) => e.name).join(", "); |
| 122 | throw new Error( |
| 123 | available.length > 0 |
| 124 | ? `Item "${name}" not found in registry. Available: ${available}` |
| 125 | : `Item "${name}" not found — registry unreachable or empty.`, |
| 126 | ); |
| 127 | } |
| 128 | |
| 129 | const entryByName = new Map(entries.map((e) => [e.name, e])); |
| 130 | const visiting = new Set<string>(); |
| 131 | const visited = new Set<string>(); |
| 132 | const ordered: RegistryItem[] = []; |
| 133 | const itemCache = new Map<string, Promise<RegistryItem>>(); |
| 134 | |
| 135 | // `async` so the missing-dependency path surfaces as a promise rejection |
| 136 | // rather than a synchronous throw, keeping the control flow consistent with |
| 137 | // the `Promise<RegistryItem>` return type. The body has no `await`, so the |
| 138 | // cache is still populated synchronously on first request (dedup intact). |
| 139 | const getItem = async (itemName: string): Promise<RegistryItem> => { |
| 140 | const existing = itemCache.get(itemName); |
| 141 | if (existing) return existing; |
| 142 | |
| 143 | const registryEntry = entryByName.get(itemName); |
| 144 | if (!registryEntry) { |
| 145 | const available = entries.map((e) => e.name).join(", "); |
| 146 | throw new Error( |
| 147 | available.length > 0 |
| 148 | ? `Dependency "${itemName}" not found in registry. Available: ${available}` |
| 149 | : `Dependency "${itemName}" not found — registry unreachable or empty.`, |
| 150 | ); |
| 151 | } |
| 152 | |
| 153 | const pending = fetchItemManifest(registryEntry.name, registryEntry.type, options.baseUrl); |
| 154 | itemCache.set(itemName, pending); |
| 155 | return pending; |
| 156 | }; |
| 157 | |
| 158 | const visit = async (itemName: string, path: string[]): Promise<void> => { |
| 159 | if (visited.has(itemName)) return; |
| 160 | if (visiting.has(itemName)) { |
| 161 | const cycleStart = path.indexOf(itemName); |
| 162 | const cyclePath = [...path.slice(cycleStart), itemName].join(" -> "); |
| 163 | throw new Error(`Circular registryDependencies detected: ${cyclePath}`); |
| 164 | } |
| 165 | |
| 166 | visiting.add(itemName); |
| 167 | const item = await getItem(itemName); |
| 168 | for (const dep of item.registryDependencies ?? []) { |
| 169 | await visit(dep, [...path, itemName]); |
| 170 | } |
| 171 | visiting.delete(itemName); |
no test coverage detected