@xyd-js/plugin-docs 0.0.0-build
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/index.d.ts +50 -0
- package/dist/index.js +4608 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
- package/src/const.ts +7 -0
- package/src/declarations.d.ts +29 -0
- package/src/index.ts +386 -0
- package/src/pages/context.tsx +9 -0
- package/src/pages/layout.tsx +340 -0
- package/src/pages/metatags.ts +96 -0
- package/src/pages/page.tsx +463 -0
- package/src/presets/docs/index.ts +317 -0
- package/src/presets/docs/settings.ts +262 -0
- package/src/presets/graphql/index.ts +69 -0
- package/src/presets/openapi/index.ts +66 -0
- package/src/presets/sources/index.ts +74 -0
- package/src/presets/uniform/index.ts +836 -0
- package/src/types.ts +40 -0
- package/src/utils.ts +19 -0
|
@@ -0,0 +1,836 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
import { promises as fs } from "fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
import matterStringify from "gray-matter/lib/stringify";
|
|
6
|
+
import { Plugin as VitePlugin } from "vite"
|
|
7
|
+
import { route } from "@react-router/dev/routes";
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Settings,
|
|
11
|
+
APIFile,
|
|
12
|
+
Sidebar,
|
|
13
|
+
SidebarRoute,
|
|
14
|
+
Metadata
|
|
15
|
+
} from "@xyd-js/core";
|
|
16
|
+
import uniform, {
|
|
17
|
+
pluginNavigation,
|
|
18
|
+
Reference,
|
|
19
|
+
ReferenceType,
|
|
20
|
+
OpenAPIReferenceContext,
|
|
21
|
+
GraphQLReferenceContext
|
|
22
|
+
} from "@xyd-js/uniform";
|
|
23
|
+
import { uniformPluginXDocsSidebar } from "@xyd-js/openapi";
|
|
24
|
+
|
|
25
|
+
import { Preset, PresetData } from "../../types";
|
|
26
|
+
|
|
27
|
+
import { createRequire } from 'module';
|
|
28
|
+
import { VIRTUAL_CONTENT_FOLDER } from "../../const";
|
|
29
|
+
import { getHostPath } from "../../utils";
|
|
30
|
+
|
|
31
|
+
const require = createRequire(import.meta.url);
|
|
32
|
+
const matter = require('gray-matter'); // TODO: !!! BETTER SOLUTION !!!
|
|
33
|
+
|
|
34
|
+
export async function ensureAndCleanupVirtualFolder() {
|
|
35
|
+
try {
|
|
36
|
+
// Create directory recursively if it doesn't exist
|
|
37
|
+
await fs.mkdir(VIRTUAL_CONTENT_FOLDER, { recursive: true });
|
|
38
|
+
|
|
39
|
+
// Read all files and directories in the folder
|
|
40
|
+
const entries = await fs.readdir(VIRTUAL_CONTENT_FOLDER, { withFileTypes: true });
|
|
41
|
+
|
|
42
|
+
// Delete each entry recursively
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
const fullPath = path.join(VIRTUAL_CONTENT_FOLDER, entry.name);
|
|
45
|
+
if (entry.isDirectory()) {
|
|
46
|
+
await fs.rm(fullPath, { recursive: true, force: true });
|
|
47
|
+
} else {
|
|
48
|
+
await fs.unlink(fullPath);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
console.error('Error managing virtual folder:', error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// TODO: !!!!! REFACTOR PLUGIN-ZERO AND ITS DEPS FOR MORE READABLE CODE AND BETTER API !!!!
|
|
57
|
+
|
|
58
|
+
export interface uniformPresetOptions {
|
|
59
|
+
urlPrefix?: string
|
|
60
|
+
sourceTheme?: boolean
|
|
61
|
+
disableFSWrite?: boolean
|
|
62
|
+
fileRouting?: { [key: string]: string }
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function flatPages(
|
|
66
|
+
sidebar: (SidebarRoute | Sidebar)[],
|
|
67
|
+
groups: { [key: string]: string },
|
|
68
|
+
resp: string[] = [],
|
|
69
|
+
) {
|
|
70
|
+
sidebar.map(async side => {
|
|
71
|
+
if ("route" in side) {
|
|
72
|
+
side?.pages.map(item => {
|
|
73
|
+
return flatPages([item], groups, resp)
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (groups[side.group || ""]) {
|
|
80
|
+
const link = groups[side.group || ""]
|
|
81
|
+
|
|
82
|
+
resp.push(link)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
side?.pages?.map(async page => {
|
|
86
|
+
if (typeof page === "string") {
|
|
87
|
+
resp.push(page)
|
|
88
|
+
return
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ("virtual" in page) {
|
|
92
|
+
resp.push(page.virtual)
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return flatPages([page], groups, resp)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
return resp
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function flatGroups(
|
|
104
|
+
sidebar: (SidebarRoute | Sidebar)[],
|
|
105
|
+
resp: { [key: string]: string } = {}
|
|
106
|
+
) {
|
|
107
|
+
sidebar.map(async side => {
|
|
108
|
+
if ("route" in side) {
|
|
109
|
+
side?.pages.map(item => {
|
|
110
|
+
return flatGroups([item], resp)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (side.group) {
|
|
117
|
+
if (resp[side.group]) {
|
|
118
|
+
console.error('group already exists', side.group)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const first = side?.pages?.[0]
|
|
122
|
+
if (first && typeof first === "string") {
|
|
123
|
+
const chunks = first.split("/")
|
|
124
|
+
chunks[chunks.length - 1] = side.group || ""
|
|
125
|
+
const groupLink = chunks.join("/")
|
|
126
|
+
|
|
127
|
+
resp[side.group] = groupLink
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
side?.pages?.map(async page => {
|
|
132
|
+
if (typeof page === "string") {
|
|
133
|
+
return
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if ("virtual" in page) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return flatGroups([page], resp)
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return resp
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function uniformSidebarLevelMap(pages: string[]) {
|
|
148
|
+
const out = {};
|
|
149
|
+
let level = 0;
|
|
150
|
+
|
|
151
|
+
function recursive(items: string[]) {
|
|
152
|
+
for (const item of items) {
|
|
153
|
+
out[item] = level++;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
recursive(pages);
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Helper function to read markdown files with support for both .mdx and .md extensions
|
|
162
|
+
async function readMarkdownFile(root: string, page: string): Promise<string> {
|
|
163
|
+
try {
|
|
164
|
+
// Try .mdx first
|
|
165
|
+
return await fs.readFile(path.join(root, page + '.mdx'), "utf-8");
|
|
166
|
+
} catch (e) {
|
|
167
|
+
// If .mdx fails, try .md
|
|
168
|
+
try {
|
|
169
|
+
return await fs.readFile(path.join(root, page + '.md'), "utf-8");
|
|
170
|
+
} catch (e) {
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return ""
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function uniformResolver(
|
|
178
|
+
settings: Settings,
|
|
179
|
+
root: string,
|
|
180
|
+
matchRoute: string,
|
|
181
|
+
apiFile: string,
|
|
182
|
+
uniformApiResolver: (filePath: string) => Promise<Reference[]>,
|
|
183
|
+
sidebar?: (SidebarRoute | Sidebar)[],
|
|
184
|
+
options?: uniformPresetOptions,
|
|
185
|
+
uniformType?: UniformType,
|
|
186
|
+
disableFSWrite?: boolean
|
|
187
|
+
) {
|
|
188
|
+
let urlPrefix = ""
|
|
189
|
+
|
|
190
|
+
if (matchRoute && sidebar) {
|
|
191
|
+
sidebar.forEach((sidebar) => {
|
|
192
|
+
if ("route" in sidebar) {
|
|
193
|
+
if (sidebar.route === matchRoute) {
|
|
194
|
+
if (urlPrefix) {
|
|
195
|
+
throw new Error('multiple sidebars found for apiFile match')
|
|
196
|
+
}
|
|
197
|
+
urlPrefix = sidebar.route
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const resolvedApiFile = path.relative(process.cwd(), path.resolve(process.cwd(), apiFile))
|
|
204
|
+
const uniformRefs = await uniformApiResolver(resolvedApiFile)
|
|
205
|
+
const plugins = globalThis.__xydUserUniformVitePlugins || []
|
|
206
|
+
|
|
207
|
+
if (!urlPrefix && options?.fileRouting?.[resolvedApiFile]) {
|
|
208
|
+
matchRoute = options.fileRouting[resolvedApiFile]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (!urlPrefix && matchRoute) {
|
|
212
|
+
sidebar?.push({
|
|
213
|
+
route: matchRoute,
|
|
214
|
+
pages: []
|
|
215
|
+
})
|
|
216
|
+
urlPrefix = matchRoute
|
|
217
|
+
}
|
|
218
|
+
if (!urlPrefix && options?.urlPrefix) {
|
|
219
|
+
urlPrefix = options.urlPrefix
|
|
220
|
+
}
|
|
221
|
+
if (!urlPrefix) {
|
|
222
|
+
throw new Error('(uniformResolver): urlPrefix not found')
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (uniformType === "openapi") {
|
|
226
|
+
plugins.push(uniformPluginXDocsSidebar)
|
|
227
|
+
}
|
|
228
|
+
const uniformWithNavigation = uniform(uniformRefs, {
|
|
229
|
+
plugins: [
|
|
230
|
+
...plugins,
|
|
231
|
+
pluginNavigation(settings, {
|
|
232
|
+
urlPrefix,
|
|
233
|
+
}),
|
|
234
|
+
]
|
|
235
|
+
}) as {
|
|
236
|
+
references: Reference[];
|
|
237
|
+
out: {
|
|
238
|
+
sidebar: Sidebar[];
|
|
239
|
+
pageFrontMatter: Record<string, any>;
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
let pageLevels = {}
|
|
244
|
+
|
|
245
|
+
const uniformData = {
|
|
246
|
+
slugs: {},
|
|
247
|
+
data: [] as any[], // TODO: fix any
|
|
248
|
+
i: 0,
|
|
249
|
+
set: (slug, content: string, options = {}) => {
|
|
250
|
+
if (uniformData.slugs[slug]) {
|
|
251
|
+
console.error('slug already exists', slug)
|
|
252
|
+
}
|
|
253
|
+
// TODO: in the future custom sort
|
|
254
|
+
const level = pageLevels[slug]
|
|
255
|
+
|
|
256
|
+
uniformData.data[level] = {
|
|
257
|
+
slug,
|
|
258
|
+
content,
|
|
259
|
+
options
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let uniformSidebars: SidebarRoute[] = []
|
|
265
|
+
|
|
266
|
+
mergeSidebarsInPlace(sidebar as (SidebarRoute | Sidebar)[])
|
|
267
|
+
|
|
268
|
+
if (sidebar && matchRoute) {
|
|
269
|
+
// TODO: DRY
|
|
270
|
+
sidebar.forEach((sidebar) => {
|
|
271
|
+
if ("route" in sidebar) {
|
|
272
|
+
if (sidebar.route === matchRoute) {
|
|
273
|
+
uniformSidebars.push(sidebar)
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
mergeSidebarsInPlace(uniformSidebars);
|
|
279
|
+
|
|
280
|
+
if (uniformSidebars.length > 1) {
|
|
281
|
+
throw new Error('multiple sidebars found for uniform match')
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
{
|
|
286
|
+
const otherUniformPages = flatPages(uniformSidebars, {})
|
|
287
|
+
const groups = flatGroups(uniformWithNavigation.out.sidebar)
|
|
288
|
+
const flatUniformPages = [
|
|
289
|
+
...otherUniformPages,
|
|
290
|
+
...flatPages(
|
|
291
|
+
uniformWithNavigation.out.sidebar,
|
|
292
|
+
groups // TODO: we dont need groups - because it comes to structured page levels
|
|
293
|
+
),
|
|
294
|
+
]
|
|
295
|
+
|
|
296
|
+
pageLevels = uniformSidebarLevelMap(flatUniformPages)
|
|
297
|
+
|
|
298
|
+
{
|
|
299
|
+
// TODO: below should be inside uniform?
|
|
300
|
+
// TODO: custom `fn` logic?
|
|
301
|
+
await Promise.all(otherUniformPages.map(async (page) => {
|
|
302
|
+
const content = await readMarkdownFile(root, page);
|
|
303
|
+
uniformData.set(page, content + "\n");
|
|
304
|
+
}))
|
|
305
|
+
|
|
306
|
+
await Promise.all(Object.keys(groups).map(async (group) => {
|
|
307
|
+
try {
|
|
308
|
+
// TODO: only if `group_index`
|
|
309
|
+
const page = groups[group]
|
|
310
|
+
const content = await readMarkdownFile(root, page);
|
|
311
|
+
uniformData.set(page, content + "\n");
|
|
312
|
+
} catch (e) {
|
|
313
|
+
// Silently continue if file not found
|
|
314
|
+
}
|
|
315
|
+
}))
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
{
|
|
320
|
+
const routeFolder = path.join(root, matchRoute)
|
|
321
|
+
try {
|
|
322
|
+
await fs.access(routeFolder);
|
|
323
|
+
} catch {
|
|
324
|
+
await fs.mkdir(routeFolder, { recursive: true });
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
let composedFileMap: Record<string, string> = {}
|
|
329
|
+
if (!settings.engine?.uniform?.store) {
|
|
330
|
+
composedFileMap = await composeFileMap(root, matchRoute)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const basePath = settings.engine?.uniform?.store
|
|
334
|
+
? root
|
|
335
|
+
: path.join(root, VIRTUAL_CONTENT_FOLDER)
|
|
336
|
+
|
|
337
|
+
await Promise.all(
|
|
338
|
+
uniformWithNavigation.references.map(async (ref) => {
|
|
339
|
+
const byCanonical = path.join(urlPrefix, ref.canonical)
|
|
340
|
+
const mdPath = path.join(basePath, byCanonical + '.md')
|
|
341
|
+
|
|
342
|
+
const frontmatter = uniformWithNavigation.out.pageFrontMatter[byCanonical]
|
|
343
|
+
|
|
344
|
+
if (!frontmatter) {
|
|
345
|
+
console.error('frontmatter not found', byCanonical)
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let meta: Metadata = {
|
|
350
|
+
title: frontmatter.title,
|
|
351
|
+
layout: "wide"
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
// const mdFilePath = path.join(basePath, byCanonical)
|
|
356
|
+
const absoluteApiFile = path.join(
|
|
357
|
+
process.cwd(),
|
|
358
|
+
apiFile,
|
|
359
|
+
)
|
|
360
|
+
// const relativeApiFile = path.relative(
|
|
361
|
+
// mdFilePath,
|
|
362
|
+
// absoluteApiFile
|
|
363
|
+
// )
|
|
364
|
+
const resolvedApiFile = absoluteApiFile // TODO: leave absolute or relative?
|
|
365
|
+
let region = ""
|
|
366
|
+
// TODO: in the future more advanced composition? - not only like `GET /users/{id}`
|
|
367
|
+
switch (uniformType) {
|
|
368
|
+
case "graphql": {
|
|
369
|
+
const ctx = ref.context as GraphQLReferenceContext;
|
|
370
|
+
region = `${ctx.graphqlTypeShort}.${ctx?.graphqlName}`
|
|
371
|
+
|
|
372
|
+
meta.graphql = `${resolvedApiFile}#${region}`
|
|
373
|
+
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
case "openapi": {
|
|
377
|
+
const ctx = ref.context as OpenAPIReferenceContext;
|
|
378
|
+
const method = (ctx?.method || "").toUpperCase()
|
|
379
|
+
if (method && ctx?.path) {
|
|
380
|
+
region = `${method} ${ctx?.path}`
|
|
381
|
+
} else if (ctx.componentSchema) {
|
|
382
|
+
region = "/components/schemas/" + ctx.componentSchema
|
|
383
|
+
}
|
|
384
|
+
meta.openapi = `${resolvedApiFile}#${region}`
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
let composedContent = ""
|
|
390
|
+
if (region && composedFileMap[region]) {
|
|
391
|
+
const content = await fs.readFile(composedFileMap[region], 'utf-8');
|
|
392
|
+
const resp = matter(content);
|
|
393
|
+
|
|
394
|
+
meta = {
|
|
395
|
+
...meta,
|
|
396
|
+
...composyingMetaProps(resp.data, "title", "description", "layout")
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
composedContent = resp.content
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const content = matterStringify({ content: composedContent }, meta);
|
|
403
|
+
|
|
404
|
+
if (!disableFSWrite) {
|
|
405
|
+
try {
|
|
406
|
+
await fs.access(path.dirname(mdPath));
|
|
407
|
+
} catch {
|
|
408
|
+
await fs.mkdir(path.dirname(mdPath), { recursive: true });
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await fs.writeFile(mdPath, content)
|
|
412
|
+
}
|
|
413
|
+
})
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
if (!sidebar) {
|
|
417
|
+
return {
|
|
418
|
+
sidebar: [
|
|
419
|
+
{
|
|
420
|
+
route: matchRoute,
|
|
421
|
+
pages: uniformWithNavigation.out.sidebar
|
|
422
|
+
}
|
|
423
|
+
] as SidebarRoute[],
|
|
424
|
+
data: uniformData.data
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if (matchRoute) {
|
|
430
|
+
// TODO: in the future custom position - before / after
|
|
431
|
+
// if (uniformSidebars.length > 0) {
|
|
432
|
+
// uniformSidebars[0].pages.unshift(...uniformWithNavigation.out.sidebar as any)
|
|
433
|
+
// }
|
|
434
|
+
|
|
435
|
+
// sidebar[0].pages.unshift({
|
|
436
|
+
// route: matchRoute,
|
|
437
|
+
// pages: uniformWithNavigation.out.sidebar
|
|
438
|
+
// })
|
|
439
|
+
|
|
440
|
+
const sidebarItem = sidebar?.find(item => {
|
|
441
|
+
if ("route" in item) {
|
|
442
|
+
return item.route === matchRoute
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
return false
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
if (sidebarItem) {
|
|
449
|
+
sidebarItem.pages?.push(...uniformWithNavigation.out.sidebar as any)
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
data: uniformData.data,
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
sidebar.unshift({
|
|
458
|
+
route: matchRoute,
|
|
459
|
+
pages: uniformWithNavigation.out.sidebar as any
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
return {
|
|
463
|
+
data: uniformData.data,
|
|
464
|
+
composedFileMap
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const allowedMetaProps = ['title', 'description', 'layout'] as const;
|
|
469
|
+
|
|
470
|
+
type AllowedMetaProps = Pick<Metadata, typeof allowedMetaProps[number]>;
|
|
471
|
+
|
|
472
|
+
function composyingMetaProps(meta: Metadata, ...props: (keyof AllowedMetaProps)[]) {
|
|
473
|
+
let newProps = {}
|
|
474
|
+
props.forEach(prop => {
|
|
475
|
+
if (allowedMetaProps.includes(prop as typeof allowedMetaProps[number]) && typeof meta[prop] === "string") {
|
|
476
|
+
newProps[prop] = meta[prop] as string;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
return newProps;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function composeFileMap(basePath: string, matchRoute: string) {
|
|
484
|
+
const routeMap: Record<string, string> = {};
|
|
485
|
+
|
|
486
|
+
async function processDirectory(dirPath: string) {
|
|
487
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
488
|
+
|
|
489
|
+
for (const entry of entries) {
|
|
490
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
491
|
+
|
|
492
|
+
if (entry.isDirectory()) {
|
|
493
|
+
await processDirectory(fullPath);
|
|
494
|
+
} else if (entry.isFile() && (entry.name.endsWith('.md') || entry.name.endsWith('.mdx'))) {
|
|
495
|
+
try {
|
|
496
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
497
|
+
const { data: frontmatter } = matter(content);
|
|
498
|
+
|
|
499
|
+
if (frontmatter && frontmatter.openapi) {
|
|
500
|
+
const route = frontmatter.openapi;
|
|
501
|
+
routeMap[route] = path.join(matchRoute, entry.name);
|
|
502
|
+
}
|
|
503
|
+
} catch (error) {
|
|
504
|
+
console.error(`Error processing file ${fullPath}:`, error);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
await processDirectory(path.join(basePath, matchRoute));
|
|
511
|
+
return routeMap;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Helper function to merge sidebars with the same route
|
|
515
|
+
function mergeSidebars(sidebars: SidebarRoute[]): SidebarRoute[] {
|
|
516
|
+
const mergedMap = new Map<string, SidebarRoute>();
|
|
517
|
+
|
|
518
|
+
for (const sidebar of sidebars) {
|
|
519
|
+
const existing = mergedMap.get(sidebar.route);
|
|
520
|
+
|
|
521
|
+
if (existing) {
|
|
522
|
+
// Merge pages from both sidebars
|
|
523
|
+
const mergedPages = [...(existing.pages || []), ...(sidebar.pages || [])];
|
|
524
|
+
mergedMap.set(sidebar.route, {
|
|
525
|
+
...existing,
|
|
526
|
+
pages: mergedPages
|
|
527
|
+
});
|
|
528
|
+
} else {
|
|
529
|
+
mergedMap.set(sidebar.route, sidebar);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
return Array.from(mergedMap.values());
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Helper function to merge sidebars in place, modifying the original array
|
|
537
|
+
function mergeSidebarsInPlace(sidebars: (SidebarRoute | Sidebar)[]): void {
|
|
538
|
+
const mergedMap = new Map<string, SidebarRoute>();
|
|
539
|
+
const nonRouteSidebars: Sidebar[] = [];
|
|
540
|
+
|
|
541
|
+
// First pass: collect all sidebars by route and separate non-route sidebars
|
|
542
|
+
for (const sidebar of sidebars) {
|
|
543
|
+
if ("route" in sidebar) {
|
|
544
|
+
const existing = mergedMap.get(sidebar.route);
|
|
545
|
+
|
|
546
|
+
if (existing) {
|
|
547
|
+
// Merge pages from both sidebars
|
|
548
|
+
const mergedPages = [...(existing.pages || []), ...(sidebar.pages || [])];
|
|
549
|
+
mergedMap.set(sidebar.route, {
|
|
550
|
+
...existing,
|
|
551
|
+
pages: mergedPages
|
|
552
|
+
});
|
|
553
|
+
} else {
|
|
554
|
+
mergedMap.set(sidebar.route, sidebar);
|
|
555
|
+
}
|
|
556
|
+
} else {
|
|
557
|
+
// Keep non-route sidebars (those with "group" property)
|
|
558
|
+
nonRouteSidebars.push(sidebar);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
// Second pass: replace the original array with merged results
|
|
563
|
+
const mergedArray = Array.from(mergedMap.values());
|
|
564
|
+
sidebars.length = 0; // Clear the original array
|
|
565
|
+
sidebars.push(...nonRouteSidebars, ...mergedArray); // Add non-route sidebars first, then merged route sidebars
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// preinstall adds uniform navigation to settings
|
|
569
|
+
function preinstall(
|
|
570
|
+
id: string,
|
|
571
|
+
uniformApiResolver: (filePath: string) => Promise<Reference[]>,
|
|
572
|
+
apiFile: APIFile,
|
|
573
|
+
uniformType: UniformType,
|
|
574
|
+
disableFSWrite?: boolean,
|
|
575
|
+
options?: uniformPresetOptions,
|
|
576
|
+
) {
|
|
577
|
+
return function preinstallInner(innerOptions: any) {
|
|
578
|
+
return async function uniformPluginInner(settings: Settings, data: PresetData) {
|
|
579
|
+
const root = process.cwd()
|
|
580
|
+
|
|
581
|
+
if (!apiFile) {
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const resp: any[] = []
|
|
586
|
+
|
|
587
|
+
// TODO: support NOT ROUTE MATCH
|
|
588
|
+
|
|
589
|
+
if (typeof apiFile === "string") {
|
|
590
|
+
const routeMatch = id
|
|
591
|
+
|
|
592
|
+
const resolved = await uniformResolver(
|
|
593
|
+
settings,
|
|
594
|
+
root,
|
|
595
|
+
routeMatch,
|
|
596
|
+
apiFile,
|
|
597
|
+
uniformApiResolver,
|
|
598
|
+
settings?.navigation?.sidebar as (SidebarRoute | Sidebar)[],
|
|
599
|
+
{
|
|
600
|
+
...options,
|
|
601
|
+
...innerOptions,
|
|
602
|
+
},
|
|
603
|
+
uniformType,
|
|
604
|
+
disableFSWrite
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
if (resolved.sidebar) {
|
|
608
|
+
settings.navigation = {
|
|
609
|
+
...settings?.navigation,
|
|
610
|
+
sidebar: !settings.navigation?.sidebar
|
|
611
|
+
? resolved.sidebar
|
|
612
|
+
: [
|
|
613
|
+
...resolved.sidebar,
|
|
614
|
+
...!settings.navigation?.sidebar || [],
|
|
615
|
+
]
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
resp.push({
|
|
620
|
+
urlPrefix: routeMatch.startsWith("/") ? routeMatch : `/${routeMatch}`,
|
|
621
|
+
data: resolved.data,
|
|
622
|
+
})
|
|
623
|
+
} else {
|
|
624
|
+
async function resolve(
|
|
625
|
+
routeMatch: string,
|
|
626
|
+
uniform: string,
|
|
627
|
+
) {
|
|
628
|
+
const resolved = await uniformResolver(
|
|
629
|
+
settings,
|
|
630
|
+
root,
|
|
631
|
+
routeMatch,
|
|
632
|
+
uniform,
|
|
633
|
+
uniformApiResolver,
|
|
634
|
+
settings?.navigation?.sidebar as (SidebarRoute | Sidebar)[],
|
|
635
|
+
{
|
|
636
|
+
...options,
|
|
637
|
+
...innerOptions,
|
|
638
|
+
},
|
|
639
|
+
uniformType,
|
|
640
|
+
disableFSWrite
|
|
641
|
+
)
|
|
642
|
+
|
|
643
|
+
if (resolved.sidebar) {
|
|
644
|
+
settings.navigation = {
|
|
645
|
+
...settings?.navigation,
|
|
646
|
+
sidebar: !settings.navigation?.sidebar
|
|
647
|
+
? resolved.sidebar
|
|
648
|
+
: [
|
|
649
|
+
...resolved.sidebar,
|
|
650
|
+
...!settings.navigation?.sidebar || [],
|
|
651
|
+
]
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
resp.push({
|
|
656
|
+
urlPrefix: routeMatch.startsWith("/") ? routeMatch : `/${routeMatch}`,
|
|
657
|
+
data: resolved.data,
|
|
658
|
+
})
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
if (apiFile["source"]) {
|
|
662
|
+
await resolve(apiFile["route"], apiFile["source"])
|
|
663
|
+
} else {
|
|
664
|
+
for (const apiKey in apiFile) {
|
|
665
|
+
const uniform = apiFile?.[apiKey]?.source || apiFile?.[apiKey] || ""
|
|
666
|
+
const routeMatch = settings.api?.[id]?.[apiKey]?.route || ""
|
|
667
|
+
|
|
668
|
+
if (!uniform) {
|
|
669
|
+
throw new Error(`uniform not found for ${apiKey}`)
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
await resolve(routeMatch, uniform)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
return resp
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function vitePluginUniformContent(pluginId: string) {
|
|
683
|
+
return function vitePluginUniformContentInner() {
|
|
684
|
+
return async function ({
|
|
685
|
+
preinstall
|
|
686
|
+
}): Promise<VitePlugin> {
|
|
687
|
+
return {
|
|
688
|
+
name: `virtual:xyd-plugin-docs/${pluginId}`, // TODO: unique name per plugin ?
|
|
689
|
+
resolveId(id) {
|
|
690
|
+
if (id == `virtual:xyd-plugin-docs/${pluginId}`) {
|
|
691
|
+
return id;
|
|
692
|
+
}
|
|
693
|
+
},
|
|
694
|
+
async load(id) {
|
|
695
|
+
if (id === `virtual:xyd-plugin-docs/${pluginId}`) {
|
|
696
|
+
if (!preinstall.data) {
|
|
697
|
+
return `export default ${JSON.stringify(preinstall)}`;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
return `export default ${JSON.stringify(preinstall.data)}`;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
type UniformType = "graphql" | "openapi" | "sources"
|
|
709
|
+
|
|
710
|
+
function uniformPreset(
|
|
711
|
+
id: string,
|
|
712
|
+
apiFile: APIFile,
|
|
713
|
+
sidebar: (SidebarRoute | Sidebar)[],
|
|
714
|
+
options: uniformPresetOptions,
|
|
715
|
+
uniformApiResolver: (filePath: string) => Promise<Reference[]>,
|
|
716
|
+
disableFSWrite?: boolean
|
|
717
|
+
) {
|
|
718
|
+
return function (settings: Settings, uniformType: UniformType) {
|
|
719
|
+
const routeMatches: string[] = []
|
|
720
|
+
|
|
721
|
+
if (apiFile) {
|
|
722
|
+
sidebar.forEach((sidebar) => {
|
|
723
|
+
if ("route" in sidebar) {
|
|
724
|
+
if (typeof apiFile === "string") {
|
|
725
|
+
const routeMatch = id
|
|
726
|
+
|
|
727
|
+
if (sidebar.route === routeMatch) {
|
|
728
|
+
routeMatches.push(routeMatch)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
return
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
if (typeof apiFile === "object" && !Array.isArray(apiFile)) {
|
|
735
|
+
for (const routeMatchKey in apiFile) {
|
|
736
|
+
// TODO: is 'id' a good idea here?
|
|
737
|
+
const routeMatch = settings?.api?.[id]?.[routeMatchKey]?.route || ""
|
|
738
|
+
if (sidebar.route === routeMatch) {
|
|
739
|
+
routeMatches.push(routeMatch)
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
// TODO: support NOT match sidebar
|
|
747
|
+
})
|
|
748
|
+
} else {
|
|
749
|
+
if (!options.urlPrefix) {
|
|
750
|
+
throw new Error('(uniformPreset): urlPrefix not found')
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
routeMatches.push(`${options.urlPrefix}/*`)
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const basePath = path.join(getHostPath(), "./plugins/xyd-plugin-docs")
|
|
757
|
+
const pageTheme = "src/pages/docs.tsx"
|
|
758
|
+
|
|
759
|
+
return {
|
|
760
|
+
preinstall: [
|
|
761
|
+
preinstall(
|
|
762
|
+
id,
|
|
763
|
+
uniformApiResolver,
|
|
764
|
+
apiFile,
|
|
765
|
+
uniformType,
|
|
766
|
+
disableFSWrite,
|
|
767
|
+
options
|
|
768
|
+
)
|
|
769
|
+
],
|
|
770
|
+
routes: routeMatches.map((routeMatch, i) => route(
|
|
771
|
+
`${routeMatch}/*`,
|
|
772
|
+
path.join(basePath, pageTheme), {
|
|
773
|
+
id: `xyd-plugin-docs/${id}-${i}`,
|
|
774
|
+
}
|
|
775
|
+
),
|
|
776
|
+
),
|
|
777
|
+
vitePlugins: [
|
|
778
|
+
vitePluginUniformContent(id),
|
|
779
|
+
],
|
|
780
|
+
basePath
|
|
781
|
+
}
|
|
782
|
+
} satisfies Preset<unknown>
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
|
|
786
|
+
// TODO: refactor to use class methods + separate functions if needed?
|
|
787
|
+
export abstract class UniformPreset {
|
|
788
|
+
private _urlPrefix: string;
|
|
789
|
+
private _sourceTheme: boolean;
|
|
790
|
+
private _fileRouting: { [key: string]: string } = {};
|
|
791
|
+
|
|
792
|
+
protected constructor(
|
|
793
|
+
private presetId: string,
|
|
794
|
+
private apiFile: APIFile,
|
|
795
|
+
private sidebar: (SidebarRoute | Sidebar)[],
|
|
796
|
+
private disableFSWrite?: boolean
|
|
797
|
+
) {
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
protected abstract uniformRefResolver(filePath: string): Promise<Reference[]>
|
|
801
|
+
|
|
802
|
+
protected urlPrefix(urlPrefix: string): this {
|
|
803
|
+
this._urlPrefix = urlPrefix
|
|
804
|
+
|
|
805
|
+
return this
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
protected sourceTheme(v: boolean): this {
|
|
809
|
+
this._sourceTheme = v
|
|
810
|
+
|
|
811
|
+
return this
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
protected fileRouting(file: string, route: string): this {
|
|
815
|
+
this._fileRouting[file] = route
|
|
816
|
+
|
|
817
|
+
return this
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
protected newUniformPreset() {
|
|
821
|
+
return uniformPreset(
|
|
822
|
+
this.presetId,
|
|
823
|
+
this.apiFile,
|
|
824
|
+
this.sidebar,
|
|
825
|
+
{
|
|
826
|
+
urlPrefix: this._urlPrefix,
|
|
827
|
+
sourceTheme: this._sourceTheme,
|
|
828
|
+
fileRouting: this._fileRouting,
|
|
829
|
+
},
|
|
830
|
+
this.uniformRefResolver,
|
|
831
|
+
this.disableFSWrite
|
|
832
|
+
)
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
|