import fs from 'node:fs'; import path from 'node:path'; import { pathToFileURL } from 'node:url'; type Page = { source: string; target: string; }; type RouteRewrite = { from: string; to: string; }; type DocsConfig = { mounts: Page[]; routeRewrites?: RouteRewrite[]; }; type ProjectConfig = { slug: string; docsDir: string; }; const MD_LINK_RE = /\]\((?!#|[a-z][a-z0-9+.-]*:|\/\/)([^)\s]+)(#[^)]*)?\)/gi; function normalizePath(value: string) { return value.split(path.sep).join('/').replace(/^\.\//, ''); } function formatRelativeMarkdownPath(fromTarget: string, toTarget: string) { const relative = path.relative(path.dirname(fromTarget), toTarget).split(path.sep).join('/'); return relative.startsWith('.') ? relative : `./${relative}`; } async function loadDocsConfig(configPath: string) { return (await import(`${pathToFileURL(configPath).href}?t=${Date.now()}`)) as DocsConfig; } export async function prepareDocs(projectDir: string, config: ProjectConfig) { const docsDir = path.join(projectDir, config.docsDir); const contentDir = path.join(docsDir, 'content'); const docsConfig = await loadDocsConfig(path.join(docsDir, 'docs.config.ts')); const targetBySource = new Map( docsConfig.mounts.map((page) => [normalizePath(path.resolve(projectDir, page.source)), normalizePath(page.target)]), ); const routeRewrites = [...(docsConfig.routeRewrites ?? [])].sort((a, b) => b.from.length - a.from.length); function applyRouteRewrites(route: string) { for (const rewrite of routeRewrites) { if (route === rewrite.from || route.startsWith(`${rewrite.from}/`) || route.startsWith(`${rewrite.from}#`)) { return `${rewrite.to}${route.slice(rewrite.from.length)}`; } } return undefined; } function formatDocsRoute(route: string) { const rewritten = applyRouteRewrites(route); if (rewritten) return rewritten; if (route === '/docs') return '/'; if (route.startsWith('/docs/')) return route.slice('/docs'.length); return undefined; } function formatRelativeRoute(hrefPath: string, sourceDir: string) { const sourcePath = normalizePath(path.relative(projectDir, path.resolve(sourceDir, hrefPath))); if (sourcePath.startsWith('canons/')) return formatDocsRoute(`/docs/${sourcePath.slice('canons/'.length)}`); return undefined; } function transformMarkdownLinks(content: string, page: Page) { const sourceDir = path.dirname(path.resolve(projectDir, page.source)); return content.replace(MD_LINK_RE, (match, href: string, hash = '') => { const [hrefPath, query = ''] = href.split('?'); const queryPart = query ? `?${query}` : ''; if (hrefPath.startsWith('/')) { const route = formatDocsRoute(hrefPath) ?? applyRouteRewrites(hrefPath); return route ? `](${route}${queryPart}${hash})` : match; } if (!hrefPath.endsWith('.md')) { const route = formatRelativeRoute(hrefPath, sourceDir); return route ? `](${route}${queryPart}${hash})` : match; } const target = targetBySource.get(normalizePath(path.resolve(sourceDir, hrefPath))); if (!target) return match; return `](${formatRelativeMarkdownPath(page.target, `${target}${queryPart}`)}${hash})`; }); } fs.rmSync(contentDir, { recursive: true, force: true }); fs.mkdirSync(contentDir, { recursive: true }); for (const page of docsConfig.mounts) { const sourcePath = path.resolve(projectDir, page.source); const targetPath = path.join(contentDir, page.target); if (!fs.existsSync(sourcePath)) throw new Error(`Не найден канон: ${sourcePath}`); fs.mkdirSync(path.dirname(targetPath), { recursive: true }); fs.writeFileSync(targetPath, transformMarkdownLinks(fs.readFileSync(sourcePath, 'utf8'), page), 'utf8'); console.log(`${page.target} -> ${path.relative(projectDir, sourcePath)}`); } console.log(`Подготовлен VitePress content для ${config.slug}: ${docsConfig.mounts.length} страниц`); }