From 96d7eb10dcebc594e31047c110cf377dcf933a05 Mon Sep 17 00:00:00 2001 From: Rishabh Date: Thu, 14 May 2026 15:23:26 +0530 Subject: [PATCH] fix: derive folder path from direct child, use NodeType constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - getFolderPath now checks direct child pages first, then recurses into subfolders using parentPath — fixes wrong path for deeply nested folder structures - Extract parentPath util to avoid duplication - Use NodeType.Page/Folder constants instead of string literals Co-Authored-By: Claude Opus 4.6 (1M context) --- packages/chronicle/src/lib/folder-utils.ts | 26 ++++++ .../chronicle/src/lib/source-utils.test.ts | 85 +++++++++++++++++++ packages/chronicle/src/lib/source.ts | 29 ++----- 3 files changed, 120 insertions(+), 20 deletions(-) create mode 100644 packages/chronicle/src/lib/folder-utils.ts create mode 100644 packages/chronicle/src/lib/source-utils.test.ts diff --git a/packages/chronicle/src/lib/folder-utils.ts b/packages/chronicle/src/lib/folder-utils.ts new file mode 100644 index 0000000..90408e7 --- /dev/null +++ b/packages/chronicle/src/lib/folder-utils.ts @@ -0,0 +1,26 @@ +import type { Node, Folder } from 'fumadocs-core/page-tree'; + +const NodeType = { + Page: 'page', + Folder: 'folder', +} as const; + +export function parentPath(url: string): string { + const parts = url.split('/').filter(Boolean); + parts.pop(); + return '/' + parts.join('/'); +} + +export function getFolderPath(node: Folder): string | null { + if (node.index) return node.index.url; + for (const child of node.children) { + if (child.type === NodeType.Page) return parentPath(child.url); + } + for (const child of node.children) { + if (child.type === NodeType.Folder) { + const childPath = getFolderPath(child as Folder); + if (childPath) return parentPath(childPath); + } + } + return null; +} diff --git a/packages/chronicle/src/lib/source-utils.test.ts b/packages/chronicle/src/lib/source-utils.test.ts new file mode 100644 index 0000000..e108302 --- /dev/null +++ b/packages/chronicle/src/lib/source-utils.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, test } from 'bun:test' +import type { Node, Folder } from 'fumadocs-core/page-tree' +import { parentPath, getFolderPath } from './folder-utils' + +function page(url: string): Node { + return { type: 'page', name: 'Page', url } as Node +} + +function folder(name: string, children: Node[], indexUrl?: string): Folder { + return { + type: 'folder', + name, + children, + ...(indexUrl ? { index: { url: indexUrl } } : {}), + } as Folder +} + +describe('parentPath', () => { + test('returns parent of page URL', () => { + expect(parentPath('/docs/guides/install')).toBe('/docs/guides') + }) + + test('returns root for top-level page', () => { + expect(parentPath('/docs')).toBe('/') + }) + + test('handles trailing segments', () => { + expect(parentPath('/a/b/c/d')).toBe('/a/b/c') + }) + + test('handles root', () => { + expect(parentPath('/')).toBe('/') + }) +}) + +describe('getFolderPath', () => { + test('returns index URL when folder has index', () => { + const f = folder('Guides', [page('/docs/guides/install')], '/docs/guides') + expect(getFolderPath(f)).toBe('/docs/guides') + }) + + test('derives path from direct child page', () => { + const f = folder('Guides', [page('/docs/guides/install')]) + expect(getFolderPath(f)).toBe('/docs/guides') + }) + + test('derives path from subfolder child (not deeply nested)', () => { + const f = folder('Tasking', [ + folder('Via Order Desk', [page('/docs/tasking/via_order_desk/package')]) + ]) + expect(getFolderPath(f)).toBe('/docs/tasking') + }) + + test('handles folder with & in path', () => { + const f = folder('Cart & Order', [page('/docs/cart&order/working_with_cart')]) + expect(getFolderPath(f)).toBe('/docs/cart&order') + }) + + test('handles folder with space in path', () => { + const f = folder('My Folder', [page('/docs/my folder/intro')]) + expect(getFolderPath(f)).toBe('/docs/my folder') + }) + + test('returns null for empty folder', () => { + const f = folder('Empty', []) + expect(getFolderPath(f)).toBeNull() + }) + + test('prefers direct child page over subfolder', () => { + const f = folder('Mixed', [ + page('/docs/mixed/intro'), + folder('Sub', [page('/docs/mixed/sub/deep')]) + ]) + expect(getFolderPath(f)).toBe('/docs/mixed') + }) + + test('deeply nested only-subfolder chain', () => { + const f = folder('Root', [ + folder('Mid', [ + folder('Deep', [page('/a/b/c/d/page')]) + ]) + ]) + expect(getFolderPath(f)).toBe('/a/b') + }) +}) diff --git a/packages/chronicle/src/lib/source.ts b/packages/chronicle/src/lib/source.ts index 89de861..06a1b01 100644 --- a/packages/chronicle/src/lib/source.ts +++ b/packages/chronicle/src/lib/source.ts @@ -3,6 +3,13 @@ import path from 'node:path'; import { loader } from 'fumadocs-core/source'; import { flattenTree } from 'fumadocs-core/page-tree'; import type { Root, Node, Folder } from 'fumadocs-core/page-tree'; + +import { parentPath, getFolderPath } from './folder-utils'; + +const NodeType = { + Page: 'page', + Folder: 'folder', +} as const; import type { MDXContent } from 'mdx/types'; import type { TableOfContents } from 'fumadocs-core/toc'; import { @@ -120,28 +127,10 @@ export function invalidate() { cachedNavMap = null; } -function getFolderPath(node: Folder): string | null { - const firstPage = findFirstPage(node); - if (!firstPage) return null; - const parts = firstPage.url.split('/').filter(Boolean); - parts.pop(); - return '/' + parts.join('/'); -} - -function findFirstPage(node: Folder): { url: string } | null { - for (const child of node.children) { - if (child.type === 'page') return child; - if (child.type === 'folder') { - const found = findFirstPage(child); - if (found) return found; - } - } - return node.index ?? null; -} function getOrder(node: Node, pageOrderMap: Map, folderOrderMap: Map): number | undefined { - if (node.type === 'page') return pageOrderMap.get(node.url); - if (node.type === 'folder') { + if (node.type === NodeType.Page) return pageOrderMap.get(node.url); + if (node.type === NodeType.Folder) { const folderPath = getFolderPath(node); if (folderPath) return folderOrderMap.get(folderPath); }