From bfa7407ab487258ff5275042c7d133b7b4bc9fca Mon Sep 17 00:00:00 2001 From: geril Date: Mon, 3 Aug 2026 15:32:08 +0300 Subject: [PATCH 1/4] =?UTF-8?q?fix-copy-page=20=F0=9F=A7=8A=20fix(docs):?= =?UTF-8?q?=20resolve=20function=20Copy=20Page=20from=20share=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generate AI-ready per-function markdown at build time, skip static MDX mirroring for function pages so it is not clobbered, and wire Copy Page to the public share artifact instead of raw MDX shells. --- .../app/(docs)/functions/[[...slug]]/page.tsx | 17 +- .../newdocs/scripts/generate-functions.ts | 236 ++++++++++++++++-- packages/newdocs/scripts/generate-static.ts | 5 + 3 files changed, 231 insertions(+), 27 deletions(-) diff --git a/packages/newdocs/app/(docs)/functions/[[...slug]]/page.tsx b/packages/newdocs/app/(docs)/functions/[[...slug]]/page.tsx index 85a51fea..1d04d577 100644 --- a/packages/newdocs/app/(docs)/functions/[[...slug]]/page.tsx +++ b/packages/newdocs/app/(docs)/functions/[[...slug]]/page.tsx @@ -67,15 +67,20 @@ const FunctionPage = async (props: FunctionPageProps) => { const doc = page.data; const neighbours = findNeighbour(functionsSource.pageTree, page.url); - const raw = await doc.getText('raw'); const lastModifiedTime = doc.lastModifiedTime; - const metadata = JSON.parse( - await fs.readFile( - path.join(process.cwd(), 'content', 'functions', `${doc.type}s`, `${doc.title}.meta.json`), + const [metadata, markdown] = await Promise.all([ + fs + .readFile( + path.join(process.cwd(), 'content', 'functions', `${doc.type}s`, `${doc.title}.meta.json`), + 'utf-8' + ) + .then((content) => JSON.parse(content) as FunctionMetadata), + fs.readFile( + path.join(process.cwd(), 'public', 'functions', `${doc.type}s`, `${doc.title}.md`), 'utf-8' ) - ) as FunctionMetadata; + ]); const MDX = doc.body; @@ -90,7 +95,7 @@ const FunctionPage = async (props: FunctionPageProps) => { category={doc.category} description={doc.description} isTest={doc.isTest} - markdown={raw} + markdown={markdown} name={doc.title} next={neighbours.next?.url} previous={neighbours.previous?.url} diff --git a/packages/newdocs/scripts/generate-functions.ts b/packages/newdocs/scripts/generate-functions.ts index 1757cd13..ea8da1de 100644 --- a/packages/newdocs/scripts/generate-functions.ts +++ b/packages/newdocs/scripts/generate-functions.ts @@ -6,7 +6,7 @@ import ts from 'typescript'; import type { CodeLanguage, FunctionMetadata } from '@/src/constants'; -import { CONTENT_ROOT, CORE_ROOT } from './constants'; +import { CONTENT_ROOT, CORE_ROOT, PUBLIC_ROOT } from './constants'; import { checkFileContent, extractDependencies, @@ -156,6 +156,152 @@ const createMdxTemplate = (metadata: FunctionMetadata) => { return result.join('\n'); }; +interface ShareMarkdownPage { + apiParameters: FunctionMetadata['apiParameters']; + browserapi?: { + description?: string; + name?: string; + }; + category: string; + demo?: string; + dependencies: FunctionMetadata['dependencies']; + description: string; + examples: string[]; + isTest: boolean; + name: string; + source: string; + type: FunctionMetadata['type']; + typeDeclarations?: string; + usage: string; + warning?: string; +} + +const createCodeFence = (language: string, code: string) => + `\`\`\`${language}\n${code.trimEnd()}\n\`\`\``; + +const createShareMarkdown = (page: ShareMarkdownPage) => { + const lines: string[] = []; + + lines.push('---'); + lines.push(`title: ${page.name}`); + if (page.description) lines.push(`description: ${page.description}`); + lines.push(`category: ${page.category.toLowerCase()}`); + lines.push(`usage: ${page.usage.toLowerCase()}`); + lines.push(`type: ${page.type}`); + lines.push(`isTest: ${page.isTest}`); + if (page.browserapi?.name) { + lines.push( + page.browserapi.description + ? `browserapi: ${page.browserapi.name} ${page.browserapi.description}` + : `browserapi: ${page.browserapi.name}` + ); + } + lines.push('---'); + lines.push(''); + lines.push(`# ${page.name}`); + lines.push(''); + + if (page.description) { + lines.push(page.description); + lines.push(''); + } + + if (page.warning) { + lines.push(`> **Warning:** ${page.warning}`); + lines.push(''); + } + + lines.push('## Installation'); + lines.push(''); + lines.push('Library:'); + lines.push(''); + lines.push(createCodeFence('bash', 'npm install @siberiacancode/reactuse')); + lines.push(''); + lines.push('CLI:'); + lines.push(''); + lines.push(createCodeFence('bash', `npx useverse@latest add ${page.name}`)); + lines.push(''); + lines.push('Manual: copy the source below into your project and update import paths.'); + lines.push(''); + + lines.push('## Source'); + lines.push(''); + lines.push(createCodeFence('ts', page.source)); + lines.push(''); + + lines.push('## Usage'); + lines.push(''); + const usage = page.examples + .map((example, index) => (index === 0 ? example : `// or\n${example}`)) + .join('\n'); + lines.push(createCodeFence('tsx', usage)); + lines.push(''); + + if (page.apiParameters.length) { + lines.push('## API'); + lines.push(''); + + for (const parameter of page.apiParameters) { + if (parameter.tag === 'overload') { + lines.push('- **Overload**'); + continue; + } + + if (parameter.tag === 'param') { + const optional = parameter.optional ? '?' : ''; + const defaultValue = + parameter.default !== undefined && parameter.default !== '' + ? ` = ${parameter.default}` + : ''; + const type = parameter.type || 'unknown'; + const description = parameter.description ? ` — ${parameter.description}` : ''; + lines.push(`- \`${parameter.name}${optional}: ${type}${defaultValue}\`${description}`); + continue; + } + + if (parameter.tag === 'returns') { + const type = parameter.type || 'unknown'; + const description = parameter.description ? ` — ${parameter.description}` : ''; + lines.push(`- **Returns:** \`${type}\`${description}`); + } + } + + lines.push(''); + } + + if (page.typeDeclarations?.trim()) { + lines.push('## Type Declarations'); + lines.push(''); + lines.push(createCodeFence('ts', page.typeDeclarations)); + lines.push(''); + } + + if (page.demo?.trim()) { + lines.push('## Demo'); + lines.push(''); + lines.push(createCodeFence('tsx', page.demo)); + lines.push(''); + } + + const { hooks, utils, packages } = page.dependencies; + if (hooks.length || utils.length || packages.length) { + lines.push('## Dependencies'); + lines.push(''); + if (hooks.length) { + lines.push(`- **Hooks:** ${hooks.map((item) => `\`${item}\``).join(', ')}`); + } + if (utils.length) { + lines.push(`- **Utils:** ${utils.map((item) => `\`${item}\``).join(', ')}`); + } + if (packages.length) { + lines.push(`- **Packages:** ${packages.map((item) => `\`${item}\``).join(', ')}`); + } + lines.push(''); + } + + return `${lines.join('\n').trimEnd()}\n`; +}; + const createHtmlCode = async (code: string, language: CodeLanguage) => await codeToHtml(code, { lang: language, @@ -321,10 +467,10 @@ const init = async () => { const metadata = await Promise.all( content.map(async (element) => { - const content = await getContentFile(element.type, element.name); + const source = await getContentFile(element.type, element.name); const extension = await getExtensionFile(element.type, element.name); - const jsdocMatch = matchJsdoc(content); + const jsdocMatch = matchJsdoc(source); if (!jsdocMatch) { console.error(`No jsdoc comment found for ${element.name}`); @@ -365,18 +511,23 @@ const init = async () => { extension ); - const sourceFile = ts.createSourceFile('temp.ts', content, ts.ScriptTarget.Latest, true); - const typeDeclarations = extractTypeInfo(sourceFile); - - const dependencies = extractDependencies(content); + const sourceFile = ts.createSourceFile('temp.ts', source, ts.ScriptTarget.Latest, true); + const typeDeclarationsSource = extractTypeInfo(sourceFile); + const dependencies = extractDependencies(source); + const demoSource = isDemo + ? await fs.promises.readFile( + path.join(CORE_ROOT, `${element.type}s`, element.name, `${element.name}.demo.tsx`), + 'utf-8' + ) + : undefined; - return { + const page = { badges: { firstCommitAt: new Date(firstCommitAt).getTime(), isNew, lastCommitAt: new Date(lastCommitAt).getTime() }, - code: await createHtmlCode(content, 'tsx'), + code: await createHtmlCode(source, 'tsx'), id: element.name, isTest, isDemo, @@ -392,25 +543,47 @@ const init = async () => { lastModified: lastCommitAt, examples: jsdoc.examples.map((example) => example.description), apiParameters: jsdoc.apiParameters ?? [], - ...(typeDeclarations && { - typeDeclarations: await createHtmlCode(typeDeclarations, 'tsx') + ...(typeDeclarationsSource && { + typeDeclarations: await createHtmlCode(typeDeclarationsSource, 'tsx') }), dependencies, contributors, - ...(isDemo && { - demo: await createHtmlCode( - await fs.promises.readFile( - path.join(CORE_ROOT, `${element.type}s`, element.name, `${element.name}.demo.tsx`), - 'utf-8' - ), - 'tsx' - ) + ...(demoSource && { + demo: await createHtmlCode(demoSource, 'tsx') }) }; + + const share: ShareMarkdownPage = { + name: page.name, + type: page.type, + description: page.description, + category: page.category, + usage: page.usage, + isTest: page.isTest, + examples: page.examples, + apiParameters: page.apiParameters, + dependencies: page.dependencies, + source, + ...(page.warning && { warning: page.warning }), + ...(page.browserapi && { + browserapi: { + name: page.browserapi.name, + description: page.browserapi.description + } + }), + ...(typeDeclarationsSource && { typeDeclarations: typeDeclarationsSource }), + ...(demoSource && { demo: demoSource }) + }; + + return { page, share }; }) ); - const pages = metadata.filter(Boolean) as unknown as FunctionMetadata[]; + const generated = metadata.filter(Boolean) as { + page: FunctionMetadata; + share: ShareMarkdownPage; + }[]; + const pages = generated.map(({ page }) => page); const testCoverage = pages.reduce((acc, page) => acc + Number(page.isTest), 0); console.log('\nElements injection report\n'); @@ -434,7 +607,7 @@ const init = async () => { console.log('\n[generate-functions] Writing files...'); - for (const page of pages) { + for (const { page, share } of generated) { const mdx = createMdxTemplate(page); await fs.promises.writeFile( path.join(CONTENT_ROOT, 'functions', `${page.type}s`, `${page.name}.mdx`), @@ -448,6 +621,15 @@ const init = async () => { 'utf-8' ); + const shareMarkdownPath = path.join( + PUBLIC_ROOT, + 'functions', + `${page.type}s`, + `${page.name}.md` + ); + await fs.promises.mkdir(path.dirname(shareMarkdownPath), { recursive: true }); + await fs.promises.writeFile(shareMarkdownPath, createShareMarkdown(share), 'utf-8'); + if (page.demo) { const demo = await createDemo(page); await fs.promises.writeFile( @@ -465,6 +647,18 @@ const init = async () => { metaJson, 'utf-8' ); + + const shareDirectory = path.join(PUBLIC_ROOT, 'functions', `${type}s`); + const keep = new Set( + pages.filter((page) => page.type === type).map((page) => `${page.name}.md`) + ); + + if (!fs.existsSync(shareDirectory)) continue; + + for (const file of await fs.promises.readdir(shareDirectory)) { + if (!file.endsWith('.md') || keep.has(file)) continue; + await fs.promises.unlink(path.join(shareDirectory, file)); + } } const functionsMd = createFunctionsMd(pages); diff --git a/packages/newdocs/scripts/generate-static.ts b/packages/newdocs/scripts/generate-static.ts index 3ce4fb51..313322ae 100644 --- a/packages/newdocs/scripts/generate-static.ts +++ b/packages/newdocs/scripts/generate-static.ts @@ -13,6 +13,11 @@ const init = () => { for (const file of files) { if (typeof file !== 'string' || !file.endsWith('.mdx')) continue; + // Function pages get a resolved markdown export from generate-functions. + // Copying the MDX shell would replace AI-ready content with internals. + const normalized = file.replaceAll('\\', '/'); + if (normalized.startsWith('functions/')) continue; + const sourcePath = path.join(CONTENT_ROOT, file); const targetPath = path.join(PUBLIC_ROOT, file.replace(/\.mdx$/i, '.md')); From 7c308bba7d51b8e5d5fa3460b04152fa95d3bdfe Mon Sep 17 00:00:00 2001 From: geril Date: Mon, 3 Aug 2026 15:32:08 +0300 Subject: [PATCH 2/4] =?UTF-8?q?fix-copy-page=20=F0=9F=A7=8A=20fix(docs):?= =?UTF-8?q?=20align=20share=20markdown=20sections=20with=20function=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match interactive page order: demo, Installation (with manual source), Usage, Type Declarations, API, Contributors. Drop invented Source/Demo/ Dependencies headings. --- .../newdocs/scripts/generate-functions.ts | 59 ++++++++----------- 1 file changed, 24 insertions(+), 35 deletions(-) diff --git a/packages/newdocs/scripts/generate-functions.ts b/packages/newdocs/scripts/generate-functions.ts index ea8da1de..30a79869 100644 --- a/packages/newdocs/scripts/generate-functions.ts +++ b/packages/newdocs/scripts/generate-functions.ts @@ -163,8 +163,8 @@ interface ShareMarkdownPage { name?: string; }; category: string; + contributors: FunctionMetadata['contributors']; demo?: string; - dependencies: FunctionMetadata['dependencies']; description: string; examples: string[]; isTest: boolean; @@ -179,6 +179,8 @@ interface ShareMarkdownPage { const createCodeFence = (language: string, code: string) => `\`\`\`${language}\n${code.trimEnd()}\n\`\`\``; +// Mirrors the interactive function page section order (like shadcn share md): +// banner/demo → Installation (library / cli / manual+source) → Usage → Type Declarations → API → Contributors const createShareMarkdown = (page: ShareMarkdownPage) => { const lines: string[] = []; @@ -211,23 +213,24 @@ const createShareMarkdown = (page: ShareMarkdownPage) => { lines.push(''); } + // FunctionBanner on the page: demo code first, no extra heading (shadcn-style). + if (page.demo?.trim()) { + lines.push(createCodeFence('tsx', page.demo)); + lines.push(''); + } + lines.push('## Installation'); lines.push(''); - lines.push('Library:'); - lines.push(''); lines.push(createCodeFence('bash', 'npm install @siberiacancode/reactuse')); lines.push(''); - lines.push('CLI:'); - lines.push(''); lines.push(createCodeFence('bash', `npx useverse@latest add ${page.name}`)); lines.push(''); - lines.push('Manual: copy the source below into your project and update import paths.'); - lines.push(''); - - lines.push('## Source'); + lines.push('Copy and paste the following code into your project.'); lines.push(''); lines.push(createCodeFence('ts', page.source)); lines.push(''); + lines.push('Update the import paths to match your project setup.'); + lines.push(''); lines.push('## Usage'); lines.push(''); @@ -237,6 +240,13 @@ const createShareMarkdown = (page: ShareMarkdownPage) => { lines.push(createCodeFence('tsx', usage)); lines.push(''); + if (page.typeDeclarations?.trim()) { + lines.push('## Type Declarations'); + lines.push(''); + lines.push(createCodeFence('ts', page.typeDeclarations)); + lines.push(''); + } + if (page.apiParameters.length) { lines.push('## API'); lines.push(''); @@ -269,32 +279,11 @@ const createShareMarkdown = (page: ShareMarkdownPage) => { lines.push(''); } - if (page.typeDeclarations?.trim()) { - lines.push('## Type Declarations'); - lines.push(''); - lines.push(createCodeFence('ts', page.typeDeclarations)); - lines.push(''); - } - - if (page.demo?.trim()) { - lines.push('## Demo'); - lines.push(''); - lines.push(createCodeFence('tsx', page.demo)); - lines.push(''); - } - - const { hooks, utils, packages } = page.dependencies; - if (hooks.length || utils.length || packages.length) { - lines.push('## Dependencies'); + if (page.contributors.length) { + lines.push('## Contributors'); lines.push(''); - if (hooks.length) { - lines.push(`- **Hooks:** ${hooks.map((item) => `\`${item}\``).join(', ')}`); - } - if (utils.length) { - lines.push(`- **Utils:** ${utils.map((item) => `\`${item}\``).join(', ')}`); - } - if (packages.length) { - lines.push(`- **Packages:** ${packages.map((item) => `\`${item}\``).join(', ')}`); + for (const contributor of page.contributors) { + lines.push(`- ${contributor.name}`); } lines.push(''); } @@ -562,7 +551,7 @@ const init = async () => { isTest: page.isTest, examples: page.examples, apiParameters: page.apiParameters, - dependencies: page.dependencies, + contributors: page.contributors, source, ...(page.warning && { warning: page.warning }), ...(page.browserapi && { From b2e1353b0d12e312faa76f4837daf8b6930a211c Mon Sep 17 00:00:00 2001 From: geril Date: Mon, 3 Aug 2026 15:32:08 +0300 Subject: [PATCH 3/4] =?UTF-8?q?fix-copy-page=20=F0=9F=A7=8A=20fix(docs):?= =?UTF-8?q?=20render=20share=20markdown=20API=20as=20tables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the page FunctionApi layout: Parameters table (Name, Type, Default, Note) and Returns per overload group. --- .../newdocs/scripts/generate-functions.ts | 99 ++++++++++++++----- 1 file changed, 72 insertions(+), 27 deletions(-) diff --git a/packages/newdocs/scripts/generate-functions.ts b/packages/newdocs/scripts/generate-functions.ts index 30a79869..f6b2b0a1 100644 --- a/packages/newdocs/scripts/generate-functions.ts +++ b/packages/newdocs/scripts/generate-functions.ts @@ -179,6 +179,77 @@ interface ShareMarkdownPage { const createCodeFence = (language: string, code: string) => `\`\`\`${language}\n${code.trimEnd()}\n\`\`\``; +const escapeMarkdownTableCell = (value: string) => + value.replaceAll('|', '\\|').replaceAll('\n', ' '); + +// Matches FunctionApi on the page: overload groups with Parameters table + Returns. +const createShareApiMarkdown = (apiParameters: FunctionMetadata['apiParameters']) => { + let groupIndex = 0; + const groups: { + parameters: FunctionMetadata['apiParameters']; + returns: FunctionMetadata['apiParameters'][number] | null; + }[] = [{ parameters: [], returns: null }]; + + apiParameters.forEach((parameter, index) => { + if (parameter.tag === 'overload') { + const isFirstOverload = apiParameters.findIndex(({ tag }) => tag === 'overload') === index; + + if (!isFirstOverload) { + groupIndex++; + groups.push({ parameters: [], returns: null }); + } + + return; + } + + if (parameter.tag === 'returns') { + groups[groupIndex]!.returns = parameter; + return; + } + + groups[groupIndex]!.parameters.push(parameter); + }); + + const lines: string[] = []; + + groups.forEach((group, index) => { + if (group.parameters.length) { + lines.push('### Parameters'); + lines.push(''); + lines.push('| Name | Type | Default | Note |'); + lines.push('| --- | --- | --- | --- |'); + + for (const parameter of group.parameters) { + const name = escapeMarkdownTableCell(parameter.name); + const type = escapeMarkdownTableCell(parameter.type || 'unknown'); + const defaultValue = escapeMarkdownTableCell(parameter.default ?? '-'); + const note = escapeMarkdownTableCell(parameter.description || ''); + lines.push(`| ${name} | \`${type}\` | ${defaultValue} | ${note} |`); + } + + lines.push(''); + } + + if (group.returns) { + lines.push('### Returns'); + lines.push(''); + lines.push('`' + (group.returns.type || 'unknown') + '`'); + if (group.returns.description) { + lines.push(''); + lines.push(group.returns.description); + } + lines.push(''); + } + + if (index < groups.length - 1) { + lines.push('---'); + lines.push(''); + } + }); + + return lines; +}; + // Mirrors the interactive function page section order (like shadcn share md): // banner/demo → Installation (library / cli / manual+source) → Usage → Type Declarations → API → Contributors const createShareMarkdown = (page: ShareMarkdownPage) => { @@ -250,33 +321,7 @@ const createShareMarkdown = (page: ShareMarkdownPage) => { if (page.apiParameters.length) { lines.push('## API'); lines.push(''); - - for (const parameter of page.apiParameters) { - if (parameter.tag === 'overload') { - lines.push('- **Overload**'); - continue; - } - - if (parameter.tag === 'param') { - const optional = parameter.optional ? '?' : ''; - const defaultValue = - parameter.default !== undefined && parameter.default !== '' - ? ` = ${parameter.default}` - : ''; - const type = parameter.type || 'unknown'; - const description = parameter.description ? ` — ${parameter.description}` : ''; - lines.push(`- \`${parameter.name}${optional}: ${type}${defaultValue}\`${description}`); - continue; - } - - if (parameter.tag === 'returns') { - const type = parameter.type || 'unknown'; - const description = parameter.description ? ` — ${parameter.description}` : ''; - lines.push(`- **Returns:** \`${type}\`${description}`); - } - } - - lines.push(''); + lines.push(...createShareApiMarkdown(page.apiParameters)); } if (page.contributors.length) { From abed2de0a4e91c7730bfbc46fb33a7974c1a9f7f Mon Sep 17 00:00:00 2001 From: geril Date: Mon, 3 Aug 2026 15:32:09 +0300 Subject: [PATCH 4/4] =?UTF-8?q?fix-copy-page=20=F0=9F=A7=8A=20fix(docs):?= =?UTF-8?q?=20use=20tsx=20fences=20in=20function=20share=20markdown?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align source and type declaration code fences with the page (tsx). --- packages/newdocs/scripts/generate-functions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/newdocs/scripts/generate-functions.ts b/packages/newdocs/scripts/generate-functions.ts index f6b2b0a1..ffeed53c 100644 --- a/packages/newdocs/scripts/generate-functions.ts +++ b/packages/newdocs/scripts/generate-functions.ts @@ -298,7 +298,7 @@ const createShareMarkdown = (page: ShareMarkdownPage) => { lines.push(''); lines.push('Copy and paste the following code into your project.'); lines.push(''); - lines.push(createCodeFence('ts', page.source)); + lines.push(createCodeFence('tsx', page.source)); lines.push(''); lines.push('Update the import paths to match your project setup.'); lines.push(''); @@ -314,7 +314,7 @@ const createShareMarkdown = (page: ShareMarkdownPage) => { if (page.typeDeclarations?.trim()) { lines.push('## Type Declarations'); lines.push(''); - lines.push(createCodeFence('ts', page.typeDeclarations)); + lines.push(createCodeFence('tsx', page.typeDeclarations)); lines.push(''); }