@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.
@@ -0,0 +1,317 @@
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
+ onUpdate?: (callback: (settings: Settings) => void) => void
18
+ appInit: any
19
+ }
20
+
21
+ // TODO: find better solution - maybe something what rr7 use?
22
+ async function loadModule(filePath: string) {
23
+ const server = await createServer({
24
+ optimizeDeps: {
25
+ include: ["react/jsx-runtime"],
26
+ },
27
+ });
28
+
29
+ try {
30
+ const module = await server.ssrLoadModule(filePath);
31
+ return module.default;
32
+ } finally {
33
+ await server.close();
34
+ }
35
+ }
36
+
37
+ function preinstall() {
38
+ return async function docsPluginInner(_, data: PresetData) {
39
+ // TODO: configurable root?
40
+ const root = process.cwd()
41
+
42
+ const settings = await readSettings()
43
+ if (settings && !settings.theme) {
44
+ settings.theme = {
45
+ name: DEFAULT_THEME
46
+ }
47
+ }
48
+
49
+ let themeRoutesExists = false
50
+ try {
51
+ await fs.access(path.join(root, THEME_CONFIG_FOLDER, "./routes.ts"))
52
+ themeRoutesExists = true
53
+ } catch (_) {
54
+ }
55
+
56
+ if (themeRoutesExists) {
57
+ const routeMod = await loadModule(path.join(root, THEME_CONFIG_FOLDER, "./routes.ts"))
58
+
59
+ const routes = routeMod((routePath, routeFile, routeOptions) => {
60
+ return route(routePath, path.join(root, THEME_CONFIG_FOLDER, routeFile), routeOptions)
61
+ })
62
+
63
+ data.routes.push(...routes)
64
+ }
65
+
66
+ return {
67
+ settings
68
+ }
69
+ }
70
+ }
71
+
72
+ function vitePluginSettings(options: docsPluginOptions) {
73
+ return function () {
74
+ return async function ({ preinstall }): Promise<VitePlugin> {
75
+ const virtualId = 'virtual:xyd-settings';
76
+ const resolvedId = virtualId + '.jsx';
77
+
78
+ let currentSettings = globalThis.__xydSettings
79
+ let settingsClone = {}
80
+ if (!currentSettings && preinstall?.settings) {
81
+ currentSettings = typeof preinstall?.settings === "string" ? preinstall?.settings : JSON.stringify(preinstall?.settings || {})
82
+ }
83
+
84
+ let currentUserPreferences = globalThis.__xydUserPreferences
85
+ if (!currentUserPreferences) {
86
+ currentUserPreferences = {}
87
+ }
88
+
89
+ let firstInit = false
90
+
91
+ if (options.onUpdate) {
92
+ options.onUpdate((settings: Settings) => {
93
+ currentSettings = settings
94
+ settingsClone = JSON.parse(JSON.stringify(currentSettings))
95
+ currentUserPreferences = globalThis.__xydUserPreferences
96
+ })
97
+ }
98
+
99
+ return {
100
+ name: 'xyd:virtual-settings',
101
+
102
+ resolveId(id) {
103
+ if (id === virtualId) {
104
+ return resolvedId;
105
+ }
106
+ return null;
107
+ },
108
+
109
+ async load(id) {
110
+ if (id === 'virtual:xyd-settings.jsx') {
111
+ // console.log("load xyd-settings.jsx")
112
+
113
+ if (!firstInit && globalThis.__xydSettings) {
114
+ currentSettings = globalThis.__xydSettings
115
+ settingsClone = JSON.parse(JSON.stringify(currentSettings))
116
+ currentUserPreferences = globalThis.__xydUserPreferences
117
+ }
118
+ firstInit = true
119
+
120
+ return `
121
+ // Always get the latest settings from globalThis
122
+ const getCurrentSettings = () => {
123
+ return globalThis.__xydSettings || ${typeof currentSettings === "string" ? currentSettings : JSON.stringify(currentSettings)}
124
+ };
125
+
126
+ const getCurrentUserPreferences = () => {
127
+ return globalThis.__xydUserPreferences || ${typeof currentUserPreferences === "string" ? currentUserPreferences : JSON.stringify(currentUserPreferences)}
128
+ }
129
+
130
+ export default {
131
+ get settings() {
132
+ return getCurrentSettings();
133
+ },
134
+ get settingsClone() {
135
+ return ${typeof settingsClone === "string" ? settingsClone : JSON.stringify(settingsClone)}
136
+ },
137
+ get userPreferences() {
138
+ return getCurrentUserPreferences()
139
+ }
140
+ }
141
+ `
142
+ }
143
+ return null;
144
+ },
145
+
146
+ // async hotUpdate(ctx) {
147
+ // console.log("hot update")
148
+
149
+ // const isConfigfileUpdated = ctx.file.includes('react-router.config.ts')
150
+ // if (isConfigfileUpdated) {
151
+ // return
152
+ // }
153
+
154
+ // const newSettings = await readSettings();
155
+ // if (!newSettings) {
156
+ // console.log('⚠️ Failed to read new settings');
157
+ // return
158
+ // }
159
+
160
+ // if (options.appInit) {
161
+ // // TODO: better way to handle that - we need this cuz otherwise its inifiite reloads
162
+ // if (newSettings.engine?.uniform?.store) {
163
+ // await options.appInit({
164
+ // disableFSWrite: true,
165
+ // })
166
+ // } else {
167
+ // await options.appInit() // TODO: !!! IN THE FUTURE MORE EFFICIENT WAY !!!
168
+ // }
169
+ // }
170
+
171
+ // currentSettings = globalThis.__xydSettings
172
+ // settingsClone = JSON.parse(JSON.stringify(currentSettings))
173
+ // currentUserPreferences = globalThis.__xydUserPreferences
174
+ // // globalThis.__xydSettingsClone = JSON.parse(JSON.stringify(globalThis.__xydSettings))
175
+
176
+ // return
177
+ // },
178
+ };
179
+ }
180
+ }
181
+ }
182
+
183
+ export function vitePluginThemeCSS() {
184
+ return async function ({
185
+ preinstall
186
+ }: {
187
+ preinstall: {
188
+ settings: Settings
189
+ }
190
+ }): Promise<VitePlugin> {
191
+ return {
192
+ name: 'virtual:xyd-theme/index.css',
193
+
194
+ resolveId(source) {
195
+ if (source === 'virtual:xyd-theme/index.css') {
196
+ const __filename = fileURLToPath(import.meta.url);
197
+ const __dirname = path.dirname(__filename);
198
+
199
+ const themeName = preinstall.settings.theme?.name || DEFAULT_THEME
200
+ let themePath = ""
201
+
202
+ if (process.env.XYD_CLI) {
203
+ themePath = path.join(getHostPath(), `node_modules/@xyd-js/theme-${themeName}/dist`)
204
+ } else {
205
+ themePath = path.join(path.resolve(__dirname, "../../"), `xyd-theme-${themeName}/dist`)
206
+ }
207
+
208
+ return path.join(themePath, "index.css")
209
+ }
210
+
211
+ return null;
212
+ }
213
+ };
214
+ }
215
+ }
216
+
217
+ export function vitePluginThemeOverrideCSS() {
218
+ return async function ({ preinstall }: { preinstall: { settings: Settings } }): Promise<VitePlugin> {
219
+ return {
220
+ name: 'virtual:xyd-theme-override-css',
221
+
222
+ async resolveId(id) {
223
+ if (id === 'virtual:xyd-theme-override/index.css') {
224
+ const root = process.cwd();
225
+ const filePath = path.join(root, THEME_CONFIG_FOLDER, "./index.css");
226
+
227
+ try {
228
+ await fs.access(filePath);
229
+ return filePath;
230
+ } catch {
231
+ // File does not exist, omit it
232
+ return 'virtual:xyd-theme-override/empty.css';
233
+ }
234
+ }
235
+ return null;
236
+ },
237
+
238
+ async load(id) {
239
+ if (id === 'virtual:xyd-theme-override/empty.css') {
240
+ // Return an empty module
241
+ return '';
242
+ }
243
+ return null;
244
+ },
245
+ };
246
+ };
247
+ }
248
+
249
+ export function vitePluginTheme() {
250
+ return async function ({
251
+ preinstall
252
+ }: {
253
+ preinstall: {
254
+ settings: Settings
255
+ }
256
+ }): Promise<VitePlugin> {
257
+ return {
258
+ name: 'virtual:xyd-theme',
259
+ resolveId(id) {
260
+ if (id === 'virtual:xyd-theme') {
261
+ return id;
262
+ }
263
+ return null;
264
+ },
265
+ async load(id) {
266
+ if (id === 'virtual:xyd-theme') {
267
+ // return ''
268
+ const __filename = fileURLToPath(import.meta.url);
269
+ const __dirname = path.dirname(__filename);
270
+
271
+ const themeName = preinstall.settings.theme?.name || DEFAULT_THEME
272
+ let themePath = ""
273
+
274
+ if (process.env.XYD_CLI) {
275
+ themePath = `@xyd-js/theme-${themeName}`
276
+ } else {
277
+ themePath = path.join(path.resolve(__dirname, "../../"), `xyd-theme-${themeName}/src`)
278
+ }
279
+
280
+ // Return a module that imports the theme from the local workspace
281
+ return `
282
+ import Theme from '${themePath}';
283
+
284
+ export default Theme;
285
+ `;
286
+ }
287
+ return null;
288
+ }
289
+ };
290
+ }
291
+ }
292
+
293
+ function preset(settings: Settings, options: docsPluginOptions) {
294
+ const basePath = getDocsPluginBasePath()
295
+
296
+ return {
297
+ preinstall: [
298
+ preinstall,
299
+ ],
300
+ routes: [
301
+ route("", path.join(basePath, "src/pages/docs.tsx")),
302
+ // TODO: custom routes
303
+ route(options.urlPrefix ? `${options.urlPrefix}/*` : "*", path.join(basePath, "src/pages/docs.tsx"), {
304
+ id: "xyd-plugin-docs/docs",
305
+ }),
306
+ ],
307
+ vitePlugins: [
308
+ vitePluginSettings(options),
309
+ vitePluginTheme,
310
+ vitePluginThemeCSS,
311
+ vitePluginThemeOverrideCSS
312
+ ],
313
+ basePath
314
+ }
315
+ }
316
+
317
+ export const docsPreset = preset as Preset<unknown>
@@ -0,0 +1,262 @@
1
+ import fs from 'fs/promises';
2
+ import path from 'node:path';
3
+ import { URL } from 'node:url';
4
+
5
+ import { createServer } from 'vite';
6
+ import { config as dotenvConfig } from 'dotenv';
7
+
8
+ import { Settings } from "@xyd-js/core";
9
+ import { getThemeColors } from '@code-hike/lighter';
10
+
11
+ const extensions = ['tsx', 'ts', 'json'];
12
+
13
+ /**
14
+ * Reads `xyd` settings from the current working directory.
15
+ *
16
+ * This function searches for a file named 'xyd' with one of the supported extensions
17
+ * (tsx, jsx, js, ts, json) in the current working directory. If found, it loads the
18
+ * settings from that file.
19
+ *
20
+ * For React-based settings files (tsx, jsx, js, ts), it uses Vite's SSR module loading
21
+ * to evaluate the file and extract the default export. For JSON files, it simply
22
+ * parses the JSON content.
23
+ *
24
+ * Environment variables in the format $ENV_VAR will be replaced with actual environment
25
+ * variable values. Environment variables are loaded from .env files before processing.
26
+ *
27
+ * @returns A Promise that resolves to:
28
+ * - The Settings object if a valid settings file was found and loaded
29
+ * - A string if the settings file contains a string value
30
+ * - null if no settings file was found or an error occurred
31
+ *
32
+ * @throws May throw errors if file reading or parsing fails
33
+ */
34
+ export async function readSettings() {
35
+ const dirPath = process.cwd();
36
+ const baseFileName = 'docs';
37
+
38
+ // Load environment variables from .env files first
39
+ await loadEnvFiles(dirPath);
40
+
41
+ let settingsFilePath = '';
42
+ let reactSettings = false;
43
+
44
+ try {
45
+ const files = await fs.readdir(dirPath);
46
+ const settingsFile = files.find(file => {
47
+ const ext = path.extname(file).slice(1);
48
+ return file.startsWith(baseFileName) && extensions.includes(ext);
49
+ });
50
+
51
+ if (settingsFile) {
52
+ settingsFilePath = path.join(dirPath, settingsFile);
53
+ reactSettings = path.extname(settingsFile) !== '.json';
54
+ } else {
55
+ console.error(`No settings file found.\nFile must be named 'docs' with one of the following extensions: ${extensions.join(', ')}`);
56
+ return null;
57
+ }
58
+ } catch (error) {
59
+ console.error(error);
60
+ return null;
61
+ }
62
+
63
+ if (reactSettings) {
64
+ const settingsPreview = await createServer({
65
+ optimizeDeps: {
66
+ include: ["react/jsx-runtime"],
67
+ },
68
+ });
69
+ const config = await settingsPreview.ssrLoadModule(settingsFilePath);
70
+ const mod = config.default as Settings;
71
+
72
+ // Replace environment variables in the settings
73
+ const processedSettings = replaceEnvVars(mod);
74
+ presets(processedSettings)
75
+
76
+ return processedSettings
77
+ } else {
78
+ const rawJsonSettings = await fs.readFile(settingsFilePath, 'utf-8');
79
+ try {
80
+ let json = JSON.parse(rawJsonSettings) as Settings
81
+
82
+ // Replace environment variables in the settings
83
+ const processedSettings = replaceEnvVars(json);
84
+ presets(processedSettings)
85
+
86
+ return processedSettings
87
+ } catch (e) {
88
+ console.error("⚠️ Error parsing settings file")
89
+
90
+ return null
91
+ }
92
+ }
93
+ }
94
+
95
+ // if (settings?.theme?.coder?.syntaxHighlight) {
96
+
97
+ // }
98
+
99
+ function presets(settings: Settings) {
100
+ if (settings?.theme?.coder?.syntaxHighlight && typeof settings.theme.coder.syntaxHighlight === 'string') {
101
+ handleSyntaxHighlight(settings.theme.coder.syntaxHighlight, settings);
102
+ }
103
+ ensureNavigation(settings)
104
+
105
+ if (settings?.theme && !settings?.theme?.head?.length) {
106
+ settings.theme.head = []
107
+ }
108
+ }
109
+
110
+ async function handleSyntaxHighlight(syntaxHighlight: string, settings: Settings) {
111
+ try {
112
+ // Ensure theme.coder exists
113
+ if (!settings.theme) {
114
+ settings.theme = { name: 'default' } as any;
115
+ }
116
+ if (!settings.theme!.coder) {
117
+ settings.theme!.coder = {};
118
+ }
119
+
120
+ // Check if it's a URL
121
+ if (isUrl(syntaxHighlight)) {
122
+ // Fetch from remote URL
123
+ const response = await fetch(syntaxHighlight);
124
+ if (!response.ok) {
125
+ console.error(`⚠️ Failed to fetch syntax highlight from URL: ${syntaxHighlight}`);
126
+ return;
127
+ }
128
+ const json = await response.json();
129
+ settings.theme!.coder!.syntaxHighlight = json;
130
+ } else {
131
+ // Handle local path - but first check if ita's actually a path
132
+ const localPath = path.resolve(process.cwd(), syntaxHighlight);
133
+ try {
134
+ // Check if the file exists before trying to read it
135
+ await fs.access(localPath);
136
+ const fileContent = await fs.readFile(localPath, 'utf-8');
137
+ const json = JSON.parse(fileContent);
138
+ settings.theme!.coder!.syntaxHighlight = json;
139
+ } catch (error) {
140
+ }
141
+ }
142
+
143
+ const syntaxHighlightTheme = settings.theme?.coder?.syntaxHighlight
144
+ if (syntaxHighlightTheme) {
145
+ try {
146
+ const themeColors = await getThemeColors(syntaxHighlightTheme);
147
+
148
+ if (themeColors) {
149
+ globalThis.__xydUserPreferences = {
150
+ themeColors
151
+ }
152
+ }
153
+ } catch (error) {
154
+ console.error(`⚠️ Error processing syntax highlight theme colors.`, error);
155
+ }
156
+ }
157
+ } catch (error) {
158
+ console.error(`⚠️ Error processing syntax highlight: ${syntaxHighlight}`, error);
159
+ }
160
+ }
161
+
162
+
163
+ /**
164
+ * Loads environment variables from .env files
165
+ * @param dirPath - The directory path to search for .env files
166
+ */
167
+ async function loadEnvFiles(dirPath: string) {
168
+ try {
169
+ // Define the order of .env files to load (later files override earlier ones)
170
+ const envFiles = [
171
+ '.env',
172
+ '.env.local',
173
+ '.env.development',
174
+ '.env.production'
175
+ ];
176
+
177
+ for (const envFile of envFiles) {
178
+ const envPath = path.join(dirPath, envFile);
179
+
180
+ try {
181
+ await fs.access(envPath);
182
+ const result = dotenvConfig({
183
+ path: envPath,
184
+ override: true // Ensure variables are overridden
185
+ });
186
+
187
+ if (result.parsed && Object.keys(result.parsed).length > 0) {
188
+ console.debug(`📄 Loaded environment variables.`);
189
+ }
190
+ } catch (error) {
191
+ // File doesn't exist, which is fine - continue to next file
192
+ }
193
+ }
194
+ } catch (error) {
195
+ console.warn('⚠️ Error loading .env files:', error);
196
+ }
197
+ }
198
+
199
+ /**
200
+ * Recursively replaces environment variable placeholders in an object
201
+ * @param obj - The object to process
202
+ * @returns The object with environment variables replaced
203
+ */
204
+ function replaceEnvVars(obj: any): any {
205
+ if (obj === null || obj === undefined) {
206
+ return obj;
207
+ }
208
+
209
+ if (typeof obj === 'string') {
210
+ // Check if the string contains environment variable placeholders
211
+ if (obj.includes('$')) {
212
+ return obj.replace(/\$([A-Z_][A-Z0-9_]*)/g, (match, varName) => {
213
+ const envValue = process.env[varName];
214
+ if (envValue === undefined) {
215
+ console.warn(`\n⚠️ Environment variable "${varName}" is not set, keeping placeholder: ${match}`);
216
+ return match;
217
+ }
218
+ return envValue;
219
+ });
220
+ }
221
+ return obj;
222
+ }
223
+
224
+ if (Array.isArray(obj)) {
225
+ return obj.map(item => replaceEnvVars(item));
226
+ }
227
+
228
+ if (typeof obj === 'object') {
229
+ const result: any = {};
230
+ for (const [key, value] of Object.entries(obj)) {
231
+ result[key] = replaceEnvVars(value);
232
+ }
233
+ return result;
234
+ }
235
+
236
+ return obj;
237
+ }
238
+
239
+ function isUrl(str: string): boolean {
240
+ try {
241
+ new URL(str);
242
+ return true;
243
+ } catch {
244
+ return false;
245
+ }
246
+ }
247
+
248
+ function ensureNavigation(json: Settings) {
249
+ if (!json?.webeditor) {
250
+ json.webeditor = {}
251
+ }
252
+
253
+ if (!json?.navigation) {
254
+ json.navigation = {
255
+ sidebar: []
256
+ }
257
+ }
258
+
259
+ if (!json?.navigation?.sidebar) {
260
+ json.navigation.sidebar = []
261
+ }
262
+ }
@@ -0,0 +1,69 @@
1
+ import { Settings } from "@xyd-js/core";
2
+ import { gqlSchemaToReferences } from "@xyd-js/gql";
3
+ import type { Reference } from "@xyd-js/uniform";
4
+
5
+ import { Preset } from "../../types"
6
+ import { UniformPreset } from "../uniform"
7
+
8
+ interface graphqlPluginOptions {
9
+ urlPrefix?: string
10
+ root?: string
11
+ disableFSWrite?: boolean
12
+ }
13
+
14
+ function preset(
15
+ settings: Settings,
16
+ options: graphqlPluginOptions
17
+ ) {
18
+ return GraphQLUniformPreset.new(settings, options)
19
+ }
20
+
21
+ export const graphqlPreset = preset satisfies Preset<unknown>
22
+
23
+ class GraphQLUniformPreset extends UniformPreset {
24
+ private constructor(
25
+ settings: Settings,
26
+ options: {
27
+ disableFSWrite?: boolean
28
+ }
29
+ ) {
30
+ super(
31
+ "graphql",
32
+ settings.api?.graphql || "",
33
+ settings?.navigation?.sidebar || [],
34
+ options.disableFSWrite
35
+ )
36
+
37
+ this.uniformRefResolver = this.uniformRefResolver.bind(this)
38
+ }
39
+
40
+ static new(
41
+ settings: Settings,
42
+ options: graphqlPluginOptions
43
+ ) {
44
+ return new GraphQLUniformPreset(settings, {
45
+ disableFSWrite: options.disableFSWrite
46
+ })
47
+ .urlPrefix(options.urlPrefix || "")
48
+ .newUniformPreset()(settings, "graphql")
49
+ }
50
+
51
+ protected override async uniformRefResolver(filePath: string): Promise<Reference[]> {
52
+ if (!filePath) {
53
+ return []
54
+ }
55
+
56
+ const resp = await gqlSchemaToReferences(filePath)
57
+
58
+ if ("__UNSAFE_route" in resp && typeof resp.__UNSAFE_route === "function") {
59
+ // If the route is a function, we need to call it to get the actual route
60
+ const route = resp.__UNSAFE_route();
61
+ if (route) {
62
+ this.fileRouting(filePath, route);
63
+ }
64
+ }
65
+
66
+ return resp
67
+ }
68
+ }
69
+