@xyd-js/plugin-docs 0.1.0-build.160

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.
@@ -0,0 +1,832 @@
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
+
540
+ // First pass: collect all sidebars by route
541
+ for (const sidebar of sidebars) {
542
+ if ("route" in sidebar) {
543
+ const existing = mergedMap.get(sidebar.route);
544
+
545
+ if (existing) {
546
+ // Merge pages from both sidebars
547
+ const mergedPages = [...(existing.pages || []), ...(sidebar.pages || [])];
548
+ mergedMap.set(sidebar.route, {
549
+ ...existing,
550
+ pages: mergedPages
551
+ });
552
+ } else {
553
+ mergedMap.set(sidebar.route, sidebar);
554
+ }
555
+ }
556
+ }
557
+
558
+ // Second pass: replace the original array with merged results
559
+ const mergedArray = Array.from(mergedMap.values());
560
+ sidebars.length = 0; // Clear the original array
561
+ sidebars.push(...mergedArray); // Add the merged items
562
+ }
563
+
564
+ // preinstall adds uniform navigation to settings
565
+ function preinstall(
566
+ id: string,
567
+ uniformApiResolver: (filePath: string) => Promise<Reference[]>,
568
+ apiFile: APIFile,
569
+ uniformType: UniformType,
570
+ disableFSWrite?: boolean,
571
+ options?: uniformPresetOptions,
572
+ ) {
573
+ return function preinstallInner(innerOptions: any) {
574
+ return async function uniformPluginInner(settings: Settings, data: PresetData) {
575
+ const root = process.cwd()
576
+
577
+ if (!apiFile) {
578
+ return
579
+ }
580
+
581
+ const resp: any[] = []
582
+
583
+ // TODO: support NOT ROUTE MATCH
584
+
585
+ if (typeof apiFile === "string") {
586
+ const routeMatch = id
587
+
588
+ const resolved = await uniformResolver(
589
+ settings,
590
+ root,
591
+ routeMatch,
592
+ apiFile,
593
+ uniformApiResolver,
594
+ settings?.navigation?.sidebar as (SidebarRoute | Sidebar)[],
595
+ {
596
+ ...options,
597
+ ...innerOptions,
598
+ },
599
+ uniformType,
600
+ disableFSWrite
601
+ )
602
+
603
+ if (resolved.sidebar) {
604
+ settings.navigation = {
605
+ ...settings?.navigation,
606
+ sidebar: !settings.navigation?.sidebar
607
+ ? resolved.sidebar
608
+ : [
609
+ ...resolved.sidebar,
610
+ ...!settings.navigation?.sidebar || [],
611
+ ]
612
+ }
613
+ }
614
+
615
+ resp.push({
616
+ urlPrefix: routeMatch.startsWith("/") ? routeMatch : `/${routeMatch}`,
617
+ data: resolved.data,
618
+ })
619
+ } else {
620
+ async function resolve(
621
+ routeMatch: string,
622
+ uniform: string,
623
+ ) {
624
+ const resolved = await uniformResolver(
625
+ settings,
626
+ root,
627
+ routeMatch,
628
+ uniform,
629
+ uniformApiResolver,
630
+ settings?.navigation?.sidebar as (SidebarRoute | Sidebar)[],
631
+ {
632
+ ...options,
633
+ ...innerOptions,
634
+ },
635
+ uniformType,
636
+ disableFSWrite
637
+ )
638
+
639
+ if (resolved.sidebar) {
640
+ settings.navigation = {
641
+ ...settings?.navigation,
642
+ sidebar: !settings.navigation?.sidebar
643
+ ? resolved.sidebar
644
+ : [
645
+ ...resolved.sidebar,
646
+ ...!settings.navigation?.sidebar || [],
647
+ ]
648
+ }
649
+ }
650
+
651
+ resp.push({
652
+ urlPrefix: routeMatch.startsWith("/") ? routeMatch : `/${routeMatch}`,
653
+ data: resolved.data,
654
+ })
655
+ }
656
+
657
+ if (apiFile["source"]) {
658
+ await resolve(apiFile["route"], apiFile["source"])
659
+ } else {
660
+ for (const apiKey in apiFile) {
661
+ const uniform = apiFile?.[apiKey]?.source || apiFile?.[apiKey] || ""
662
+ const routeMatch = settings.api?.[id]?.[apiKey]?.route || ""
663
+
664
+ if (!uniform) {
665
+ throw new Error(`uniform not found for ${apiKey}`)
666
+ }
667
+
668
+ await resolve(routeMatch, uniform)
669
+ }
670
+ }
671
+ }
672
+
673
+ return resp
674
+ }
675
+ }
676
+ }
677
+
678
+ function vitePluginUniformContent(pluginId: string) {
679
+ return function vitePluginUniformContentInner() {
680
+ return async function ({
681
+ preinstall
682
+ }): Promise<VitePlugin> {
683
+ return {
684
+ name: `virtual:xyd-plugin-docs/${pluginId}`, // TODO: unique name per plugin ?
685
+ resolveId(id) {
686
+ if (id == `virtual:xyd-plugin-docs/${pluginId}`) {
687
+ return id;
688
+ }
689
+ },
690
+ async load(id) {
691
+ if (id === `virtual:xyd-plugin-docs/${pluginId}`) {
692
+ if (!preinstall.data) {
693
+ return `export default ${JSON.stringify(preinstall)}`;
694
+ }
695
+
696
+ return `export default ${JSON.stringify(preinstall.data)}`;
697
+ }
698
+ }
699
+ };
700
+ }
701
+ }
702
+ }
703
+
704
+ type UniformType = "graphql" | "openapi" | "sources"
705
+
706
+ function uniformPreset(
707
+ id: string,
708
+ apiFile: APIFile,
709
+ sidebar: (SidebarRoute | Sidebar)[],
710
+ options: uniformPresetOptions,
711
+ uniformApiResolver: (filePath: string) => Promise<Reference[]>,
712
+ disableFSWrite?: boolean
713
+ ) {
714
+ return function (settings: Settings, uniformType: UniformType) {
715
+ const routeMatches: string[] = []
716
+
717
+ if (apiFile) {
718
+ sidebar.forEach((sidebar) => {
719
+ if ("route" in sidebar) {
720
+ if (typeof apiFile === "string") {
721
+ const routeMatch = id
722
+
723
+ if (sidebar.route === routeMatch) {
724
+ routeMatches.push(routeMatch)
725
+ }
726
+
727
+ return
728
+ }
729
+
730
+ if (typeof apiFile === "object" && !Array.isArray(apiFile)) {
731
+ for (const routeMatchKey in apiFile) {
732
+ // TODO: is 'id' a good idea here?
733
+ const routeMatch = settings?.api?.[id]?.[routeMatchKey]?.route || ""
734
+ if (sidebar.route === routeMatch) {
735
+ routeMatches.push(routeMatch)
736
+ }
737
+ }
738
+ }
739
+
740
+ return
741
+ }
742
+ // TODO: support NOT match sidebar
743
+ })
744
+ } else {
745
+ if (!options.urlPrefix) {
746
+ throw new Error('(uniformPreset): urlPrefix not found')
747
+ }
748
+
749
+ routeMatches.push(`${options.urlPrefix}/*`)
750
+ }
751
+
752
+ const basePath = path.join(getHostPath(), "./plugins/xyd-plugin-docs")
753
+ const pageTheme = "src/pages/docs.tsx"
754
+
755
+ return {
756
+ preinstall: [
757
+ preinstall(
758
+ id,
759
+ uniformApiResolver,
760
+ apiFile,
761
+ uniformType,
762
+ disableFSWrite,
763
+ options
764
+ )
765
+ ],
766
+ routes: routeMatches.map((routeMatch, i) => route(
767
+ `${routeMatch}/*`,
768
+ path.join(basePath, pageTheme), {
769
+ id: `xyd-plugin-docs/${id}-${i}`,
770
+ }
771
+ ),
772
+ ),
773
+ vitePlugins: [
774
+ vitePluginUniformContent(id),
775
+ ],
776
+ basePath
777
+ }
778
+ } satisfies Preset<unknown>
779
+ }
780
+
781
+
782
+ // TODO: refactor to use class methods + separate functions if needed?
783
+ export abstract class UniformPreset {
784
+ private _urlPrefix: string;
785
+ private _sourceTheme: boolean;
786
+ private _fileRouting: { [key: string]: string } = {};
787
+
788
+ protected constructor(
789
+ private presetId: string,
790
+ private apiFile: APIFile,
791
+ private sidebar: (SidebarRoute | Sidebar)[],
792
+ private disableFSWrite?: boolean
793
+ ) {
794
+ }
795
+
796
+ protected abstract uniformRefResolver(filePath: string): Promise<Reference[]>
797
+
798
+ protected urlPrefix(urlPrefix: string): this {
799
+ this._urlPrefix = urlPrefix
800
+
801
+ return this
802
+ }
803
+
804
+ protected sourceTheme(v: boolean): this {
805
+ this._sourceTheme = v
806
+
807
+ return this
808
+ }
809
+
810
+ protected fileRouting(file: string, route: string): this {
811
+ this._fileRouting[file] = route
812
+
813
+ return this
814
+ }
815
+
816
+ protected newUniformPreset() {
817
+ return uniformPreset(
818
+ this.presetId,
819
+ this.apiFile,
820
+ this.sidebar,
821
+ {
822
+ urlPrefix: this._urlPrefix,
823
+ sourceTheme: this._sourceTheme,
824
+ fileRouting: this._fileRouting,
825
+ },
826
+ this.uniformRefResolver,
827
+ this.disableFSWrite
828
+ )
829
+ }
830
+ }
831
+
832
+