refactor: перенести сборку в проекты
- перенесены каноны и VitePress-конфиги в projects/<slug> - добавлены корневой и проектные build.ts для сборки артефактов - добавлены shared-библиотеки сборки в projects/_shared/lib - обновлены CI, Dockerfile, package.json, gitignore и README - удалена сборка frontend-агента
This commit is contained in:
112
projects/_shared/lib/prepare-docs.ts
Normal file
112
projects/_shared/lib/prepare-docs.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
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} страниц`);
|
||||
}
|
||||
Reference in New Issue
Block a user