fix: path

This commit is contained in:
hypercross 2026-02-26 13:37:10 +08:00
parent 8d7f745df2
commit b5fbdc17d2
1 changed files with 135 additions and 88 deletions

View File

@ -2,13 +2,40 @@ import type { ServeCommandHandler } from '../types.js';
import { createServer, Server, IncomingMessage, ServerResponse } from 'http'; import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
import { readdirSync, statSync, readFileSync, existsSync } from 'fs'; import { readdirSync, statSync, readFileSync, existsSync } from 'fs';
import { createReadStream } from 'fs'; import { createReadStream } from 'fs';
import { join, resolve, extname, normalize, sep, relative } from 'path'; import { join, resolve, extname, sep, relative } from 'path';
import { watch } from 'chokidar'; import { watch } from 'chokidar';
interface ContentIndex { interface ContentIndex {
[path: string]: string; [path: string]: string;
} }
/**
* MIME
*/
const MIME_TYPES: Record<string, string> = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'application/javascript',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.jpeg': 'image/jpeg',
'.gif': 'image/gif',
'.svg': 'image/svg+xml',
'.md': 'text/markdown',
'.csv': 'text/csv',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
};
/**
* MIME
*/
function getMimeType(filePath: string): string {
const ext = extname(filePath).toLowerCase();
return MIME_TYPES[ext] || 'application/octet-stream';
}
/** /**
* .md .csv * .md .csv
*/ */
@ -44,26 +71,96 @@ function scanDirectory(dir: string): ContentIndex {
} }
/** /**
* MIME *
*/ */
function getMimeType(filePath: string): string { function sendFile(res: ServerResponse, filePath: string) {
const ext = extname(filePath).toLowerCase(); res.writeHead(200, {
const mimeTypes: Record<string, string> = { 'Content-Type': getMimeType(filePath),
'.html': 'text/html', 'Access-Control-Allow-Origin': '*',
'.css': 'text/css', });
'.js': 'application/javascript', createReadStream(filePath).pipe(res);
'.json': 'application/json', }
'.png': 'image/png',
'.jpg': 'image/jpeg', /**
'.jpeg': 'image/jpeg', * 404
'.gif': 'image/gif', */
'.svg': 'image/svg+xml', function send404(res: ServerResponse) {
'.md': 'text/markdown', res.writeHead(404, { 'Content-Type': 'text/plain' });
'.csv': 'text/csv', res.end('Not Found');
'.woff': 'font/woff', }
'.woff2': 'font/woff2',
/**
* JSON
*/
function sendJson(res: ServerResponse, data: unknown) {
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
});
res.end(JSON.stringify(data, null, 2));
}
/**
*
* @returns true
*/
function tryServeStatic(res: ServerResponse, filePath: string, dir: string): boolean {
const fullPath = join(dir, filePath);
if (!existsSync(fullPath)) {
return false;
}
const stats = statSync(fullPath);
if (!stats.isFile()) {
return false;
}
sendFile(res, fullPath);
return true;
}
/**
*
*/
function createRequestHandler(
contentDir: string,
distDir: string,
getIndex: () => ContentIndex,
) {
return (req: IncomingMessage, res: ServerResponse) => {
const url = req.url || '/';
const filePath = decodeURIComponent(url.split('?')[0]);
// 1. 处理 /__CONTENT_INDEX.json
if (filePath === '/__CONTENT_INDEX.json') {
sendJson(res, getIndex());
return;
}
// 2. 处理 /static/ 目录(从 dist/web
if (filePath.startsWith('/static/')) {
if (tryServeStatic(res, filePath, distDir)) {
return;
}
send404(res);
return;
}
// 3. 处理 /content/ 目录
const relativePath = filePath.slice(1); // 去掉开头的 /
if (tryServeStatic(res, relativePath, contentDir)) {
return;
}
// 4. 处理 SPA 路由:返回 index.html
if (tryServeStatic(res, 'index.html', distDir)) {
return;
}
// 5. 404
send404(res);
}; };
return mimeTypes[ext] || 'application/octet-stream';
} }
/** /**
@ -120,65 +217,15 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
} }
}); });
// 创建请求处理器
const handleRequest = createRequestHandler(
contentDir,
distDir,
() => contentIndex,
);
// 创建 HTTP 服务器 // 创建 HTTP 服务器
const server: Server = createServer((req: IncomingMessage, res: ServerResponse) => { const server = createServer(handleRequest);
const url = req.url || '/';
// 处理 /__CONTENT_INDEX.json 请求
if (url === '/__CONTENT_INDEX.json') {
res.writeHead(200, {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
});
res.end(JSON.stringify(contentIndex, null, 2));
return;
}
// 解析 URL 路径
let filePath = decodeURIComponent(url.split('?')[0]);
// 处理 /content/ 路径
if (filePath.startsWith('/content/')) {
const relativePath = filePath.slice(1); // 去掉开头的 /
const fullPath = join(contentDir, relativePath);
if (existsSync(fullPath)) {
const stats = statSync(fullPath);
if (stats.isFile()) {
res.writeHead(200, {
'Content-Type': getMimeType(fullPath),
'Access-Control-Allow-Origin': '*'
});
createReadStream(fullPath).pipe(res);
return;
}
}
}
// 处理静态资源(从 dist/web
let staticPath = join(distDir, filePath === '/' ? 'index.html' : filePath);
// 如果文件不存在,回退到 index.html
if (!existsSync(staticPath)) {
staticPath = join(distDir, 'index.html');
}
if (existsSync(staticPath)) {
const stats = statSync(staticPath);
if (stats.isFile()) {
res.writeHead(200, {
'Content-Type': getMimeType(staticPath),
'Access-Control-Allow-Origin': '*'
});
createReadStream(staticPath).pipe(res);
return;
}
}
// 404
res.writeHead(404, { 'Content-Type': 'text/plain' });
res.end('Not Found');
});
const port = parseInt(options.port, 10); const port = parseInt(options.port, 10);