134 lines
3.6 KiB
TypeScript
134 lines
3.6 KiB
TypeScript
import { existsSync, readFileSync } from "node:fs";
|
|
import { resolve } from "node:path";
|
|
import type { Plugin } from "vite";
|
|
|
|
/** 虚拟 stub 模块标识。 */
|
|
const STUB_ID = "\0timi-web-ssr-stub";
|
|
|
|
/** 被 stub 的模块说明符。 */
|
|
const STUBBED_MODULES = new Set(["timi-web", "timi-web/editor"]);
|
|
|
|
/**
|
|
* SSR 阶段必须存在的具名导出。
|
|
*
|
|
* 取自 `src` 与 `docs` 中所有 `timi-web` / `timi-web/editor` 值导入的并集,
|
|
* 是 stub 的保底名单;若构建产物可读,`collectExportNames` 会在此基础上补全全部导出。
|
|
*/
|
|
const REQUIRED_EXPORTS = [
|
|
// ---------- timi-web ----------
|
|
"VPopup",
|
|
"Popup",
|
|
"PopupType",
|
|
"Copyright",
|
|
"MarkdownView",
|
|
"MarkdownEditor",
|
|
"Toolkit",
|
|
"WebToolkit",
|
|
|
|
// ---------- timi-web/editor ----------
|
|
"findTextMatches",
|
|
"replaceAllText",
|
|
"replaceMatchText",
|
|
"buildSearchRegex",
|
|
"highlightCode",
|
|
"resolveCodeEditorKeydown",
|
|
"resolvePrismLanguage",
|
|
"useTiptapEditor",
|
|
"useTiptapView",
|
|
"headingMeta",
|
|
"ATTACHMENT_ID_ATTR",
|
|
"TEMP_FILE_ID_ATTR",
|
|
"LOCAL_MEDIA_ID_PREFIX",
|
|
"isLocalMediaId",
|
|
"normalizeContent",
|
|
"supportedCodeLanguages",
|
|
"TABLE_PICKER_SIZE"
|
|
];
|
|
|
|
/** 从构建产物中提取具名导出,让 stub 自动跟上上游变化。 */
|
|
function collectExportNames(distFile: string): Set<string> {
|
|
const names = new Set<string>(REQUIRED_EXPORTS);
|
|
if (!existsSync(distFile)) {
|
|
return names;
|
|
}
|
|
try {
|
|
const source = readFileSync(distFile, "utf-8");
|
|
for (const match of source.matchAll(/export\s*\{([^}]*)\}/g)) {
|
|
for (const part of match[1].split(",")) {
|
|
const bits = part.trim().split(/\s+as\s+/);
|
|
const name = (bits[1] ?? bits[0]).trim();
|
|
if (/^[A-Za-z_$][\w$]*$/.test(name)) {
|
|
names.add(name);
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// 读取失败时退回保底名单,不影响构建。
|
|
}
|
|
return names;
|
|
}
|
|
|
|
/** 生成 stub 模块源码:每个导出都是可调用、可解构、可当插件安装的空实现。 */
|
|
function buildStubSource(names: Set<string>): string {
|
|
const named = [...names]
|
|
.filter(name => name !== "default")
|
|
.map(name => `export const ${name} = stub;`)
|
|
.join("\n");
|
|
return `const stub = new Proxy(function () {}, {
|
|
get(target, key) {
|
|
if (key === Symbol.toPrimitive || key === "valueOf" || key === "toString") {
|
|
return () => "";
|
|
}
|
|
return stub;
|
|
},
|
|
apply() {
|
|
return stub;
|
|
},
|
|
construct() {
|
|
return {};
|
|
}
|
|
});
|
|
export default stub;
|
|
${named}
|
|
`;
|
|
}
|
|
|
|
/**
|
|
* 在 SSR 阶段把 timi-web 替换为空实现。
|
|
*
|
|
* timi-web 的构建产物在模块顶层就访问 `window` 并启动 Prism 自动高亮定时器,
|
|
* 在 Node 中 import 会直接抛错,导致 VitePress 的 SSR 构建必然失败。
|
|
*
|
|
* 文档站里真正需要 timi-web 的只有演示组件,它们全部通过 `clientOnly` 延迟到浏览器渲染,
|
|
* SSR 阶段只需要「能 import 且不报错」,因此这里返回一个 Proxy 空实现。
|
|
* 客户端构建不受影响,`options.ssr` 为 false 时照常解析到真实模块。
|
|
* @param docsDir docs 目录绝对路径,用于定位 timi-web 构建产物
|
|
*/
|
|
export function timiWebSsrStub(docsDir: string): Plugin {
|
|
const distFile = resolve(docsDir, "node_modules/timi-web/dist/timi-web.mjs");
|
|
let stubSource: string | null = null;
|
|
|
|
return {
|
|
name: "timi-web-ssr-stub",
|
|
enforce: "pre",
|
|
|
|
resolveId(source, importer, options) {
|
|
// 只拦截精确的包名,`timi-web/style.css` 等子路径照常交给 Vite 处理。
|
|
if (options?.ssr && STUBBED_MODULES.has(source)) {
|
|
return STUB_ID;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
load(id) {
|
|
if (id !== STUB_ID) {
|
|
return null;
|
|
}
|
|
if (stubSource === null) {
|
|
stubSource = buildStubSource(collectExportNames(distFile));
|
|
}
|
|
return stubSource;
|
|
}
|
|
};
|
|
}
|