fix: path
This commit is contained in:
parent
8d7f745df2
commit
b5fbdc17d2
|
|
@ -2,29 +2,56 @@ 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 文件,生成索引
|
||||||
*/
|
*/
|
||||||
function scanDirectory(dir: string): ContentIndex {
|
function scanDirectory(dir: string): ContentIndex {
|
||||||
const index: ContentIndex = {};
|
const index: ContentIndex = {};
|
||||||
|
|
||||||
function scan(currentPath: string, relativePath: string) {
|
function scan(currentPath: string, relativePath: string) {
|
||||||
const entries = readdirSync(currentPath);
|
const entries = readdirSync(currentPath);
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
||||||
|
|
||||||
const fullPath = join(currentPath, entry);
|
const fullPath = join(currentPath, entry);
|
||||||
const relPath = relativePath ? join(relativePath, entry) : entry;
|
const relPath = relativePath ? join(relativePath, entry) : entry;
|
||||||
const normalizedRelPath = '/' + relPath.split(sep).join('/');
|
const normalizedRelPath = '/' + relPath.split(sep).join('/');
|
||||||
|
|
||||||
const stats = statSync(fullPath);
|
const stats = statSync(fullPath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
scan(fullPath, relPath);
|
scan(fullPath, relPath);
|
||||||
|
|
@ -38,32 +65,102 @@ function scanDirectory(dir: string): ContentIndex {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
scan(dir, '');
|
scan(dir, '');
|
||||||
return index;
|
return index;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取文件扩展名对应的 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';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -73,12 +170,12 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
const contentDir = resolve(dir);
|
const contentDir = resolve(dir);
|
||||||
const distDir = resolve(process.cwd(), 'dist/web');
|
const distDir = resolve(process.cwd(), 'dist/web');
|
||||||
let contentIndex: ContentIndex = {};
|
let contentIndex: ContentIndex = {};
|
||||||
|
|
||||||
// 扫描内容目录生成索引
|
// 扫描内容目录生成索引
|
||||||
console.log('扫描内容目录...');
|
console.log('扫描内容目录...');
|
||||||
contentIndex = scanDirectory(contentDir);
|
contentIndex = scanDirectory(contentDir);
|
||||||
console.log(`已索引 ${Object.keys(contentIndex).length} 个文件`);
|
console.log(`已索引 ${Object.keys(contentIndex).length} 个文件`);
|
||||||
|
|
||||||
// 监听文件变化
|
// 监听文件变化
|
||||||
console.log('监听文件变化...');
|
console.log('监听文件变化...');
|
||||||
const watcher = watch(contentDir, {
|
const watcher = watch(contentDir, {
|
||||||
|
|
@ -86,7 +183,7 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
persistent: true,
|
persistent: true,
|
||||||
ignoreInitial: true,
|
ignoreInitial: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
watcher
|
watcher
|
||||||
.on('add', (path) => {
|
.on('add', (path) => {
|
||||||
if (path.endsWith('.md') || path.endsWith('.csv')) {
|
if (path.endsWith('.md') || path.endsWith('.csv')) {
|
||||||
|
|
@ -119,69 +216,19 @@ export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
console.log(`[删除] ${relPath}`);
|
console.log(`[删除] ${relPath}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 创建请求处理器
|
||||||
|
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);
|
||||||
|
|
||||||
server.listen(port, () => {
|
server.listen(port, () => {
|
||||||
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
||||||
console.log(`内容目录:${contentDir}`);
|
console.log(`内容目录:${contentDir}`);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue