import { promises as fs } from 'fs'; import * as path from 'path'; import { HttpError } from './http-helpers'; const MIME_TYPES_BY_EXTENSION: Record = { '.css': 'text/css; charset=utf-8', '.gif': 'image/gif', '.html': 'text/html; charset=utf-8', '.ico': 'image/x-icon', '.jpeg': 'image/jpeg', '.jpg': 'image/jpeg', '.js': 'application/javascript; charset=utf-8', '.json': 'application/json; charset=utf-8', '.map': 'application/json; charset=utf-8', '.png': 'image/png', '.svg': 'image/svg+xml; charset=utf-8', '.txt': 'text/plain; charset=utf-8', '.webp': 'image/webp', '.woff': 'font/woff', '.woff2': 'font/woff2' }; function getDocsRootCandidates(): string[] { const processWithResources = process as NodeJS.Process & { resourcesPath?: string }; const candidates: string[] = []; if (processWithResources.resourcesPath) { candidates.push(path.join(processWithResources.resourcesPath, 'docusaurus')); } candidates.push(path.join(process.cwd(), 'docs-site', 'build')); return candidates; } async function getDocusaurusBuildRoot(): Promise { for (const candidate of getDocsRootCandidates()) { try { const stat = await fs.stat(candidate); if (stat.isDirectory()) { return candidate; } } catch { // try next candidate } } return null; } function isPathInside(parent: string, child: string): boolean { const relative = path.relative(parent, child); return relative === '' || (!!relative && !relative.startsWith('..') && !path.isAbsolute(relative)); } function resolveAssetPath(root: string, pathname: string): string { const withoutPrefix = pathname.replace(/^\/docusaurus\/?/u, ''); const decoded = decodeURIComponent(withoutPrefix); const normalized = decoded.endsWith('/') || decoded === '' ? path.join(decoded, 'index.html') : decoded; const absolutePath = path.resolve(root, normalized); if (!isPathInside(root, absolutePath)) { throw new HttpError(400, 'Invalid Docusaurus asset path', 'INVALID_DOCS_PATH'); } return absolutePath; } export async function resolveDocusaurusRoute(pathname: string): Promise<{ filePath: string; contentType: string }> { const root = await getDocusaurusBuildRoot(); if (!root) { throw new HttpError(503, 'Docusaurus build is not available. Run npm run build:docs before opening the docs endpoint.', 'DOCUSAURUS_BUILD_MISSING'); } let filePath = resolveAssetPath(root, pathname); try { const stat = await fs.stat(filePath); if (stat.isDirectory()) { filePath = path.join(filePath, 'index.html'); } } catch { const directoryIndexPath = path.join(filePath, 'index.html'); try { await fs.access(directoryIndexPath); filePath = directoryIndexPath; } catch { filePath = path.join(root, '404.html'); } } if (!isPathInside(root, filePath)) { throw new HttpError(400, 'Invalid Docusaurus asset path', 'INVALID_DOCS_PATH'); } const contentType = MIME_TYPES_BY_EXTENSION[path.extname(filePath).toLowerCase()] ?? 'application/octet-stream'; return { filePath, contentType }; }