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..ffeed53c 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,186 @@ const createMdxTemplate = (metadata: FunctionMetadata) => { return result.join('\n'); }; +interface ShareMarkdownPage { + apiParameters: FunctionMetadata['apiParameters']; + browserapi?: { + description?: string; + name?: string; + }; + category: string; + contributors: FunctionMetadata['contributors']; + demo?: string; + 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 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) => { + 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(''); + } + + // 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(createCodeFence('bash', 'npm install @siberiacancode/reactuse')); + lines.push(''); + lines.push(createCodeFence('bash', `npx useverse@latest add ${page.name}`)); + lines.push(''); + lines.push('Copy and paste the following code into your project.'); + lines.push(''); + lines.push(createCodeFence('tsx', page.source)); + lines.push(''); + lines.push('Update the import paths to match your project setup.'); + 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.typeDeclarations?.trim()) { + lines.push('## Type Declarations'); + lines.push(''); + lines.push(createCodeFence('tsx', page.typeDeclarations)); + lines.push(''); + } + + if (page.apiParameters.length) { + lines.push('## API'); + lines.push(''); + lines.push(...createShareApiMarkdown(page.apiParameters)); + } + + if (page.contributors.length) { + lines.push('## Contributors'); + lines.push(''); + for (const contributor of page.contributors) { + lines.push(`- ${contributor.name}`); + } + lines.push(''); + } + + return `${lines.join('\n').trimEnd()}\n`; +}; + const createHtmlCode = async (code: string, language: CodeLanguage) => await codeToHtml(code, { lang: language, @@ -321,10 +501,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 +545,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 +577,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, + contributors: page.contributors, + 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 +641,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 +655,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 +681,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'));