@wsxjs/wsx-press 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/client.cjs +1 -0
- package/dist/client.js +1256 -0
- package/dist/index-ChO3PMD5.js +461 -0
- package/dist/index-uNJnOC7n.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.js +8 -0
- package/dist/node.cjs +47 -0
- package/dist/node.js +1708 -0
- package/package.json +90 -0
- package/src/client/components/DocLayout.css +49 -0
- package/src/client/components/DocLayout.wsx +92 -0
- package/src/client/components/DocPage.css +56 -0
- package/src/client/components/DocPage.wsx +480 -0
- package/src/client/components/DocSearch.css +113 -0
- package/src/client/components/DocSearch.wsx +328 -0
- package/src/client/components/DocSidebar.css +97 -0
- package/src/client/components/DocSidebar.wsx +173 -0
- package/src/client/components/DocTOC.css +105 -0
- package/src/client/components/DocTOC.wsx +262 -0
- package/src/client/index.ts +32 -0
- package/src/client/styles/code.css +242 -0
- package/src/client/styles/index.css +12 -0
- package/src/client/styles/reset.css +116 -0
- package/src/client/styles/theme.css +171 -0
- package/src/client/styles/typography.css +239 -0
- package/src/index.ts +26 -0
- package/src/node/index.ts +16 -0
- package/src/node/metadata.ts +113 -0
- package/src/node/plugin.ts +223 -0
- package/src/node/search.ts +53 -0
- package/src/node/toc.ts +148 -0
- package/src/node/typedoc.ts +96 -0
- package/src/types/wsx.d.ts +11 -0
- package/src/types.test.ts +118 -0
- package/src/types.ts +150 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wsxjs/wsx-press/node/plugin
|
|
3
|
+
* Vite plugin for WSX-Press documentation system
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Plugin } from "vite";
|
|
7
|
+
import path from "path";
|
|
8
|
+
import fs from "fs-extra";
|
|
9
|
+
import { createLogger } from "@wsxjs/wsx-logger";
|
|
10
|
+
import { scanDocsMetadata } from "./metadata";
|
|
11
|
+
import { generateSearchIndex } from "./search";
|
|
12
|
+
import { generateTOCCollection } from "./toc";
|
|
13
|
+
import { generateApiDocs, type TypeDocConfig } from "./typedoc";
|
|
14
|
+
|
|
15
|
+
const logger = createLogger("WSXPress");
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* WSX-Press 插件配置
|
|
19
|
+
*/
|
|
20
|
+
export interface WSXPressOptions {
|
|
21
|
+
/** 文档根目录 */
|
|
22
|
+
docsRoot: string;
|
|
23
|
+
/** API 文档配置(可选) */
|
|
24
|
+
api?: TypeDocConfig;
|
|
25
|
+
/** 输出目录(默认:.wsx-press) */
|
|
26
|
+
outputDir?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* WSX-Press Vite 插件
|
|
31
|
+
*
|
|
32
|
+
* @param options - 插件配置选项
|
|
33
|
+
* @returns Vite 插件实例
|
|
34
|
+
*/
|
|
35
|
+
export function wsxPress(options: WSXPressOptions): Plugin {
|
|
36
|
+
const { docsRoot, api, outputDir = ".wsx-press" } = options;
|
|
37
|
+
|
|
38
|
+
let absoluteOutputDir: string;
|
|
39
|
+
let absoluteDocsRoot: string;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
name: "vite-plugin-wsx-press",
|
|
43
|
+
enforce: "pre",
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Vite 配置解析后,解析绝对路径
|
|
47
|
+
*/
|
|
48
|
+
configResolved(config) {
|
|
49
|
+
// 在配置解析后解析绝对路径
|
|
50
|
+
// 使用简单的字符串检查,避免构建时 path 模块的问题
|
|
51
|
+
const isAbsolutePath = (p: string) => {
|
|
52
|
+
// Unix/Linux/Mac 绝对路径以 / 开头
|
|
53
|
+
// Windows 绝对路径以盘符开头(如 C:)或 \\ 开头
|
|
54
|
+
return p.startsWith("/") || /^[A-Za-z]:/.test(p) || p.startsWith("\\\\");
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
absoluteOutputDir = isAbsolutePath(outputDir)
|
|
58
|
+
? outputDir
|
|
59
|
+
: path.resolve(config.root, outputDir);
|
|
60
|
+
|
|
61
|
+
absoluteDocsRoot = isAbsolutePath(docsRoot)
|
|
62
|
+
? docsRoot
|
|
63
|
+
: path.resolve(config.root, docsRoot);
|
|
64
|
+
},
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 构建开始时生成元数据和搜索索引
|
|
68
|
+
*/
|
|
69
|
+
async buildStart() {
|
|
70
|
+
try {
|
|
71
|
+
// 如果路径还未解析(configResolved 未调用),使用默认值
|
|
72
|
+
if (!absoluteOutputDir || !absoluteDocsRoot) {
|
|
73
|
+
const isAbsolutePath = (p: string) => {
|
|
74
|
+
return p.startsWith("/") || /^[A-Za-z]:/.test(p) || p.startsWith("\\\\");
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
absoluteOutputDir = isAbsolutePath(outputDir)
|
|
78
|
+
? outputDir
|
|
79
|
+
: path.resolve(process.cwd(), outputDir);
|
|
80
|
+
|
|
81
|
+
absoluteDocsRoot = isAbsolutePath(docsRoot)
|
|
82
|
+
? docsRoot
|
|
83
|
+
: path.resolve(process.cwd(), docsRoot);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 确保输出目录存在
|
|
87
|
+
await fs.ensureDir(absoluteOutputDir);
|
|
88
|
+
|
|
89
|
+
logger.info("Starting documentation generation...");
|
|
90
|
+
logger.info(`Docs root: ${absoluteDocsRoot}`);
|
|
91
|
+
logger.info(`Output dir: ${absoluteOutputDir}`);
|
|
92
|
+
|
|
93
|
+
// 扫描文档元数据
|
|
94
|
+
const metadata = await scanDocsMetadata(absoluteDocsRoot);
|
|
95
|
+
const metadataPath = path.join(absoluteOutputDir, "docs-meta.json");
|
|
96
|
+
await fs.writeJSON(metadataPath, metadata, { spaces: 2 });
|
|
97
|
+
logger.info(
|
|
98
|
+
`✅ Generated docs-meta.json with ${Object.keys(metadata).length} documents`
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
// 生成搜索索引
|
|
102
|
+
const searchIndex = await generateSearchIndex(metadata, absoluteDocsRoot);
|
|
103
|
+
const searchIndexPath = path.join(absoluteOutputDir, "search-index.json");
|
|
104
|
+
await fs.writeJSON(searchIndexPath, searchIndex, { spaces: 2 });
|
|
105
|
+
logger.info("✅ Generated search-index.json");
|
|
106
|
+
|
|
107
|
+
// 生成 TOC 数据
|
|
108
|
+
logger.info("Generating TOC collection...");
|
|
109
|
+
try {
|
|
110
|
+
const tocCollection = await generateTOCCollection(metadata, absoluteDocsRoot);
|
|
111
|
+
const tocPath = path.join(absoluteOutputDir, "docs-toc.json");
|
|
112
|
+
await fs.writeJSON(tocPath, tocCollection, { spaces: 2 });
|
|
113
|
+
logger.info(
|
|
114
|
+
`✅ Generated docs-toc.json with ${Object.keys(tocCollection).length} documents`
|
|
115
|
+
);
|
|
116
|
+
} catch (tocError) {
|
|
117
|
+
logger.error("❌ Failed to generate TOC collection:", tocError);
|
|
118
|
+
// 不抛出错误,继续构建流程
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 如果配置了 API 文档,生成 API 文档
|
|
122
|
+
if (api) {
|
|
123
|
+
const apiOutputDir = api.outputDir || path.join(absoluteDocsRoot, "api");
|
|
124
|
+
await generateApiDocs({
|
|
125
|
+
...api,
|
|
126
|
+
outputDir: path.isAbsolute(apiOutputDir)
|
|
127
|
+
? apiOutputDir
|
|
128
|
+
: path.resolve(process.cwd(), apiOutputDir),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
} catch (error) {
|
|
132
|
+
logger.error("Failed to generate documentation:", error);
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* 配置开发服务器
|
|
139
|
+
* 在开发模式下通过 /.wsx-press 路径提供元数据和搜索索引
|
|
140
|
+
*/
|
|
141
|
+
configureServer(server) {
|
|
142
|
+
// 添加中间件,在开发模式下提供元数据和搜索索引
|
|
143
|
+
server.middlewares.use("/.wsx-press", async (req, res, next) => {
|
|
144
|
+
try {
|
|
145
|
+
const url = new URL(req.url || "/", `http://${req.headers.host}`);
|
|
146
|
+
// 当中间件挂载在 /.wsx-press 下时,url.pathname 会是 /.wsx-press/docs-meta.json
|
|
147
|
+
// 需要提取文件名部分(移除 /.wsx-press 前缀)
|
|
148
|
+
const fullPath = url.pathname;
|
|
149
|
+
// 移除 /.wsx-press 前缀,处理有/无尾部斜杠的情况
|
|
150
|
+
const fileName = fullPath.replace(/^\/\.wsx-press\/?/, "").replace(/^\//, "");
|
|
151
|
+
|
|
152
|
+
// 检查是否是支持的文件
|
|
153
|
+
if (
|
|
154
|
+
fileName === "docs-meta.json" ||
|
|
155
|
+
fileName === "search-index.json" ||
|
|
156
|
+
fileName === "docs-toc.json"
|
|
157
|
+
) {
|
|
158
|
+
const file = path.join(absoluteOutputDir, fileName);
|
|
159
|
+
if (await fs.pathExists(file)) {
|
|
160
|
+
const content = await fs.readFile(file, "utf-8");
|
|
161
|
+
res.setHeader("Content-Type", "application/json");
|
|
162
|
+
res.end(content);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 如果文件不存在,尝试重新生成
|
|
168
|
+
if (fileName === "docs-meta.json") {
|
|
169
|
+
const metadata = await scanDocsMetadata(absoluteDocsRoot);
|
|
170
|
+
await fs.ensureDir(absoluteOutputDir);
|
|
171
|
+
await fs.writeJSON(
|
|
172
|
+
path.join(absoluteOutputDir, "docs-meta.json"),
|
|
173
|
+
metadata,
|
|
174
|
+
{
|
|
175
|
+
spaces: 2,
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
res.setHeader("Content-Type", "application/json");
|
|
179
|
+
res.end(JSON.stringify(metadata, null, 2));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (fileName === "search-index.json") {
|
|
184
|
+
const metadata = await scanDocsMetadata(absoluteDocsRoot);
|
|
185
|
+
const searchIndex = await generateSearchIndex(metadata, absoluteDocsRoot);
|
|
186
|
+
await fs.ensureDir(absoluteOutputDir);
|
|
187
|
+
await fs.writeJSON(
|
|
188
|
+
path.join(absoluteOutputDir, "search-index.json"),
|
|
189
|
+
searchIndex,
|
|
190
|
+
{ spaces: 2 }
|
|
191
|
+
);
|
|
192
|
+
res.setHeader("Content-Type", "application/json");
|
|
193
|
+
res.end(JSON.stringify(searchIndex, null, 2));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (fileName === "docs-toc.json") {
|
|
198
|
+
const metadata = await scanDocsMetadata(absoluteDocsRoot);
|
|
199
|
+
const tocCollection = await generateTOCCollection(
|
|
200
|
+
metadata,
|
|
201
|
+
absoluteDocsRoot
|
|
202
|
+
);
|
|
203
|
+
await fs.ensureDir(absoluteOutputDir);
|
|
204
|
+
await fs.writeJSON(
|
|
205
|
+
path.join(absoluteOutputDir, "docs-toc.json"),
|
|
206
|
+
tocCollection,
|
|
207
|
+
{ spaces: 2 }
|
|
208
|
+
);
|
|
209
|
+
res.setHeader("Content-Type", "application/json");
|
|
210
|
+
res.end(JSON.stringify(tocCollection, null, 2));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
next();
|
|
215
|
+
} catch (error) {
|
|
216
|
+
logger.error("Middleware error:", error);
|
|
217
|
+
res.statusCode = 500;
|
|
218
|
+
res.end(JSON.stringify({ error: String(error) }));
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wsxjs/wsx-press/node/search
|
|
3
|
+
* Search index generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import type { DocsMetaCollection, SearchIndex, SearchDocument } from "../types";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 生成搜索索引
|
|
12
|
+
*
|
|
13
|
+
* @param metadata - 文档元数据集合
|
|
14
|
+
* @param docsRoot - 文档根目录路径
|
|
15
|
+
* @returns 搜索索引
|
|
16
|
+
*/
|
|
17
|
+
export async function generateSearchIndex(
|
|
18
|
+
metadata: DocsMetaCollection,
|
|
19
|
+
docsRoot: string
|
|
20
|
+
): Promise<SearchIndex> {
|
|
21
|
+
const documents: SearchDocument[] = [];
|
|
22
|
+
|
|
23
|
+
for (const [key, meta] of Object.entries(metadata)) {
|
|
24
|
+
const filePath = path.join(docsRoot, `${key}.md`);
|
|
25
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
26
|
+
const textContent = content
|
|
27
|
+
.replace(/^---[\s\S]*?---/, "") // 移除 frontmatter
|
|
28
|
+
.replace(/```[\s\S]*?```/g, "") // 移除代码块
|
|
29
|
+
.replace(/[#*`_[\]()]/g, "") // 移除 Markdown 标记
|
|
30
|
+
.trim();
|
|
31
|
+
|
|
32
|
+
documents.push({
|
|
33
|
+
id: key,
|
|
34
|
+
title: meta.title,
|
|
35
|
+
category: meta.category,
|
|
36
|
+
route: meta.route,
|
|
37
|
+
content: textContent.substring(0, 500),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
documents,
|
|
43
|
+
options: {
|
|
44
|
+
keys: [
|
|
45
|
+
{ name: "title", weight: 0.7 },
|
|
46
|
+
{ name: "content", weight: 0.3 },
|
|
47
|
+
],
|
|
48
|
+
threshold: 0.3,
|
|
49
|
+
includeScore: true,
|
|
50
|
+
includeMatches: true,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/node/toc.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wsxjs/wsx-press/node/toc
|
|
3
|
+
* Table of Contents generation from markdown files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import path from "path";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
import { marked, type Tokens } from "marked";
|
|
9
|
+
import type { Token } from "marked";
|
|
10
|
+
import type { DocsMetaCollection } from "../types";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* TOC 项接口
|
|
14
|
+
*/
|
|
15
|
+
export interface TOCItem {
|
|
16
|
+
/** 标题级别 (1-6) */
|
|
17
|
+
level: number;
|
|
18
|
+
/** 标题文本 */
|
|
19
|
+
text: string;
|
|
20
|
+
/** 锚点 ID */
|
|
21
|
+
id: string;
|
|
22
|
+
/** 子项 */
|
|
23
|
+
children: TOCItem[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* 文档 TOC 数据
|
|
28
|
+
*/
|
|
29
|
+
export interface DocTOC {
|
|
30
|
+
/** 文档路径(相对于 docsRoot) */
|
|
31
|
+
docPath: string;
|
|
32
|
+
/** TOC 项列表 */
|
|
33
|
+
items: TOCItem[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* TOC 数据集合(按文档路径索引)
|
|
38
|
+
*/
|
|
39
|
+
export type TOCCollection = Record<string, TOCItem[]>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 从 Markdown 内容中提取标题并生成 TOC
|
|
43
|
+
*
|
|
44
|
+
* @param markdown - Markdown 内容
|
|
45
|
+
* @returns TOC 项列表
|
|
46
|
+
*/
|
|
47
|
+
export function extractTOCFromMarkdown(markdown: string): TOCItem[] {
|
|
48
|
+
// 移除 frontmatter
|
|
49
|
+
const contentWithoutFrontmatter = markdown.replace(/^---[\s\S]*?---\n/, "");
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// 使用 marked 解析 markdown
|
|
53
|
+
const tokens = marked.lexer(contentWithoutFrontmatter);
|
|
54
|
+
const tocItems: TOCItem[] = [];
|
|
55
|
+
const stack: TOCItem[] = [];
|
|
56
|
+
|
|
57
|
+
// 遍历 tokens,提取标题
|
|
58
|
+
for (const token of tokens) {
|
|
59
|
+
if (token.type === "heading") {
|
|
60
|
+
const headingToken = token as Tokens.Heading;
|
|
61
|
+
const level = headingToken.depth;
|
|
62
|
+
const text = extractTextFromTokens(headingToken.tokens);
|
|
63
|
+
const id = generateId(text);
|
|
64
|
+
|
|
65
|
+
const item: TOCItem = {
|
|
66
|
+
level,
|
|
67
|
+
text,
|
|
68
|
+
id,
|
|
69
|
+
children: [],
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// 构建层级结构
|
|
73
|
+
while (stack.length > 0 && stack[stack.length - 1].level >= level) {
|
|
74
|
+
stack.pop();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (stack.length === 0) {
|
|
78
|
+
tocItems.push(item);
|
|
79
|
+
} else {
|
|
80
|
+
stack[stack.length - 1].children.push(item);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
stack.push(item);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return tocItems;
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.warn("Failed to extract TOC from markdown:", error);
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 从 tokens 中提取纯文本
|
|
96
|
+
*/
|
|
97
|
+
function extractTextFromTokens(tokens: Token[]): string {
|
|
98
|
+
return tokens
|
|
99
|
+
.map((token) => {
|
|
100
|
+
if (token.type === "text") {
|
|
101
|
+
return (token as Tokens.Text).text;
|
|
102
|
+
} else if (token.type === "strong" || token.type === "em") {
|
|
103
|
+
const inlineToken = token as Tokens.Generic;
|
|
104
|
+
if ("tokens" in inlineToken && Array.isArray(inlineToken.tokens)) {
|
|
105
|
+
return extractTextFromTokens(inlineToken.tokens);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return "";
|
|
109
|
+
})
|
|
110
|
+
.join("");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 生成锚点 ID
|
|
115
|
+
*/
|
|
116
|
+
function generateId(text: string): string {
|
|
117
|
+
return text
|
|
118
|
+
.toLowerCase()
|
|
119
|
+
.replace(/[^\w\s-]/g, "")
|
|
120
|
+
.replace(/\s+/g, "-")
|
|
121
|
+
.replace(/-+/g, "-")
|
|
122
|
+
.trim();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* 为所有文档生成 TOC 数据
|
|
127
|
+
*
|
|
128
|
+
* @param metadata - 文档元数据集合
|
|
129
|
+
* @param docsRoot - 文档根目录路径
|
|
130
|
+
* @returns TOC 数据集合
|
|
131
|
+
*/
|
|
132
|
+
export async function generateTOCCollection(
|
|
133
|
+
metadata: DocsMetaCollection,
|
|
134
|
+
docsRoot: string
|
|
135
|
+
): Promise<TOCCollection> {
|
|
136
|
+
const tocCollection: TOCCollection = {};
|
|
137
|
+
|
|
138
|
+
for (const [key, _meta] of Object.entries(metadata)) {
|
|
139
|
+
const filePath = path.join(docsRoot, `${key}.md`);
|
|
140
|
+
if (await fs.pathExists(filePath)) {
|
|
141
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
142
|
+
const tocItems = extractTOCFromMarkdown(content);
|
|
143
|
+
tocCollection[key] = tocItems;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return tocCollection;
|
|
148
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @wsxjs/wsx-press/node/typedoc
|
|
3
|
+
* TypeDoc API documentation generation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Application, TSConfigReader, TypeDocReader } from "typedoc";
|
|
7
|
+
import fs from "fs-extra";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* TypeDoc 配置选项
|
|
11
|
+
*/
|
|
12
|
+
export interface TypeDocConfig {
|
|
13
|
+
/** 入口点文件路径数组 */
|
|
14
|
+
entryPoints: string[];
|
|
15
|
+
/** TypeScript 配置文件路径 */
|
|
16
|
+
tsconfig: string;
|
|
17
|
+
/** 输出目录 */
|
|
18
|
+
outputDir: string;
|
|
19
|
+
/** 是否排除私有成员 */
|
|
20
|
+
excludePrivate?: boolean;
|
|
21
|
+
/** 是否排除受保护成员 */
|
|
22
|
+
excludeProtected?: boolean;
|
|
23
|
+
/** 是否排除内部成员 */
|
|
24
|
+
excludeInternal?: boolean;
|
|
25
|
+
/** 公共路径前缀 */
|
|
26
|
+
publicPath?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 生成 API 文档
|
|
31
|
+
*
|
|
32
|
+
* @param config - TypeDoc 配置
|
|
33
|
+
* @throws {Error} 如果生成失败
|
|
34
|
+
*/
|
|
35
|
+
export async function generateApiDocs(config: TypeDocConfig): Promise<void> {
|
|
36
|
+
const {
|
|
37
|
+
entryPoints,
|
|
38
|
+
tsconfig,
|
|
39
|
+
outputDir,
|
|
40
|
+
excludePrivate = true,
|
|
41
|
+
excludeProtected = false,
|
|
42
|
+
excludeInternal = true,
|
|
43
|
+
publicPath = "/api/",
|
|
44
|
+
} = config;
|
|
45
|
+
|
|
46
|
+
// 验证入口点存在
|
|
47
|
+
for (const entryPoint of entryPoints) {
|
|
48
|
+
if (!(await fs.pathExists(entryPoint))) {
|
|
49
|
+
throw new Error(`Entry point not found: ${entryPoint}`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 验证 tsconfig 存在
|
|
54
|
+
if (!(await fs.pathExists(tsconfig))) {
|
|
55
|
+
throw new Error(`TypeScript config not found: ${tsconfig}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 确保输出目录存在
|
|
59
|
+
await fs.ensureDir(outputDir);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// 创建 TypeDoc 应用实例
|
|
63
|
+
// 注意:publicPath 是 typedoc-plugin-markdown 的选项,不是 TypeDoc 核心选项
|
|
64
|
+
const app = await Application.bootstrapWithPlugins({
|
|
65
|
+
entryPoints,
|
|
66
|
+
tsconfig,
|
|
67
|
+
plugin: ["typedoc-plugin-markdown"],
|
|
68
|
+
theme: "markdown",
|
|
69
|
+
// 输出配置
|
|
70
|
+
readme: "none",
|
|
71
|
+
excludePrivate,
|
|
72
|
+
excludeProtected,
|
|
73
|
+
excludeInternal,
|
|
74
|
+
// Markdown 插件配置(使用类型断言,因为这些是插件选项)
|
|
75
|
+
...({ publicPath } as Record<string, unknown>),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// 添加配置读取器
|
|
79
|
+
app.options.addReader(new TSConfigReader());
|
|
80
|
+
app.options.addReader(new TypeDocReader());
|
|
81
|
+
|
|
82
|
+
// 转换项目
|
|
83
|
+
const project = await app.convert();
|
|
84
|
+
|
|
85
|
+
if (!project) {
|
|
86
|
+
throw new Error("Failed to convert TypeScript project");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 生成文档
|
|
90
|
+
await app.generateDocs(project, outputDir);
|
|
91
|
+
} catch (error) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Failed to generate API documentation: ${error instanceof Error ? error.message : String(error)}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WSX TypeScript 声明文件
|
|
3
|
+
* 支持 .wsx 文件导入
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
declare module "*.wsx" {
|
|
7
|
+
import type { LightComponent, WebComponent } from "@wsxjs/wsx-core";
|
|
8
|
+
// Allow any class that extends WebComponent or LightComponent
|
|
9
|
+
const Component: new (...args: unknown[]) => WebComponent | LightComponent;
|
|
10
|
+
export default Component;
|
|
11
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { DocumentLoadError } from "./types";
|
|
3
|
+
import type { DocMetadata, SearchDocument, DocsMetaCollection } from "./types";
|
|
4
|
+
|
|
5
|
+
describe("类型定义", () => {
|
|
6
|
+
describe("DocumentLoadError", () => {
|
|
7
|
+
it("应该正确创建 NOT_FOUND 错误", () => {
|
|
8
|
+
const error = new DocumentLoadError("文档未找到", "NOT_FOUND");
|
|
9
|
+
expect(error.message).toBe("文档未找到");
|
|
10
|
+
expect(error.code).toBe("NOT_FOUND");
|
|
11
|
+
expect(error.name).toBe("DocumentLoadError");
|
|
12
|
+
expect(error.details).toBeUndefined();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it("应该正确创建 NETWORK_ERROR 错误", () => {
|
|
16
|
+
const error = new DocumentLoadError("网络错误", "NETWORK_ERROR", { status: 500 });
|
|
17
|
+
expect(error.code).toBe("NETWORK_ERROR");
|
|
18
|
+
expect(error.details).toEqual({ status: 500 });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("应该正确创建 PARSE_ERROR 错误", () => {
|
|
22
|
+
const error = new DocumentLoadError("解析错误", "PARSE_ERROR");
|
|
23
|
+
expect(error.code).toBe("PARSE_ERROR");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("应该正确继承 Error", () => {
|
|
27
|
+
const error = new DocumentLoadError("测试错误", "NOT_FOUND");
|
|
28
|
+
expect(error).toBeInstanceOf(Error);
|
|
29
|
+
expect(error.stack).toBeDefined();
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe("类型兼容性", () => {
|
|
34
|
+
it("DocMetadata 应该符合接口定义", () => {
|
|
35
|
+
const metadata: DocMetadata = {
|
|
36
|
+
title: "测试文档",
|
|
37
|
+
category: "guide",
|
|
38
|
+
route: "/docs/guide/test",
|
|
39
|
+
};
|
|
40
|
+
expect(metadata.title).toBe("测试文档");
|
|
41
|
+
expect(metadata.category).toBe("guide");
|
|
42
|
+
expect(metadata.route).toBe("/docs/guide/test");
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it("DocMetadata 应该支持可选字段", () => {
|
|
46
|
+
const metadata: DocMetadata = {
|
|
47
|
+
title: "测试",
|
|
48
|
+
category: "guide",
|
|
49
|
+
route: "/docs/guide/test",
|
|
50
|
+
prev: "/docs/guide/prev",
|
|
51
|
+
next: "/docs/guide/next",
|
|
52
|
+
description: "描述",
|
|
53
|
+
tags: ["tag1", "tag2"],
|
|
54
|
+
};
|
|
55
|
+
expect(metadata.prev).toBe("/docs/guide/prev");
|
|
56
|
+
expect(metadata.next).toBe("/docs/guide/next");
|
|
57
|
+
expect(metadata.description).toBe("描述");
|
|
58
|
+
expect(metadata.tags).toEqual(["tag1", "tag2"]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("DocMetadata 应该支持 null 值", () => {
|
|
62
|
+
const metadata: DocMetadata = {
|
|
63
|
+
title: "测试",
|
|
64
|
+
category: "guide",
|
|
65
|
+
route: "/docs/guide/test",
|
|
66
|
+
prev: null,
|
|
67
|
+
next: null,
|
|
68
|
+
};
|
|
69
|
+
expect(metadata.prev).toBeNull();
|
|
70
|
+
expect(metadata.next).toBeNull();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("SearchDocument 应该符合接口定义", () => {
|
|
74
|
+
const doc: SearchDocument = {
|
|
75
|
+
id: "test",
|
|
76
|
+
title: "测试",
|
|
77
|
+
category: "guide",
|
|
78
|
+
route: "/docs/guide/test",
|
|
79
|
+
content: "内容",
|
|
80
|
+
};
|
|
81
|
+
expect(doc.id).toBe("test");
|
|
82
|
+
expect(doc.title).toBe("测试");
|
|
83
|
+
expect(doc.category).toBe("guide");
|
|
84
|
+
expect(doc.route).toBe("/docs/guide/test");
|
|
85
|
+
expect(doc.content).toBe("内容");
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("DocsMetaCollection 应该符合类型定义", () => {
|
|
89
|
+
const collection: DocsMetaCollection = {
|
|
90
|
+
"guide/intro": {
|
|
91
|
+
title: "介绍",
|
|
92
|
+
category: "guide",
|
|
93
|
+
route: "/docs/guide/intro",
|
|
94
|
+
},
|
|
95
|
+
"guide/advanced": {
|
|
96
|
+
title: "高级",
|
|
97
|
+
category: "guide",
|
|
98
|
+
route: "/docs/guide/advanced",
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
expect(Object.keys(collection)).toHaveLength(2);
|
|
102
|
+
expect(collection["guide/intro"].title).toBe("介绍");
|
|
103
|
+
expect(collection["guide/advanced"].title).toBe("高级");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("DocMetadata 应该支持扩展字段", () => {
|
|
107
|
+
const metadata: DocMetadata = {
|
|
108
|
+
title: "测试",
|
|
109
|
+
category: "guide",
|
|
110
|
+
route: "/docs/guide/test",
|
|
111
|
+
customField: "custom value",
|
|
112
|
+
anotherField: 123,
|
|
113
|
+
};
|
|
114
|
+
expect((metadata as Record<string, unknown>).customField).toBe("custom value");
|
|
115
|
+
expect((metadata as Record<string, unknown>).anotherField).toBe(123);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|