From 4d62bd04513f70c365c713ca3640f36bb5a5a85a Mon Sep 17 00:00:00 2001 From: Gergo Magyar Date: Mon, 19 Oct 2026 15:49:47 +0000 Subject: [PATCH] fix(routes): treat export aliases of one wrapper as one star-export target Star-export candidates were deduplicated by the resolved export name, so `export { request, request as send }` reached through two barrels counted as two targets and blocked the import, dropping the consumer's FETCHES edge. Candidates are now keyed by the declaration each export name names, matching ECMAScript ResolveExport's binding comparison; distinct declarations exported under the same name stay ambiguous. Co-Authored-By: Claude Opus 5.5 --- .../route-extractors/fetch-wrapper-index.ts | 21 ++++++++++++++--- .../lib/diamond-distinct/http.ts | 6 +++++ .../lib/diamond-distinct/index.ts | 2 ++ .../lib/diamond-distinct/left.ts | 1 + .../lib/diamond-distinct/right.ts | 1 + .../lib/diamond/http.ts | 4 ++++ .../lib/diamond/index.ts | 2 ++ .../lib/diamond/left.ts | 1 + .../lib/diamond/right.ts | 1 + .../server/api.ts | 2 ++ .../web/diamond.ts | 4 ++++ .../resolvers/fetch-wrapper-consumers.test.ts | 3 ++- .../test/unit/fetch-wrapper-index.test.ts | 23 +++++++++++++++++++ 13 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts create mode 100644 gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts diff --git a/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts b/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts index cb6ef0e1c..7afb88b81 100644 --- a/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts +++ b/gitnexus/src/core/ingestion/route-extractors/fetch-wrapper-index.ts @@ -141,10 +141,17 @@ export function resolveFetchWrapperTargets( } return targets; }; - const declared = new Set(); + // Each export name maps to the declaration it names; a name two declarations + // claim keeps a name-only identity, so it never merges with another export. + const declared = new Map(); for (const definition of definitions) if (definition.moduleScope) - for (const name of definition.exportedNames) declared.add(`${definition.filePath}\0${name}`); + for (const name of definition.exportedNames) { + const key = `${definition.filePath}\0${name}`; + const identity = `${definition.filePath}\0${definition.definitionId}`; + const existing = declared.get(key); + declared.set(key, existing === undefined || existing === identity ? identity : `${key}\0`); + } const named = new Map(); const stars = new Map(); for (const reexport of reexports) { @@ -181,8 +188,16 @@ export function resolveFetchWrapperTargets( named.get(key) ?? (name === 'default' ? undefined : stars.get(filePath)) ?? []; return { frame: { filePath, name, candidates, cursor: 0, found: new Map() } }; }; + // Star exports conflict only when they reach different bindings, so two + // export names of one declaration are one candidate. const record = (frame: ExportFrame, target: CalleeTarget | undefined) => { - if (target) frame.found.set(JSON.stringify(target), target); + if (!target) return; + const identity = + target.kind === 'resolved-import' + ? declared.get(`${target.filePath}\0${target.importedName}`) + : undefined; + const key = identity ?? JSON.stringify(target); + if (!frame.found.has(key)) frame.found.set(key, target); }; const first = open(startFile, startName); if ('value' in first) return first.value; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts new file mode 100644 index 000000000..48c0bfba7 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/http.ts @@ -0,0 +1,6 @@ +export function distinctRequest(url: string) { + return fetch(url); +} +export function distinctSend(url: string) { + return fetch(url); +} diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts new file mode 100644 index 000000000..2f1a5bbd0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/index.ts @@ -0,0 +1,2 @@ +export * from './left'; +export * from './right'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts new file mode 100644 index 000000000..0d83255e4 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/left.ts @@ -0,0 +1 @@ +export { distinctRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts new file mode 100644 index 000000000..8034cf2f9 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond-distinct/right.ts @@ -0,0 +1 @@ +export { distinctSend as distinctRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts new file mode 100644 index 000000000..87f39c442 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/http.ts @@ -0,0 +1,4 @@ +function diamondRequest(url: string) { + return fetch(url); +} +export { diamondRequest, diamondRequest as diamondSend }; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts new file mode 100644 index 000000000..2f1a5bbd0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/index.ts @@ -0,0 +1,2 @@ +export * from './left'; +export * from './right'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts new file mode 100644 index 000000000..9f61165e1 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/left.ts @@ -0,0 +1 @@ +export { diamondRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts new file mode 100644 index 000000000..8e39815e0 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/lib/diamond/right.ts @@ -0,0 +1 @@ +export { diamondSend as diamondRequest } from './http'; diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts index 17089694f..9e28aef07 100644 --- a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/server/api.ts @@ -22,3 +22,5 @@ app.get('/api/hoc', (_req, res) => res.json({})); app.get('/api/cjs', (_req, res) => res.json({})); app.get('/api/key', (_req, res) => res.json({})); app.get('/api/node', (_req, res) => res.json({})); +app.get('/api/diamond', (_req, res) => res.json({})); +app.get('/api/diamond-distinct', (_req, res) => res.json({})); diff --git a/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts new file mode 100644 index 000000000..37cf4681d --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/fetch-wrapper-target-identity/web/diamond.ts @@ -0,0 +1,4 @@ +import { diamondRequest } from '../lib/diamond'; +import { distinctRequest } from '../lib/diamond-distinct'; +export const loadDiamond = () => diamondRequest('/api/diamond'); +export const loadDistinct = () => distinctRequest('/api/diamond-distinct'); diff --git a/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts b/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts index f4cefe6f2..587bf367a 100644 --- a/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts +++ b/gitnexus/test/integration/resolvers/fetch-wrapper-consumers.test.ts @@ -70,7 +70,7 @@ describe('Base-URL wrapper consumer FETCHES edge extraction', () => { }); describe('Fetch wrapper lexical and project target identity', () => { - it('resolves relative, alias, workspace, barrel, overload, multi-declarator, HOC, CommonJS and package-fetch targets while rejecting shadowed and ambiguous names', async () => { + it('resolves relative, alias, workspace, barrel, export-alias diamond, overload, multi-declarator, HOC, CommonJS and package-fetch targets while rejecting shadowed and ambiguous names', async () => { const result = await runPipelineFromRepo( path.join(FIXTURES, 'fetch-wrapper-target-identity'), () => {}, @@ -88,6 +88,7 @@ describe('Fetch wrapper lexical and project target identity', () => { 'web/barrel.ts -> /api/barrel-imported', 'web/barrel.ts -> /api/barrel-star', 'web/cjs.js -> /api/cjs', + 'web/diamond.ts -> /api/diamond', 'web/hoc.ts -> /api/hoc', 'web/multi.ts -> /api/multi', 'web/nested.ts -> /api/nested', diff --git a/gitnexus/test/unit/fetch-wrapper-index.test.ts b/gitnexus/test/unit/fetch-wrapper-index.test.ts index b23c73439..9b3661d5f 100644 --- a/gitnexus/test/unit/fetch-wrapper-index.test.ts +++ b/gitnexus/test/unit/fetch-wrapper-index.test.ts @@ -388,6 +388,29 @@ describe('fetch wrapper barrel resolution', () => { expect(resolveIn(reexports)).toEqual(expected); }); + // `export * from './left'; export * from './right'`, where left re-exports + // `request` and right re-exports `send as request` from the same module. + const diamond = [ + forward('index.ts', '*', './left', '*'), + forward('index.ts', '*', './right', '*'), + forward('left.ts', 'request', './http'), + forward('right.ts', 'request', './http', 'send'), + ]; + it.each([ + [ + 'two export names of one declaration', + [{ ...declares('http.ts', 'request'), exportedNames: ['request', 'send'] }], + at('http.ts'), + ], + [ + 'two declarations', + [declares('http.ts', 'request'), declares('http.ts', 'send')], + { kind: 'blocked' }, + ], + ])('resolves a star-export diamond over %s by binding identity', (_shape, defs, expected) => { + expect(resolveIn(diamond, 'request', defs)).toEqual(expected); + }); + it('never forwards default through a star export', () => { expect( resolveIn([forward('index.ts', '*', './request', '*')], 'default', [