Optimized the root .gitignore to exclude virtual environments, node modules, and temp folders to ensure clean and lightweight version tracking. Co-authored-by: Cursor <cursoragent@cursor.com>
610 lines
23 KiB
TypeScript
610 lines
23 KiB
TypeScript
import type { Plugin } from 'vite';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
import {
|
|
createManualDocTemplate,
|
|
getDocsDir,
|
|
getTemplatesDir,
|
|
isTemplateDocName,
|
|
isProtectedDocName,
|
|
safeDecodeURIComponent,
|
|
sanitizeDocBaseName,
|
|
scanDocReferences,
|
|
} from './utils/docUtils';
|
|
import { getRequestPathname, readJsonBody } from './utils/httpUtils';
|
|
|
|
type DocAction = 'rename' | 'delete';
|
|
|
|
function getProtectedDocPayload(action: DocAction) {
|
|
return {
|
|
error: action === 'rename'
|
|
? '项目总览入口文档禁止改名,可继续编辑内容'
|
|
: '项目总览入口文档禁止删除',
|
|
code: 'PROTECTED_DOC',
|
|
protected: true,
|
|
references: [],
|
|
hasReferences: false,
|
|
};
|
|
}
|
|
|
|
function getReferencedDocPayload(action: DocAction, references: string[]) {
|
|
return {
|
|
error: action === 'rename'
|
|
? '文档存在项目内引用,请先处理引用后再改名'
|
|
: '文档存在项目内引用,请先处理引用后再删除',
|
|
code: 'DOC_REFERENCED',
|
|
protected: false,
|
|
references,
|
|
hasReferences: references.length > 0,
|
|
};
|
|
}
|
|
|
|
function normalizeRenameBaseNameForPath(docPath: string, nextBaseName: string) {
|
|
const ext = path.extname(docPath);
|
|
let normalizedBaseName = String(nextBaseName || '').trim();
|
|
if (ext && normalizedBaseName.toLowerCase().endsWith(ext.toLowerCase())) {
|
|
normalizedBaseName = normalizedBaseName.slice(0, -ext.length).trim();
|
|
}
|
|
return {
|
|
ext,
|
|
safeBaseName: sanitizeDocBaseName(normalizedBaseName),
|
|
};
|
|
}
|
|
|
|
export function docsApiPlugin(): Plugin {
|
|
return {
|
|
name: 'docs-api-plugin',
|
|
configureServer(server: any) {
|
|
server.middlewares.use(async (req: any, res: any, next: any) => {
|
|
const pathname = getRequestPathname(req);
|
|
if (!pathname.startsWith('/api/docs')) {
|
|
return next();
|
|
}
|
|
if (pathname === '/api/docs/templates' || pathname.startsWith('/api/docs/templates/')) {
|
|
return next();
|
|
}
|
|
const docsDir = getDocsDir(process.cwd());
|
|
const encodedDocName = pathname.startsWith('/api/docs/')
|
|
? pathname.slice('/api/docs/'.length)
|
|
: '';
|
|
const decodedDocName = safeDecodeURIComponent(encodedDocName);
|
|
const isCompatTemplateRequest = Boolean(encodedDocName) && isTemplateDocName(decodedDocName);
|
|
|
|
if (isCompatTemplateRequest && (req.method === 'GET' || req.method === 'PUT')) {
|
|
try {
|
|
const templateName = decodedDocName.slice('templates/'.length);
|
|
if (!templateName) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Missing template name' }));
|
|
return;
|
|
}
|
|
|
|
const templatesDir = getTemplatesDir(process.cwd());
|
|
const templatePath = path.join(templatesDir, templateName);
|
|
if (!templatePath.startsWith(templatesDir)) {
|
|
res.statusCode = 403;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
|
|
if (!fs.existsSync(templatePath)) {
|
|
res.statusCode = 404;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Template not found' }));
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'GET') {
|
|
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
|
|
res.end(fs.readFileSync(templatePath, 'utf8'));
|
|
return;
|
|
}
|
|
|
|
const bodyData = await readJsonBody(req);
|
|
if (typeof bodyData?.content !== 'string') {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Missing content parameter' }));
|
|
return;
|
|
}
|
|
|
|
fs.writeFileSync(templatePath, String(bodyData.content), 'utf8');
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ success: true, name: templateName }));
|
|
return;
|
|
} catch (error: any) {
|
|
console.error('Error handling encoded template doc path:', error);
|
|
res.statusCode = 500;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: error?.message || 'Template request failed' }));
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (req.method === 'POST' && (pathname === '/api/docs/check-references' || pathname === '/api/docs/check-references/')) {
|
|
try {
|
|
const body = await readJsonBody(req);
|
|
const docName = String(body?.docName || '').trim();
|
|
const action = body?.action === 'rename' ? 'rename' : body?.action === 'delete' ? 'delete' : '';
|
|
const nextBaseName = String(body?.nextBaseName || '').trim();
|
|
|
|
if (!docName) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Missing docName parameter' }));
|
|
return;
|
|
}
|
|
if (isTemplateDocName(docName)) {
|
|
res.statusCode = 403;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Templates must be managed via /api/docs/templates' }));
|
|
return;
|
|
}
|
|
|
|
if (action !== 'rename' && action !== 'delete') {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Invalid action parameter' }));
|
|
return;
|
|
}
|
|
|
|
const docPath = path.join(docsDir, docName);
|
|
if (!docPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
if (!fs.existsSync(docPath)) {
|
|
res.statusCode = 404;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Document not found' }));
|
|
return;
|
|
}
|
|
|
|
let hasActualRename = action === 'delete';
|
|
if (action === 'rename') {
|
|
if (!nextBaseName) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Missing nextBaseName parameter' }));
|
|
return;
|
|
}
|
|
|
|
if (/[/\\:*?"<>|]/.test(nextBaseName)) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Invalid nextBaseName format' }));
|
|
return;
|
|
}
|
|
|
|
const { ext, safeBaseName } = normalizeRenameBaseNameForPath(docPath, nextBaseName);
|
|
if (!safeBaseName) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Invalid nextBaseName format' }));
|
|
return;
|
|
}
|
|
|
|
const nextPath = path.join(path.dirname(docPath), `${safeBaseName}${ext}`);
|
|
hasActualRename = nextPath !== docPath;
|
|
}
|
|
|
|
if (hasActualRename && isProtectedDocName(docName)) {
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({
|
|
docName,
|
|
...getProtectedDocPayload(action),
|
|
}));
|
|
return;
|
|
}
|
|
|
|
const references = hasActualRename ? scanDocReferences(docName) : [];
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({
|
|
docName,
|
|
references,
|
|
hasReferences: references.length > 0,
|
|
protected: false,
|
|
...(references.length > 0 ? { code: 'DOC_REFERENCED' } : {}),
|
|
}));
|
|
} catch (error: any) {
|
|
console.error('Error checking doc references:', error);
|
|
res.statusCode = 500;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: error?.message || 'Check doc references failed' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (
|
|
req.method === 'POST' &&
|
|
(pathname === '/api/docs/manual-create' || pathname === '/api/docs/manual-create/' || pathname === '/manual-create')
|
|
) {
|
|
try {
|
|
const body = await readJsonBody(req);
|
|
const displayName = String(body?.displayName || '').trim();
|
|
const fileNameInput = String(body?.fileName || body?.displayName || '').trim();
|
|
|
|
if (!displayName) {
|
|
res.statusCode = 400;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Missing displayName' }));
|
|
return;
|
|
}
|
|
|
|
fs.mkdirSync(docsDir, { recursive: true });
|
|
|
|
const fallbackBase = `doc-${Date.now().toString(36)}`;
|
|
const sanitizedBase = sanitizeDocBaseName(fileNameInput || displayName) || fallbackBase;
|
|
let baseName = sanitizedBase;
|
|
let suffix = 2;
|
|
while (fs.existsSync(path.join(docsDir, `${baseName}.md`))) {
|
|
baseName = `${sanitizedBase}-${suffix}`;
|
|
suffix += 1;
|
|
}
|
|
|
|
const docFileName = `${baseName}.md`;
|
|
const docPath = path.join(docsDir, docFileName);
|
|
|
|
if (!docPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
|
|
fs.writeFileSync(docPath, createManualDocTemplate(displayName), 'utf8');
|
|
|
|
res.statusCode = 201;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
name: docFileName,
|
|
displayName,
|
|
path: `src/docs/${docFileName}`,
|
|
}));
|
|
} catch (error: any) {
|
|
console.error('Error manual creating doc:', error);
|
|
res.statusCode = 500;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: error?.message || 'Create doc failed' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'POST' && pathname.startsWith('/api/docs/') && pathname.endsWith('/copy')) {
|
|
try {
|
|
const encodedDocName = pathname.slice('/api/docs/'.length, -'/copy'.length);
|
|
const docName = decodeURIComponent(encodedDocName);
|
|
if (!docName) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: 'Missing document name' }));
|
|
return;
|
|
}
|
|
if (isTemplateDocName(docName)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Templates must be managed via /api/docs/templates' }));
|
|
return;
|
|
}
|
|
|
|
const sourcePath = path.join(docsDir, docName);
|
|
if (!sourcePath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
if (!fs.existsSync(sourcePath)) {
|
|
res.statusCode = 404;
|
|
res.end(JSON.stringify({ error: 'Document not found' }));
|
|
return;
|
|
}
|
|
|
|
const sourceDir = path.dirname(sourcePath);
|
|
const ext = path.extname(sourcePath);
|
|
const sourceBaseName = path.basename(sourcePath, ext);
|
|
const safeBaseName = sanitizeDocBaseName(sourceBaseName) || sourceBaseName;
|
|
const candidateBase = `${safeBaseName}-copy`;
|
|
|
|
let nextBaseName = candidateBase;
|
|
let suffix = 2;
|
|
let nextName = `${nextBaseName}${ext}`;
|
|
let nextPath = path.join(sourceDir, nextName);
|
|
while (fs.existsSync(nextPath)) {
|
|
nextBaseName = `${candidateBase}${suffix}`;
|
|
nextName = `${nextBaseName}${ext}`;
|
|
nextPath = path.join(sourceDir, nextName);
|
|
suffix += 1;
|
|
}
|
|
|
|
if (!nextPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
|
|
fs.copyFileSync(sourcePath, nextPath);
|
|
|
|
const relativeName = path.relative(docsDir, nextPath).split(path.sep).join('/');
|
|
const relativeDisplayName = relativeName.replace(/\.[^./\\]+$/u, '');
|
|
|
|
res.statusCode = 201;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({
|
|
success: true,
|
|
name: relativeName,
|
|
displayName: relativeDisplayName,
|
|
path: `src/docs/${relativeName}`,
|
|
}));
|
|
} catch (error: any) {
|
|
console.error('Error copying doc:', error);
|
|
res.statusCode = 500;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify({ error: error?.message || 'Copy doc failed' }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'DELETE' && pathname.startsWith('/api/docs/')) {
|
|
try {
|
|
const encodedDocName = pathname.replace('/api/docs/', '');
|
|
const docName = decodeURIComponent(encodedDocName);
|
|
if (!docName) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: 'Missing document name' }));
|
|
return;
|
|
}
|
|
if (isTemplateDocName(docName)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Templates must be managed via /api/docs/templates' }));
|
|
return;
|
|
}
|
|
const docPath = path.join(docsDir, docName);
|
|
|
|
if (!docPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
|
|
if (!fs.existsSync(docPath)) {
|
|
res.statusCode = 404;
|
|
res.end(JSON.stringify({ error: 'Document not found' }));
|
|
return;
|
|
}
|
|
|
|
if (isProtectedDocName(docName)) {
|
|
res.statusCode = 409;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify(getProtectedDocPayload('delete')));
|
|
return;
|
|
}
|
|
|
|
const references = scanDocReferences(docName);
|
|
if (references.length > 0) {
|
|
res.statusCode = 409;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify(getReferencedDocPayload('delete', references)));
|
|
return;
|
|
}
|
|
|
|
fs.unlinkSync(docPath);
|
|
res.statusCode = 200;
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ success: true }));
|
|
} catch (error: any) {
|
|
console.error('Error deleting doc:', error);
|
|
res.statusCode = 500;
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (req.method === 'PUT' && pathname.startsWith('/api/docs/')) {
|
|
try {
|
|
const encodedDocName = pathname.replace('/api/docs/', '');
|
|
if (!encodedDocName) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: 'Missing document name' }));
|
|
return;
|
|
}
|
|
|
|
const bodyData = await readJsonBody(req);
|
|
const hasContentUpdate = typeof bodyData?.content === 'string';
|
|
let newBaseName = String(bodyData?.newBaseName || '').trim();
|
|
const hasRename = Boolean(newBaseName);
|
|
if (!hasContentUpdate && !hasRename) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: 'Missing content or newBaseName parameter' }));
|
|
return;
|
|
}
|
|
if (hasRename && /[/\\:*?"<>|]/.test(newBaseName)) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: 'Invalid newBaseName format' }));
|
|
return;
|
|
}
|
|
|
|
const docName = decodeURIComponent(encodedDocName);
|
|
if (isTemplateDocName(docName)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Templates must be managed via /api/docs/templates' }));
|
|
return;
|
|
}
|
|
const oldPath = path.join(docsDir, docName);
|
|
if (!oldPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
if (!fs.existsSync(oldPath)) {
|
|
res.statusCode = 404;
|
|
res.end(JSON.stringify({ error: 'Document not found' }));
|
|
return;
|
|
}
|
|
|
|
let finalPath = oldPath;
|
|
if (hasRename) {
|
|
const { ext, safeBaseName } = normalizeRenameBaseNameForPath(oldPath, newBaseName);
|
|
if (!safeBaseName) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: 'Invalid newBaseName format' }));
|
|
return;
|
|
}
|
|
|
|
const oldDir = path.dirname(oldPath);
|
|
const newFileName = `${safeBaseName}${ext}`;
|
|
const newPath = path.join(oldDir, newFileName);
|
|
|
|
if (!newPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
if (newPath !== oldPath && fs.existsSync(newPath)) {
|
|
res.statusCode = 400;
|
|
res.end(JSON.stringify({ error: '目标文件已存在' }));
|
|
return;
|
|
}
|
|
|
|
if (newPath !== oldPath) {
|
|
if (isProtectedDocName(docName)) {
|
|
res.statusCode = 409;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify(getProtectedDocPayload('rename')));
|
|
return;
|
|
}
|
|
|
|
const references = scanDocReferences(docName);
|
|
if (references.length > 0) {
|
|
res.statusCode = 409;
|
|
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
res.end(JSON.stringify(getReferencedDocPayload('rename', references)));
|
|
return;
|
|
}
|
|
|
|
fs.renameSync(oldPath, newPath);
|
|
}
|
|
finalPath = newPath;
|
|
}
|
|
|
|
if (hasContentUpdate) {
|
|
fs.writeFileSync(finalPath, String(bodyData.content), 'utf8');
|
|
}
|
|
|
|
const relativeName = path.relative(docsDir, finalPath).split(path.sep).join('/');
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify({ success: true, name: relativeName }));
|
|
} catch (error: any) {
|
|
console.error('Error updating doc:', error);
|
|
res.statusCode = 500;
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (req.method !== 'GET') {
|
|
return next();
|
|
}
|
|
|
|
if (pathname.startsWith('/api/docs/') && pathname !== '/api/docs' && pathname !== '/api/docs/') {
|
|
try {
|
|
const encodedDocName = pathname.replace('/api/docs/', '');
|
|
if (!encodedDocName) {
|
|
return next();
|
|
}
|
|
|
|
const docName = decodeURIComponent(encodedDocName);
|
|
if (isTemplateDocName(docName)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Templates must be managed via /api/docs/templates' }));
|
|
return;
|
|
}
|
|
const docPath = path.join(docsDir, docName);
|
|
|
|
if (!docPath.startsWith(docsDir)) {
|
|
res.statusCode = 403;
|
|
res.end(JSON.stringify({ error: 'Forbidden' }));
|
|
return;
|
|
}
|
|
|
|
if (fs.existsSync(docPath)) {
|
|
const content = fs.readFileSync(docPath, 'utf8');
|
|
const ext = path.extname(docPath);
|
|
const contentTypeMap: Record<string, string> = {
|
|
'.md': 'text/markdown; charset=utf-8',
|
|
'.csv': 'text/csv; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.yaml': 'text/yaml; charset=utf-8',
|
|
'.yml': 'text/yaml; charset=utf-8',
|
|
'.txt': 'text/plain; charset=utf-8',
|
|
};
|
|
res.setHeader('Content-Type', contentTypeMap[ext] || 'text/plain; charset=utf-8');
|
|
res.end(content);
|
|
} else {
|
|
res.statusCode = 404;
|
|
res.end(JSON.stringify({ error: 'Document not found' }));
|
|
}
|
|
} catch (error: any) {
|
|
console.error('Error loading doc:', error);
|
|
res.statusCode = 500;
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (pathname === '/api/docs' || pathname === '/api/docs/') {
|
|
try {
|
|
const docs: any[] = [];
|
|
const supportedExtensions = ['.md', '.csv', '.json', '.yaml', '.yml', '.txt'];
|
|
|
|
if (fs.existsSync(docsDir)) {
|
|
const walkDocsDir = (dirPath: string) => {
|
|
const items = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
items.forEach((item) => {
|
|
const fullPath = path.join(dirPath, item.name);
|
|
if (item.isDirectory()) {
|
|
walkDocsDir(fullPath);
|
|
return;
|
|
}
|
|
if (!item.isFile()) {
|
|
return;
|
|
}
|
|
const ext = path.extname(item.name).toLowerCase();
|
|
if (!supportedExtensions.includes(ext)) {
|
|
return;
|
|
}
|
|
const relativePath = path.relative(docsDir, fullPath).split(path.sep).join('/');
|
|
if (isTemplateDocName(relativePath)) {
|
|
return;
|
|
}
|
|
docs.push({
|
|
name: relativePath,
|
|
displayName: relativePath,
|
|
});
|
|
});
|
|
};
|
|
walkDocsDir(docsDir);
|
|
}
|
|
|
|
docs.sort((a, b) => String(a.name || '').localeCompare(String(b.name || '')));
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.end(JSON.stringify(docs));
|
|
} catch (error: any) {
|
|
console.error('Error loading docs:', error);
|
|
res.statusCode = 500;
|
|
res.end(JSON.stringify({ error: error.message }));
|
|
}
|
|
return;
|
|
}
|
|
|
|
next();
|
|
});
|
|
},
|
|
};
|
|
}
|