From 62d6d28888859ca377b055a8b83a4bdd03b1dfd8 Mon Sep 17 00:00:00 2001 From: conico974 Date: Tue, 9 Jun 2026 15:10:03 +0200 Subject: [PATCH] Fix context handling in resolveContentRef for page resolution issues (#4290) --- packages/gitbook/src/lib/references.test.ts | 132 +++++++++++++++++++- packages/gitbook/src/lib/references.tsx | 15 ++- 2 files changed, 145 insertions(+), 2 deletions(-) diff --git a/packages/gitbook/src/lib/references.test.ts b/packages/gitbook/src/lib/references.test.ts index de29df3b6..a093f9214 100644 --- a/packages/gitbook/src/lib/references.test.ts +++ b/packages/gitbook/src/lib/references.test.ts @@ -1,6 +1,10 @@ import { describe, expect, it } from 'bun:test'; +import type { Revision, RevisionPageDocument, Space } from '@gitbook/api'; -import { resolveStringContentRef } from './references'; +import type { GitBookAnyContext } from '@/lib/context'; +import type { GitBookDataFetcher } from '@/lib/data'; +import { createLinker } from '@/lib/links'; +import { resolveContentRef, resolveStringContentRef } from './references'; describe('resolveStringContentRef', () => { it.each([ @@ -136,3 +140,129 @@ describe('resolveStringContentRef', () => { expect(resolveStringContentRef(input)).toBeNull(); }); }); + +describe('resolveContentRef', () => { + it("resolves a page in a different space using that space's page data, not the current context page", async () => { + const sharedPageId = 'page-shared'; + + const spaceAPage = { + object: 'page', + id: sharedPageId, + type: 'document', + kind: 'sheet', + title: 'Space A Page', + path: 'space-a-page', + slug: 'space-a-page', + pages: [], + tags: [], + layout: {}, + urls: { app: 'https://app.gitbook.com/page' }, + } as unknown as RevisionPageDocument; + + const spaceBPage = { + object: 'page', + id: sharedPageId, + type: 'document', + kind: 'sheet', + title: 'Space B Page', + path: 'space-b-page', + slug: 'space-b-page', + pages: [], + tags: [], + layout: {}, + urls: { app: 'https://app.gitbook.com/page' }, + } as unknown as RevisionPageDocument; + + const revisionA = { + object: 'revision', + id: 'rev-a', + type: 'edits', + pages: [spaceAPage], + files: [], + reusableContents: [], + tags: [], + parents: [], + createdAt: '', + urls: { app: '' }, + } as unknown as Revision; + + const revisionB = { + object: 'revision', + id: 'rev-b', + type: 'edits', + pages: [spaceBPage], + files: [], + reusableContents: [], + tags: [], + parents: [], + createdAt: '', + urls: { app: '' }, + } as unknown as Revision; + + const spaceA = { + object: 'space', + id: 'space-a', + title: 'Space A', + organization: 'org', + revision: 'rev-a', + urls: { + location: 'https://api.gitbook.com/spaces/space-a', + app: 'https://app.gitbook.com/o/org/s/space-a/', + published: 'https://space-a.gitbook.io/', + }, + } as unknown as Space; + + const spaceB = { + object: 'space', + id: 'space-b', + title: 'Space B', + organization: 'org', + revision: 'rev-b', + urls: { + location: 'https://api.gitbook.com/spaces/space-b', + app: 'https://app.gitbook.com/o/org/s/space-b/', + published: 'https://space-b.gitbook.io/', + }, + } as unknown as Space; + + const dataFetcher = { + getSpace: async ({ spaceId }: { spaceId: string; shareKey: string | undefined }) => { + if (spaceId === 'space-b') return { data: spaceB }; + return { error: { code: 404, message: 'Not found' } }; + }, + getRevision: async ({ spaceId }: { spaceId: string; revisionId: string }) => { + if (spaceId === 'space-b') return { data: revisionB }; + return { error: { code: 404, message: 'Not found' } }; + }, + getChangeRequest: async () => ({ error: { code: 404, message: 'Not found' } }), + withToken: function () { + return this; + }, + } as unknown as GitBookDataFetcher; + + const context: GitBookAnyContext = { + dataFetcher, + linker: createLinker({ + host: 'docs.example.com', + spaceBasePath: '/space-a/', + siteBasePath: '/', + }), + organizationId: 'org', + space: spaceA, + revision: revisionA, + revisionId: 'rev-a', + changeRequest: null, + shareKey: undefined, + // This page (from space A) has the same ID as the target page in space B. + // It should NOT contaminate resolution in space B. + page: spaceAPage, + } as unknown as GitBookAnyContext; + + const result = await resolveContentRef( + { kind: 'page', space: 'space-b', page: sharedPageId }, + context + ); + + expect(result?.text).toBe('Space B Page'); + }); +}); diff --git a/packages/gitbook/src/lib/references.tsx b/packages/gitbook/src/lib/references.tsx index 8e7ebd60a..347db15a1 100644 --- a/packages/gitbook/src/lib/references.tsx +++ b/packages/gitbook/src/lib/references.tsx @@ -164,7 +164,20 @@ export async function resolveContentRef( case 'anchor': case 'page': { if (isContentRefInDifferentSpace(contentRef, context)) { - return resolveContentRefInSpace(contentRef.space, context, contentRef, options); + let contextWithoutPage: GitBookAnyContext = context; + if ('page' in contextWithoutPage) { + // We need to remove the page from the context to avoid issues when resolving the content ref in the target space. + // The problem happens when the same page exists in the target space (i.e. a duplicate space), it then contaminates the resolution because it looks like the page is already resolved + // while it is not the case as it is a different page with the same id in another space. + const { page, ...rest } = contextWithoutPage; + contextWithoutPage = rest; + } + return resolveContentRefInSpace( + contentRef.space, + contextWithoutPage, + contentRef, + options + ); } const resolvePageResult =