@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,463 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useMemo, useContext, ReactElement, SVGProps, useEffect } from "react";
|
|
6
|
+
import { redirect, ScrollRestoration, useLocation, useNavigation } from "react-router";
|
|
7
|
+
|
|
8
|
+
import { MetadataMap, Metadata, Settings } from "@xyd-js/core"
|
|
9
|
+
import { ContentFS } from "@xyd-js/content"
|
|
10
|
+
import { markdownPlugins } from "@xyd-js/content/md"
|
|
11
|
+
import { pageMetaLayout } from "@xyd-js/framework";
|
|
12
|
+
import { mapSettingsToProps } from "@xyd-js/framework/hydration";
|
|
13
|
+
import { FrameworkPage, type FwSidebarItemProps } from "@xyd-js/framework/react";
|
|
14
|
+
import type { IBreadcrumb, INavLinks } from "@xyd-js/ui";
|
|
15
|
+
import { UXNode } from "openux-js";
|
|
16
|
+
|
|
17
|
+
// @ts-ignore
|
|
18
|
+
import virtualSettings from "virtual:xyd-settings";
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
const { settings } = virtualSettings as Settings
|
|
21
|
+
|
|
22
|
+
import { PageContext } from "./context";
|
|
23
|
+
import { SUPPORTED_META_TAGS } from "./metatags";
|
|
24
|
+
import { useAnalytics } from "@xyd-js/analytics";
|
|
25
|
+
|
|
26
|
+
function getPathname(url: string) {
|
|
27
|
+
const parsedUrl = new URL(url);
|
|
28
|
+
return parsedUrl.pathname.replace(/^\//, '');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface loaderData {
|
|
32
|
+
sidebarGroups: FwSidebarItemProps[]
|
|
33
|
+
breadcrumbs: IBreadcrumb[],
|
|
34
|
+
toc: MetadataMap,
|
|
35
|
+
slug: string
|
|
36
|
+
code: string
|
|
37
|
+
metadata: Metadata | null
|
|
38
|
+
rawPage: string // TODO: in the future routing like /docs/quickstart.md but some issues with react-router like *.md in `route` config
|
|
39
|
+
navlinks?: INavLinks,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
class timedebugLoader {
|
|
43
|
+
static get total() {
|
|
44
|
+
console.time('loader:total')
|
|
45
|
+
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static get totalEnd() {
|
|
50
|
+
console.timeEnd('loader:total')
|
|
51
|
+
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
static get compile() {
|
|
56
|
+
console.time('loader:compile')
|
|
57
|
+
|
|
58
|
+
return
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static get compileEnd() {
|
|
62
|
+
console.timeEnd('loader:compile')
|
|
63
|
+
|
|
64
|
+
return
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
static get mapSettingsToProps() {
|
|
68
|
+
console.time('loader:mapSettingsToProps')
|
|
69
|
+
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
static get mapSettingsToPropsEnd() {
|
|
74
|
+
console.timeEnd('loader:mapSettingsToProps')
|
|
75
|
+
|
|
76
|
+
return
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export async function loader({ request }: { request: any }) {
|
|
81
|
+
if (!globalThis.__xydPagePathMapping) {
|
|
82
|
+
throw new Error("PagePathMapping not found")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const timedebug = timedebugLoader
|
|
86
|
+
|
|
87
|
+
timedebug.total
|
|
88
|
+
|
|
89
|
+
const slug = getPathname(request.url || "index") || "index"
|
|
90
|
+
if (path.extname(slug)) {
|
|
91
|
+
console.log(`(loader): currently not supporting file extension in slug: ${slug}`);
|
|
92
|
+
timedebug.totalEnd
|
|
93
|
+
return {}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
timedebug.mapSettingsToProps
|
|
97
|
+
|
|
98
|
+
const {
|
|
99
|
+
groups: sidebarGroups,
|
|
100
|
+
breadcrumbs,
|
|
101
|
+
navlinks,
|
|
102
|
+
hiddenPages,
|
|
103
|
+
metadata
|
|
104
|
+
} = await mapSettingsToProps( // TOOD: we use mapSettingsToProps twice (in layout) - refactor
|
|
105
|
+
settings || globalThis.__xydSettings,
|
|
106
|
+
globalThis.__xydPagePathMapping,
|
|
107
|
+
slug,
|
|
108
|
+
)
|
|
109
|
+
timedebug.mapSettingsToPropsEnd
|
|
110
|
+
|
|
111
|
+
function redirectFallback() {
|
|
112
|
+
const fallbackUrl = findFallbackUrl(sidebarGroups, slug)
|
|
113
|
+
|
|
114
|
+
return redirect(fallbackUrl)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (hiddenPages?.[slug]) {
|
|
118
|
+
const resp = redirectFallback()
|
|
119
|
+
if (resp) {
|
|
120
|
+
timedebug.totalEnd
|
|
121
|
+
return resp
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
let code = ""
|
|
126
|
+
let rawPage = ""
|
|
127
|
+
|
|
128
|
+
const mdPlugins = await markdownPlugins({
|
|
129
|
+
maxDepth: metadata?.maxTocDepth || settings?.theme?.writer?.maxTocDepth || 2,
|
|
130
|
+
}, settings)
|
|
131
|
+
|
|
132
|
+
const contentFs = new ContentFS(settings, mdPlugins.remarkPlugins, mdPlugins.rehypePlugins, mdPlugins.recmaPlugins)
|
|
133
|
+
|
|
134
|
+
const pagePath = globalThis.__xydPagePathMapping[slug]
|
|
135
|
+
if (pagePath) {
|
|
136
|
+
timedebug.compile
|
|
137
|
+
code = await contentFs.compile(pagePath)
|
|
138
|
+
rawPage = await contentFs.readRaw(pagePath)
|
|
139
|
+
timedebug.compileEnd
|
|
140
|
+
} else {
|
|
141
|
+
const resp = redirectFallback()
|
|
142
|
+
if (resp) {
|
|
143
|
+
timedebug.totalEnd
|
|
144
|
+
return resp
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
timedebug.totalEnd
|
|
149
|
+
|
|
150
|
+
if (metadata) {
|
|
151
|
+
metadata.layout = pageMetaLayout(metadata)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
sidebarGroups,
|
|
156
|
+
breadcrumbs,
|
|
157
|
+
navlinks,
|
|
158
|
+
slug,
|
|
159
|
+
code,
|
|
160
|
+
metadata,
|
|
161
|
+
rawPage,
|
|
162
|
+
} as loaderData
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
interface MetaTag {
|
|
166
|
+
title?: string
|
|
167
|
+
|
|
168
|
+
name?: string
|
|
169
|
+
|
|
170
|
+
property?: string
|
|
171
|
+
|
|
172
|
+
content?: string
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// TODO: better SEO (use https://github.com/unjs/unhead?)
|
|
176
|
+
export function meta(props: any) {
|
|
177
|
+
const {
|
|
178
|
+
title = "",
|
|
179
|
+
description = "",
|
|
180
|
+
} = props?.data?.metadata || {}
|
|
181
|
+
|
|
182
|
+
const metaTags: MetaTag[] = [
|
|
183
|
+
{ title: title },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
if (description) {
|
|
187
|
+
metaTags.push({
|
|
188
|
+
name: "description",
|
|
189
|
+
content: description,
|
|
190
|
+
})
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const metaTagsMap: { [key: string]: MetaTag } = {}
|
|
194
|
+
|
|
195
|
+
for (const key in settings?.seo?.metatags) {
|
|
196
|
+
const metaType = SUPPORTED_META_TAGS[key]
|
|
197
|
+
if (!metaType) {
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (description && key === "description") {
|
|
202
|
+
continue
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
metaTagsMap[key] = {
|
|
206
|
+
[metaType]: key,
|
|
207
|
+
content: settings?.seo?.metatags[key],
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// TOOD: filter?
|
|
212
|
+
for (const key in props?.data?.metadata) {
|
|
213
|
+
const metaType = SUPPORTED_META_TAGS[key]
|
|
214
|
+
if (!metaType) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (description && key === "description") {
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
metaTagsMap[key] = {
|
|
223
|
+
[metaType]: key,
|
|
224
|
+
content: props?.data?.metadata[key],
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (props?.data?.metadata?.noindex) {
|
|
229
|
+
metaTagsMap["robots"] = {
|
|
230
|
+
name: "robots",
|
|
231
|
+
content: "noindex",
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
for (const key in metaTagsMap) {
|
|
236
|
+
metaTags.push(metaTagsMap[key])
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return metaTags
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function findFirstUrl(items: any = []): string {
|
|
243
|
+
const queue = [...items];
|
|
244
|
+
|
|
245
|
+
while (queue.length > 0) {
|
|
246
|
+
const item = queue.shift();
|
|
247
|
+
|
|
248
|
+
if (item.href) {
|
|
249
|
+
return item.href;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (item.items) {
|
|
253
|
+
queue.push(...item.items);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return "";
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function findFallbackUrl(sidebarGroups: FwSidebarItemProps[], currentSlug: string): string {
|
|
261
|
+
if (!sidebarGroups || sidebarGroups.length === 0) {
|
|
262
|
+
throw new Error("No sidebar groups available for fallback redirect")
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Iterate through all sidebar groups to find the first valid URL
|
|
266
|
+
for (const group of sidebarGroups) {
|
|
267
|
+
if (!group.items || group.items.length === 0) {
|
|
268
|
+
continue
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const firstItem = findFirstUrl(group.items)
|
|
272
|
+
|
|
273
|
+
if (!firstItem) {
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Avoid infinite redirects by checking if the found URL is the same as current slug
|
|
278
|
+
if (sanitizeUrl(firstItem) === sanitizeUrl(currentSlug)) {
|
|
279
|
+
console.log("Avoiding infinite redirect: found URL matches current slug", firstItem, currentSlug)
|
|
280
|
+
continue
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
return firstItem
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// If we get here, no valid URL was found in any sidebar group
|
|
287
|
+
throw new Error(`No valid fallback URL found for slug: ${currentSlug}.`)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const createElementWithKeys = (type: any, props: any) => {
|
|
291
|
+
// Process children to add keys to all elements
|
|
292
|
+
const processChildren = (childrenArray: any[]): any[] => {
|
|
293
|
+
return childrenArray.map((child, index) => {
|
|
294
|
+
// If the child is a React element and doesn't have a key, add one
|
|
295
|
+
if (React.isValidElement(child) && !child.key) {
|
|
296
|
+
return React.cloneElement(child, { key: `mdx-${index}` });
|
|
297
|
+
}
|
|
298
|
+
// If the child is an array, process it recursively
|
|
299
|
+
if (Array.isArray(child)) {
|
|
300
|
+
return processChildren(child);
|
|
301
|
+
}
|
|
302
|
+
return child;
|
|
303
|
+
});
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
// Handle both cases: children as separate args or as props.children
|
|
307
|
+
let processedChildren;
|
|
308
|
+
|
|
309
|
+
if (props && props.children) {
|
|
310
|
+
if (Array.isArray(props.children)) {
|
|
311
|
+
processedChildren = processChildren(props.children);
|
|
312
|
+
} else if (React.isValidElement(props.children) && !props.children.key) {
|
|
313
|
+
// Single child without key
|
|
314
|
+
processedChildren = React.cloneElement(props.children, { key: 'mdx-child' });
|
|
315
|
+
} else {
|
|
316
|
+
// Single child with key or non-React element
|
|
317
|
+
processedChildren = props.children;
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
processedChildren = [];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Create the element with processed children
|
|
324
|
+
return React.createElement(type, {
|
|
325
|
+
...props,
|
|
326
|
+
children: processedChildren
|
|
327
|
+
});
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
// TODO: move to content?
|
|
331
|
+
function mdxExport(code: string, themeContentComponents: any, themeFileComponents: any, globalAPI: any) {
|
|
332
|
+
// Create a wrapper around React.createElement that adds keys to elements in lists
|
|
333
|
+
const scope = {
|
|
334
|
+
Fragment: React.Fragment,
|
|
335
|
+
jsxs: createElementWithKeys,
|
|
336
|
+
jsx: createElementWithKeys,
|
|
337
|
+
jsxDEV: createElementWithKeys,
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const global = {
|
|
341
|
+
...themeContentComponents,
|
|
342
|
+
React,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const fn = new Function("_$scope", ...Object.keys(global), "fileComponents", ...Object.keys(globalAPI || {}), code);
|
|
346
|
+
|
|
347
|
+
return fn(scope, ...Object.values(global), themeFileComponents, ...Object.values(globalAPI));
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// // TODO: move to content?
|
|
351
|
+
function mdxContent(code: string, themeContentComponents: any, themeFileComponents: any, globalAPI: any) {
|
|
352
|
+
const content = mdxExport(code, themeContentComponents, themeFileComponents, globalAPI) // TODO: fix any
|
|
353
|
+
if (!mdxExport) {
|
|
354
|
+
return {}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// TODO: IN THE FUTURE BETTER API
|
|
358
|
+
const layout = pageMetaLayout(content?.frontmatter)
|
|
359
|
+
if (content?.frontmatter && layout) {
|
|
360
|
+
content.frontmatter.layout = layout
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
component: content?.default,
|
|
365
|
+
toc: content?.toc,
|
|
366
|
+
metadata: content?.frontmatter,
|
|
367
|
+
themeSettings: content?.themeSettings || {},
|
|
368
|
+
page: content?.page || false,
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
export function MemoMDXComponent(codeComponent: any) {
|
|
373
|
+
return useMemo(
|
|
374
|
+
() => codeComponent ? codeComponent : null,
|
|
375
|
+
[codeComponent]
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export default function DocsPage({ loaderData }: { loaderData: loaderData }) {
|
|
380
|
+
const location = useLocation()
|
|
381
|
+
const navigation = useNavigation()
|
|
382
|
+
|
|
383
|
+
const analytics = useAnalytics()
|
|
384
|
+
|
|
385
|
+
const { theme } = useContext(PageContext)
|
|
386
|
+
if (!theme) {
|
|
387
|
+
throw new Error("BaseTheme not found")
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Dispatch custom event when location changes
|
|
391
|
+
useEffect(() => {
|
|
392
|
+
const event = new CustomEvent('xyd::pathnameChange', {
|
|
393
|
+
detail: {
|
|
394
|
+
pathname: location.pathname,
|
|
395
|
+
}
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
window.dispatchEvent(event);
|
|
399
|
+
}, [location.pathname]);
|
|
400
|
+
|
|
401
|
+
const themeContentComponents = theme.reactContentComponents()
|
|
402
|
+
const themeFileComponents = theme.reactFileComponents()
|
|
403
|
+
const globalAPI = {
|
|
404
|
+
analytics,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const createContent = (fileComponents) => {
|
|
408
|
+
return mdxContent(loaderData.code, themeContentComponents, fileComponents ? themeFileComponents : undefined, globalAPI)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const content = createContent(true)
|
|
412
|
+
const Content = MemoMDXComponent(content.component)
|
|
413
|
+
|
|
414
|
+
const contentOriginal = createContent(false)
|
|
415
|
+
const ContentOriginal = MemoMDXComponent(contentOriginal.component)
|
|
416
|
+
|
|
417
|
+
const { Page } = theme
|
|
418
|
+
|
|
419
|
+
return <>
|
|
420
|
+
<UXNode
|
|
421
|
+
name="Framework"
|
|
422
|
+
props={{
|
|
423
|
+
location: location.pathname + location.search + location.hash,
|
|
424
|
+
}}
|
|
425
|
+
>
|
|
426
|
+
<FrameworkPage
|
|
427
|
+
key={location.pathname}
|
|
428
|
+
metadata={content.metadata}
|
|
429
|
+
breadcrumbs={loaderData.breadcrumbs}
|
|
430
|
+
rawPage={loaderData.rawPage}
|
|
431
|
+
toc={content.toc || []}
|
|
432
|
+
navlinks={loaderData.navlinks}
|
|
433
|
+
ContentComponent={Content}
|
|
434
|
+
ContentOriginal={ContentOriginal}
|
|
435
|
+
>
|
|
436
|
+
<Page>
|
|
437
|
+
<ContentOriginal components={{
|
|
438
|
+
...themeContentComponents,
|
|
439
|
+
wrapper: (props) => {
|
|
440
|
+
// TODO: in the future delete uxnod
|
|
441
|
+
return <UXNode
|
|
442
|
+
name=".ContentPage"
|
|
443
|
+
props={{}}
|
|
444
|
+
>
|
|
445
|
+
{props.children}
|
|
446
|
+
</UXNode>
|
|
447
|
+
}
|
|
448
|
+
}} />
|
|
449
|
+
<ScrollRestoration />
|
|
450
|
+
</Page>
|
|
451
|
+
</FrameworkPage>
|
|
452
|
+
</UXNode>
|
|
453
|
+
</>
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
function sanitizeUrl(url: string) {
|
|
458
|
+
if (url.startsWith("/")) {
|
|
459
|
+
return url
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
return `/${url}`
|
|
463
|
+
}
|