Files
ONE-OS/axhub-make/vite-plugins/virtualHtml/index.ts
王冕 a27e3b8e43 feat: sync full workspace including web modules, docs, and configurations to Gitea
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>
2026-06-09 18:12:25 +08:00

208 lines
7.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
}
});
}
};
}