109 lines
3.1 KiB
TypeScript
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 };
|
|
}
|