Files
timi-tdesign-mobile/docs/.vitepress/plugins/docgen.ts
T
2026-09-21 16:05:14 +08:00

161 lines
4.8 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 { existsSync } from "node:fs";
import { resolve } from "node:path";
import { parse } from "vue-docgen-api";
import type { Plugin } from "vite";
import { COMPONENT_LIST } from "../components";
import {
createEmptyApi,
type ApiEventRow,
type ApiExposeRow,
type ApiPropRow,
type ApiSlotRow,
type ComponentApi,
type ComponentApiMap
} from "../api-types";
/** 虚拟模块的引用标识,业务代码与类型声明共用。 */
export const API_MODULE_ID = "virtual:timi-api";
/** Vite 内部使用的解析后标识,前置 `\0` 避免被其他插件处理。 */
const RESOLVED_ID = `\0${API_MODULE_ID}`;
/**
* 把 vue-docgen-api 的联合类型描述压平成可读文本。
* 不同版本对 `defineEmits` 的产出结构不一致,这里统一兜底。
*/
function normalizeType(type: unknown): string {
if (!type || typeof type !== "object") {
return "-";
}
const candidate = type as { name?: unknown; names?: unknown };
if (Array.isArray(candidate.names)) {
return candidate.names.length > 0 ? candidate.names.join(" | ") : "-";
}
if (typeof candidate.name === "string" && candidate.name.length > 0) {
return candidate.name;
}
return "-";
}
/** 转换 prop 描述符。 */
function toPropRow(descriptor: any): ApiPropRow {
return {
name: String(descriptor.name ?? ""),
type: normalizeType(descriptor.type),
defaultValue: descriptor.defaultValue?.value === undefined ? "-" : String(descriptor.defaultValue.value),
required: descriptor.required === true,
description: String(descriptor.description ?? "")
};
}
/** 转换事件描述符。 */
function toEventRow(descriptor: any): ApiEventRow {
return {
name: String(descriptor.name ?? ""),
type: normalizeType(descriptor.type),
description: String(descriptor.description ?? "")
};
}
/** 转换插槽描述符,把作用域参数并入说明列。 */
function toSlotRow(descriptor: any): ApiSlotRow {
const bindings = Array.isArray(descriptor.bindings)
? descriptor.bindings.map((item: any) => String(item.name ?? "")).filter(Boolean)
: [];
const description = String(descriptor.description ?? "");
const scope = bindings.length > 0 ? `作用域参数:${bindings.join("、")}` : "";
return {
name: String(descriptor.name ?? ""),
description: [description, scope].filter(Boolean).join(";")
};
}
/** 转换暴露描述符。 */
function toExposeRow(descriptor: any): ApiExposeRow {
return {
name: String(descriptor.name ?? ""),
type: normalizeType(descriptor.type),
description: String(descriptor.description ?? "")
};
}
/** 把单个组件文档规范化为纯数据。 */
function toComponentApi(name: string, doc: any): ComponentApi {
return {
name,
description: String(doc?.description ?? ""),
parsed: true,
props: Array.isArray(doc?.props) ? doc.props.map(toPropRow) : [],
events: Array.isArray(doc?.events) ? doc.events.map(toEventRow) : [],
slots: Array.isArray(doc?.slots) ? doc.slots.map(toSlotRow) : [],
exposes: Array.isArray(doc?.expose) ? doc.expose.map(toExposeRow) : []
};
}
/**
* 逐个解析组件源码。
* 单个组件解析失败只降级为空 API,不让整个文档站因一处语法而无法构建。
*/
async function parseAll(repoRoot: string): Promise<ComponentApiMap> {
const result: ComponentApiMap = {};
for (const meta of COMPONENT_LIST) {
const filePath = resolve(repoRoot, meta.entry);
if (!existsSync(filePath)) {
console.warn(`[timi-docgen] 未找到组件入口,已跳过:${meta.entry}`);
result[meta.name] = createEmptyApi(meta.name);
continue;
}
try {
const doc = await parse(filePath);
result[meta.name] = toComponentApi(meta.name, doc);
} catch (error) {
console.warn(`[timi-docgen] 解析 ${meta.name} 失败,已降级为空 API:`, error);
result[meta.name] = createEmptyApi(meta.name);
}
}
return result;
}
/**
* 提供 `virtual:timi-api` 虚拟模块,内容为全部组件的 API 数据。
*
* 解析结果按需缓存,仅在组件源码变更时失效,避免每次页面请求都重复解析。
* @param repoRoot 仓库根目录的绝对路径
*/
export function docgenPlugin(repoRoot: string): Plugin {
const entryPathSet = new Set(COMPONENT_LIST.map(meta => resolve(repoRoot, meta.entry)));
let cache: ComponentApiMap | null = null;
return {
name: "timi-docgen",
resolveId(id: string) {
return id === API_MODULE_ID ? RESOLVED_ID : null;
},
async load(id: string) {
if (id !== RESOLVED_ID) {
return null;
}
if (cache === null) {
cache = await parseAll(repoRoot);
}
return `export default ${JSON.stringify(cache)};`;
},
handleHotUpdate({ file, server }) {
if (!entryPathSet.has(file)) {
return;
}
cache = null;
const invalidated = server.moduleGraph.getModuleById(RESOLVED_ID);
if (invalidated) {
server.moduleGraph.invalidateModule(invalidated);
server.ws.send({ type: "full-reload" });
}
}
};
}