@rspress/plugin-typedoc 0.0.0-nightly-20241127160234 → 0.0.0-nightly-20241128160245

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/dist/es/index.mjs CHANGED
@@ -3,134 +3,69 @@ import * as __WEBPACK_EXTERNAL_MODULE_typedoc__ from "typedoc";
3
3
  import * as __WEBPACK_EXTERNAL_MODULE_typedoc_plugin_markdown__ from "typedoc-plugin-markdown";
4
4
  import * as __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__ from "@rspress/shared/fs-extra";
5
5
  const API_DIR = 'api';
6
- const ROUTE_PREFIX = `/${API_DIR}`;
7
- function transformModuleName(name) {
8
- return name.replace(/\//g, '_').replace(/-/g, '_');
9
- }
10
6
  async function patchLinks(outputDir) {
11
7
  // Patch links in markdown files
12
8
  // Scan all the markdown files in the output directory
13
- // replace [foo](bar) -> [foo](./bar)
9
+ // replace
10
+ // 1. [foo](bar) -> [foo](./bar)
11
+ // 2. [foo](./bar) -> [foo](./bar) no change
14
12
  const normlizeLinksInFile = async (filePath)=>{
15
13
  const content = await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].readFile(filePath, 'utf-8');
16
- // replace: [foo](bar) -> [foo](./bar)
17
- const newContent = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, p1, p2)=>`[${p1}](./${p2})`);
14
+ // 1. [foo](bar) -> [foo](./bar)
15
+ const newContent = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, p1, p2)=>{
16
+ // 2. [foo](./bar) -> [foo](./bar) no change
17
+ if ([
18
+ '/',
19
+ '.'
20
+ ].includes(p2[0])) return `[${p1}](${p2})`;
21
+ return `[${p1}](./${p2})`;
22
+ });
18
23
  await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].writeFile(filePath, newContent);
19
24
  };
20
25
  const traverse = async (dir)=>{
21
26
  const files = await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].readdir(dir);
22
- for (const file of files){
23
- const filePath = __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(dir, file);
24
- const stat = await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].stat(filePath);
25
- if (stat.isDirectory()) await traverse(filePath);
26
- else if (stat.isFile() && /\.mdx?/.test(file)) await normlizeLinksInFile(filePath);
27
- }
27
+ const filePaths = files.map((file)=>__WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(dir, file));
28
+ const stats = await Promise.all(filePaths.map((fp)=>__WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].stat(fp)));
29
+ await Promise.all(stats.map((stat, index)=>{
30
+ const file = files[index];
31
+ const filePath = filePaths[index];
32
+ if (stat.isDirectory()) return traverse(filePath);
33
+ if (stat.isFile() && /\.mdx?/.test(file)) return normlizeLinksInFile(filePath);
34
+ }));
28
35
  };
29
36
  await traverse(outputDir);
30
37
  }
31
- async function resolveSidebarForSingleEntry(jsonFile) {
32
- const result = [];
33
- const data = JSON.parse(await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].readFile(jsonFile, 'utf-8'));
34
- if (!data.children || data.children.length <= 0) return [];
35
- const symbolMap = new Map();
36
- data.groups.forEach((group)=>{
37
- const groupItem = {
38
- text: group.title,
39
- items: []
40
- };
41
- group.children.forEach((id)=>{
42
- const dataItem = data.children.find((item)=>item.id === id);
43
- if (dataItem) {
44
- // Note: we should handle the case that classes and interfaces have the same name
45
- // Such as class `Env` and variable `env`
46
- // The final output file name should be `classes/Env.md` and `variables/env-1.md`
47
- let fileName = dataItem.name;
48
- if (symbolMap.has(dataItem.name)) {
49
- const count = symbolMap.get(dataItem.name) + 1;
50
- symbolMap.set(dataItem.name, count);
51
- fileName = `${dataItem.name}-${count}`;
52
- } else symbolMap.set(dataItem.name.toLocaleLowerCase(), 0);
53
- groupItem.items.push({
54
- text: dataItem.name,
55
- link: `${ROUTE_PREFIX}/${group.title.toLocaleLowerCase()}/${fileName}`
56
- });
57
- }
58
- });
59
- result.push(groupItem);
60
- });
61
- await patchLinks(__WEBPACK_EXTERNAL_MODULE_node_path__["default"].dirname(jsonFile));
62
- return result;
38
+ async function generateMetaJson(absoluteApiDir) {
39
+ const metaJsonPath = __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(absoluteApiDir, '_meta.json');
40
+ const files = await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].readdir(absoluteApiDir);
41
+ const filePaths = files.map((file)=>__WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(absoluteApiDir, file));
42
+ const stats = await Promise.all(filePaths.map((fp)=>__WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].stat(fp)));
43
+ const dirs = stats.map((stat, index)=>stat.isDirectory() ? files[index] : null).filter(Boolean);
44
+ const meta = dirs.map((dir)=>({
45
+ type: 'dir',
46
+ label: dir.slice(0, 1).toUpperCase() + dir.slice(1),
47
+ name: dir
48
+ }));
49
+ await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].writeFile(metaJsonPath, JSON.stringify([
50
+ 'index',
51
+ ...meta
52
+ ]));
63
53
  }
64
- async function resolveSidebarForMultiEntry(jsonFile) {
65
- const result = [];
66
- const data = JSON.parse(await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].readFile(jsonFile, 'utf-8'));
67
- if (!data.children || data.children.length <= 0) return result;
68
- function getModulePath(name) {
69
- return __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(`${ROUTE_PREFIX}/modules`, `${transformModuleName(name)}`).replace(/\\/g, '/');
70
- }
71
- function getClassPath(moduleName, className) {
72
- return __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(`${ROUTE_PREFIX}/classes`, `${transformModuleName(moduleName)}.${className}`).replace(/\\/g, '/');
73
- }
74
- function getInterfacePath(moduleName, interfaceName) {
75
- return __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(`${ROUTE_PREFIX}/interfaces`, `${transformModuleName(moduleName)}.${interfaceName}`).replace(/\\/g, '/');
76
- }
77
- function getFunctionPath(moduleName, functionName) {
78
- return __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(`${ROUTE_PREFIX}/functions`, `${transformModuleName(moduleName)}.${functionName}`).replace(/\\/g, '/');
79
- }
80
- data.children.forEach((module)=>{
81
- const moduleNames = module.name.split('/');
82
- const name = moduleNames[moduleNames.length - 1];
83
- const moduleConfig = {
84
- text: `Module:${name}`,
85
- items: [
86
- {
87
- text: 'Overview',
88
- link: getModulePath(module.name)
89
- }
90
- ]
91
- };
92
- if (!module.children || module.children.length <= 0) return;
93
- module.children.forEach((sub)=>{
94
- var _module_groups_find;
95
- const kindString = null === (_module_groups_find = module.groups.find((item)=>item.children.includes(sub.id))) || void 0 === _module_groups_find ? void 0 : _module_groups_find.title.slice(0, -1);
96
- if (!kindString) return;
97
- switch(kindString){
98
- case 'Class':
99
- moduleConfig.items.push({
100
- text: `Class:${sub.name}`,
101
- link: getClassPath(module.name, sub.name)
102
- });
103
- break;
104
- case 'Interface':
105
- moduleConfig.items.push({
106
- text: `Interface:${sub.name}`,
107
- link: getInterfacePath(module.name, sub.name)
108
- });
109
- break;
110
- case 'Function':
111
- moduleConfig.items.push({
112
- text: `Function:${sub.name}`,
113
- link: getFunctionPath(module.name, sub.name)
114
- });
115
- break;
116
- default:
117
- break;
118
- }
119
- });
120
- result.push(moduleConfig);
121
- });
122
- await patchLinks(__WEBPACK_EXTERNAL_MODULE_node_path__["default"].dirname(jsonFile));
123
- return result;
54
+ async function patchGeneratedApiDocs(absoluteApiDir) {
55
+ await patchLinks(absoluteApiDir);
56
+ await __WEBPACK_EXTERNAL_MODULE__rspress_shared_fs_extra__["default"].rename(__WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(absoluteApiDir, 'README.md'), __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(absoluteApiDir, 'index.md'));
57
+ await generateMetaJson(absoluteApiDir);
124
58
  }
125
59
  function pluginTypeDoc(options) {
126
60
  let docRoot;
127
61
  const { entryPoints = [], outDir = API_DIR } = options;
62
+ const apiPageRoute = `/${outDir.replace(/(^\/)|(\/$)/, '')}/`; // e.g: /api/
128
63
  return {
129
64
  name: '@rspress/plugin-typedoc',
130
65
  async addPages () {
131
66
  return [
132
67
  {
133
- routePath: `${outDir.replace(/\/$/, '')}/`,
68
+ routePath: apiPageRoute,
134
69
  filepath: __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(docRoot, outDir, 'README.md')
135
70
  }
136
71
  ];
@@ -162,34 +97,34 @@ function pluginTypeDoc(options) {
162
97
  });
163
98
  const project = app.convert();
164
99
  if (project) {
165
- // 1. Generate module doc by typedoc
166
- const absoluteOutputdir = __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(docRoot, outDir);
167
- await app.generateDocs(project, absoluteOutputdir);
168
- const jsonDir = __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(absoluteOutputdir, 'documentation.json');
169
- await app.generateJson(project, jsonDir);
170
- // 2. Generate sidebar
100
+ // 1. Generate doc/api, doc/api/_meta.json by typedoc
101
+ const absoluteApiDir = __WEBPACK_EXTERNAL_MODULE_node_path__["default"].join(docRoot, outDir);
102
+ await app.generateDocs(project, absoluteApiDir);
103
+ await patchGeneratedApiDocs(absoluteApiDir);
104
+ // 2. Generate "api" nav bar
171
105
  config.themeConfig = config.themeConfig || {};
172
106
  config.themeConfig.nav = config.themeConfig.nav || [];
173
- const apiIndexLink = `/${outDir.replace(/(^\/)|(\/$)/, '')}/`;
174
107
  const { nav } = config.themeConfig;
108
+ // avoid that user config "api" in doc/_meta.json
109
+ function isApiAlreadyInNav(navList) {
110
+ return navList.some((item)=>{
111
+ if ('link' in item && 'string' == typeof item.link && item.link.startsWith(apiPageRoute.slice(0, apiPageRoute.length - 1))) return true;
112
+ return false;
113
+ });
114
+ }
175
115
  // Note: TypeDoc does not support i18n
176
- if (Array.isArray(nav)) nav.push({
177
- text: 'API',
178
- link: apiIndexLink
179
- });
180
- else if ('default' in nav) nav.default.push({
181
- text: 'API',
182
- link: apiIndexLink
183
- });
184
- config.themeConfig.sidebar = config.themeConfig.sidebar || {};
185
- config.themeConfig.sidebar[apiIndexLink] = entryPoints.length > 1 ? await resolveSidebarForMultiEntry(jsonDir) : await resolveSidebarForSingleEntry(jsonDir);
186
- config.themeConfig.sidebar[apiIndexLink].unshift({
187
- text: 'Overview',
188
- link: `${apiIndexLink}README`
189
- });
116
+ if (Array.isArray(nav)) {
117
+ if (!isApiAlreadyInNav(nav)) nav.push({
118
+ text: 'API',
119
+ link: apiPageRoute
120
+ });
121
+ } else if ('default' in nav) {
122
+ if (!isApiAlreadyInNav(nav.default)) nav.default.push({
123
+ text: 'API',
124
+ link: apiPageRoute
125
+ });
126
+ }
190
127
  }
191
- config.route = config.route || {};
192
- config.route.exclude = config.route.exclude || [];
193
128
  return config;
194
129
  }
195
130
  };
package/dist/lib/index.js CHANGED
@@ -55,136 +55,71 @@ var external_node_path_default = /*#__PURE__*/ __webpack_require__.n(external_no
55
55
  const external_typedoc_namespaceObject = require("typedoc");
56
56
  const external_typedoc_plugin_markdown_namespaceObject = require("typedoc-plugin-markdown");
57
57
  const API_DIR = 'api';
58
- const ROUTE_PREFIX = `/${API_DIR}`;
59
58
  const fs_extra_namespaceObject = require("@rspress/shared/fs-extra");
60
59
  var fs_extra_default = /*#__PURE__*/ __webpack_require__.n(fs_extra_namespaceObject);
61
- function transformModuleName(name) {
62
- return name.replace(/\//g, '_').replace(/-/g, '_');
63
- }
64
60
  async function patchLinks(outputDir) {
65
61
  // Patch links in markdown files
66
62
  // Scan all the markdown files in the output directory
67
- // replace [foo](bar) -> [foo](./bar)
63
+ // replace
64
+ // 1. [foo](bar) -> [foo](./bar)
65
+ // 2. [foo](./bar) -> [foo](./bar) no change
68
66
  const normlizeLinksInFile = async (filePath)=>{
69
67
  const content = await fs_extra_default().readFile(filePath, 'utf-8');
70
- // replace: [foo](bar) -> [foo](./bar)
71
- const newContent = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, p1, p2)=>`[${p1}](./${p2})`);
68
+ // 1. [foo](bar) -> [foo](./bar)
69
+ const newContent = content.replace(/\[([^\]]+)\]\(([^)]+)\)/g, (_match, p1, p2)=>{
70
+ // 2. [foo](./bar) -> [foo](./bar) no change
71
+ if ([
72
+ '/',
73
+ '.'
74
+ ].includes(p2[0])) return `[${p1}](${p2})`;
75
+ return `[${p1}](./${p2})`;
76
+ });
72
77
  await fs_extra_default().writeFile(filePath, newContent);
73
78
  };
74
79
  const traverse = async (dir)=>{
75
80
  const files = await fs_extra_default().readdir(dir);
76
- for (const file of files){
77
- const filePath = external_node_path_default().join(dir, file);
78
- const stat = await fs_extra_default().stat(filePath);
79
- if (stat.isDirectory()) await traverse(filePath);
80
- else if (stat.isFile() && /\.mdx?/.test(file)) await normlizeLinksInFile(filePath);
81
- }
81
+ const filePaths = files.map((file)=>external_node_path_default().join(dir, file));
82
+ const stats = await Promise.all(filePaths.map((fp)=>fs_extra_default().stat(fp)));
83
+ await Promise.all(stats.map((stat, index)=>{
84
+ const file = files[index];
85
+ const filePath = filePaths[index];
86
+ if (stat.isDirectory()) return traverse(filePath);
87
+ if (stat.isFile() && /\.mdx?/.test(file)) return normlizeLinksInFile(filePath);
88
+ }));
82
89
  };
83
90
  await traverse(outputDir);
84
91
  }
85
- async function resolveSidebarForSingleEntry(jsonFile) {
86
- const result = [];
87
- const data = JSON.parse(await fs_extra_default().readFile(jsonFile, 'utf-8'));
88
- if (!data.children || data.children.length <= 0) return [];
89
- const symbolMap = new Map();
90
- data.groups.forEach((group)=>{
91
- const groupItem = {
92
- text: group.title,
93
- items: []
94
- };
95
- group.children.forEach((id)=>{
96
- const dataItem = data.children.find((item)=>item.id === id);
97
- if (dataItem) {
98
- // Note: we should handle the case that classes and interfaces have the same name
99
- // Such as class `Env` and variable `env`
100
- // The final output file name should be `classes/Env.md` and `variables/env-1.md`
101
- let fileName = dataItem.name;
102
- if (symbolMap.has(dataItem.name)) {
103
- const count = symbolMap.get(dataItem.name) + 1;
104
- symbolMap.set(dataItem.name, count);
105
- fileName = `${dataItem.name}-${count}`;
106
- } else symbolMap.set(dataItem.name.toLocaleLowerCase(), 0);
107
- groupItem.items.push({
108
- text: dataItem.name,
109
- link: `${ROUTE_PREFIX}/${group.title.toLocaleLowerCase()}/${fileName}`
110
- });
111
- }
112
- });
113
- result.push(groupItem);
114
- });
115
- await patchLinks(external_node_path_default().dirname(jsonFile));
116
- return result;
92
+ async function generateMetaJson(absoluteApiDir) {
93
+ const metaJsonPath = external_node_path_default().join(absoluteApiDir, '_meta.json');
94
+ const files = await fs_extra_default().readdir(absoluteApiDir);
95
+ const filePaths = files.map((file)=>external_node_path_default().join(absoluteApiDir, file));
96
+ const stats = await Promise.all(filePaths.map((fp)=>fs_extra_default().stat(fp)));
97
+ const dirs = stats.map((stat, index)=>stat.isDirectory() ? files[index] : null).filter(Boolean);
98
+ const meta = dirs.map((dir)=>({
99
+ type: 'dir',
100
+ label: dir.slice(0, 1).toUpperCase() + dir.slice(1),
101
+ name: dir
102
+ }));
103
+ await fs_extra_default().writeFile(metaJsonPath, JSON.stringify([
104
+ 'index',
105
+ ...meta
106
+ ]));
117
107
  }
118
- async function resolveSidebarForMultiEntry(jsonFile) {
119
- const result = [];
120
- const data = JSON.parse(await fs_extra_default().readFile(jsonFile, 'utf-8'));
121
- if (!data.children || data.children.length <= 0) return result;
122
- function getModulePath(name) {
123
- return external_node_path_default().join(`${ROUTE_PREFIX}/modules`, `${transformModuleName(name)}`).replace(/\\/g, '/');
124
- }
125
- function getClassPath(moduleName, className) {
126
- return external_node_path_default().join(`${ROUTE_PREFIX}/classes`, `${transformModuleName(moduleName)}.${className}`).replace(/\\/g, '/');
127
- }
128
- function getInterfacePath(moduleName, interfaceName) {
129
- return external_node_path_default().join(`${ROUTE_PREFIX}/interfaces`, `${transformModuleName(moduleName)}.${interfaceName}`).replace(/\\/g, '/');
130
- }
131
- function getFunctionPath(moduleName, functionName) {
132
- return external_node_path_default().join(`${ROUTE_PREFIX}/functions`, `${transformModuleName(moduleName)}.${functionName}`).replace(/\\/g, '/');
133
- }
134
- data.children.forEach((module)=>{
135
- const moduleNames = module.name.split('/');
136
- const name = moduleNames[moduleNames.length - 1];
137
- const moduleConfig = {
138
- text: `Module:${name}`,
139
- items: [
140
- {
141
- text: 'Overview',
142
- link: getModulePath(module.name)
143
- }
144
- ]
145
- };
146
- if (!module.children || module.children.length <= 0) return;
147
- module.children.forEach((sub)=>{
148
- var _module_groups_find;
149
- const kindString = null === (_module_groups_find = module.groups.find((item)=>item.children.includes(sub.id))) || void 0 === _module_groups_find ? void 0 : _module_groups_find.title.slice(0, -1);
150
- if (!kindString) return;
151
- switch(kindString){
152
- case 'Class':
153
- moduleConfig.items.push({
154
- text: `Class:${sub.name}`,
155
- link: getClassPath(module.name, sub.name)
156
- });
157
- break;
158
- case 'Interface':
159
- moduleConfig.items.push({
160
- text: `Interface:${sub.name}`,
161
- link: getInterfacePath(module.name, sub.name)
162
- });
163
- break;
164
- case 'Function':
165
- moduleConfig.items.push({
166
- text: `Function:${sub.name}`,
167
- link: getFunctionPath(module.name, sub.name)
168
- });
169
- break;
170
- default:
171
- break;
172
- }
173
- });
174
- result.push(moduleConfig);
175
- });
176
- await patchLinks(external_node_path_default().dirname(jsonFile));
177
- return result;
108
+ async function patchGeneratedApiDocs(absoluteApiDir) {
109
+ await patchLinks(absoluteApiDir);
110
+ await fs_extra_default().rename(external_node_path_default().join(absoluteApiDir, 'README.md'), external_node_path_default().join(absoluteApiDir, 'index.md'));
111
+ await generateMetaJson(absoluteApiDir);
178
112
  }
179
113
  function pluginTypeDoc(options) {
180
114
  let docRoot;
181
115
  const { entryPoints = [], outDir = API_DIR } = options;
116
+ const apiPageRoute = `/${outDir.replace(/(^\/)|(\/$)/, '')}/`; // e.g: /api/
182
117
  return {
183
118
  name: '@rspress/plugin-typedoc',
184
119
  async addPages () {
185
120
  return [
186
121
  {
187
- routePath: `${outDir.replace(/\/$/, '')}/`,
122
+ routePath: apiPageRoute,
188
123
  filepath: external_node_path_default().join(docRoot, outDir, 'README.md')
189
124
  }
190
125
  ];
@@ -216,34 +151,34 @@ function pluginTypeDoc(options) {
216
151
  });
217
152
  const project = app.convert();
218
153
  if (project) {
219
- // 1. Generate module doc by typedoc
220
- const absoluteOutputdir = external_node_path_default().join(docRoot, outDir);
221
- await app.generateDocs(project, absoluteOutputdir);
222
- const jsonDir = external_node_path_default().join(absoluteOutputdir, 'documentation.json');
223
- await app.generateJson(project, jsonDir);
224
- // 2. Generate sidebar
154
+ // 1. Generate doc/api, doc/api/_meta.json by typedoc
155
+ const absoluteApiDir = external_node_path_default().join(docRoot, outDir);
156
+ await app.generateDocs(project, absoluteApiDir);
157
+ await patchGeneratedApiDocs(absoluteApiDir);
158
+ // 2. Generate "api" nav bar
225
159
  config.themeConfig = config.themeConfig || {};
226
160
  config.themeConfig.nav = config.themeConfig.nav || [];
227
- const apiIndexLink = `/${outDir.replace(/(^\/)|(\/$)/, '')}/`;
228
161
  const { nav } = config.themeConfig;
162
+ // avoid that user config "api" in doc/_meta.json
163
+ function isApiAlreadyInNav(navList) {
164
+ return navList.some((item)=>{
165
+ if ('link' in item && 'string' == typeof item.link && item.link.startsWith(apiPageRoute.slice(0, apiPageRoute.length - 1))) return true;
166
+ return false;
167
+ });
168
+ }
229
169
  // Note: TypeDoc does not support i18n
230
- if (Array.isArray(nav)) nav.push({
231
- text: 'API',
232
- link: apiIndexLink
233
- });
234
- else if ('default' in nav) nav.default.push({
235
- text: 'API',
236
- link: apiIndexLink
237
- });
238
- config.themeConfig.sidebar = config.themeConfig.sidebar || {};
239
- config.themeConfig.sidebar[apiIndexLink] = entryPoints.length > 1 ? await resolveSidebarForMultiEntry(jsonDir) : await resolveSidebarForSingleEntry(jsonDir);
240
- config.themeConfig.sidebar[apiIndexLink].unshift({
241
- text: 'Overview',
242
- link: `${apiIndexLink}README`
243
- });
170
+ if (Array.isArray(nav)) {
171
+ if (!isApiAlreadyInNav(nav)) nav.push({
172
+ text: 'API',
173
+ link: apiPageRoute
174
+ });
175
+ } else if ('default' in nav) {
176
+ if (!isApiAlreadyInNav(nav.default)) nav.default.push({
177
+ text: 'API',
178
+ link: apiPageRoute
179
+ });
180
+ }
244
181
  }
245
- config.route = config.route || {};
246
- config.route.exclude = config.route.exclude || [];
247
182
  return config;
248
183
  }
249
184
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rspress/plugin-typedoc",
3
- "version": "0.0.0-nightly-20241127160234",
3
+ "version": "0.0.0-nightly-20241128160245",
4
4
  "description": "A plugin for rspress to integrate typedoc",
5
5
  "bugs": "https://github.com/web-infra-dev/rspress/issues",
6
6
  "repository": {
@@ -17,9 +17,9 @@
17
17
  "node": ">=14.17.6"
18
18
  },
19
19
  "devDependencies": {
20
- "@modern-js/tsconfig": "2.62.1",
21
- "@rslib/core": "0.1.0",
22
20
  "@microsoft/api-extractor": "^7.48.0",
21
+ "@modern-js/tsconfig": "2.63.0",
22
+ "@rslib/core": "0.1.0",
23
23
  "@types/node": "^18.11.17",
24
24
  "@types/react": "^18.3.12",
25
25
  "@types/react-dom": "^18.3.1",
@@ -28,7 +28,7 @@
28
28
  "vitest": "2.1.5"
29
29
  },
30
30
  "peerDependencies": {
31
- "rspress": "0.0.0-nightly-20241127160234"
31
+ "rspress": "0.0.0-nightly-20241128160245"
32
32
  },
33
33
  "sideEffects": [
34
34
  "*.css",
@@ -48,7 +48,7 @@
48
48
  "dependencies": {
49
49
  "typedoc": "0.24.8",
50
50
  "typedoc-plugin-markdown": "3.17.1",
51
- "@rspress/shared": "0.0.0-nightly-20241127160234"
51
+ "@rspress/shared": "0.0.0-nightly-20241128160245"
52
52
  },
53
53
  "scripts": {
54
54
  "dev": "rslib build -w",
package/src/constants.ts CHANGED
@@ -1,2 +1 @@
1
1
  export const API_DIR = 'api';
2
- export const ROUTE_PREFIX = `/${API_DIR}`;
package/src/index.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { Application, TSConfigReader } from 'typedoc';
3
- import type { RspressPlugin } from '@rspress/shared';
3
+ import type { NavItem, RspressPlugin } from '@rspress/shared';
4
4
  import { load } from 'typedoc-plugin-markdown';
5
5
  import { API_DIR } from './constants';
6
- import {
7
- resolveSidebarForMultiEntry,
8
- resolveSidebarForSingleEntry,
9
- } from './sidebar';
6
+ import { patchGeneratedApiDocs } from './patch';
10
7
 
11
8
  export interface PluginTypeDocOptions {
12
9
  /**
@@ -24,12 +21,13 @@ export interface PluginTypeDocOptions {
24
21
  export function pluginTypeDoc(options: PluginTypeDocOptions): RspressPlugin {
25
22
  let docRoot: string | undefined;
26
23
  const { entryPoints = [], outDir = API_DIR } = options;
24
+ const apiPageRoute = `/${outDir.replace(/(^\/)|(\/$)/, '')}/`; // e.g: /api/
27
25
  return {
28
26
  name: '@rspress/plugin-typedoc',
29
27
  async addPages() {
30
28
  return [
31
29
  {
32
- routePath: `${outDir.replace(/\/$/, '')}/`,
30
+ routePath: apiPageRoute,
33
31
  filepath: path.join(docRoot!, outDir, 'README.md'),
34
32
  },
35
33
  ];
@@ -56,41 +54,49 @@ export function pluginTypeDoc(options: PluginTypeDocOptions): RspressPlugin {
56
54
  const project = app.convert();
57
55
 
58
56
  if (project) {
59
- // 1. Generate module doc by typedoc
60
- const absoluteOutputdir = path.join(docRoot!, outDir);
61
- await app.generateDocs(project, absoluteOutputdir);
62
- const jsonDir = path.join(absoluteOutputdir, 'documentation.json');
63
- await app.generateJson(project, jsonDir);
64
- // 2. Generate sidebar
57
+ // 1. Generate doc/api, doc/api/_meta.json by typedoc
58
+ const absoluteApiDir = path.join(docRoot!, outDir);
59
+ await app.generateDocs(project, absoluteApiDir);
60
+ await patchGeneratedApiDocs(absoluteApiDir);
61
+
62
+ // 2. Generate "api" nav bar
65
63
  config.themeConfig = config.themeConfig || {};
66
64
  config.themeConfig.nav = config.themeConfig.nav || [];
67
- const apiIndexLink = `/${outDir.replace(/(^\/)|(\/$)/, '')}/`;
68
65
  const { nav } = config.themeConfig;
66
+
67
+ // avoid that user config "api" in doc/_meta.json
68
+ function isApiAlreadyInNav(navList: NavItem[]) {
69
+ return navList.some(item => {
70
+ if (
71
+ 'link' in item &&
72
+ typeof item.link === 'string' &&
73
+ item.link.startsWith(
74
+ apiPageRoute.slice(0, apiPageRoute.length - 1), // /api
75
+ )
76
+ ) {
77
+ return true;
78
+ }
79
+ return false;
80
+ });
81
+ }
82
+
69
83
  // Note: TypeDoc does not support i18n
70
84
  if (Array.isArray(nav)) {
71
- nav.push({
72
- text: 'API',
73
- link: apiIndexLink,
74
- });
85
+ if (!isApiAlreadyInNav(nav)) {
86
+ nav.push({
87
+ text: 'API',
88
+ link: apiPageRoute,
89
+ });
90
+ }
75
91
  } else if ('default' in nav) {
76
- nav.default.push({
77
- text: 'API',
78
- link: apiIndexLink,
79
- });
92
+ if (!isApiAlreadyInNav(nav.default)) {
93
+ nav.default.push({
94
+ text: 'API',
95
+ link: apiPageRoute,
96
+ });
97
+ }
80
98
  }
81
-
82
- config.themeConfig.sidebar = config.themeConfig.sidebar || {};
83
- config.themeConfig.sidebar[apiIndexLink] =
84
- entryPoints.length > 1
85
- ? await resolveSidebarForMultiEntry(jsonDir)
86
- : await resolveSidebarForSingleEntry(jsonDir);
87
- config.themeConfig.sidebar[apiIndexLink].unshift({
88
- text: 'Overview',
89
- link: `${apiIndexLink}README`,
90
- });
91
99
  }
92
- config.route = config.route || {};
93
- config.route.exclude = config.route.exclude || [];
94
100
  return config;
95
101
  },
96
102
  };
package/src/patch.ts ADDED
@@ -0,0 +1,72 @@
1
+ import fs from '@rspress/shared/fs-extra';
2
+ import path from 'node:path';
3
+
4
+ async function patchLinks(outputDir: string) {
5
+ // Patch links in markdown files
6
+ // Scan all the markdown files in the output directory
7
+ // replace
8
+ // 1. [foo](bar) -> [foo](./bar)
9
+ // 2. [foo](./bar) -> [foo](./bar) no change
10
+ const normlizeLinksInFile = async (filePath: string) => {
11
+ const content = await fs.readFile(filePath, 'utf-8');
12
+ // 1. [foo](bar) -> [foo](./bar)
13
+ const newContent = content.replace(
14
+ /\[([^\]]+)\]\(([^)]+)\)/g,
15
+ (_match, p1, p2) => {
16
+ // 2. [foo](./bar) -> [foo](./bar) no change
17
+ if (['/', '.'].includes(p2[0])) {
18
+ return `[${p1}](${p2})`;
19
+ }
20
+ return `[${p1}](./${p2})`;
21
+ },
22
+ );
23
+ await fs.writeFile(filePath, newContent);
24
+ };
25
+
26
+ const traverse = async (dir: string) => {
27
+ const files = await fs.readdir(dir);
28
+ const filePaths = files.map(file => path.join(dir, file));
29
+ const stats = await Promise.all(filePaths.map(fp => fs.stat(fp)));
30
+
31
+ await Promise.all(
32
+ stats.map((stat, index) => {
33
+ const file = files[index];
34
+ const filePath = filePaths[index];
35
+ if (stat.isDirectory()) {
36
+ return traverse(filePath);
37
+ }
38
+ if (stat.isFile() && /\.mdx?/.test(file)) {
39
+ return normlizeLinksInFile(filePath);
40
+ }
41
+ }),
42
+ );
43
+ };
44
+ await traverse(outputDir);
45
+ }
46
+
47
+ async function generateMetaJson(absoluteApiDir: string) {
48
+ const metaJsonPath = path.join(absoluteApiDir, '_meta.json');
49
+
50
+ const files = await fs.readdir(absoluteApiDir);
51
+ const filePaths = files.map(file => path.join(absoluteApiDir, file));
52
+ const stats = await Promise.all(filePaths.map(fp => fs.stat(fp)));
53
+ const dirs = stats
54
+ .map((stat, index) => (stat.isDirectory() ? files[index] : null))
55
+ .filter(Boolean) as string[];
56
+
57
+ const meta = dirs.map(dir => ({
58
+ type: 'dir',
59
+ label: dir.slice(0, 1).toUpperCase() + dir.slice(1),
60
+ name: dir,
61
+ }));
62
+ await fs.writeFile(metaJsonPath, JSON.stringify(['index', ...meta]));
63
+ }
64
+
65
+ export async function patchGeneratedApiDocs(absoluteApiDir: string) {
66
+ await patchLinks(absoluteApiDir);
67
+ await fs.rename(
68
+ path.join(absoluteApiDir, 'README.md'),
69
+ path.join(absoluteApiDir, 'index.md'),
70
+ );
71
+ await generateMetaJson(absoluteApiDir);
72
+ }
package/src/sidebar.ts DELETED
@@ -1,179 +0,0 @@
1
- import path from 'node:path';
2
- import fs from '@rspress/shared/fs-extra';
3
- import type { SidebarGroup } from '@rspress/shared';
4
- import { transformModuleName } from './utils';
5
- import { ROUTE_PREFIX } from './constants';
6
-
7
- interface ModuleItem {
8
- id: number;
9
- name: string;
10
- children: {
11
- id: number;
12
- name: string;
13
- }[];
14
- groups: {
15
- title: string;
16
- children: number[];
17
- }[];
18
- }
19
-
20
- async function patchLinks(outputDir: string) {
21
- // Patch links in markdown files
22
- // Scan all the markdown files in the output directory
23
- // replace [foo](bar) -> [foo](./bar)
24
- const normlizeLinksInFile = async (filePath: string) => {
25
- const content = await fs.readFile(filePath, 'utf-8');
26
- // replace: [foo](bar) -> [foo](./bar)
27
- const newContent = content.replace(
28
- /\[([^\]]+)\]\(([^)]+)\)/g,
29
- (_match, p1, p2) => {
30
- return `[${p1}](./${p2})`;
31
- },
32
- );
33
- await fs.writeFile(filePath, newContent);
34
- };
35
-
36
- const traverse = async (dir: string) => {
37
- const files = await fs.readdir(dir);
38
- for (const file of files) {
39
- const filePath = path.join(dir, file);
40
- const stat = await fs.stat(filePath);
41
- if (stat.isDirectory()) {
42
- await traverse(filePath);
43
- } else if (stat.isFile() && /\.mdx?/.test(file)) {
44
- await normlizeLinksInFile(filePath);
45
- }
46
- }
47
- };
48
- await traverse(outputDir);
49
- }
50
-
51
- export async function resolveSidebarForSingleEntry(
52
- jsonFile: string,
53
- ): Promise<SidebarGroup[]> {
54
- const result: SidebarGroup[] = [];
55
- const data = JSON.parse(await fs.readFile(jsonFile, 'utf-8'));
56
- if (!data.children || data.children.length <= 0) {
57
- return [];
58
- }
59
- const symbolMap = new Map<string, number>();
60
- data.groups.forEach((group: { title: string; children: number[] }) => {
61
- const groupItem: SidebarGroup = {
62
- text: group.title,
63
- items: [],
64
- };
65
- group.children.forEach((id: number) => {
66
- const dataItem = data.children.find((item: ModuleItem) => item.id === id);
67
- if (dataItem) {
68
- // Note: we should handle the case that classes and interfaces have the same name
69
- // Such as class `Env` and variable `env`
70
- // The final output file name should be `classes/Env.md` and `variables/env-1.md`
71
- let fileName = dataItem.name;
72
- if (symbolMap.has(dataItem.name)) {
73
- const count = symbolMap.get(dataItem.name)! + 1;
74
- symbolMap.set(dataItem.name, count);
75
- fileName = `${dataItem.name}-${count}`;
76
- } else {
77
- symbolMap.set(dataItem.name.toLocaleLowerCase(), 0);
78
- }
79
- groupItem.items.push({
80
- text: dataItem.name,
81
- link: `${ROUTE_PREFIX}/${group.title.toLocaleLowerCase()}/${fileName}`,
82
- });
83
- }
84
- });
85
- result.push(groupItem);
86
- });
87
-
88
- await patchLinks(path.dirname(jsonFile));
89
-
90
- return result;
91
- }
92
-
93
- export async function resolveSidebarForMultiEntry(
94
- jsonFile: string,
95
- ): Promise<SidebarGroup[]> {
96
- const result: SidebarGroup[] = [];
97
- const data = JSON.parse(await fs.readFile(jsonFile, 'utf-8'));
98
- if (!data.children || data.children.length <= 0) {
99
- return result;
100
- }
101
-
102
- function getModulePath(name: string) {
103
- return path
104
- .join(`${ROUTE_PREFIX}/modules`, `${transformModuleName(name)}`)
105
- .replace(/\\/g, '/');
106
- }
107
-
108
- function getClassPath(moduleName: string, className: string) {
109
- return path
110
- .join(
111
- `${ROUTE_PREFIX}/classes`,
112
- `${transformModuleName(moduleName)}.${className}`,
113
- )
114
- .replace(/\\/g, '/');
115
- }
116
-
117
- function getInterfacePath(moduleName: string, interfaceName: string) {
118
- return path
119
- .join(
120
- `${ROUTE_PREFIX}/interfaces`,
121
- `${transformModuleName(moduleName)}.${interfaceName}`,
122
- )
123
- .replace(/\\/g, '/');
124
- }
125
-
126
- function getFunctionPath(moduleName: string, functionName: string) {
127
- return path
128
- .join(
129
- `${ROUTE_PREFIX}/functions`,
130
- `${transformModuleName(moduleName)}.${functionName}`,
131
- )
132
- .replace(/\\/g, '/');
133
- }
134
-
135
- data.children.forEach((module: ModuleItem) => {
136
- const moduleNames = module.name.split('/');
137
- const name = moduleNames[moduleNames.length - 1];
138
- const moduleConfig = {
139
- text: `Module:${name}`,
140
- items: [{ text: 'Overview', link: getModulePath(module.name) }],
141
- };
142
- if (!module.children || module.children.length <= 0) {
143
- return;
144
- }
145
- module.children.forEach(sub => {
146
- const kindString = module.groups
147
- .find(item => item.children.includes(sub.id))
148
- ?.title.slice(0, -1);
149
- if (!kindString) {
150
- return;
151
- }
152
- switch (kindString) {
153
- case 'Class':
154
- moduleConfig.items.push({
155
- text: `Class:${sub.name}`,
156
- link: getClassPath(module.name, sub.name),
157
- });
158
- break;
159
- case 'Interface':
160
- moduleConfig.items.push({
161
- text: `Interface:${sub.name}`,
162
- link: getInterfacePath(module.name, sub.name),
163
- });
164
- break;
165
- case 'Function':
166
- moduleConfig.items.push({
167
- text: `Function:${sub.name}`,
168
- link: getFunctionPath(module.name, sub.name),
169
- });
170
- break;
171
- default:
172
- break;
173
- }
174
- });
175
- result.push(moduleConfig);
176
- });
177
- await patchLinks(path.dirname(jsonFile));
178
- return result;
179
- }