swgto-ts 2.0.1 → 3.0.1

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/README.md CHANGED
@@ -81,6 +81,7 @@ export interface SwaggerTsConfig {
81
81
  fileNaming?: 'module' | 'path'
82
82
  flattenQueryParam?: boolean
83
83
  mergeParams?: boolean
84
+ apiDocs?: ApiDocsConfig
84
85
  }
85
86
  ```
86
87
 
@@ -101,6 +102,7 @@ export interface SwaggerTsConfig {
101
102
  | `cleanOutput` | `boolean` | `false` | 生成前是否清空输出目录 |
102
103
  | `flattenQueryParam` | `boolean` | `false` | 当 query 参数只有一个且为 `$ref` 引用类型时,直接用引用类型代替 `{ query: RefType }` |
103
104
  | `mergeParams` | `boolean` | `false` | 合并参数层级:`true` 时展平 `path/query/body` 嵌套,直接使用 `params: { field1, field2 }` 代替 `params: { path: {...}, query: {...} }` |
105
+ | `apiDocs` | `ApiDocsConfig` | 见说明 | 自动生成 API 文档(HTML/Markdown),详细配置见 [API 文档生成](#api-文档生成) |
104
106
 
105
107
  ### 函数名生成规则
106
108
 
@@ -145,3 +147,50 @@ getUser({ path: { id: '1' }, query: { name: 'foo' } })
145
147
  // mergeParams: true,参数展平
146
148
  getUser({ id: '1', name: 'foo' })
147
149
  ```
150
+
151
+ ### API 文档生成
152
+
153
+ 配置 `apiDocs` 可在生成代码的同时,自动输出一份人类可读的 API 文档:
154
+
155
+ ```ts
156
+ export default {
157
+ // ... 其他配置
158
+ apiDocs: {
159
+ enable: true,
160
+ format: 'html', // 'html' 或 'markdown'
161
+ output: 'api-docs.html', // 输出文件名
162
+ title: '我的 API 文档', // 文档标题,默认取 OpenAPI info.title
163
+ companyName: 'XX 公司', // 封面公司名称(仅 HTML)
164
+ template: './my-template.html', // 自定义 HTML 模板路径(仅 HTML)
165
+ theme: './my-theme.css', // 自定义样式文件路径(仅 HTML)
166
+ },
167
+ }
168
+ ```
169
+
170
+ | 配置项 | 类型 | 默认值 | 说明 |
171
+ | ------------- | --------------------------- | ----------------------- | -------------------------------------------------------------- |
172
+ | `enable` | `boolean` | `false` | 是否开启文档生成 |
173
+ | `format` | `'html' \| 'markdown'` | `'html'` | 输出格式:HTML 适合浏览器打印为 PDF,Markdown 适合仓库内查看 |
174
+ | `output` | `string` | `'api-docs.html'` | 输出文件名,相对 `outputDir` |
175
+ | `title` | `string` | OpenAPI `info.title` | 文档标题 |
176
+ | `companyName` | `string` | 无 | 封面上的公司名称(仅 HTML) |
177
+ | `template` | `string` | 内置模板 | 自定义 HTML 模板路径(仅 HTML)。优先级:`template` 配置 > 项目根目录 `.swagger.docs.html` > 内置默认模板 |
178
+ | `theme` | `string` | 无 | 自定义 CSS 文件路径,覆盖文档样式(仅 HTML) |
179
+
180
+ #### HTML 文档
181
+
182
+ 输出为带书本风格的 HTML 页面,包含:
183
+ - **封面页**:A4 整页,显示公司名称(如配置)、API 名称和版本号
184
+ - **目录**:方法 + 路径 | 点线 | 摘要,点击跳转到对应接口
185
+ - **接口卡片**:包含路径参数、查询参数、请求体、响应体的字段表格(名称 / 类型 / 必填 / 描述)
186
+
187
+ 可通过项目根目录的 `.swagger.docs.html` 文件自定义文档样式。如果设置了 `apiDocs.template`,则优先使用该路径的模板文件。
188
+
189
+ #### Markdown 文档
190
+
191
+ 输出为 `.md` 文件,包含:
192
+ - 封面区域:公司名称(如配置)、API 名称和版本号
193
+ - 目录(Table of Contents):所有接口的锚点链接列表
194
+ - 每个接口的详细说明:HTTP 方法 + 路径、描述、参数表格和响应表格
195
+
196
+ 适合直接在代码仓库中查看或用于 GitBook、VuePress 等文档工具。```
@@ -0,0 +1,64 @@
1
+ // src/generators/schemaToTs.ts
2
+ function formatPropertyName(name) {
3
+ return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
4
+ }
5
+ function refToTypeName(ref) {
6
+ const parts = ref.split("/");
7
+ return parts[parts.length - 1] || "unknown";
8
+ }
9
+ function schemaToTs(schema) {
10
+ if (!schema) {
11
+ return "unknown";
12
+ }
13
+ if (schema.$ref) {
14
+ return refToTypeName(schema.$ref);
15
+ }
16
+ if (schema.enum?.length) {
17
+ return schema.enum.map((item) => JSON.stringify(item)).join(" | ");
18
+ }
19
+ if (schema.anyOf?.length) {
20
+ return schema.anyOf.map((item) => schemaToTs(item)).join(" | ");
21
+ }
22
+ if (schema.oneOf?.length) {
23
+ return schema.oneOf.map((item) => schemaToTs(item)).join(" | ");
24
+ }
25
+ if (schema.allOf?.length) {
26
+ return schema.allOf.map((item) => schemaToTs(item)).join(" & ");
27
+ }
28
+ if (schema.type === "array") {
29
+ return `${schemaToTs(schema.items)}[]`;
30
+ }
31
+ if (schema.type === "object" || schema.properties) {
32
+ const requiredSet = new Set(schema.required ?? []);
33
+ const properties = Object.entries(schema.properties ?? {}).map(([key, value]) => {
34
+ const optional = requiredSet.has(key) ? "" : "?";
35
+ return `${formatPropertyName(key)}${optional}: ${schemaToTs(value)};`;
36
+ });
37
+ if (!properties.length && schema.additionalProperties) {
38
+ const valueType = schema.additionalProperties === true ? "unknown" : schemaToTs(schema.additionalProperties);
39
+ return `{ [key: string]: ${valueType} }`;
40
+ }
41
+ return `{ ${properties.join(" ")} }`;
42
+ }
43
+ switch (schema.type) {
44
+ case "integer":
45
+ case "number":
46
+ return "number";
47
+ case "boolean":
48
+ return "boolean";
49
+ case "string":
50
+ return "string";
51
+ case "null":
52
+ return "null";
53
+ default:
54
+ return "unknown";
55
+ }
56
+ }
57
+ function toTypePropertyName(name) {
58
+ return formatPropertyName(name);
59
+ }
60
+
61
+ export {
62
+ schemaToTs,
63
+ toTypePropertyName
64
+ };
@@ -1,5 +1,12 @@
1
+ import {
2
+ schemaToTs,
3
+ toTypePropertyName
4
+ } from "./chunk-HBBM5HFP.js";
5
+
1
6
  // src/generate.ts
2
7
  import path4 from "path";
8
+ import { existsSync as existsSync2 } from "fs";
9
+ import { readFile as readFile2 } from "fs/promises";
3
10
 
4
11
  // src/config/loadConfig.ts
5
12
  import { existsSync } from "fs";
@@ -33,6 +40,8 @@ async function loadConfig(cwd) {
33
40
  const rawConfig = loaded?.default ?? loaded;
34
41
  assertConfig(rawConfig);
35
42
  const docUrls = Array.isArray(rawConfig.docUrls) ? rawConfig.docUrls : [rawConfig.docUrls];
43
+ const apiDocsFormat = rawConfig.apiDocs?.format ?? "html";
44
+ const defaultOutput = apiDocsFormat === "markdown" ? "api-docs.md" : "api-docs.html";
36
45
  const config = {
37
46
  ...rawConfig,
38
47
  docUrls,
@@ -42,7 +51,16 @@ async function loadConfig(cwd) {
42
51
  cleanOutput: rawConfig.cleanOutput ?? false,
43
52
  fileNaming: rawConfig.fileNaming ?? "path",
44
53
  flattenQueryParam: rawConfig.flattenQueryParam ?? false,
45
- mergeParams: rawConfig.mergeParams ?? false
54
+ mergeParams: rawConfig.mergeParams ?? false,
55
+ apiDocs: {
56
+ enable: rawConfig.apiDocs?.enable ?? false,
57
+ output: rawConfig.apiDocs?.output ?? defaultOutput,
58
+ format: apiDocsFormat,
59
+ title: rawConfig.apiDocs?.title,
60
+ companyName: rawConfig.apiDocs?.companyName,
61
+ template: rawConfig.apiDocs?.template,
62
+ theme: rawConfig.apiDocs?.theme
63
+ }
46
64
  };
47
65
  return { configPath, config };
48
66
  }
@@ -142,66 +160,6 @@ function groupByController(operations) {
142
160
  }, {});
143
161
  }
144
162
 
145
- // src/generators/schemaToTs.ts
146
- function formatPropertyName(name) {
147
- return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
148
- }
149
- function refToTypeName(ref) {
150
- const parts = ref.split("/");
151
- return parts[parts.length - 1] || "unknown";
152
- }
153
- function schemaToTs(schema) {
154
- if (!schema) {
155
- return "unknown";
156
- }
157
- if (schema.$ref) {
158
- return refToTypeName(schema.$ref);
159
- }
160
- if (schema.enum?.length) {
161
- return schema.enum.map((item) => JSON.stringify(item)).join(" | ");
162
- }
163
- if (schema.anyOf?.length) {
164
- return schema.anyOf.map((item) => schemaToTs(item)).join(" | ");
165
- }
166
- if (schema.oneOf?.length) {
167
- return schema.oneOf.map((item) => schemaToTs(item)).join(" | ");
168
- }
169
- if (schema.allOf?.length) {
170
- return schema.allOf.map((item) => schemaToTs(item)).join(" & ");
171
- }
172
- if (schema.type === "array") {
173
- return `${schemaToTs(schema.items)}[]`;
174
- }
175
- if (schema.type === "object" || schema.properties) {
176
- const requiredSet = new Set(schema.required ?? []);
177
- const properties = Object.entries(schema.properties ?? {}).map(([key, value]) => {
178
- const optional = requiredSet.has(key) ? "" : "?";
179
- return `${formatPropertyName(key)}${optional}: ${schemaToTs(value)};`;
180
- });
181
- if (!properties.length && schema.additionalProperties) {
182
- const valueType = schema.additionalProperties === true ? "unknown" : schemaToTs(schema.additionalProperties);
183
- return `{ [key: string]: ${valueType} }`;
184
- }
185
- return `{ ${properties.join(" ")} }`;
186
- }
187
- switch (schema.type) {
188
- case "integer":
189
- case "number":
190
- return "number";
191
- case "boolean":
192
- return "boolean";
193
- case "string":
194
- return "string";
195
- case "null":
196
- return "null";
197
- default:
198
- return "unknown";
199
- }
200
- }
201
- function toTypePropertyName(name) {
202
- return formatPropertyName(name);
203
- }
204
-
205
163
  // src/utils/naming.ts
206
164
  function toPascalCase(value) {
207
165
  return value.split(/[^a-zA-Z0-9]+/).filter(Boolean).map((segment) => segment[0].toUpperCase() + segment.slice(1)).join("");
@@ -503,15 +461,13 @@ ${queryLine}${bodyLine} ...config,
503
461
  }`;
504
462
  }
505
463
  function generateTsRequestFile(operation, httpClientPath, typeImportPath, mergeParams = false) {
506
- const importTypes = [
507
- ...operation.requestImportTypes,
508
- operation.responseTypeName,
509
- "RequestConfig"
510
- ].filter((value, index, array) => Boolean(value) && array.indexOf(value) === index);
464
+ const importTypes = [...operation.requestImportTypes, operation.responseTypeName, "RequestConfig"].filter(
465
+ (value, index, array) => Boolean(value) && array.indexOf(value) === index
466
+ );
511
467
  const importLine = importTypes.length ? `import type { ${importTypes.join(", ")} } from ${JSON.stringify(typeImportPath)};
512
468
  ` : "";
513
469
  const buildUrlHelper = operation.pathParams.length ? `
514
- function buildUrl(path?: Record<string, unknown>): string {
470
+ function buildUrl(path?: Record<string, any>): string {
515
471
  return ${JSON.stringify(operation.requestPath)}.replace(/\\{([^}]+)\\}/g, (_, key) => String(path?.[key] ?? ''));
516
472
  }
517
473
  ` : "";
@@ -789,14 +745,52 @@ async function generateFromConfig(cwd = process.cwd()) {
789
745
  path4.relative(cwd, typesFile),
790
746
  path4.relative(cwd, indexFile)
791
747
  );
748
+ if (config.apiDocs.enable) {
749
+ let content;
750
+ const docFile = path4.join(cwd, config.outputDir, config.apiDocs.output);
751
+ if (config.apiDocs.format === "markdown") {
752
+ const { generateApiDocsMd } = await import("./genApiDocsMd-WVDJY3ST.js");
753
+ content = generateApiDocsMd(documentMap, operations, config);
754
+ } else {
755
+ const { generateApiDocsHtml, DEFAULT_TEMPLATE } = await import("./genApiDocsHtml-V7PDR4PW.js");
756
+ const templateFile = path4.join(cwd, ".swagger.docs.html");
757
+ if (!existsSync2(templateFile)) {
758
+ await writeTextFile(templateFile, DEFAULT_TEMPLATE);
759
+ console.log(`Created template: .swagger.docs.html`);
760
+ }
761
+ const templatePaths = [];
762
+ if (config.apiDocs.template) {
763
+ templatePaths.push(path4.resolve(cwd, config.apiDocs.template));
764
+ }
765
+ templatePaths.push(templateFile);
766
+ let templateHtml;
767
+ for (const tp of templatePaths) {
768
+ if (existsSync2(tp)) {
769
+ templateHtml = await readFile2(tp, "utf-8");
770
+ break;
771
+ }
772
+ }
773
+ let themeCss;
774
+ if (config.apiDocs.theme) {
775
+ const themePath = path4.resolve(cwd, config.apiDocs.theme);
776
+ if (existsSync2(themePath)) {
777
+ themeCss = await readFile2(themePath, "utf-8");
778
+ }
779
+ }
780
+ content = generateApiDocsHtml(documentMap, operations, config, templateHtml, themeCss);
781
+ }
782
+ await writeTextFile(docFile, content);
783
+ files.push(path4.relative(cwd, docFile));
784
+ }
792
785
  await saveSnapshot(snapshotPath, operations);
786
+ const htmlGenerated = config.apiDocs.enable ? 1 : 0;
793
787
  return {
794
788
  configPath,
795
789
  files,
796
790
  operationCount: operations.length,
797
791
  moduleCount: Object.keys(grouped).length,
798
- apiFileCount: files.length - 2,
799
- // exclude types + index
792
+ apiFileCount: files.length - 2 - htmlGenerated,
793
+ // exclude types + index + optional html
800
794
  newOperations,
801
795
  removedOperations
802
796
  };
package/dist/cli.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  generateFromConfig
4
- } from "./chunk-ZURWUVPF.js";
4
+ } from "./chunk-OMXGXES2.js";
5
+ import "./chunk-HBBM5HFP.js";
5
6
 
6
7
  // src/cli.ts
7
8
  async function main() {
@@ -12,6 +13,10 @@ async function main() {
12
13
  console.log(`swgto loaded config: ${result.configPath}`);
13
14
  console.log(`Generated ${result.apiFileCount} api file(s), ${result.operationCount} operation(s) in ${result.moduleCount} module(s).`);
14
15
  console.log(`Wrote ${result.files.length} file(s) total (including types, index).`);
16
+ const docFile = result.files.find((f) => f.endsWith(".html") || f.endsWith(".md"));
17
+ if (docFile) {
18
+ console.log(`Generated API docs: ${docFile}`);
19
+ }
15
20
  console.log(`Done in ${elapsedMs}ms.`);
16
21
  if (result.newOperations.length > 0) {
17
22
  console.log(`
@@ -0,0 +1,382 @@
1
+ import {
2
+ schemaToTs
3
+ } from "./chunk-HBBM5HFP.js";
4
+
5
+ // src/generators/genApiDocsHtml.ts
6
+ function getMethodColor(method) {
7
+ const colors = {
8
+ get: "#1677ff",
9
+ post: "#52c41a",
10
+ put: "#fa8c16",
11
+ patch: "#722ed1",
12
+ delete: "#ff4d4f",
13
+ head: "#8c8c8c",
14
+ options: "#8c8c8c"
15
+ };
16
+ return colors[method.toLowerCase()] ?? "#8c8c8c";
17
+ }
18
+ function paramTypeDisplay(schema) {
19
+ if (!schema) return "unknown";
20
+ if (schema.type && !schema.$ref && !schema.enum && !schema.properties) {
21
+ return schema.type;
22
+ }
23
+ return schemaToTs(schema);
24
+ }
25
+ function escapeHtml(str) {
26
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
27
+ }
28
+ function resolveSchemaRef(schema, docUrl, documentMap) {
29
+ if (!schema?.$ref) return schema;
30
+ const name = schema.$ref.split("/").pop();
31
+ const doc = documentMap.get(docUrl);
32
+ const schemas = doc?.components?.schemas;
33
+ return schemas?.[name];
34
+ }
35
+ function refName(schema) {
36
+ if (schema?.$ref) return schema.$ref.split("/").pop();
37
+ if (schema?.type === "array" && schema.items?.$ref) return schema.items.$ref.split("/").pop();
38
+ return void 0;
39
+ }
40
+ function renderSchemaFieldsTable(schema, docUrl, documentMap, visitedRefs = /* @__PURE__ */ new Set()) {
41
+ const resolved = resolveSchemaRef(schema, docUrl, documentMap);
42
+ if (!resolved) {
43
+ return `<div class="schema-block">${escapeHtml(schemaToTs(schema))}</div>`;
44
+ }
45
+ if (resolved.properties && Object.keys(resolved.properties).length > 0) {
46
+ const requiredSet = new Set(resolved.required ?? []);
47
+ const doc = documentMap.get(docUrl);
48
+ const allSchemas = doc?.components?.schemas;
49
+ let html = '<table><thead><tr><th class="col-name">\u540D\u79F0</th><th class="col-type">\u7C7B\u578B</th><th class="col-req">\u5FC5\u586B</th><th class="col-desc">\u63CF\u8FF0</th></tr></thead><tbody>';
50
+ for (const [key, prop] of Object.entries(resolved.properties)) {
51
+ const subRef = refName(prop);
52
+ const subSchema = subRef && !visitedRefs.has(subRef) ? allSchemas?.[subRef] : void 0;
53
+ if (subSchema?.properties) {
54
+ visitedRefs.add(subRef);
55
+ const typeText = schemaToTs(prop);
56
+ html += '<tr class="ref-row"><td colspan="4">';
57
+ html += `<details class="ref-details"><summary class="ref-summary">`;
58
+ html += `<span class="ref-toggle">\u25B6</span>`;
59
+ html += `<span class="code ref-fname">${escapeHtml(key)}</span>`;
60
+ html += `<span class="code ref-ftype">${escapeHtml(typeText)}</span>`;
61
+ html += `<span class="${requiredSet.has(key) ? "required ref-freq" : "ref-freq"}">${requiredSet.has(key) ? "\u662F" : "\u5426"}</span>`;
62
+ html += `<span class="ref-fdesc">${prop.description ? escapeHtml(prop.description) : "-"}</span>`;
63
+ html += `</summary><div class="ref-body">`;
64
+ html += renderSchemaFieldsTable(subSchema, docUrl, documentMap, visitedRefs);
65
+ html += `</div></details></td></tr>`;
66
+ } else {
67
+ const typeText = schemaToTs(prop);
68
+ html += "<tr>";
69
+ html += `<td class="code">${escapeHtml(key)}</td>`;
70
+ html += `<td class="code">${escapeHtml(typeText)}</td>`;
71
+ html += `<td class="${requiredSet.has(key) ? "required" : ""}">${requiredSet.has(key) ? "\u662F" : "\u5426"}</td>`;
72
+ html += `<td>${prop.description ? escapeHtml(prop.description) : "-"}</td>`;
73
+ html += "</tr>";
74
+ }
75
+ }
76
+ html += "</tbody></table>";
77
+ return html;
78
+ }
79
+ return `<div class="schema-block">${escapeHtml(schemaToTs(schema))}</div>`;
80
+ }
81
+ var PLACEHOLDER = {
82
+ TITLE: "{{TITLE}}",
83
+ COVER_COMPANY: "{{COVER_COMPANY}}",
84
+ COVER_TITLE: "{{COVER_TITLE}}",
85
+ COVER_VERSION: "{{COVER_VERSION}}",
86
+ COVER_DATE: "{{COVER_DATE}}",
87
+ STAT_ENDPOINT_COUNT: "{{STAT_ENDPOINT_COUNT}}",
88
+ STAT_MODULE_COUNT: "{{STAT_MODULE_COUNT}}",
89
+ STYLES: "{{STYLES}}",
90
+ TOC: "{{TOC}}",
91
+ ENDPOINT_CARDS: "{{ENDPOINT_CARDS}}"
92
+ };
93
+ var DEFAULT_STYLES = `
94
+ * { margin: 0; padding: 0; box-sizing: border-box; }
95
+ body {
96
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Arial, sans-serif;
97
+ color: #1f1f1f;
98
+ background: #f5f5f5;
99
+ font-size: 14px;
100
+ line-height: 1.6;
101
+ }
102
+
103
+ .cover {
104
+ display: flex; flex-direction: column; justify-content: center; align-items: center;
105
+ min-height: 100vh; min-height: 297mm;
106
+ background: linear-gradient(135deg, #1677ff 0%, #0958d9 100%);
107
+ color: #fff; text-align: center; padding: 60px 40px;
108
+ page-break-after: always;
109
+ }
110
+ .cover-company { font-size: 14px; letter-spacing: 4px; opacity: 0.6; margin-bottom: 32px; text-transform: uppercase; }
111
+ .cover h1 { font-size: 40px; font-weight: 700; margin-bottom: 8px; letter-spacing: 2px; }
112
+ .cover .version { font-size: 18px; opacity: 0.85; margin-bottom: 48px; }
113
+ .cover .meta { font-size: 13px; opacity: 0.55; }
114
+ .cover .stats { margin-top: 16px; display: flex; justify-content: center; gap: 40px; }
115
+ .cover .stat-item { text-align: center; }
116
+ .cover .stat-num { font-size: 28px; font-weight: 600; }
117
+ .cover .stat-label { font-size: 12px; opacity: 0.65; }
118
+
119
+ .container { max-width: 960px; margin: 0 auto; padding: 0 24px 48px; }
120
+
121
+ .toc { margin-bottom: 40px; }
122
+ .toc h2 {
123
+ font-size: 22px; font-weight: 700; color: #1f1f1f; margin-bottom: 16px;
124
+ padding-bottom: 10px; border-bottom: 2px solid #1f1f1f; letter-spacing: 2px;
125
+ }
126
+ .toc-group { margin-bottom: 24px; }
127
+ .toc-group:last-child { margin-bottom: 0; }
128
+ .toc-module {
129
+ font-size: 14px; font-weight: 600; color: #1677ff; margin-bottom: 8px;
130
+ padding-left: 2px;
131
+ }
132
+ .toc-entry {
133
+ display: flex; align-items: baseline; text-decoration: none;
134
+ padding: 3px 4px; color: #434343; page-break-inside: avoid;
135
+ overflow: hidden;
136
+ }
137
+ .toc-entry:hover { background: #f5f5f5; border-radius: 3px; }
138
+ .toc-badge {
139
+ display: inline-block; padding: 1px 5px; border-radius: 3px; font-size: 10px;
140
+ font-weight: 700; color: #fff; font-family: 'SFMono-Regular', Consolas, monospace;
141
+ min-width: 40px; text-align: center; flex-shrink: 0; margin-right: 8px;
142
+ }
143
+ .toc-path {
144
+ font-family: 'SFMono-Regular', Consolas, monospace; font-size: 13px;
145
+ white-space: nowrap; overflow: hidden; text-overflow: ellipsis; flex-shrink: 1; min-width: 40px;
146
+ }
147
+ .toc-leader {
148
+ flex: 1; min-width: 12px; margin: 0 6px;
149
+ border-bottom: 1px dotted #d9d9d9; height: 0; align-self: center;
150
+ }
151
+ .toc-summary {
152
+ font-size: 12px; color: #8c8c8c; white-space: nowrap;
153
+ flex-shrink: 0; text-align: right; max-width: 45%;
154
+ overflow: hidden; text-overflow: ellipsis;
155
+ }
156
+
157
+ .card {
158
+ background: #fff; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,.08);
159
+ margin-bottom: 20px; overflow: hidden; page-break-inside: avoid;
160
+ }
161
+ .card-header {
162
+ display: flex; align-items: center; gap: 12px; padding: 14px 20px;
163
+ border-bottom: 1px solid #f0f0f0;
164
+ }
165
+ .method-badge {
166
+ display: inline-block; padding: 3px 10px; border-radius: 4px; font-size: 13px;
167
+ font-weight: 700; color: #fff; font-family: 'SFMono-Regular', Consolas, monospace; min-width: 56px; text-align: center;
168
+ }
169
+ .card-path { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 14px; font-weight: 500; color: #1f1f1f; word-break: break-all; }
170
+ .card-summary { flex: 1; text-align: right; font-size: 13px; color: #8c8c8c; }
171
+ .card-body { padding: 16px 20px; }
172
+ .card-section { margin-bottom: 14px; }
173
+ .card-section:last-child { margin-bottom: 0; }
174
+ .card-section-title { font-size: 13px; font-weight: 600; color: #434343; margin-bottom: 6px; }
175
+ .card-section-desc { font-size: 13px; color: #595959; margin-bottom: 8px; line-height: 1.5; }
176
+ .card-desc-empty { font-style: italic; color: #bfbfbf; font-size: 13px; }
177
+
178
+ table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
179
+ thead th {
180
+ background: #fafafa; text-align: left; padding: 6px 10px; font-weight: 600;
181
+ color: #595959; border-bottom: 1px solid #e8e8e8; font-size: 12px;
182
+ }
183
+ tbody td { padding: 6px 10px; border-bottom: 1px solid #f5f5f5; color: #595959; vertical-align: top; word-break: break-word; }
184
+ tbody tr:hover { background: #fafafa; }
185
+ td.code { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px; }
186
+ td.required { color: #ff4d4f; font-weight: 600; }
187
+ .col-name { width: 28%; }
188
+ .col-type { width: 18%; }
189
+ .col-loc { width: 10%; }
190
+ .col-req { width: 7%; }
191
+ .col-desc { width: auto; }
192
+
193
+ .schema-block {
194
+ background: #f6f8fa; border-radius: 4px; padding: 10px 14px;
195
+ font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px; line-height: 1.7;
196
+ color: #1f1f1f; overflow-x: auto; white-space: pre-wrap; word-break: break-word;
197
+ }
198
+
199
+ .ref-row td { padding: 0 !important; border-bottom: none !important; }
200
+ .ref-row:hover { background: transparent; }
201
+ .ref-summary {
202
+ display: flex; align-items: center; gap: 8px; cursor: pointer; user-select: none;
203
+ padding: 6px 10px; list-style: none; font-size: 12px;
204
+ }
205
+ .ref-summary::-webkit-details-marker { display: none; }
206
+ .ref-summary::marker { display: none; }
207
+ .ref-toggle { font-size: 10px; color: #8c8c8c; width: 14px; flex-shrink: 0; text-align: center; transition: transform .15s; }
208
+ details[open] .ref-toggle { transform: rotate(90deg); }
209
+ .ref-fname { width: 28%; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px; flex-shrink: 0; }
210
+ .ref-ftype { width: 18%; font-family: 'SFMono-Regular', Consolas, monospace; font-size: 12px; color: #595959; flex-shrink: 0; }
211
+ .ref-freq { width: 7%; font-size: 12px; flex-shrink: 0; }
212
+ .ref-fdesc { flex: 1; font-size: 12px; color: #595959; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
213
+ .ref-body { padding: 2px 0 8px 0; }
214
+ .ref-body table { font-size: 12px; }
215
+ .ref-body th { font-size: 11px; padding: 4px 8px; }
216
+ .ref-body td { padding: 4px 8px; }
217
+
218
+ @media print {
219
+ body { background: #fff; }
220
+ .cover { border-radius: 0; min-height: 297mm; page-break-after: always; }
221
+ .card { box-shadow: none; border: 1px solid #e8e8e8; page-break-inside: avoid; }
222
+ @page { margin: 0; }
223
+ }
224
+ `;
225
+ var DEFAULT_TEMPLATE = `<!DOCTYPE html>
226
+ <html lang="zh-CN">
227
+ <head>
228
+ <meta charset="UTF-8">
229
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
230
+ <title>${PLACEHOLDER.TITLE}</title>
231
+ <style>${PLACEHOLDER.STYLES}</style>
232
+ </head>
233
+ <body>
234
+ <div class="container">
235
+ <div class="cover">
236
+ <div class="cover-company">${PLACEHOLDER.COVER_COMPANY}</div>
237
+ <h1>${PLACEHOLDER.COVER_TITLE}</h1>
238
+ <div class="version">${PLACEHOLDER.COVER_VERSION}</div>
239
+ <div class="meta">${PLACEHOLDER.COVER_DATE}</div>
240
+ <div class="stats">
241
+ <div class="stat-item"><div class="stat-num">${PLACEHOLDER.STAT_ENDPOINT_COUNT}</div><div class="stat-label">\u63A5\u53E3</div></div>
242
+ <div class="stat-item"><div class="stat-num">${PLACEHOLDER.STAT_MODULE_COUNT}</div><div class="stat-label">\u6A21\u5757</div></div>
243
+ </div>
244
+ </div>
245
+ ${PLACEHOLDER.TOC}
246
+ ${PLACEHOLDER.ENDPOINT_CARDS}
247
+ </div>
248
+ </body>
249
+ </html>`;
250
+ function renderTocHtml(operations) {
251
+ const grouped = /* @__PURE__ */ new Map();
252
+ for (const op of operations) {
253
+ const list = grouped.get(op.moduleName);
254
+ if (list) {
255
+ list.push(op);
256
+ } else {
257
+ grouped.set(op.moduleName, [op]);
258
+ }
259
+ }
260
+ const groups = Array.from(grouped.entries());
261
+ let html = '<div class="toc"><h2>\u76EE\u5F55</h2>';
262
+ for (const [moduleName, ops] of groups) {
263
+ html += `<div class="toc-group"><div class="toc-module">${escapeHtml(moduleName)}</div>`;
264
+ for (const op of ops) {
265
+ const anchor = `#${op.functionName}`;
266
+ const color = getMethodColor(op.method);
267
+ html += `<a class="toc-entry" href="${anchor}">`;
268
+ html += `<span class="toc-badge" style="background:${color}">${op.method.toUpperCase()}</span>`;
269
+ html += `<span class="toc-path">${escapeHtml(op.path)}</span>`;
270
+ html += '<span class="toc-leader"></span>';
271
+ if (op.summary) {
272
+ html += `<span class="toc-summary">${escapeHtml(op.summary)}</span>`;
273
+ }
274
+ html += "</a>";
275
+ }
276
+ html += "</div>";
277
+ }
278
+ html += "</div>";
279
+ return html;
280
+ }
281
+ function renderParamsTableHtml(params) {
282
+ if (!params.length) {
283
+ return '<span class="card-desc-empty">\u65E0</span>';
284
+ }
285
+ let html = '<table><thead><tr><th class="col-name">\u540D\u79F0</th><th class="col-type">\u7C7B\u578B</th><th class="col-req">\u5FC5\u586B</th><th class="col-desc">\u63CF\u8FF0</th></tr></thead><tbody>';
286
+ for (const p of params) {
287
+ const typeText = paramTypeDisplay(p.schema);
288
+ html += "<tr>";
289
+ html += `<td class="code">${escapeHtml(p.name)}</td>`;
290
+ html += `<td class="code">${escapeHtml(typeText)}</td>`;
291
+ html += `<td class="${p.required ? "required" : ""}">${p.required ? "\u662F" : "\u5426"}</td>`;
292
+ html += `<td>${p.description ? escapeHtml(p.description) : "-"}</td>`;
293
+ html += "</tr>";
294
+ }
295
+ html += "</tbody></table>";
296
+ return html;
297
+ }
298
+ function renderEndpointCardHtml(op, documentMap) {
299
+ const color = getMethodColor(op.method);
300
+ const anchor = op.functionName;
301
+ let html = `<div class="card" id="${anchor}">`;
302
+ html += '<div class="card-header">';
303
+ html += `<span class="method-badge" style="background:${color}">${op.method.toUpperCase()}</span>`;
304
+ html += `<span class="card-path">${escapeHtml(op.path)}</span>`;
305
+ if (op.summary) {
306
+ html += `<span class="card-summary">${escapeHtml(op.summary)}</span>`;
307
+ }
308
+ html += "</div>";
309
+ html += '<div class="card-body">';
310
+ if (op.description) {
311
+ html += '<div class="card-section">';
312
+ html += `<div class="card-section-desc">${escapeHtml(op.description)}</div>`;
313
+ html += "</div>";
314
+ }
315
+ if (op.pathParams.length) {
316
+ html += '<div class="card-section">';
317
+ html += '<div class="card-section-title">\u8DEF\u5F84\u53C2\u6570</div>';
318
+ html += renderParamsTableHtml(op.pathParams);
319
+ html += "</div>";
320
+ }
321
+ if (op.queryParams.length) {
322
+ html += '<div class="card-section">';
323
+ html += '<div class="card-section-title">\u67E5\u8BE2\u53C2\u6570</div>';
324
+ html += renderParamsTableHtml(op.queryParams);
325
+ html += "</div>";
326
+ }
327
+ if (op.requestBodySchema) {
328
+ html += '<div class="card-section">';
329
+ html += '<div class="card-section-title">\u8BF7\u6C42\u4F53</div>';
330
+ html += renderSchemaFieldsTable(op.requestBodySchema, op.docUrl, documentMap);
331
+ html += "</div>";
332
+ }
333
+ if (op.responseSchema) {
334
+ html += '<div class="card-section">';
335
+ html += '<div class="card-section-title">\u54CD\u5E94</div>';
336
+ html += renderSchemaFieldsTable(op.responseSchema, op.docUrl, documentMap);
337
+ html += "</div>";
338
+ }
339
+ html += "</div></div>";
340
+ return html;
341
+ }
342
+ function buildTemplateContext(documentMap, operations, config, themeCss) {
343
+ const firstDoc = documentMap.values().next().value;
344
+ const title = config.apiDocs.title || firstDoc?.info?.title || "API Documentation";
345
+ const version = firstDoc?.info?.version ? `v${firstDoc.info.version}` : "";
346
+ const now = /* @__PURE__ */ new Date();
347
+ const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
348
+ const modules = new Set(operations.map((op) => op.moduleName));
349
+ const moduleCount = modules.size;
350
+ const fullStyles = themeCss ? DEFAULT_STYLES + "\n" + themeCss : DEFAULT_STYLES;
351
+ return {
352
+ title: escapeHtml(title),
353
+ coverCompany: config.apiDocs.companyName ? escapeHtml(config.apiDocs.companyName) : "",
354
+ coverTitle: escapeHtml(title),
355
+ coverVersion: escapeHtml(version),
356
+ coverDate: dateStr,
357
+ statEndpointCount: String(operations.length),
358
+ statModuleCount: String(moduleCount),
359
+ styles: fullStyles,
360
+ toc: renderTocHtml(operations),
361
+ endpointCards: operations.map((op) => renderEndpointCardHtml(op, documentMap)).join("\n")
362
+ };
363
+ }
364
+ function applyTemplate(templateHtml, ctx) {
365
+ return templateHtml.replace(PLACEHOLDER.TITLE, ctx.title).replace(PLACEHOLDER.COVER_COMPANY, ctx.coverCompany).replace(PLACEHOLDER.COVER_TITLE, ctx.coverTitle).replace(PLACEHOLDER.COVER_VERSION, ctx.coverVersion).replace(PLACEHOLDER.COVER_DATE, ctx.coverDate).replace(PLACEHOLDER.STAT_ENDPOINT_COUNT, ctx.statEndpointCount).replace(PLACEHOLDER.STAT_MODULE_COUNT, ctx.statModuleCount).replace(PLACEHOLDER.STYLES, ctx.styles).replace(PLACEHOLDER.TOC, ctx.toc).replace(PLACEHOLDER.ENDPOINT_CARDS, ctx.endpointCards);
366
+ }
367
+ function generateApiDocsHtml(documentMap, operations, config, templateHtml, themeCss) {
368
+ const ctx = buildTemplateContext(documentMap, operations, config, themeCss);
369
+ const template = templateHtml ?? DEFAULT_TEMPLATE;
370
+ return applyTemplate(template, ctx);
371
+ }
372
+ export {
373
+ DEFAULT_STYLES,
374
+ DEFAULT_TEMPLATE,
375
+ PLACEHOLDER,
376
+ applyTemplate,
377
+ buildTemplateContext,
378
+ generateApiDocsHtml,
379
+ renderEndpointCardHtml,
380
+ renderParamsTableHtml,
381
+ renderTocHtml
382
+ };
@@ -0,0 +1,153 @@
1
+ import {
2
+ schemaToTs
3
+ } from "./chunk-HBBM5HFP.js";
4
+
5
+ // src/generators/genApiDocsMd.ts
6
+ function paramTypeDisplay(schema) {
7
+ if (!schema) return "unknown";
8
+ if (schema.type && !schema.$ref && !schema.enum && !schema.properties) {
9
+ return schema.type;
10
+ }
11
+ return schemaToTs(schema);
12
+ }
13
+ function escapeMd(str) {
14
+ return str.replace(/\|/g, "\\|").replace(/\n/g, " ");
15
+ }
16
+ function resolveSchemaRef(schema, docUrl, documentMap) {
17
+ if (!schema?.$ref) return schema;
18
+ const name = schema.$ref.split("/").pop();
19
+ const doc = documentMap.get(docUrl);
20
+ const schemas = doc?.components?.schemas;
21
+ return schemas?.[name];
22
+ }
23
+ function refName(schema) {
24
+ if (schema?.$ref) return schema.$ref.split("/").pop();
25
+ if (schema?.type === "array" && schema.items?.$ref) return schema.items.$ref.split("/").pop();
26
+ return void 0;
27
+ }
28
+ function renderMdTable(rows, headers) {
29
+ const cols = headers.length;
30
+ const lines = [];
31
+ lines.push(`| ${headers.join(" | ")} |`);
32
+ lines.push(`| ${headers.map(() => "---").join(" | ")} |`);
33
+ for (const row of rows) {
34
+ lines.push(`| ${row.join(" | ")} |`);
35
+ }
36
+ return lines.join("\n");
37
+ }
38
+ function renderParamsMdTable(params) {
39
+ if (!params.length) return "\u65E0";
40
+ const rows = params.map((p) => [
41
+ `\`${escapeMd(p.name)}\``,
42
+ `\`${escapeMd(paramTypeDisplay(p.schema))}\``,
43
+ p.required ? "**\u662F**" : "\u5426",
44
+ escapeMd(p.description ?? "-")
45
+ ]);
46
+ return renderMdTable(rows, ["\u540D\u79F0", "\u7C7B\u578B", "\u5FC5\u586B", "\u63CF\u8FF0"]);
47
+ }
48
+ function renderSchemaFieldsMd(schema, docUrl, documentMap, visitedRefs = /* @__PURE__ */ new Set(), depth = 0) {
49
+ const resolved = resolveSchemaRef(schema, docUrl, documentMap);
50
+ if (!resolved) return `\`${escapeMd(schemaToTs(schema))}\``;
51
+ if (resolved.properties && Object.keys(resolved.properties).length > 0) {
52
+ const requiredSet = new Set(resolved.required ?? []);
53
+ const doc = documentMap.get(docUrl);
54
+ const allSchemas = doc?.components?.schemas;
55
+ const rows = [];
56
+ const subBlocks = [];
57
+ for (const [key, prop] of Object.entries(resolved.properties)) {
58
+ const typeText = schemaToTs(prop);
59
+ const subRef = refName(prop);
60
+ const subSchema = subRef && !visitedRefs.has(subRef) ? allSchemas?.[subRef] : void 0;
61
+ rows.push([
62
+ `\`${escapeMd(key)}\``,
63
+ `\`${escapeMd(typeText)}\``,
64
+ requiredSet.has(key) ? "**\u662F**" : "\u5426",
65
+ escapeMd(prop.description ?? "-")
66
+ ]);
67
+ if (subSchema?.properties) {
68
+ visitedRefs.add(subRef);
69
+ subBlocks.push(`
70
+ <details>
71
+ <summary>${escapeMd(key)}: ${escapeMd(subRef)}</summary>
72
+
73
+ ${renderSchemaFieldsMd(subSchema, docUrl, documentMap, visitedRefs, depth + 1)}
74
+ </details>
75
+ `);
76
+ }
77
+ }
78
+ let md = renderMdTable(rows, ["\u540D\u79F0", "\u7C7B\u578B", "\u5FC5\u586B", "\u63CF\u8FF0"]);
79
+ if (subBlocks.length > 0) {
80
+ md += "\n" + subBlocks.join("\n");
81
+ }
82
+ return md;
83
+ }
84
+ return `\`${escapeMd(schemaToTs(schema))}\``;
85
+ }
86
+ function generateApiDocsMd(documentMap, operations, config) {
87
+ const firstDoc = documentMap.values().next().value;
88
+ const title = config.apiDocs.title || firstDoc?.info?.title || "API Documentation";
89
+ const version = firstDoc?.info?.version ? `v${firstDoc.info.version}` : "";
90
+ const now = /* @__PURE__ */ new Date();
91
+ const dateStr = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
92
+ const modules = new Set(operations.map((op) => op.moduleName));
93
+ const moduleCount = modules.size;
94
+ const lines = [];
95
+ if (config.apiDocs.companyName) {
96
+ lines.push(`> ${config.apiDocs.companyName}`);
97
+ lines.push("");
98
+ }
99
+ lines.push(`# ${title}`);
100
+ if (version) lines.push(`**\u7248\u672C**: ${version}`);
101
+ lines.push(`**\u751F\u6210\u65E5\u671F**: ${dateStr}`);
102
+ lines.push(`**\u63A5\u53E3\u603B\u6570**: ${operations.length} | **\u6A21\u5757\u6570**: ${moduleCount}`);
103
+ lines.push("");
104
+ lines.push("---");
105
+ lines.push("");
106
+ lines.push("## \u76EE\u5F55");
107
+ lines.push("");
108
+ for (const op of operations) {
109
+ const anchor = op.summary ? `${op.method.toUpperCase()} ${op.path} \u2014 ${op.summary}` : `${op.method.toUpperCase()} ${op.path}`;
110
+ lines.push(`- [${escapeMd(anchor)}](#${op.functionName})`);
111
+ }
112
+ lines.push("");
113
+ lines.push("---");
114
+ lines.push("");
115
+ for (const op of operations) {
116
+ lines.push(`## ${op.summary || op.functionName}`);
117
+ lines.push("");
118
+ lines.push(`\`${op.method.toUpperCase()}\` \`${op.path}\``);
119
+ lines.push("");
120
+ if (op.description) {
121
+ lines.push(op.description);
122
+ lines.push("");
123
+ }
124
+ if (op.pathParams.length) {
125
+ lines.push("### \u8DEF\u5F84\u53C2\u6570");
126
+ lines.push("");
127
+ lines.push(renderParamsMdTable(op.pathParams));
128
+ lines.push("");
129
+ }
130
+ if (op.queryParams.length) {
131
+ lines.push("### \u67E5\u8BE2\u53C2\u6570");
132
+ lines.push("");
133
+ lines.push(renderParamsMdTable(op.queryParams));
134
+ lines.push("");
135
+ }
136
+ if (op.requestBodySchema) {
137
+ lines.push("### \u8BF7\u6C42\u4F53");
138
+ lines.push("");
139
+ lines.push(renderSchemaFieldsMd(op.requestBodySchema, op.docUrl, documentMap));
140
+ lines.push("");
141
+ }
142
+ if (op.responseSchema) {
143
+ lines.push("### \u54CD\u5E94");
144
+ lines.push("");
145
+ lines.push(renderSchemaFieldsMd(op.responseSchema, op.docUrl, documentMap));
146
+ lines.push("");
147
+ }
148
+ }
149
+ return lines.join("\n");
150
+ }
151
+ export {
152
+ generateApiDocsMd
153
+ };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,15 @@
1
1
  type OutputType = 'ts' | 'js';
2
2
  type RequestConfig = Record<string, unknown>;
3
+ type ApiDocsFormat = 'html' | 'markdown';
4
+ interface ApiDocsConfig {
5
+ enable?: boolean;
6
+ output?: string;
7
+ format?: ApiDocsFormat;
8
+ title?: string;
9
+ companyName?: string;
10
+ template?: string;
11
+ theme?: string;
12
+ }
3
13
  interface SwaggerTsConfig {
4
14
  docUrls: string | string[];
5
15
  httpClientPath: string;
@@ -14,6 +24,7 @@ interface SwaggerTsConfig {
14
24
  fileNaming?: 'module' | 'path';
15
25
  flattenQueryParam?: boolean;
16
26
  mergeParams?: boolean;
27
+ apiDocs?: ApiDocsConfig;
17
28
  }
18
29
 
19
30
  interface SnapshotEntry {
@@ -37,4 +48,4 @@ interface GenerateResult {
37
48
  }
38
49
  declare function generateFromConfig(cwd?: string): Promise<GenerateResult>;
39
50
 
40
- export { type GenerateResult, type RequestConfig, type SwaggerTsConfig, generateFromConfig };
51
+ export { type ApiDocsConfig, type GenerateResult, type RequestConfig, type SwaggerTsConfig, generateFromConfig };
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  generateFromConfig
3
- } from "./chunk-ZURWUVPF.js";
3
+ } from "./chunk-OMXGXES2.js";
4
+ import "./chunk-HBBM5HFP.js";
4
5
  export {
5
6
  generateFromConfig
6
7
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "swgto-ts",
3
- "version": "2.0.1",
3
+ "version": "3.0.1",
4
4
  "description": "Generate API request files from OpenAPI 3.x documents.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,6 +25,9 @@
25
25
  "generator",
26
26
  "cli"
27
27
  ],
28
+ "publishConfig": {
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
28
31
  "engines": {
29
32
  "node": ">=18"
30
33
  },