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>
208 lines
7.0 KiB
TypeScript
208 lines
7.0 KiB
TypeScript
import type { Plugin } from 'vite';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import { handleHackCssRequest } from './handlers/hackCssHandler';
|
||
import { handleHackCssSave } from './handlers/hackCssSaveHandler';
|
||
import { handleHackCssClear } from './handlers/hackCssClearHandler';
|
||
import { handleEntriesApi } from './handlers/entriesApiHandler';
|
||
import { handleSpecHtml } from './handlers/specHtmlHandler';
|
||
import { handleIndexHtml } from './handlers/indexHtmlHandler';
|
||
import { handleAssetsRequest } from './handlers/assetsHandler';
|
||
import { handleDocImageAssets } from './handlers/docImageAssetsHandler';
|
||
import { handleBuildRequest } from './handlers/buildHandler';
|
||
import { handleDocsMarkdown } from './handlers/docsMarkdownHandler';
|
||
import { handleTextReplaceCount } from './handlers/textReplaceCountHandler';
|
||
import { handleTextReplace } from './handlers/textReplaceHandler';
|
||
import { handlePathRedirect } from './handlers/pathNormalizer';
|
||
import {
|
||
createDocUpdatePayload,
|
||
createHackCssUpdatePayload,
|
||
createPreviewHostModuleCode,
|
||
parsePreviewHostModuleId,
|
||
PREVIEW_HOST_MODULE_PREFIX,
|
||
} from './previewHost';
|
||
|
||
/**
|
||
* 虚拟 HTML 插件 - 在内存中生成 HTML,不写入文件系统
|
||
*/
|
||
export function virtualHtmlPlugin(): Plugin {
|
||
const devTemplatePath = path.resolve(process.cwd(), 'admin/dev-template.html');
|
||
const specTemplatePath = path.resolve(process.cwd(), 'admin/spec-template.html');
|
||
const htmlTemplatePath = path.resolve(process.cwd(), 'admin/html-template.html');
|
||
let devTemplate: string;
|
||
let specTemplate: string;
|
||
let htmlTemplate: string;
|
||
|
||
return {
|
||
name: 'virtual-html',
|
||
apply: 'serve',
|
||
|
||
resolveId(id) {
|
||
if (id.startsWith(PREVIEW_HOST_MODULE_PREFIX)) {
|
||
return `\0${id}`;
|
||
}
|
||
return null;
|
||
},
|
||
|
||
load(id) {
|
||
if (!id.startsWith(`\0${PREVIEW_HOST_MODULE_PREFIX}`)) {
|
||
return null;
|
||
}
|
||
|
||
const options = parsePreviewHostModuleId(id.slice(1));
|
||
if (!options) {
|
||
throw new Error(`Invalid preview host module id: ${id}`);
|
||
}
|
||
|
||
return createPreviewHostModuleCode(options);
|
||
},
|
||
|
||
handleHotUpdate(ctx) {
|
||
const payload = createDocUpdatePayload(ctx.file, 'change');
|
||
if (!payload) {
|
||
return;
|
||
}
|
||
|
||
ctx.server.ws.send({
|
||
type: 'custom',
|
||
event: 'axhub:spec-doc-update',
|
||
data: payload,
|
||
});
|
||
|
||
// src 下的 markdown 文档不再走 Vite 默认的全局 full-reload,
|
||
// 只让对应的 spec/doc 页面自行按 URL 维度刷新内容。
|
||
return [];
|
||
},
|
||
|
||
async configureServer(server) {
|
||
try {
|
||
devTemplate = fs.readFileSync(devTemplatePath, 'utf8');
|
||
} catch (err) {
|
||
console.error('无法读取 dev-template 模板文件:', devTemplatePath);
|
||
}
|
||
|
||
try {
|
||
specTemplate = fs.readFileSync(specTemplatePath, 'utf8');
|
||
} catch (err) {
|
||
console.error('无法读取 spec-template 模板文件:', specTemplatePath);
|
||
}
|
||
|
||
try {
|
||
htmlTemplate = fs.readFileSync(htmlTemplatePath, 'utf8');
|
||
} catch (err) {
|
||
console.error('无法读取 html-template 模板文件:', htmlTemplatePath);
|
||
}
|
||
|
||
const broadcastHackCssUpdate = (filePath: string, changeType: 'add' | 'change' | 'unlink') => {
|
||
const payload = createHackCssUpdatePayload(filePath, changeType);
|
||
if (!payload) {
|
||
return;
|
||
}
|
||
|
||
server.ws.send({
|
||
type: 'custom',
|
||
event: 'axhub:hack-css-update',
|
||
data: payload,
|
||
});
|
||
};
|
||
|
||
server.watcher.on('add', (filePath) => broadcastHackCssUpdate(filePath, 'add'));
|
||
server.watcher.on('change', (filePath) => broadcastHackCssUpdate(filePath, 'change'));
|
||
server.watcher.on('unlink', (filePath) => broadcastHackCssUpdate(filePath, 'unlink'));
|
||
|
||
server.middlewares.use(async (req, res, next) => {
|
||
try {
|
||
// CORS headers
|
||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||
|
||
if (req.method === 'OPTIONS') {
|
||
res.statusCode = 204;
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
if (!req.url) {
|
||
return next();
|
||
}
|
||
|
||
const respondHtml = async (html: string, transformUrl?: string) => {
|
||
const htmlUrl = transformUrl || req.url || '/index.html';
|
||
const transformedHtml = await server.transformIndexHtml(htmlUrl, html, req.originalUrl || req.url || htmlUrl);
|
||
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
||
res.statusCode = 200;
|
||
res.end(transformedHtml);
|
||
};
|
||
|
||
// 🔥 处理旧路径重定向(必须在最前面)
|
||
if (handlePathRedirect(req, res)) return;
|
||
|
||
// Handle hack.css GET request
|
||
if (handleHackCssRequest(req, res)) return;
|
||
|
||
// Handle hack.css save POST request
|
||
if (handleHackCssSave(req, res)) return;
|
||
|
||
// Handle hack.css clear POST request
|
||
if (handleHackCssClear(req, res)) return;
|
||
|
||
// Handle text replace count POST request
|
||
if (handleTextReplaceCount(req, res)) return;
|
||
|
||
// Handle text replace POST request
|
||
if (handleTextReplace(req, res)) return;
|
||
|
||
// Handle root path
|
||
if (req.url === '/' || req.url === '/index.html') {
|
||
const indexHtmlPath = path.resolve(process.cwd(), 'admin/index.html');
|
||
if (fs.existsSync(indexHtmlPath)) {
|
||
try {
|
||
const html = fs.readFileSync(indexHtmlPath, 'utf8');
|
||
res.setHeader('Content-Type', 'text/html');
|
||
res.statusCode = 200;
|
||
res.end(html);
|
||
return;
|
||
} catch (err) {
|
||
console.error('读取 index.html 失败:', err);
|
||
}
|
||
}
|
||
}
|
||
|
||
// Handle assets
|
||
if (handleAssetsRequest(req, res)) return;
|
||
|
||
// Handle build requests
|
||
if (handleBuildRequest(req, res)) return;
|
||
|
||
// Handle entries API
|
||
if (handleEntriesApi(req, res)) return;
|
||
|
||
// Handle markdown-relative document images (assets/images/*)
|
||
if (handleDocImageAssets(req, res)) return;
|
||
|
||
// Handle docs markdown files
|
||
if (handleDocsMarkdown(req, res)) return;
|
||
|
||
// Handle spec.html
|
||
if (await handleSpecHtml(req, res, specTemplate, respondHtml)) return;
|
||
|
||
// Handle index.html
|
||
if (req.url?.includes('/themes/') && req.url?.includes('/index.html')) {
|
||
try {
|
||
htmlTemplate = fs.readFileSync(htmlTemplatePath, 'utf8');
|
||
} catch (err) {
|
||
console.error('无法读取 html-template 模板文件:', htmlTemplatePath);
|
||
}
|
||
}
|
||
if (await handleIndexHtml(req, res, devTemplate, htmlTemplate, respondHtml)) return;
|
||
|
||
next();
|
||
} catch (error) {
|
||
next(error);
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|