From 9c9c7f9a5066f75e415352a6faafa152cdc007dd Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 21:04:27 +0000 Subject: [PATCH 1/2] feat(ast): strip /index suffix from path when using index files Index files (e.g. index.md) should resolve to their parent directory path rather than /index. For example: - /index.md -> path: / - /api/index.md -> path: /api - /api/fs.md -> path: /api/fs (unchanged) --- .../ast/__tests__/generate.test.mjs | 70 +++++++++++++++++++ src/generators/ast/generate.mjs | 9 ++- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 src/generators/ast/__tests__/generate.test.mjs diff --git a/src/generators/ast/__tests__/generate.test.mjs b/src/generators/ast/__tests__/generate.test.mjs new file mode 100644 index 00000000..846433dc --- /dev/null +++ b/src/generators/ast/__tests__/generate.test.mjs @@ -0,0 +1,70 @@ +'use strict'; + +import assert from 'node:assert/strict'; +import { describe, it, mock } from 'node:test'; + +// Mock fs/promises so processChunk doesn't touch the real filesystem +mock.module('node:fs/promises', { + namedExports: { + readFile: async () => '# Hello', + }, +}); + +// Mock remark to avoid parsing overhead +mock.module('../../../utils/remark.mjs', { + namedExports: { + getRemark: () => ({ parse: () => ({ type: 'root', children: [] }) }), + }, +}); + +// Mock queries to avoid regex replacements interfering +mock.module('../../../utils/queries/index.mjs', { + namedExports: { + QUERIES: { + standardYamlFrontmatter: /(?!x)x/, // never matches + stabilityIndexPrefix: /(?!x)x/, // never matches + }, + }, +}); + +const { processChunk } = await import('../generate.mjs'); + +describe('processChunk path computation', () => { + it('strips /index suffix from a top-level index file', async () => { + const results = await processChunk([['doc/api/index.md', 'doc/api']], [0]); + assert.strictEqual(results[0].path, '/'); + }); + + it('strips /index suffix from a nested index file', async () => { + const results = await processChunk( + [['doc/api/sub/index.md', 'doc/api']], + [0] + ); + assert.strictEqual(results[0].path, '/sub'); + }); + + it('keeps path unchanged for non-index files', async () => { + const results = await processChunk([['doc/api/fs.md', 'doc/api']], [0]); + assert.strictEqual(results[0].path, '/fs'); + }); + + it('keeps path unchanged for files whose name contains index but is not index', async () => { + const results = await processChunk( + [['doc/api/indexes.md', 'doc/api']], + [0] + ); + assert.strictEqual(results[0].path, '/indexes'); + }); + + it('processes multiple files correctly', async () => { + const input = [ + ['doc/api/index.md', 'doc/api'], + ['doc/api/fs.md', 'doc/api'], + ['doc/api/sub/index.md', 'doc/api'], + ]; + const results = await processChunk(input, [0, 1, 2]); + assert.strictEqual(results[0].path, '/'); + assert.strictEqual(results[1].path, '/fs'); + assert.strictEqual(results[2].path, '/sub'); + }); +}); diff --git a/src/generators/ast/generate.mjs b/src/generators/ast/generate.mjs index e0c5a6c5..3be5e192 100644 --- a/src/generators/ast/generate.mjs +++ b/src/generators/ast/generate.mjs @@ -35,11 +35,16 @@ export async function processChunk(inputSlice, itemIndices) { match => `[${match}](${STABILITY_INDEX_URL})` ); - const relativePath = sep + withExt(relative(parent, path)); + const strippedPath = withExt(relative(parent, path)); + // Treat index files as the directory root (e.g. /index → /, /api/index → /api) + const relativePath = + strippedPath === 'index' + ? sep + : sep + strippedPath.replace(/(\/|^)index$/, ''); results.push({ tree: remark().parse(value), - // The path is the relative path minus the extension + // The path is the relative path minus the extension (and /index suffix) path: relativePath, }); } From 808b8d0ff63ffefe05c72c0ab0eb962a3801c63e Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 6 Apr 2026 21:11:02 +0000 Subject: [PATCH 2/2] fix(metadata): preserve api/basename identifiers for root index files When path is '/' (set by the ast generator for index.md files), the derived api and basename values were empty strings. This broke all downstream generators that rely on api/basename as file identifiers (e.g. legacy-json writing index.json, legacy-json-all skipping the index section, legacy-html writing index.html, etc.). Add an 'index' fallback so that path='/' correctly maps to api='index' and basename='index', matching the pre-existing behavior for those identifiers. --- src/generators/metadata/utils/parse.mjs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/generators/metadata/utils/parse.mjs b/src/generators/metadata/utils/parse.mjs index 29192653..7436e601 100644 --- a/src/generators/metadata/utils/parse.mjs +++ b/src/generators/metadata/utils/parse.mjs @@ -42,7 +42,8 @@ export const parseApiDoc = ({ path, tree }, typeMap) => { const nodeSlugger = createNodeSlugger(); // Slug the API (We use a non-class slugger, since we are fairly certain that `path` is unique) - const api = slug(path.slice(1).replace(sep, '-')); + // When path is the root ('/'), the file is an index and the api identifier falls back to 'index' + const api = slug(path.slice(1).replace(sep, '-')) || 'index'; // Get all Markdown Footnote definitions from the tree const markdownDefinitions = selectAll('definition', tree); @@ -83,7 +84,7 @@ export const parseApiDoc = ({ path, tree }, typeMap) => { const metadata = /** @type {import('../types').MetadataEntry} */ ({ api, path, - basename: basename(path), + basename: basename(path) || 'index', heading: headingNode, });