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); } }); } }; }