From b58d24403bf6eae39a8e4faff81f5cda284e5df7 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Thu, 14 May 2026 20:07:27 +0000 Subject: [PATCH] fix(website): query every raw-id alias when pack slugs collide Addresses Oracle review feedback: when two raw upstream ids slugify to the same URL slug (e.g. ComfyUI-QwenVL + ComfyUI_QwenVL both -> comfyui-qwenvl) the previous merge kept only the first rawId and used only that single alias to fetch registry metadata. If that one alias missed but its twin would have resolved, the merged pack lost banner/icon/license info. Now NodePack carries rawIds: string[] holding every raw alias seen for the slug. parseCloudNodes flattens all aliases into a single registry batch and pickRegistryPack walks the alias list in insertion order to find the first non-null hit. --- apps/website/src/utils/cloudNodes.test.ts | 47 +++++++++++++++++++ apps/website/src/utils/cloudNodes.ts | 21 +++++++-- .../src/__tests__/groupNodesByPack.test.ts | 11 +++++ .../src/helpers/groupNodesByPack.ts | 5 ++ 4 files changed, 79 insertions(+), 5 deletions(-) diff --git a/apps/website/src/utils/cloudNodes.test.ts b/apps/website/src/utils/cloudNodes.test.ts index 9b96cc3dd9..01028cbe51 100644 --- a/apps/website/src/utils/cloudNodes.test.ts +++ b/apps/website/src/utils/cloudNodes.test.ts @@ -345,6 +345,53 @@ describe('fetchCloudNodesForBuild', () => { ) }) + it('queries every raw-id alias when packs collide on the same slug and picks the first hit', async () => { + fetchRegistryPacksMock.mockResolvedValue( + new Map([ + ['ComfyUI-QwenVL', null], + [ + 'ComfyUI_QwenVL', + { + id: 'ComfyUI_QwenVL', + name: 'ComfyUI QwenVL', + repository: 'https://github.com/example/ComfyUI_QwenVL' + } + ] + ]) + ) + + const fetchImpl = vi.fn(async () => + response({ + QwenDash: validNode({ + name: 'QwenDash', + python_module: 'custom_nodes.ComfyUI-QwenVL.nodes' + }), + QwenUnder: validNode({ + name: 'QwenUnder', + python_module: 'custom_nodes.ComfyUI_QwenVL.nodes' + }) + }) + ) + const outcome = await fetchCloudNodesForBuild({ + apiKey: KEY, + baseUrl: BASE_URL, + fetchImpl: fetchImpl as typeof fetch + }) + + expect(outcome.status).toBe('fresh') + if (outcome.status !== 'fresh') return + expect(outcome.snapshot.packs).toHaveLength(1) + expect(outcome.snapshot.packs[0]?.id).toBe('comfyui-qwenvl') + expect(outcome.snapshot.packs[0]?.registryId).toBe('ComfyUI_QwenVL') + expect(outcome.snapshot.packs[0]?.repoUrl).toBe( + 'https://github.com/example/ComfyUI_QwenVL' + ) + expect(fetchRegistryPacksMock).toHaveBeenCalledWith( + ['ComfyUI-QwenVL', 'ComfyUI_QwenVL'], + expect.anything() + ) + }) + it('normalizes pack ids when reading a fallback snapshot', async () => { const snapshotUrl = withSnapshotDir({ fetchedAt: '2026-04-01T00:00:00.000Z', diff --git a/apps/website/src/utils/cloudNodes.ts b/apps/website/src/utils/cloudNodes.ts index 2947c2ae8d..2d059c4224 100644 --- a/apps/website/src/utils/cloudNodes.ts +++ b/apps/website/src/utils/cloudNodes.ts @@ -238,12 +238,12 @@ async function parseCloudNodes( ) const grouped = groupNodesByPack(sanitizedDefs) + const allAliases = grouped.flatMap((pack) => pack.rawIds) let registryMap = new Map() try { - registryMap = await fetchRegistryPacks( - grouped.map((pack) => pack.rawId), - { fetchImpl: options.fetchImpl } - ) + registryMap = await fetchRegistryPacks(allAliases, { + fetchImpl: options.fetchImpl + }) } catch { registryMap = new Map() } @@ -253,13 +253,24 @@ async function parseCloudNodes( pack.id, pack.displayName, pack.nodes, - registryMap.get(pack.rawId) + pickRegistryPack(registryMap, pack.rawIds) ) ) return { kind: 'ok', packs, droppedNodes } } +function pickRegistryPack( + registryMap: Map, + aliases: readonly string[] +): RegistryPack | null | undefined { + for (const alias of aliases) { + const hit = registryMap.get(alias) + if (hit) return hit + } + return registryMap.get(aliases[0]) +} + function safeExternalUrl(value: string | undefined): string | undefined { if (!value) return undefined try { diff --git a/packages/object-info-parser/src/__tests__/groupNodesByPack.test.ts b/packages/object-info-parser/src/__tests__/groupNodesByPack.test.ts index fcf2b678b8..ac977f13a1 100644 --- a/packages/object-info-parser/src/__tests__/groupNodesByPack.test.ts +++ b/packages/object-info-parser/src/__tests__/groupNodesByPack.test.ts @@ -92,6 +92,17 @@ describe('groupNodesByPack', () => { 'QwenA', 'QwenB' ]) + expect(grouped[0].rawIds).toEqual(['ComfyUI-QwenVL', 'ComfyUI_QwenVL']) + }) + + it('does not record duplicate aliases when the same raw id appears twice', () => { + const grouped = groupNodesByPack({ + QwenA: makeNodeDef('QwenA', 'custom_nodes.ComfyUI-QwenVL.nodes'), + QwenB: makeNodeDef('QwenB', 'custom_nodes.ComfyUI-QwenVL.nodes') + }) + + expect(grouped).toHaveLength(1) + expect(grouped[0].rawIds).toEqual(['ComfyUI-QwenVL']) }) it('strips version suffix before slugifying', () => { diff --git a/packages/object-info-parser/src/helpers/groupNodesByPack.ts b/packages/object-info-parser/src/helpers/groupNodesByPack.ts index fbee637755..b9073060a9 100644 --- a/packages/object-info-parser/src/helpers/groupNodesByPack.ts +++ b/packages/object-info-parser/src/helpers/groupNodesByPack.ts @@ -10,6 +10,7 @@ export interface PackedNode { export interface NodePack { id: string rawId: string + rawIds: string[] displayName: string nodes: PackedNode[] } @@ -40,12 +41,16 @@ export function groupNodesByPack( if (existing) { existing.nodes.push(node) + if (!existing.rawIds.includes(rawId)) { + existing.rawIds.push(rawId) + } continue } byPackId.set(slug, { id: slug, rawId, + rawIds: [rawId], displayName: source.displayText, nodes: [node] })