@xyd-js/plugin-docs 0.1.0-xyd.2 → 0.1.0-xyd.4

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,96 @@
1
+ import { MetaTags } from "@xyd-js/core";
2
+
3
+ export const SUPPORTED_META_TAGS: MetaTags = {
4
+ "robots": "name",
5
+ "charset": "name",
6
+ "viewport": "name",
7
+ "description": "name",
8
+ "keywords": "name",
9
+ "author": "name",
10
+ "googlebot": "name",
11
+ "google": "name",
12
+ "google-site-verification": "name",
13
+ "generator": "name",
14
+ "theme-color": "name",
15
+ "color-scheme": "name",
16
+ "format-detection": "name",
17
+ "referrer": "name",
18
+ "refresh": "name",
19
+ "rating": "name",
20
+ "revisit-after": "name",
21
+ "language": "name",
22
+ "copyright": "name",
23
+ "reply-to": "name",
24
+ "distribution": "name",
25
+ "coverage": "name",
26
+ "category": "name",
27
+ "target": "name",
28
+ "HandheldFriendly": "name",
29
+ "MobileOptimized": "name",
30
+ "apple-mobile-web-app-capable": "name",
31
+ "apple-mobile-web-app-status-bar-style": "name",
32
+ "apple-mobile-web-app-title": "name",
33
+ "application-name": "name",
34
+ "msapplication-TileColor": "name",
35
+ "msapplication-TileImage": "name",
36
+ "msapplication-config": "name",
37
+
38
+ "og:title": "property",
39
+ "og:type": "property",
40
+ "og:url": "property",
41
+ "og:image": "property",
42
+ "og:description": "property",
43
+ "og:site_name": "property",
44
+ "og:locale": "property",
45
+ "og:video": "property",
46
+ "og:audio": "property",
47
+
48
+ "twitter:card": "property",
49
+ "twitter:site": "property",
50
+ "twitter:creator": "property",
51
+ "twitter:title": "property",
52
+ "twitter:description": "property",
53
+ "twitter:image": "property",
54
+ "twitter:image:alt": "property",
55
+ "twitter:player": "property",
56
+ "twitter:player:width": "property",
57
+ "twitter:player:height": "property",
58
+ "twitter:app:name:iphone": "property",
59
+ "twitter:app:id:iphone": "property",
60
+ "twitter:app:url:iphone": "property",
61
+
62
+ "article:published_time": "property",
63
+ "article:modified_time": "property",
64
+ "article:expiration_time": "property",
65
+ "article:author": "property",
66
+ "article:section": "property",
67
+ "article:tag": "property",
68
+
69
+ "book:author": "property",
70
+ "book:isbn": "property",
71
+ "book:release_date": "property",
72
+ "book:tag": "property",
73
+
74
+ "profile:first_name": "property",
75
+ "profile:last_name": "property",
76
+ "profile:username": "property",
77
+ "profile:gender": "property",
78
+
79
+ "music:duration": "property",
80
+ "music:album": "property",
81
+ "music:album:disc": "property",
82
+ "music:album:track": "property",
83
+ "music:musician": "property",
84
+ "music:song": "property",
85
+ "music:song:disc": "property",
86
+ "music:song:track": "property",
87
+
88
+ "video:actor": "property",
89
+ "video:actor:role": "property",
90
+ "video:director": "property",
91
+ "video:writer": "property",
92
+ "video:duration": "property",
93
+ "video:release_date": "property",
94
+ "video:tag": "property",
95
+ "video:series": "property"
96
+ }
@@ -0,0 +1,363 @@
1
+ import path from "node:path";
2
+
3
+ import * as React from "react";
4
+ import { useMemo, useContext, ReactElement, SVGProps } from "react";
5
+ import { redirect, ScrollRestoration, useLocation } from "react-router";
6
+
7
+ import { MetadataMap, Metadata, Settings } from "@xyd-js/core"
8
+ import { ContentFS } from "@xyd-js/content"
9
+ import { markdownPlugins } from "@xyd-js/content/md"
10
+ import { mapSettingsToProps } from "@xyd-js/framework/hydration";
11
+ import { FrameworkPage, type FwSidebarGroupProps } from "@xyd-js/framework/react";
12
+ import type { IBreadcrumb, INavLinks } from "@xyd-js/ui";
13
+
14
+ // @ts-ignore
15
+ import virtualSettings from "virtual:xyd-settings";
16
+ // @ts-ignore
17
+ const { settings } = virtualSettings as Settings
18
+ import { PageContext } from "./context";
19
+ import { SUPPORTED_META_TAGS } from "./metatags";
20
+
21
+ function getPathname(url: string) {
22
+ const parsedUrl = new URL(url);
23
+ return parsedUrl.pathname.replace(/^\//, '');
24
+ }
25
+
26
+ interface loaderData {
27
+ sidebarGroups: FwSidebarGroupProps[]
28
+ breadcrumbs: IBreadcrumb[],
29
+ toc: MetadataMap,
30
+ slug: string
31
+ code: string
32
+ metadata: Metadata | null
33
+ rawPage: string // TODO: in the future routing like /docs/quickstart.md but some issues with react-router like *.md in `route` config
34
+ navlinks?: INavLinks,
35
+ }
36
+
37
+ class timedebugLoader {
38
+ static get total() {
39
+ console.time('loader:total')
40
+
41
+ return
42
+ }
43
+
44
+ static get totalEnd() {
45
+ console.timeEnd('loader:total')
46
+
47
+ return
48
+ }
49
+
50
+ static get compile() {
51
+ console.time('loader:compile')
52
+
53
+ return
54
+ }
55
+
56
+ static get compileEnd() {
57
+ console.timeEnd('loader:compile')
58
+
59
+ return
60
+ }
61
+
62
+ static get mapSettingsToProps() {
63
+ console.time('loader:mapSettingsToProps')
64
+
65
+ return
66
+ }
67
+
68
+ static get mapSettingsToPropsEnd() {
69
+ console.timeEnd('loader:mapSettingsToProps')
70
+
71
+ return
72
+ }
73
+ }
74
+
75
+ export async function loader({ request }: { request: any }) {
76
+ if (!globalThis.__xydPagePathMapping) {
77
+ throw new Error("PagePathMapping not found")
78
+ }
79
+
80
+ const timedebug = timedebugLoader
81
+
82
+ timedebug.total
83
+
84
+ const slug = getPathname(request.url || "index") || "index"
85
+ if (path.extname(slug)) {
86
+ console.log(`(loader): currently not supporting file extension in slug: ${slug}`);
87
+ timedebug.totalEnd
88
+ return {}
89
+ }
90
+
91
+ timedebug.mapSettingsToProps
92
+
93
+ const {
94
+ groups: sidebarGroups,
95
+ breadcrumbs,
96
+ navlinks,
97
+ hiddenPages,
98
+ metadata
99
+ } = await mapSettingsToProps( // TOOD: we use mapSettingsToProps twice (in layout) - refactor
100
+ settings || globalThis.__xydSettings,
101
+ globalThis.__xydPagePathMapping,
102
+ slug,
103
+ )
104
+ timedebug.mapSettingsToPropsEnd
105
+
106
+ function redirectFallback() {
107
+ if (!sidebarGroups) {
108
+ return
109
+ }
110
+ const firstItem = findFirstUrl(sidebarGroups?.[0]?.items);
111
+
112
+ if (firstItem) {
113
+ return redirect(firstItem)
114
+ }
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 = markdownPlugins({
129
+ maxDepth: metadata?.maxTocDepth || settings?.theme?.maxTocDepth || 2,
130
+ }, settings)
131
+
132
+ const contentFs = new ContentFS(settings, mdPlugins.remarkPlugins, mdPlugins.rehypePlugins)
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
+ return {
151
+ sidebarGroups,
152
+ breadcrumbs,
153
+ navlinks,
154
+ slug,
155
+ code,
156
+ metadata,
157
+ rawPage,
158
+ } as loaderData
159
+ }
160
+
161
+ interface MetaTag {
162
+ title?: string
163
+
164
+ name?: string
165
+
166
+ property?: string
167
+
168
+ content?: string
169
+ }
170
+
171
+ // TODO: better SEO (use https://github.com/unjs/unhead?)
172
+ export function meta(props: any) {
173
+ const {
174
+ title = "",
175
+ description = "",
176
+ } = props?.data?.metadata || {}
177
+
178
+ const metaTags: MetaTag[] = [
179
+ { title: title },
180
+ ]
181
+
182
+ if (description) {
183
+ metaTags.push({
184
+ name: "description",
185
+ content: description,
186
+ })
187
+ }
188
+
189
+ const metaTagsMap: {[key: string]: MetaTag} = {}
190
+
191
+ for (const key in settings?.seo?.metatags) {
192
+ const metaType = SUPPORTED_META_TAGS[key]
193
+ if (!metaType) {
194
+ continue
195
+ }
196
+
197
+ if (description && key === "description") {
198
+ continue
199
+ }
200
+
201
+ metaTagsMap[key] = {
202
+ [metaType]: key,
203
+ content: settings?.seo?.metatags[key],
204
+ }
205
+ }
206
+
207
+ // TOOD: filter?
208
+ for (const key in props?.data?.metadata) {
209
+ const metaType = SUPPORTED_META_TAGS[key]
210
+ if (!metaType) {
211
+ continue
212
+ }
213
+
214
+ if (description && key === "description") {
215
+ continue
216
+ }
217
+
218
+ metaTagsMap[key] = {
219
+ [metaType]: key,
220
+ content: props?.data?.metadata[key],
221
+ }
222
+ }
223
+
224
+ if (props?.data?.metadata?.noindex) {
225
+ metaTagsMap["robots"] = {
226
+ name: "robots",
227
+ content: "noindex",
228
+ }
229
+ }
230
+
231
+ for (const key in metaTagsMap) {
232
+ metaTags.push(metaTagsMap[key])
233
+ }
234
+
235
+ return metaTags
236
+ }
237
+
238
+ function findFirstUrl(items: any = []): string {
239
+ const queue = [...items];
240
+
241
+ while (queue.length > 0) {
242
+ const item = queue.shift();
243
+
244
+ if (item.href) {
245
+ return item.href;
246
+ }
247
+
248
+ if (item.items) {
249
+ queue.push(...item.items);
250
+ }
251
+ }
252
+
253
+ return "";
254
+ }
255
+
256
+ const createElementWithKeys = (type: any, props: any) => {
257
+ // Process children to add keys to all elements
258
+ const processChildren = (childrenArray: any[]): any[] => {
259
+ return childrenArray.map((child, index) => {
260
+ // If the child is a React element and doesn't have a key, add one
261
+ if (React.isValidElement(child) && !child.key) {
262
+ return React.cloneElement(child, { key: `mdx-${index}` });
263
+ }
264
+ // If the child is an array, process it recursively
265
+ if (Array.isArray(child)) {
266
+ return processChildren(child);
267
+ }
268
+ return child;
269
+ });
270
+ };
271
+
272
+ // Handle both cases: children as separate args or as props.children
273
+ let processedChildren;
274
+
275
+ if (props && props.children) {
276
+ if (Array.isArray(props.children)) {
277
+ processedChildren = processChildren(props.children);
278
+ } else if (React.isValidElement(props.children) && !props.children.key) {
279
+ // Single child without key
280
+ processedChildren = React.cloneElement(props.children, { key: 'mdx-child' });
281
+ } else {
282
+ // Single child with key or non-React element
283
+ processedChildren = props.children;
284
+ }
285
+ } else {
286
+ processedChildren = [];
287
+ }
288
+
289
+ // Create the element with processed children
290
+ return React.createElement(type, {
291
+ ...props,
292
+ children: processedChildren
293
+ });
294
+ };
295
+
296
+ // TODO: move to content?
297
+ function mdxExport(code: string) {
298
+ // Create a wrapper around React.createElement that adds keys to elements in lists
299
+ const scope = {
300
+ Fragment: React.Fragment,
301
+ jsxs: createElementWithKeys,
302
+ jsx: createElementWithKeys,
303
+ jsxDEV: createElementWithKeys,
304
+ }
305
+ const fn = new Function(...Object.keys(scope), code)
306
+
307
+ return fn(scope)
308
+ }
309
+
310
+ // // TODO: move to content?
311
+ function mdxContent(code: string) {
312
+ const content = mdxExport(code) // TODO: fix any
313
+ if (!mdxExport) {
314
+ return {}
315
+ }
316
+
317
+ return {
318
+ component: content?.default,
319
+ toc: content?.toc,
320
+ metadata: content?.frontmatter,
321
+ themeSettings: content?.themeSettings || {},
322
+ page: content?.page || false,
323
+ }
324
+ }
325
+
326
+ export function MemoMDXComponent(codeComponent: any) {
327
+ return useMemo(
328
+ () => codeComponent ? codeComponent : null,
329
+ [codeComponent]
330
+ )
331
+ }
332
+
333
+ export default function DocsPage({ loaderData }: { loaderData: loaderData }) {
334
+ const location = useLocation()
335
+
336
+ const { theme } = useContext(PageContext)
337
+ if (!theme) {
338
+ throw new Error("BaseTheme not found")
339
+ }
340
+
341
+ const content = mdxContent(loaderData.code)
342
+ const Content = MemoMDXComponent(content.component)
343
+
344
+ const themeContentComponents = theme.reactContentComponents()
345
+ const { Page } = theme
346
+
347
+ return <FrameworkPage
348
+ key={location.pathname}
349
+ metadata={content.metadata}
350
+ breadcrumbs={loaderData.breadcrumbs}
351
+ rawPage={loaderData.rawPage}
352
+ toc={content.toc || []}
353
+ navlinks={loaderData.navlinks}
354
+ ContentComponent={Content}
355
+ >
356
+ <Page>
357
+ <Content components={themeContentComponents} />
358
+ <ScrollRestoration />
359
+ </Page>
360
+ </FrameworkPage>
361
+ }
362
+
363
+
@@ -0,0 +1,232 @@
1
+ import { promises as fs } from 'fs';
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ import { createServer, Plugin as VitePlugin } from "vite";
6
+ import { route } from "@react-router/dev/routes";
7
+
8
+ import { Settings } from "@xyd-js/core";
9
+
10
+ import { Preset, PresetData } from "../../types";
11
+ import { readSettings } from "./settings";
12
+ import { DEFAULT_THEME, THEME_CONFIG_FOLDER } from "../../const";
13
+ import { getDocsPluginBasePath, getHostPath } from "../../utils";
14
+
15
+ interface docsPluginOptions {
16
+ urlPrefix?: string
17
+ }
18
+
19
+ // TODO: find better solution - maybe something what rr7 use?
20
+ async function loadModule(filePath: string) {
21
+ const server = await createServer({
22
+ optimizeDeps: {
23
+ include: ["react/jsx-runtime"],
24
+ },
25
+ });
26
+
27
+ try {
28
+ const module = await server.ssrLoadModule(filePath);
29
+ return module.default;
30
+ } finally {
31
+ await server.close();
32
+ }
33
+ }
34
+
35
+ function preinstall() {
36
+ return async function docsPluginInner(_, data: PresetData) {
37
+ // TODO: configurable root?
38
+ const root = process.cwd()
39
+
40
+ const settings = await readSettings()
41
+ if (settings && !settings.theme) {
42
+ settings.theme = {
43
+ name: DEFAULT_THEME
44
+ }
45
+ }
46
+
47
+ let themeRoutesExists = false
48
+ try {
49
+ await fs.access(path.join(root, THEME_CONFIG_FOLDER, "./routes.ts"))
50
+ themeRoutesExists = true
51
+ } catch (_) {
52
+ }
53
+
54
+ if (themeRoutesExists) {
55
+ const routeMod = await loadModule(path.join(root, THEME_CONFIG_FOLDER, "./routes.ts"))
56
+
57
+ const routes = routeMod((routePath, routeFile, routeOptions) => {
58
+ return route(routePath, path.join(root, THEME_CONFIG_FOLDER, routeFile), routeOptions)
59
+ })
60
+
61
+ data.routes.push(...routes)
62
+ }
63
+
64
+ return {
65
+ settings
66
+ }
67
+ }
68
+ }
69
+
70
+ // TODO: maybe later as a separate plugin?
71
+ function vitePluginSettings() {
72
+ return async function ({ preinstall }): Promise<VitePlugin> {
73
+ return {
74
+ name: 'virtual:xyd-settings',
75
+ resolveId(id) {
76
+ if (id === 'virtual:xyd-settings') {
77
+ return id + '.jsx'; // Return the module with .jsx extension
78
+ }
79
+ return null;
80
+ },
81
+ async load(id) { // TODO: better cuz we probably dont neeed `get settings()`
82
+ if (id === 'virtual:xyd-settings.jsx') {
83
+ return `
84
+ export default {
85
+ get settings() {
86
+ return globalThis.__xydSettings || ${typeof preinstall.settings === "string" ? preinstall.settings : JSON.stringify(preinstall.settings)}
87
+ }
88
+ }
89
+ `
90
+ }
91
+ return null;
92
+ },
93
+ };
94
+ }
95
+ }
96
+
97
+
98
+ export function vitePluginThemeCSS() {
99
+ return async function ({
100
+ preinstall
101
+ }: {
102
+ preinstall: {
103
+ settings: Settings
104
+ }
105
+ }): Promise<VitePlugin> {
106
+ return {
107
+ name: 'virtual:xyd-theme/index.css',
108
+
109
+ resolveId(source) {
110
+ if (source === 'virtual:xyd-theme/index.css') {
111
+ const __filename = fileURLToPath(import.meta.url);
112
+ const __dirname = path.dirname(__filename);
113
+
114
+ const themeName = preinstall.settings.theme?.name || DEFAULT_THEME
115
+ let themePath = ""
116
+
117
+ if (process.env.XYD_CLI) {
118
+ themePath = path.join(getHostPath(), `node_modules/@xyd-js/theme-${themeName}/dist`)
119
+ } else {
120
+ themePath = path.join(path.resolve(__dirname, "../../"), `xyd-theme-${themeName}/dist`)
121
+ }
122
+
123
+ return path.join(themePath, "index.css")
124
+ }
125
+
126
+ return null;
127
+ }
128
+ };
129
+ }
130
+ }
131
+
132
+ export function vitePluginThemeOverrideCSS() {
133
+ return async function ({ preinstall }: { preinstall: { settings: Settings } }): Promise<VitePlugin> {
134
+ return {
135
+ name: 'virtual:xyd-theme-override-css',
136
+
137
+ async resolveId(id) {
138
+ if (id === 'virtual:xyd-theme-override/index.css') {
139
+ const root = process.cwd();
140
+ const filePath = path.join(root, THEME_CONFIG_FOLDER, "./index.css");
141
+
142
+ try {
143
+ await fs.access(filePath);
144
+ return filePath;
145
+ } catch {
146
+ // File does not exist, omit it
147
+ return 'virtual:xyd-theme-override/empty.css';
148
+ }
149
+ }
150
+ return null;
151
+ },
152
+
153
+ async load(id) {
154
+ if (id === 'virtual:xyd-theme-override/empty.css') {
155
+ // Return an empty module
156
+ return '';
157
+ }
158
+ return null;
159
+ },
160
+ };
161
+ };
162
+ }
163
+
164
+ export function vitePluginTheme() {
165
+ return async function ({
166
+ preinstall
167
+ }: {
168
+ preinstall: {
169
+ settings: Settings
170
+ }
171
+ }): Promise<VitePlugin> {
172
+ return {
173
+ name: 'virtual:xyd-theme',
174
+ resolveId(id) {
175
+ if (id === 'virtual:xyd-theme') {
176
+ return id;
177
+ }
178
+ return null;
179
+ },
180
+ async load(id) {
181
+ if (id === 'virtual:xyd-theme') {
182
+ // return ''
183
+ const __filename = fileURLToPath(import.meta.url);
184
+ const __dirname = path.dirname(__filename);
185
+
186
+ const themeName = preinstall.settings.theme?.name || DEFAULT_THEME
187
+ let themePath = ""
188
+
189
+ if (process.env.XYD_CLI) {
190
+ themePath = `@xyd-js/theme-${themeName}`
191
+ } else {
192
+ themePath = path.join(path.resolve(__dirname, "../../"), `xyd-theme-${themeName}/src`)
193
+ }
194
+
195
+ // Return a module that imports the theme from the local workspace
196
+ return `
197
+ import Theme from '${themePath}';
198
+
199
+ export default Theme;
200
+ `;
201
+ }
202
+ return null;
203
+ }
204
+ };
205
+ }
206
+ }
207
+
208
+ function preset(settings: Settings, options: docsPluginOptions) {
209
+ const basePath = getDocsPluginBasePath()
210
+
211
+ return {
212
+ preinstall: [
213
+ preinstall,
214
+ ],
215
+ routes: [
216
+ route("", path.join(basePath, "src/pages/docs.tsx")),
217
+ // TODO: custom routes
218
+ route(options.urlPrefix ? `${options.urlPrefix}/*` : "*", path.join(basePath, "src/pages/docs.tsx"), {
219
+ id: "xyd-plugin-docs/docs",
220
+ }),
221
+ ],
222
+ vitePlugins: [
223
+ vitePluginSettings,
224
+ vitePluginTheme,
225
+ vitePluginThemeCSS,
226
+ vitePluginThemeOverrideCSS,
227
+ ],
228
+ basePath
229
+ }
230
+ }
231
+
232
+ export const docsPreset = preset as Preset<unknown>