import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import type { ApiDocsMethod } from '../../../docs/.vitepress/components/api-docs/method'; import type { RawApiDocsPage } from '../processing/class'; import type { RawApiDocsMethod } from '../processing/method'; import { formatMarkdown, formatTypescript } from '../utils/format'; import { adjustUrls, codeToHtml, mdToHtml } from '../utils/markdown'; import { FILE_PATH_API_DOCS } from '../utils/paths'; import { required } from '../utils/value-checks'; import { SCRIPT_COMMAND } from './constants'; // Extracted to a constant because the contents must not be formatted by prettier const vitePressInFileOptions = `--- editLink: false --- `; /** * Writes the api docs page and data for the given modules to the correct location. * * @param pages The pages to write. */ export async function writePages(pages: RawApiDocsPage[]): Promise { await Promise.all(pages.map(writePage)); } /** * Writes the api docs page and data for the given module to the correct location. * * @param page The page to write. */ async function writePage(page: RawApiDocsPage): Promise { try { await writePageMarkdown(page); await writePageData(page); } catch (error) { throw new Error(`Error writing page ${page.title}`, { cause: error }); } } /** * Writes the api docs page for the given module to the correct location. * * @param page The page to write. */ async function writePageMarkdown(page: RawApiDocsPage): Promise { const { title, camelTitle, deprecated, description, examples, methods } = page; // Write api docs page let content = ` # ${title} ::: v-pre ${ deprecated == null ? '' : `

Deprecated

This module is deprecated and will be removed in a future version.

${deprecated}
` } ${adjustUrls(description)} ${examples.length === 0 ? '' : `
${codeToHtml(examples.join('\n'))}
`} ::: ${methods .map( (method) => ` ## ${method.name} ` ) .join('')} `.replaceAll(/\n +/g, '\n'); content = vitePressInFileOptions + (await formatMarkdown(content)); writeFileSync(resolve(FILE_PATH_API_DOCS, `${camelTitle}.md`), content); } /** * Writes the api docs data for the given module to correct location. * * @param page The page to write. */ async function writePageData(page: RawApiDocsPage): Promise { const { camelTitle, methods } = page; const pageData: Record = Object.fromEntries( await Promise.all( methods.map(async (method) => [method.name, await toMethodData(method)]) ) ); const refreshFunctions: Record = Object.fromEntries( await Promise.all( methods.map(async (method) => [ method.name, await toRefreshFunction(method), ]) ) ); const content = `export default ${JSON.stringify(pageData, undefined, 2)}`.replaceAll( /"refresh-([^"-]+)-placeholder"/g, (_, name) => refreshFunctions[name] ); writeFileSync( resolve(FILE_PATH_API_DOCS, `${camelTitle}.ts`), await formatTypescript(content) ); } const defaultCommentRegex = /\s+Defaults to `([^`]+)`\..*/; async function toMethodData(method: RawApiDocsMethod): Promise { const { name, signatures, source } = method; const signatureData = required(signatures.at(-1), 'method signature'); const { deprecated, description, since, parameters, remarks, returns, throws, signature, examples, seeAlsos, } = signatureData; const { filePath, line } = source; let formattedSignature = await formatTypescript(signature); formattedSignature = formattedSignature.trim(); // eslint-disable-next-line @typescript-eslint/require-await const refresh = async () => ['refresh', name, 'placeholder']; // This is a placeholder to be replaced by the actual refresh function code // If we put the actual code here, it would be a string and not executable refresh.toJSON = () => `refresh-${name}-placeholder`; /* Target order, omitted to improve diff to old files return { name, deprecated: mdToHtml(deprecated), description: mdToHtml(description), remark: remarks.length === 0 ? undefined : mdToHtml(remarks.join('\n')), since, parameters: parameters.map((param) => ({ ...param, type: param.type.text, default: param.default ?? defaultCommentRegex.exec(param.description)?.[1], description: mdToHtml(param.description.replace(defaultCommentRegex, '')), })), returns: returns.text, throws: throws.length === 0 ? undefined : mdToHtml(throws.join('\n'), true), // signature: codeToHtml(signature), examples: codeToHtml([signature, ...examples].join('\n')), seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)), sourcePath: sourcePath.replace(/:(\d+):\d+/g, '#L$1'), }; */ return { name, description: mdToHtml(description), remark: remarks.length === 0 ? undefined : mdToHtml(remarks.join('\n')), parameters: parameters.map((param) => ({ ...param, type: param.type.text, default: param.default ?? extractSummaryDefault(param.description), description: mdToHtml(param.description.replace(defaultCommentRegex, '')), })), since, sourcePath: `${filePath}#L${line}`, throws: throws.length === 0 ? undefined : mdToHtml(throws.join('\n'), true), returns: returns.text, signature: codeToHtml(formattedSignature), examples: codeToHtml(examples.join('\n')), refresh, deprecated: mdToHtml(deprecated), seeAlsos: seeAlsos.map((seeAlso) => mdToHtml(seeAlso, true)), }; } export function extractSummaryDefault(description: string): string | undefined { return defaultCommentRegex.exec(description)?.[1]; } export async function toRefreshFunction( method: RawApiDocsMethod ): Promise { const { name, signatures } = method; const signatureData = required(signatures.at(-1), 'method signature'); const { examples } = signatureData; const exampleCode = examples.join('\n'); if (!/^\w*faker\w*\./im.test(exampleCode)) { // No recordable faker calls in examples return 'undefined'; } const exampleLines = exampleCode .replaceAll(/ ?\/\/.*$/gm, '') // Remove comments .replaceAll(/^import .*$/gm, '') // Remove imports .replaceAll( // record results of faker calls /^(\w*faker\w*\..+(?:(?:.|\n..)*\n[^ ])?\)(?:\.\w+)?);?$/gim, `try { result.push($1); } catch (error: unknown) { result.push(error instanceof Error ? error.name : 'Error'); }\n` ); const fullMethod = `async (): Promise => { await enableFaker(); faker.seed(); faker.setDefaultRefDate(); const result: unknown[] = []; ${exampleLines} return result; }`; try { const formattedMethod = await formatTypescript(fullMethod); return formattedMethod.replace(/;\s+$/, ''); // Remove trailing semicolon } catch (error: unknown) { console.error( 'Failed to format refresh function for', name, fullMethod, error ); return 'undefined'; } }