feat: ai's attempt
This commit is contained in:
parent
990888a204
commit
8d7f745df2
|
|
@ -10,6 +10,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/router": "^0.15.0",
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"csv-parse": "^5.5.6",
|
"csv-parse": "^5.5.6",
|
||||||
"marked": "^14.1.0",
|
"marked": "^14.1.0",
|
||||||
|
|
@ -2376,6 +2377,21 @@
|
||||||
],
|
],
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
|
"node_modules/chokidar": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"readdirp": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "12.1.0",
|
"version": "12.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
|
||||||
|
|
@ -3116,6 +3132,19 @@
|
||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/readdirp": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20.19.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "individual",
|
||||||
|
"url": "https://paulmillr.com/funding/"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reduce-configs": {
|
"node_modules/reduce-configs": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/reduce-configs/-/reduce-configs-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/reduce-configs/-/reduce-configs-1.1.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@solidjs/router": "^0.15.0",
|
"@solidjs/router": "^0.15.0",
|
||||||
|
"chokidar": "^5.0.0",
|
||||||
"commander": "^12.1.0",
|
"commander": "^12.1.0",
|
||||||
"csv-parse": "^5.5.6",
|
"csv-parse": "^5.5.6",
|
||||||
"marked": "^14.1.0",
|
"marked": "^14.1.0",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import type { CommandHandler } from '../types.js';
|
import type { CompileCommandHandler } from '../types.js';
|
||||||
|
|
||||||
export const compileCommand: CommandHandler = async (dir, options) => {
|
export const compileCommand: CompileCommandHandler = async (dir, options) => {
|
||||||
console.log(`开始编译...`);
|
console.log(`开始编译...`);
|
||||||
console.log(`目录:${dir}`);
|
console.log(`目录:${dir}`);
|
||||||
console.log(`输出目录:${options.output}`);
|
console.log(`输出目录:${options.output}`);
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,190 @@
|
||||||
import type { CommandHandler } from '../types.js';
|
import type { ServeCommandHandler } from '../types.js';
|
||||||
|
import { createServer, Server, IncomingMessage, ServerResponse } from 'http';
|
||||||
|
import { readdirSync, statSync, readFileSync, existsSync } from 'fs';
|
||||||
|
import { createReadStream } from 'fs';
|
||||||
|
import { join, resolve, extname, normalize, sep, relative } from 'path';
|
||||||
|
import { watch } from 'chokidar';
|
||||||
|
|
||||||
export const serveCommand: CommandHandler = async (dir, options) => {
|
interface ContentIndex {
|
||||||
console.log(`启动开发服务器...`);
|
[path: string]: string;
|
||||||
console.log(`目录:${dir}`);
|
}
|
||||||
console.log(`端口:${options.port}`);
|
|
||||||
|
/**
|
||||||
|
* 扫描目录内的 .md 和 .csv 文件,生成索引
|
||||||
|
*/
|
||||||
|
function scanDirectory(dir: string): ContentIndex {
|
||||||
|
const index: ContentIndex = {};
|
||||||
|
|
||||||
// TODO: 实现开发服务器逻辑
|
function scan(currentPath: string, relativePath: string) {
|
||||||
// 1. 扫描目录下的所有 .md 文件
|
const entries = readdirSync(currentPath);
|
||||||
// 2. 启动 rsbuild 开发服务器
|
|
||||||
// 3. 监听文件变化
|
for (const entry of entries) {
|
||||||
|
if (entry.startsWith('.') || entry === 'node_modules') continue;
|
||||||
|
|
||||||
|
const fullPath = join(currentPath, entry);
|
||||||
|
const relPath = relativePath ? join(relativePath, entry) : entry;
|
||||||
|
const normalizedRelPath = '/' + relPath.split(sep).join('/');
|
||||||
|
|
||||||
|
const stats = statSync(fullPath);
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
scan(fullPath, relPath);
|
||||||
|
} else if (entry.endsWith('.md') || entry.endsWith('.csv')) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(fullPath, 'utf-8');
|
||||||
|
index[normalizedRelPath] = content;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`读取文件失败:${fullPath}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
console.log('开发服务器已启动:http://localhost:' + options.port);
|
scan(dir, '');
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文件扩展名对应的 MIME 类型
|
||||||
|
*/
|
||||||
|
function getMimeType(filePath: string): string {
|
||||||
|
const ext = extname(filePath).toLowerCase();
|
||||||
|
const mimeTypes: 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',
|
||||||
|
};
|
||||||
|
return mimeTypes[ext] || 'application/octet-stream';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 启动开发服务器
|
||||||
|
*/
|
||||||
|
export const serveCommand: ServeCommandHandler = async (dir, options) => {
|
||||||
|
const contentDir = resolve(dir);
|
||||||
|
const distDir = resolve(process.cwd(), 'dist/web');
|
||||||
|
let contentIndex: ContentIndex = {};
|
||||||
|
|
||||||
|
// 扫描内容目录生成索引
|
||||||
|
console.log('扫描内容目录...');
|
||||||
|
contentIndex = scanDirectory(contentDir);
|
||||||
|
console.log(`已索引 ${Object.keys(contentIndex).length} 个文件`);
|
||||||
|
|
||||||
|
// 监听文件变化
|
||||||
|
console.log('监听文件变化...');
|
||||||
|
const watcher = watch(contentDir, {
|
||||||
|
ignored: /(^|[\/\\])\../,
|
||||||
|
persistent: true,
|
||||||
|
ignoreInitial: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
watcher
|
||||||
|
.on('add', (path) => {
|
||||||
|
if (path.endsWith('.md') || path.endsWith('.csv')) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(path, 'utf-8');
|
||||||
|
const relPath = '/' + relative(contentDir, path).split(sep).join('/');
|
||||||
|
contentIndex[relPath] = content;
|
||||||
|
console.log(`[新增] ${relPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`读取新增文件失败:${path}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('change', (path) => {
|
||||||
|
if (path.endsWith('.md') || path.endsWith('.csv')) {
|
||||||
|
try {
|
||||||
|
const content = readFileSync(path, 'utf-8');
|
||||||
|
const relPath = '/' + relative(contentDir, path).split(sep).join('/');
|
||||||
|
contentIndex[relPath] = content;
|
||||||
|
console.log(`[更新] ${relPath}`);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`读取更新文件失败:${path}`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.on('unlink', (path) => {
|
||||||
|
if (path.endsWith('.md') || path.endsWith('.csv')) {
|
||||||
|
const relPath = '/' + relative(contentDir, path).split(sep).join('/');
|
||||||
|
delete contentIndex[relPath];
|
||||||
|
console.log(`[删除] ${relPath}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 创建 HTTP 服务器
|
||||||
|
const server: Server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||||
|
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);
|
||||||
|
|
||||||
|
server.listen(port, () => {
|
||||||
|
console.log(`\n开发服务器已启动:http://localhost:${port}`);
|
||||||
|
console.log(`内容目录:${contentDir}`);
|
||||||
|
console.log(`索引文件:http://localhost:${port}/__CONTENT_INDEX.json\n`);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
import { Command } from 'commander';
|
import { Command } from 'commander';
|
||||||
import { serveCommand } from './commands/serve.js';
|
import { serveCommand } from './commands/serve.js';
|
||||||
import { compileCommand } from './commands/compile.js';
|
import { compileCommand } from './commands/compile.js';
|
||||||
|
import type { ServeOptions, CompileOptions } from './types.js';
|
||||||
|
|
||||||
const program = new Command();
|
const program = new Command();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,8 @@ export interface CompileOptions {
|
||||||
output: string;
|
output: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CommandHandler = (dir: string, options: ServeOptions | CompileOptions) => Promise<void>;
|
export type ServeCommandHandler = (dir: string, options: ServeOptions) => Promise<void>;
|
||||||
|
export type CompileCommandHandler = (dir: string, options: CompileOptions) => Promise<void>;
|
||||||
|
|
||||||
export interface MarkdownFile {
|
export interface MarkdownFile {
|
||||||
path: string;
|
path: string;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
let dataIndex: Record<string, string> | null = null;
|
let dataIndex: Record<string, string> | null = null;
|
||||||
let isDevIndexLoaded = false;
|
let isDevIndexLoaded = false;
|
||||||
|
let isCliIndexLoaded = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置全局数据索引(CLI 环境使用)
|
* 设置全局数据索引(CLI 环境使用)
|
||||||
|
|
@ -42,13 +43,35 @@ async function loadDevIndex(): Promise<void> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在 CLI 环境从 /__CONTENT_INDEX.json 加载索引
|
||||||
|
*/
|
||||||
|
async function loadCliIndex(): Promise<void> {
|
||||||
|
if (isCliIndexLoaded) return;
|
||||||
|
isCliIndexLoaded = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/__CONTENT_INDEX.json');
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch content index');
|
||||||
|
}
|
||||||
|
const index = await response.json();
|
||||||
|
dataIndex = { ...dataIndex, ...index };
|
||||||
|
} catch (e) {
|
||||||
|
// CLI 索引不可用时忽略(可能不在 CLI 环境)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 异步加载数据
|
* 异步加载数据
|
||||||
* @param path 数据路径
|
* @param path 数据路径
|
||||||
* @returns 数据内容
|
* @returns 数据内容
|
||||||
*/
|
*/
|
||||||
export async function fetchData(path: string): Promise<string> {
|
export async function fetchData(path: string): Promise<string> {
|
||||||
// dev 环境:先加载 glob 索引
|
// CLI 环境:先从 /__CONTENT_INDEX.json 加载索引
|
||||||
|
await loadCliIndex();
|
||||||
|
|
||||||
|
// dev 环境:加载 glob 索引
|
||||||
await loadDevIndex();
|
await loadDevIndex();
|
||||||
|
|
||||||
// 首先尝试从索引获取
|
// 首先尝试从索引获取
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue