Files
Toju/electron/api/docusaurus-static.ts

109 lines
3.1 KiB
TypeScript

import { promises as fs } from 'fs';
import * as path from 'path';
import { HttpError } from './http-helpers';
const MIME_TYPES_BY_EXTENSION: Record<string, string> = {
'.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<string | null> {
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 };
}