@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.
package/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "@xyd-js/plugin-docs",
3
+ "version": "0.1.0-build.160",
4
+ "description": "",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ "./package.json": "./package.json",
10
+ ".": "./dist/index.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "src",
15
+ "package.json"
16
+ ],
17
+ "dependencies": {
18
+ "codehike": "^1.0.3",
19
+ "@code-hike/lighter": "^1.0.3",
20
+ "dotenv": "^16.4.7",
21
+ "@xyd-js/gql": "0.1.0-build.170",
22
+ "@xyd-js/uniform": "0.1.0-build.172",
23
+ "@xyd-js/sources": "0.1.1-build.160",
24
+ "@xyd-js/openapi": "0.1.0-build.168"
25
+ },
26
+ "peerDependencies": {
27
+ "@react-router/dev": "^7.7.1",
28
+ "@react-router/node": "^7.7.1",
29
+ "@react-router/serve": "^7.7.1",
30
+ "openux-js": "0.0.0-pre.1",
31
+ "react": "^19.1.0",
32
+ "react-dom": "^19.1.0",
33
+ "react-router": "^7.7.1",
34
+ "@xyd-js/framework": "0.1.0-build.189",
35
+ "@xyd-js/composer": "0.1.0-build.157",
36
+ "@xyd-js/themes": "0.1.1-build.160",
37
+ "@xyd-js/components": "0.1.0-build.168",
38
+ "@xyd-js/core": "0.1.0-build.170",
39
+ "@xyd-js/content": "0.1.0-build.171"
40
+ },
41
+ "devDependencies": {
42
+ "@types/react-dom": "^19.1.0",
43
+ "@types/react": "^19.1.0",
44
+ "@vitejs/plugin-react": "^4.3.2",
45
+ "tsup": "^8.3.0",
46
+ "typescript": "^5.6.2",
47
+ "vite": "^7.0.0",
48
+ "vite-tsconfig-paths": "^5.1.4",
49
+ "rimraf": "^3.0.2"
50
+ },
51
+ "scripts": {
52
+ "clean": "rimraf build",
53
+ "prebuild": "pnpm clean",
54
+ "build": "tsup",
55
+ "watch": "tsup --watch"
56
+ }
57
+ }
package/src/const.ts ADDED
@@ -0,0 +1,7 @@
1
+ import {ThemePresetName} from "@xyd-js/core";
2
+
3
+ export const THEME_CONFIG_FOLDER = ".docs/theme" // TODO: in the future shared internal const like `.xyd` and `.docs` ?
4
+ // TODO: SHARED
5
+ export const VIRTUAL_CONTENT_FOLDER = ".xyd/.cache/.content"
6
+
7
+ export const DEFAULT_THEME: ThemePresetName = "poetry"
@@ -0,0 +1,29 @@
1
+ // declarations.d.ts
2
+
3
+
4
+ declare module 'virtual:xyd-settings' {
5
+ import type {Settings} from "@xyd-js/core";
6
+
7
+ const settings: Settings;
8
+ export default settings;
9
+ export {
10
+ settings as Settings
11
+ }
12
+ }
13
+
14
+ declare module 'virtual:xyd-theme' {
15
+ import {BaseTheme} from "@xyd-js/themes";
16
+ import {Theme as ThemeSettings} from "@xyd-js/core";
17
+
18
+ // Export a concrete theme class that extends BaseTheme
19
+ class ConcreteTheme extends BaseTheme {
20
+ constructor();
21
+ }
22
+ export default ConcreteTheme;
23
+ }
24
+ declare module 'virtual:xyd-user-components' {
25
+ const components: any
26
+ export {
27
+ components
28
+ }
29
+ }
package/src/index.ts ADDED
@@ -0,0 +1,386 @@
1
+ import { Navigation, Settings } from "@xyd-js/core";
2
+ import type { Plugin as VitePlugin } from "vite";
3
+ import { RouteConfigEntry } from "@react-router/dev/routes";
4
+ import type { PageURL, Sidebar, SidebarRoute } from "@xyd-js/core";
5
+ import fs from "fs";
6
+ import path from "path";
7
+
8
+ import { docsPreset } from "./presets/docs";
9
+ import { graphqlPreset } from "./presets/graphql";
10
+ import { openapiPreset } from "./presets/openapi";
11
+ import { sourcesPreset } from "./presets/sources";
12
+
13
+ import type { PluginOutput, Plugin } from "./types";
14
+ import { ensureAndCleanupVirtualFolder } from "./presets/uniform";
15
+
16
+ export { readSettings } from "./presets/docs/settings"
17
+
18
+ export interface PluginDocsOptions {
19
+ disableAPIGeneration?: boolean
20
+ disableFSWrite?: boolean
21
+ appInit?: any
22
+ onUpdate?: (callback: (settings: Settings) => void) => void
23
+ doNotInstallPluginDependencies?: boolean
24
+ }
25
+
26
+ // TODO: better plugin runner
27
+ // TODO: REFACTOR
28
+ export async function pluginDocs(options?: PluginDocsOptions): Promise<PluginOutput | null> {
29
+ let settings: Settings | null = null
30
+ const vitePlugins: VitePlugin[] = []
31
+ const routes: RouteConfigEntry[] = []
32
+ let basePath = ""
33
+
34
+ // base docs preset setup
35
+ {
36
+ const presetOptions = {
37
+ urlPrefix: "", // TODO: configurable,
38
+ appInit: options?.appInit,
39
+ onUpdate: options?.onUpdate
40
+ }
41
+
42
+ const docs = docsPreset(undefined, presetOptions)
43
+ docs.preinstall = docs.preinstall || []
44
+
45
+ let preinstallMerge = {}
46
+
47
+ for (const preinstall of docs.preinstall) {
48
+ const resp = await preinstall()({}, {
49
+ routes: docs.routes
50
+ })
51
+
52
+ if (resp && typeof resp === 'object') {
53
+ preinstallMerge = {
54
+ ...preinstallMerge,
55
+ ...resp
56
+ }
57
+ }
58
+ }
59
+
60
+ docs.vitePlugins = docs.vitePlugins || []
61
+ for (const vitePlugin of docs.vitePlugins) {
62
+ const vitePlug = await vitePlugin()({
63
+ preinstall: preinstallMerge
64
+ })
65
+
66
+ vitePlugins.push(vitePlug)
67
+ }
68
+
69
+ if ("settings" in preinstallMerge) {
70
+ settings = preinstallMerge.settings as Settings
71
+ }
72
+
73
+ docs.routes = docs.routes || []
74
+ routes.push(...docs.routes)
75
+ basePath = docs.basePath
76
+ }
77
+
78
+ if (!settings) {
79
+ return null
80
+ }
81
+
82
+ await ensureAndCleanupVirtualFolder()
83
+
84
+ // graphql preset setup
85
+ if (!options?.disableAPIGeneration && settings?.api?.graphql) {
86
+ const opt = {
87
+ disableFSWrite: options?.disableFSWrite
88
+ }
89
+
90
+ const gql = graphqlPreset(settings, opt)
91
+ gql.preinstall = gql.preinstall || []
92
+
93
+ let preinstallMerge = {}
94
+
95
+ for (const preinstall of gql.preinstall) {
96
+ const resp = await preinstall(opt)(settings, {
97
+ routes: gql.routes,
98
+ })
99
+
100
+ if (resp && typeof resp === 'object') {
101
+ preinstallMerge = {
102
+ ...preinstallMerge,
103
+ ...resp
104
+ }
105
+ }
106
+ }
107
+
108
+ gql.vitePlugins = gql.vitePlugins || []
109
+ for (const vitePlugin of gql.vitePlugins) {
110
+ const vitePlug = await vitePlugin()({
111
+ preinstall: preinstallMerge
112
+ })
113
+
114
+ vitePlugins.push(vitePlug)
115
+ }
116
+
117
+ gql.routes = gql.routes || []
118
+ routes.push(...gql.routes)
119
+ }
120
+
121
+ // openapi preset setup
122
+ if (!options?.disableAPIGeneration && settings?.api?.openapi) {
123
+ const opt = {
124
+ disableFSWrite: options?.disableFSWrite
125
+ }
126
+
127
+ const oap = openapiPreset(settings, opt)
128
+ oap.preinstall = oap.preinstall || []
129
+
130
+ let preinstallMerge = {}
131
+
132
+ for (const preinstall of oap.preinstall) {
133
+ const resp = await preinstall(opt)(settings, {
134
+ routes: oap.routes,
135
+ })
136
+
137
+ if (resp && typeof resp === 'object') {
138
+ preinstallMerge = {
139
+ ...preinstallMerge,
140
+ ...resp
141
+ }
142
+ }
143
+ }
144
+
145
+ oap.vitePlugins = oap.vitePlugins || []
146
+ for (const vitePlugin of oap.vitePlugins) {
147
+ const vitePlug = await vitePlugin()({
148
+ preinstall: preinstallMerge
149
+ })
150
+
151
+ vitePlugins.push(vitePlug)
152
+ }
153
+
154
+ oap.routes = oap.routes || []
155
+ routes.push(...oap.routes)
156
+ }
157
+
158
+ if (!options?.disableAPIGeneration && settings?.api?.sources) {
159
+ const opt = {
160
+ disableFSWrite: options?.disableFSWrite
161
+ }
162
+
163
+ const src = sourcesPreset(settings, opt)
164
+ src.preinstall = src.preinstall || []
165
+
166
+ let preinstallMerge = {}
167
+
168
+ for (const preinstall of src.preinstall) {
169
+ const resp = await preinstall(opt)(settings, {
170
+ routes: src.routes,
171
+ })
172
+
173
+ if (resp && typeof resp === 'object') {
174
+ preinstallMerge = {
175
+ ...preinstallMerge,
176
+ ...resp
177
+ }
178
+ }
179
+ }
180
+
181
+ src.vitePlugins = src.vitePlugins || []
182
+ for (const vitePlugin of src.vitePlugins) {
183
+ const vitePlug = await vitePlugin()({
184
+ preinstall: preinstallMerge
185
+ })
186
+
187
+ vitePlugins.push(vitePlug)
188
+ }
189
+
190
+ src.routes = src.routes || []
191
+ routes.push(...src.routes)
192
+ }
193
+
194
+ let pagePathMapping: Record<string, string> = {}
195
+
196
+ if (settings?.navigation) {
197
+ pagePathMapping = mapNavigationToPagePathMapping(settings?.navigation)
198
+ } else {
199
+ console.warn("No navigation found in settings")
200
+ }
201
+
202
+ sortSidebarGroups(settings?.navigation?.sidebar || [])
203
+
204
+ const indexPage = await findIndexPage()
205
+
206
+ if (indexPage) {
207
+ pagePathMapping["index"] = indexPage
208
+ }
209
+
210
+ return {
211
+ vitePlugins,
212
+ settings,
213
+ routes,
214
+ basePath,
215
+ pagePathMapping,
216
+ hasIndexPage: !!indexPage
217
+ }
218
+ }
219
+
220
+ async function findIndexPage(): Promise<string> {
221
+ if (fs.existsSync("index.md")) {
222
+ return "index.md"
223
+ }
224
+
225
+ if (fs.existsSync("index.mdx")) {
226
+ return "index.mdx"
227
+ }
228
+
229
+ return ""
230
+ }
231
+
232
+ export function sortSidebarGroups(sidebar: (SidebarRoute | Sidebar | string)[]) {
233
+ // Apply recursive sorting to all routes
234
+ for (const entry of sidebar) {
235
+ if (typeof entry === 'string') continue;
236
+ if (!entry.pages) continue;
237
+
238
+ // Recursively sort nested groups first
239
+ for (const page of entry.pages) {
240
+ if (typeof page === 'object' && 'pages' in page && page.pages) {
241
+ sortSidebarGroups([page as Sidebar]);
242
+ }
243
+ }
244
+
245
+ // Separate groups with order from other items
246
+ const groupsWithOrder: Sidebar[] = [];
247
+ const otherItems: (Sidebar | string)[] = [];
248
+
249
+ for (const page of entry.pages) {
250
+ if (typeof page === 'object' && 'group' in page && page.group && 'order' in page && page.order !== undefined) {
251
+ groupsWithOrder.push(page as Sidebar);
252
+ } else {
253
+ otherItems.push(page as Sidebar | string);
254
+ }
255
+ }
256
+
257
+ // Sort groups with order
258
+ if (groupsWithOrder.length > 0) {
259
+ const groupMap = new Map<string, Sidebar>();
260
+ for (const group of groupsWithOrder) {
261
+ if (group.group) {
262
+ groupMap.set(group.group, group);
263
+ }
264
+ }
265
+
266
+ const sortedGroups: Sidebar[] = [];
267
+
268
+ // First pass: order: 0 (always on top)
269
+ for (const [_, group] of groupMap) {
270
+ if (group.order === 0) sortedGroups.push(group);
271
+ }
272
+
273
+ // Second pass: groups without "before/after"
274
+ for (const [name, group] of groupMap) {
275
+ if (!group.order || typeof group.order === 'number') {
276
+ if (group.order !== 0 && group.order !== -1) {
277
+ sortedGroups.push(group);
278
+ }
279
+ }
280
+ }
281
+
282
+ // Third pass: before/after
283
+ for (const [name, group] of groupMap) {
284
+ if (group.order && typeof group.order === 'object' && ('before' in group.order || 'after' in group.order)) {
285
+ const target = 'before' in group.order ? group.order.before : group.order.after;
286
+ const idx = sortedGroups.findIndex(g => g.group === target);
287
+ if (idx !== -1) {
288
+ if ('before' in group.order) {
289
+ sortedGroups.splice(idx, 0, group);
290
+ } else {
291
+ sortedGroups.splice(idx + 1, 0, group);
292
+ }
293
+ } else {
294
+ sortedGroups.push(group); // fallback
295
+ }
296
+ }
297
+ }
298
+
299
+ // Last: order: -1 (always at end)
300
+ for (const [_, group] of groupMap) {
301
+ if (group.order === -1) sortedGroups.push(group);
302
+ }
303
+
304
+ // Replace the original pages with sorted groups + other items
305
+ entry.pages = [...sortedGroups, ...otherItems];
306
+ }
307
+ }
308
+ }
309
+
310
+ // TODO: in the future better algorithm - we should be .md/.mdx faster than checking fs here
311
+ function mapNavigationToPagePathMapping(navigation: Navigation) {
312
+ const mapping: Record<string, string> = {}
313
+
314
+ function getExistingFilePath(basePath: string): string | null {
315
+ const mdPath = `${basePath}.md`
316
+ const mdxPath = `${basePath}.mdx`
317
+
318
+ if (fs.existsSync(mdPath)) {
319
+ return mdPath
320
+ }
321
+ if (fs.existsSync(mdxPath)) {
322
+ return mdxPath
323
+ }
324
+ return null
325
+ }
326
+
327
+ function processPages(pages: PageURL[]) {
328
+ for (const page of pages) {
329
+ if (typeof page === 'string') {
330
+ // Handle regular page
331
+ const existingPath = getExistingFilePath(page)
332
+ if (existingPath) {
333
+ mapping[page] = existingPath
334
+ }
335
+ } else if (typeof page === 'object' && 'virtual' in page) {
336
+ // Handle virtual page
337
+ const virtualPath = page.virtual
338
+ const pagePath = page.page
339
+ const existingPath = getExistingFilePath(virtualPath)
340
+ if (existingPath) {
341
+ mapping[pagePath] = existingPath
342
+ }
343
+ } else if (typeof page === 'object' && 'pages' in page) {
344
+ // Handle nested sidebar
345
+ processPages(page.pages || [])
346
+ }
347
+ }
348
+ }
349
+
350
+ let sidebarFlatOnly = false
351
+ // Process each sidebar route
352
+ for (const sidebar of navigation?.sidebar || []) {
353
+ if (typeof sidebar === 'string') {
354
+ sidebarFlatOnly = true
355
+ break
356
+ } else if ('pages' in sidebar && "route" in sidebar) {
357
+ // Handle SidebarRoute
358
+ for (const item of sidebar.pages) {
359
+ if (item?.pages) {
360
+ processPages(item.pages)
361
+ } else if (typeof item === 'string') {
362
+ // Handle direct string pages in SidebarRoute
363
+ const existingPath = getExistingFilePath(item)
364
+ if (existingPath) {
365
+ mapping[item] = existingPath
366
+ }
367
+ }
368
+ }
369
+ } else if ('pages' in sidebar) {
370
+ // Handle Sidebar
371
+ processPages(sidebar.pages || [])
372
+ }
373
+ }
374
+
375
+ if (sidebarFlatOnly) {
376
+ const sidebar = navigation?.sidebar as string[] || []
377
+ processPages(sidebar)
378
+ }
379
+
380
+ return mapping
381
+ }
382
+
383
+ export type {
384
+ Plugin,
385
+ PluginOutput
386
+ }
@@ -0,0 +1,9 @@
1
+ import { createContext } from "react";
2
+
3
+ import { BaseTheme } from "@xyd-js/themes";
4
+
5
+ export const PageContext = createContext<{
6
+ theme: BaseTheme | null
7
+ }>({
8
+ theme: null
9
+ })