@imjp/writenex-astro 0.1.0

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.
Files changed (141) hide show
  1. package/README.md +539 -0
  2. package/dist/chunk-5PM6EQE5.js +151 -0
  3. package/dist/chunk-5PM6EQE5.js.map +1 -0
  4. package/dist/chunk-7XU5X6CW.js +1331 -0
  5. package/dist/chunk-7XU5X6CW.js.map +1 -0
  6. package/dist/chunk-AAOQHQPU.js +574 -0
  7. package/dist/chunk-AAOQHQPU.js.map +1 -0
  8. package/dist/chunk-CF2XXJFF.js +1410 -0
  9. package/dist/chunk-CF2XXJFF.js.map +1 -0
  10. package/dist/chunk-CRPZUUDU.js +52 -0
  11. package/dist/chunk-CRPZUUDU.js.map +1 -0
  12. package/dist/chunk-CYLDJ3HZ.js +310 -0
  13. package/dist/chunk-CYLDJ3HZ.js.map +1 -0
  14. package/dist/chunk-KIKIPIFA.js +1 -0
  15. package/dist/chunk-KIKIPIFA.js.map +1 -0
  16. package/dist/chunk-XNTQTTJU.js +145 -0
  17. package/dist/chunk-XNTQTTJU.js.map +1 -0
  18. package/dist/client/index.css +2 -0
  19. package/dist/client/index.css.map +1 -0
  20. package/dist/client/index.js +375 -0
  21. package/dist/client/index.js.map +1 -0
  22. package/dist/client/styles.css +584 -0
  23. package/dist/client/variables.css +304 -0
  24. package/dist/config/index.d.ts +54 -0
  25. package/dist/config/index.js +38 -0
  26. package/dist/config/index.js.map +1 -0
  27. package/dist/config-BmEdBDo_.d.ts +220 -0
  28. package/dist/content-BWR52vD-.d.ts +64 -0
  29. package/dist/discovery/index.d.ts +310 -0
  30. package/dist/discovery/index.js +38 -0
  31. package/dist/discovery/index.js.map +1 -0
  32. package/dist/errors-C0iYiDTv.d.ts +107 -0
  33. package/dist/filesystem/index.d.ts +1292 -0
  34. package/dist/filesystem/index.js +203 -0
  35. package/dist/filesystem/index.js.map +1 -0
  36. package/dist/image-FP7w5ZIs.d.ts +47 -0
  37. package/dist/index.d.ts +64 -0
  38. package/dist/index.js +151 -0
  39. package/dist/index.js.map +1 -0
  40. package/dist/loader-55LWCXHA.js +12 -0
  41. package/dist/loader-55LWCXHA.js.map +1 -0
  42. package/dist/loader-CrdnaAWR.d.ts +327 -0
  43. package/dist/server/index.d.ts +357 -0
  44. package/dist/server/index.js +37 -0
  45. package/dist/server/index.js.map +1 -0
  46. package/package.json +94 -0
  47. package/src/client/App.tsx +900 -0
  48. package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
  49. package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
  50. package/src/client/components/ConfigPanel/index.ts +6 -0
  51. package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
  52. package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
  53. package/src/client/components/CreateContentModal/index.ts +7 -0
  54. package/src/client/components/Editor/Editor.css +885 -0
  55. package/src/client/components/Editor/Editor.tsx +484 -0
  56. package/src/client/components/Editor/ImageDialog.css +344 -0
  57. package/src/client/components/Editor/ImageDialog.tsx +367 -0
  58. package/src/client/components/Editor/LinkDialog.css +326 -0
  59. package/src/client/components/Editor/LinkDialog.tsx +332 -0
  60. package/src/client/components/Editor/index.ts +6 -0
  61. package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
  62. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
  63. package/src/client/components/FrontmatterForm/index.ts +7 -0
  64. package/src/client/components/Header/Header.css +300 -0
  65. package/src/client/components/Header/Header.tsx +300 -0
  66. package/src/client/components/Header/index.ts +7 -0
  67. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
  68. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
  69. package/src/client/components/KeyboardShortcuts/index.ts +6 -0
  70. package/src/client/components/LazyEditor.tsx +75 -0
  71. package/src/client/components/LiveRegion/LiveRegion.css +19 -0
  72. package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
  73. package/src/client/components/LiveRegion/index.ts +7 -0
  74. package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
  75. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
  76. package/src/client/components/SearchReplace/index.ts +7 -0
  77. package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
  78. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
  79. package/src/client/components/SelectCollectionModal/index.ts +7 -0
  80. package/src/client/components/Sidebar/Sidebar.css +570 -0
  81. package/src/client/components/Sidebar/Sidebar.tsx +617 -0
  82. package/src/client/components/Sidebar/index.ts +7 -0
  83. package/src/client/components/SkipLink/SkipLink.css +51 -0
  84. package/src/client/components/SkipLink/SkipLink.tsx +67 -0
  85. package/src/client/components/SkipLink/index.ts +7 -0
  86. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
  87. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
  88. package/src/client/components/UnsavedChangesModal/index.ts +1 -0
  89. package/src/client/components/VersionHistory/DiffViewer.css +430 -0
  90. package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
  91. package/src/client/components/VersionHistory/VersionActions.css +318 -0
  92. package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
  93. package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
  94. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
  95. package/src/client/components/VersionHistory/index.ts +9 -0
  96. package/src/client/context/ApiContext.tsx +154 -0
  97. package/src/client/context/ThemeContext.tsx +172 -0
  98. package/src/client/hooks/useAnnounce.ts +201 -0
  99. package/src/client/hooks/useApi.ts +374 -0
  100. package/src/client/hooks/useArrowNavigation.ts +286 -0
  101. package/src/client/hooks/useAutosave.ts +241 -0
  102. package/src/client/hooks/useFocusTrap.ts +178 -0
  103. package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
  104. package/src/client/hooks/useSearch.ts +206 -0
  105. package/src/client/hooks/useVersionHistory.ts +451 -0
  106. package/src/client/index.tsx +70 -0
  107. package/src/client/styles.css +584 -0
  108. package/src/client/utils/focus.ts +57 -0
  109. package/src/client/utils/openInEditor.ts +130 -0
  110. package/src/client/variables.css +304 -0
  111. package/src/config/defaults.ts +109 -0
  112. package/src/config/index.ts +32 -0
  113. package/src/config/loader.ts +174 -0
  114. package/src/config/schema.ts +161 -0
  115. package/src/core/constants.ts +39 -0
  116. package/src/core/errors.ts +739 -0
  117. package/src/core/index.ts +11 -0
  118. package/src/discovery/collections.ts +216 -0
  119. package/src/discovery/index.ts +33 -0
  120. package/src/discovery/patterns.ts +702 -0
  121. package/src/discovery/schema.ts +453 -0
  122. package/src/filesystem/images.ts +798 -0
  123. package/src/filesystem/index.ts +107 -0
  124. package/src/filesystem/reader.ts +452 -0
  125. package/src/filesystem/version-config.ts +390 -0
  126. package/src/filesystem/versions.ts +1339 -0
  127. package/src/filesystem/watcher.ts +226 -0
  128. package/src/filesystem/writer.ts +540 -0
  129. package/src/index.ts +61 -0
  130. package/src/integration.ts +228 -0
  131. package/src/server/assets.ts +254 -0
  132. package/src/server/cache.ts +355 -0
  133. package/src/server/index.ts +33 -0
  134. package/src/server/middleware.ts +209 -0
  135. package/src/server/routes.ts +1428 -0
  136. package/src/types/api.ts +61 -0
  137. package/src/types/config.ts +134 -0
  138. package/src/types/content.ts +64 -0
  139. package/src/types/image.ts +48 -0
  140. package/src/types/index.ts +58 -0
  141. package/src/types/version.ts +117 -0
@@ -0,0 +1,228 @@
1
+ /**
2
+ * @fileoverview Astro integration for Writenex visual editor
3
+ *
4
+ * This module provides the main Astro integration that injects the Writenex
5
+ * editor UI and API routes into an Astro project.
6
+ *
7
+ * ## Features:
8
+ * - Injects editor UI at /_writenex
9
+ * - Provides API routes for content CRUD operations
10
+ * - Auto-discovers content collections
11
+ * - Production guard to prevent accidental exposure
12
+ *
13
+ * ## Usage:
14
+ * ```typescript
15
+ * // astro.config.mjs
16
+ * import { defineConfig } from 'astro/config';
17
+ * import writenex from '@writenex/astro';
18
+ *
19
+ * export default defineConfig({
20
+ * integrations: [writenex()],
21
+ * });
22
+ * ```
23
+ *
24
+ * @module @writenex/astro/integration
25
+ */
26
+
27
+ import type { AstroIntegration } from "astro";
28
+ import type { WritenexOptions, WritenexConfig } from "@/types";
29
+ import { loadConfig } from "@/config/loader";
30
+ import { createMiddleware } from "@/server/middleware";
31
+ import { ContentWatcher } from "@/filesystem/watcher";
32
+ import { getCache } from "@/server/cache";
33
+
34
+ /**
35
+ * Default base path for the Writenex editor UI
36
+ */
37
+ const DEFAULT_BASE_PATH = "/_writenex";
38
+
39
+ /**
40
+ * Package name for logging
41
+ */
42
+ const PACKAGE_NAME = "@writenex/astro";
43
+
44
+ /**
45
+ * Creates the Writenex Astro integration.
46
+ *
47
+ * This integration injects the Writenex visual editor into your Astro project,
48
+ * providing a WYSIWYG interface for editing content collections.
49
+ *
50
+ * @param options - Integration options
51
+ * @param options.allowProduction - Allow running in production (default: false)
52
+ * @returns Astro integration object
53
+ *
54
+ * @example
55
+ * ```typescript
56
+ * // Basic usage
57
+ * export default defineConfig({
58
+ * integrations: [writenex()],
59
+ * });
60
+ *
61
+ * // With options
62
+ * export default defineConfig({
63
+ * integrations: [
64
+ * writenex({
65
+ * allowProduction: true, // Enable in production (use with caution)
66
+ * }),
67
+ * ],
68
+ * });
69
+ * ```
70
+ */
71
+ export default function writenex(options?: WritenexOptions): AstroIntegration {
72
+ const { allowProduction = false } = options ?? {};
73
+
74
+ // Use fixed base path for consistency and branding
75
+ const basePath = DEFAULT_BASE_PATH;
76
+
77
+ // Track if we should be active
78
+ let isActive = true;
79
+
80
+ // Store loaded configuration
81
+ let resolvedConfig: Required<WritenexConfig> | null = null;
82
+
83
+ // Store project root
84
+ let projectRoot = "";
85
+
86
+ // Store Astro's trailingSlash setting
87
+ let astroTrailingSlash: "always" | "never" | "ignore" = "ignore";
88
+
89
+ // File watcher instance
90
+ let watcher: ContentWatcher | null = null;
91
+
92
+ // Track if editor URL has been logged (to avoid duplicate logs)
93
+ let hasLoggedEditorUrl = false;
94
+
95
+ return {
96
+ name: PACKAGE_NAME,
97
+ hooks: {
98
+ /**
99
+ * Configuration setup hook
100
+ *
101
+ * This hook runs during Astro's config resolution phase.
102
+ * We use it to:
103
+ * 1. Check if we should run (production guard)
104
+ * 2. Load Writenex configuration
105
+ * 3. Register any necessary Vite plugins
106
+ */
107
+ "astro:config:setup": async ({ command, logger, config }) => {
108
+ // Production guard: disable in production unless explicitly allowed
109
+ if (command === "build" && !allowProduction) {
110
+ logger.warn(
111
+ "Disabled in production build. Use allowProduction: true to override."
112
+ );
113
+ isActive = false;
114
+ return;
115
+ }
116
+
117
+ // Store project root
118
+ projectRoot = config.root.pathname;
119
+
120
+ // Capture Astro's trailingSlash setting for preview URLs
121
+ astroTrailingSlash = config.trailingSlash ?? "ignore";
122
+
123
+ // Load Writenex configuration
124
+ const { config: loadedConfig, warnings } =
125
+ await loadConfig(projectRoot);
126
+ resolvedConfig = loadedConfig;
127
+
128
+ // Log any configuration warnings
129
+ for (const warning of warnings) {
130
+ logger.warn(warning);
131
+ }
132
+ },
133
+
134
+ /**
135
+ * Server setup hook
136
+ *
137
+ * This hook runs when the Astro dev server starts.
138
+ * We use it to:
139
+ * 1. Inject middleware for API routes
140
+ * 2. Serve the editor UI
141
+ * 3. Start file watcher for cache invalidation
142
+ */
143
+ "astro:server:setup": ({ server }) => {
144
+ // Skip if disabled (production guard triggered)
145
+ if (!isActive || !resolvedConfig) {
146
+ return;
147
+ }
148
+
149
+ // Create and register the middleware
150
+ const middleware = createMiddleware({
151
+ basePath,
152
+ projectRoot,
153
+ config: resolvedConfig,
154
+ trailingSlash: astroTrailingSlash,
155
+ });
156
+
157
+ server.middlewares.use(middleware);
158
+
159
+ // Setup cache with file watcher integration
160
+ const cache = getCache({ hasWatcher: true });
161
+
162
+ // Start file watcher for cache invalidation
163
+ watcher = new ContentWatcher(projectRoot, "src/content", {
164
+ onChange: (event) => {
165
+ cache.handleFileChange(event.type, event.collection);
166
+ },
167
+ });
168
+
169
+ watcher.start();
170
+ },
171
+
172
+ /**
173
+ * Server start hook
174
+ *
175
+ * This hook runs after the dev server has started and is listening.
176
+ * We use it to log the full editor URL with the actual server address.
177
+ */
178
+ "astro:server:start": ({ address, logger }) => {
179
+ if (!isActive || hasLoggedEditorUrl) {
180
+ return;
181
+ }
182
+
183
+ // Build the full URL from the server address
184
+ // Normalize loopback addresses to "localhost" for better readability
185
+ const protocol = "http";
186
+ const rawHost = address.address;
187
+ const isLoopback =
188
+ rawHost === "" ||
189
+ rawHost === "::" ||
190
+ rawHost === "127.0.0.1" ||
191
+ rawHost === "::1";
192
+ const host = isLoopback ? "localhost" : rawHost;
193
+ const port = address.port;
194
+ const editorUrl = `${protocol}://${host}:${port}${basePath}`;
195
+
196
+ logger.info(`Writenex editor running at: ${editorUrl}`);
197
+ hasLoggedEditorUrl = true;
198
+ },
199
+
200
+ /**
201
+ * Server done hook
202
+ *
203
+ * This hook runs when the server is shutting down.
204
+ * We use it to clean up the file watcher.
205
+ */
206
+ "astro:server:done": async () => {
207
+ if (watcher) {
208
+ await watcher.stop();
209
+ watcher = null;
210
+ }
211
+ },
212
+
213
+ /**
214
+ * Build done hook
215
+ *
216
+ * This hook runs after the build completes.
217
+ * Currently just logs a warning if production mode is enabled.
218
+ */
219
+ "astro:build:done": ({ logger }) => {
220
+ if (allowProduction) {
221
+ logger.warn(
222
+ "Production mode enabled. Ensure your deployment is secured."
223
+ );
224
+ }
225
+ },
226
+ },
227
+ };
228
+ }
@@ -0,0 +1,254 @@
1
+ /**
2
+ * @fileoverview Static asset serving for Writenex editor
3
+ *
4
+ * This module handles serving the editor UI HTML and static assets
5
+ * (JavaScript, CSS) for the Writenex editor interface.
6
+ *
7
+ * ## Asset Strategy:
8
+ * - In development: Serve from source with Vite transform
9
+ * - In production: Serve pre-bundled assets from dist/client
10
+ *
11
+ * @module @writenex/astro/server/assets
12
+ */
13
+
14
+ import type { IncomingMessage, ServerResponse } from "node:http";
15
+ import { readFile } from "node:fs/promises";
16
+ import { existsSync } from "node:fs";
17
+ import { join, extname } from "node:path";
18
+ import { fileURLToPath } from "node:url";
19
+ import type { MiddlewareContext } from "./middleware";
20
+
21
+ /**
22
+ * Get the package root directory
23
+ *
24
+ * This function determines the package root based on where the code is running from.
25
+ * When installed from npm, the structure is:
26
+ * node_modules/@writenex/astro/dist/index.js
27
+ *
28
+ * We need to find the package root to locate dist/client/ assets.
29
+ */
30
+ function getPackageRoot(): string {
31
+ const currentFile = fileURLToPath(import.meta.url);
32
+ const currentDir = fileURLToPath(new URL(".", import.meta.url));
33
+
34
+ // When bundled, import.meta.url points to the dist/index.js file
35
+ // We need to go up one level to get to package root
36
+ if (currentFile.endsWith("dist/index.js") || currentDir.endsWith("dist/")) {
37
+ return join(currentDir, "..");
38
+ }
39
+
40
+ // When running from source (development), we're in src/server/
41
+ // Go up 2 levels to get to package root
42
+ if (currentDir.includes("/src/")) {
43
+ return join(currentDir, "..", "..");
44
+ }
45
+
46
+ // Fallback: assume we're in dist
47
+ return join(currentDir, "..");
48
+ }
49
+
50
+ const PACKAGE_ROOT = getPackageRoot();
51
+
52
+ /**
53
+ * MIME types for static assets
54
+ */
55
+ const MIME_TYPES: Record<string, string> = {
56
+ ".js": "application/javascript",
57
+ ".mjs": "application/javascript",
58
+ ".css": "text/css",
59
+ ".json": "application/json",
60
+ ".svg": "image/svg+xml",
61
+ ".png": "image/png",
62
+ ".jpg": "image/jpeg",
63
+ ".jpeg": "image/jpeg",
64
+ ".gif": "image/gif",
65
+ ".woff": "font/woff",
66
+ ".woff2": "font/woff2",
67
+ ".ttf": "font/ttf",
68
+ };
69
+
70
+ /**
71
+ * Serve the editor HTML page
72
+ *
73
+ * This generates the HTML shell that loads the React editor application.
74
+ * The actual React components will be loaded via the bundled client assets.
75
+ *
76
+ * @param _req - The incoming request
77
+ * @param res - The server response
78
+ * @param context - Middleware context
79
+ */
80
+ export async function serveEditorHtml(
81
+ _req: IncomingMessage,
82
+ res: ServerResponse,
83
+ context: MiddlewareContext
84
+ ): Promise<void> {
85
+ const { basePath } = context;
86
+
87
+ const html = generateEditorHtml(basePath);
88
+
89
+ res.statusCode = 200;
90
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
91
+ res.setHeader("Cache-Control", "no-cache");
92
+ res.end(html);
93
+ }
94
+
95
+ /**
96
+ * Serve static assets (JS, CSS, etc.)
97
+ *
98
+ * @param _req - The incoming request
99
+ * @param res - The server response
100
+ * @param assetPath - Path to the asset (relative to assets directory)
101
+ * @param _context - Middleware context
102
+ */
103
+ export async function serveAsset(
104
+ _req: IncomingMessage,
105
+ res: ServerResponse,
106
+ assetPath: string,
107
+ _context: MiddlewareContext
108
+ ): Promise<void> {
109
+ // Determine asset location
110
+ // Assets are always in dist/client (pre-bundled by tsup)
111
+ const distPath = join(PACKAGE_ROOT, "dist", "client", assetPath);
112
+
113
+ if (!existsSync(distPath)) {
114
+ console.error("[writenex] Asset not found:", distPath);
115
+ res.statusCode = 404;
116
+ res.setHeader("Content-Type", "text/plain");
117
+ res.end(`Asset not found: ${assetPath}`);
118
+ return;
119
+ }
120
+
121
+ const filePath = distPath;
122
+
123
+ try {
124
+ const content = await readFile(filePath);
125
+ const ext = extname(assetPath).toLowerCase();
126
+ const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
127
+
128
+ res.statusCode = 200;
129
+ res.setHeader("Content-Type", mimeType);
130
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
131
+ res.end(content);
132
+ } catch (error) {
133
+ console.error(`[writenex] Failed to serve asset: ${assetPath}`, error);
134
+ res.statusCode = 500;
135
+ res.setHeader("Content-Type", "text/plain");
136
+ res.end("Failed to read asset");
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Generate the editor HTML shell
142
+ *
143
+ * This creates the HTML page that bootstraps the React editor application.
144
+ * It includes:
145
+ * - Meta tags for viewport and charset
146
+ * - CSS for the editor
147
+ * - React mount point
148
+ * - JavaScript bundle
149
+ *
150
+ * @param basePath - Base path for the editor
151
+ * @returns HTML string
152
+ */
153
+ function generateEditorHtml(basePath: string): string {
154
+ return `<!DOCTYPE html>
155
+ <html lang="en">
156
+ <head>
157
+ <meta charset="UTF-8">
158
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
159
+ <meta name="robots" content="noindex, nofollow">
160
+ <title>Writenex - Content Editor</title>
161
+
162
+ <!-- Editor styles -->
163
+ <link rel="stylesheet" href="${basePath}/assets/index.css">
164
+ <link rel="stylesheet" href="${basePath}/assets/styles.css">
165
+
166
+ <style>
167
+ /* Critical CSS for initial load */
168
+ * {
169
+ margin: 0;
170
+ padding: 0;
171
+ box-sizing: border-box;
172
+ }
173
+
174
+ html, body, #root {
175
+ height: 100%;
176
+ width: 100%;
177
+ }
178
+
179
+ body {
180
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
181
+ background-color: #0a0a0a;
182
+ color: #fafafa;
183
+ }
184
+
185
+ /* Loading state */
186
+ .writenex-loading {
187
+ display: flex;
188
+ flex-direction: column;
189
+ align-items: center;
190
+ justify-content: center;
191
+ height: 100%;
192
+ gap: 1rem;
193
+ }
194
+
195
+ .writenex-loading-spinner {
196
+ width: 40px;
197
+ height: 40px;
198
+ border: 3px solid rgba(59, 130, 246, 0.2);
199
+ border-top-color: #3b82f6;
200
+ border-radius: 50%;
201
+ animation: spin 1s linear infinite;
202
+ }
203
+
204
+ @keyframes spin {
205
+ to { transform: rotate(360deg); }
206
+ }
207
+
208
+ .writenex-loading-text {
209
+ color: #71717a;
210
+ font-size: 0.875rem;
211
+ }
212
+ </style>
213
+ </head>
214
+ <body>
215
+ <div id="root">
216
+ <!-- Loading state shown while React loads -->
217
+ <div class="writenex-loading">
218
+ <div class="writenex-loading-spinner"></div>
219
+ <div class="writenex-loading-text">Loading Writenex Editor...</div>
220
+ </div>
221
+ </div>
222
+
223
+ <!-- Configuration for the client app -->
224
+ <script>
225
+ window.__WRITENEX_CONFIG__ = {
226
+ basePath: "${basePath}",
227
+ apiBase: "${basePath}/api",
228
+ };
229
+ </script>
230
+
231
+ <!-- Editor application -->
232
+ <script type="module" src="${basePath}/assets/index.js"></script>
233
+ </body>
234
+ </html>`;
235
+ }
236
+
237
+ /**
238
+ * Get the path to bundled client assets
239
+ *
240
+ * @returns Path to the client dist directory
241
+ */
242
+ export function getClientDistPath(): string {
243
+ return join(PACKAGE_ROOT, "dist", "client");
244
+ }
245
+
246
+ /**
247
+ * Check if client assets are bundled
248
+ *
249
+ * @returns True if bundled assets exist
250
+ */
251
+ export function hasClientBundle(): boolean {
252
+ const indexPath = join(getClientDistPath(), "index.js");
253
+ return existsSync(indexPath);
254
+ }