@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,1331 @@
1
+ import {
2
+ discoverCollections,
3
+ mergeCollections
4
+ } from "./chunk-CYLDJ3HZ.js";
5
+ import {
6
+ ApiBadRequestError,
7
+ ApiMethodNotAllowedError,
8
+ CollectionDiscoveryError,
9
+ CollectionNotFoundError,
10
+ ContentNotFoundError,
11
+ ImageInvalidTypeError,
12
+ ImageNotFoundError,
13
+ PathTraversalError,
14
+ VersionNotFoundError,
15
+ clearVersions,
16
+ createContent,
17
+ deleteContent,
18
+ deleteVersion,
19
+ discoverContentImages,
20
+ getVersion,
21
+ getVersions,
22
+ isValidImageFile,
23
+ isWritenexError,
24
+ parseMultipartFormData,
25
+ restoreVersion,
26
+ saveVersion,
27
+ updateContent,
28
+ uploadImage,
29
+ wrapError
30
+ } from "./chunk-CF2XXJFF.js";
31
+ import {
32
+ getCollectionSummaries,
33
+ getContentFilePath,
34
+ readContentFile
35
+ } from "./chunk-AAOQHQPU.js";
36
+
37
+ // src/server/cache.ts
38
+ var DEFAULT_TTL_MS = 5 * 60 * 1e3;
39
+ var DEV_TTL_MS = 30 * 1e3;
40
+ var ServerCache = class {
41
+ /** Cache for collection discovery results */
42
+ collectionsCache = null;
43
+ /** Cache for content summaries, keyed by collection name */
44
+ contentCache = /* @__PURE__ */ new Map();
45
+ /** Cache for discovered images, keyed by "collection:contentId" */
46
+ imagesCache = /* @__PURE__ */ new Map();
47
+ /** Time-to-live for cache entries in milliseconds */
48
+ ttl;
49
+ /** Whether file watcher is integrated (allows longer TTL) */
50
+ hasWatcher = false;
51
+ constructor(options = {}) {
52
+ this.ttl = options.ttl ?? (options.hasWatcher ? DEFAULT_TTL_MS : DEV_TTL_MS);
53
+ this.hasWatcher = options.hasWatcher ?? false;
54
+ }
55
+ /**
56
+ * Enable file watcher integration
57
+ *
58
+ * When watcher is enabled, cache can use longer TTL since
59
+ * invalidation happens via watcher events.
60
+ */
61
+ enableWatcher() {
62
+ this.hasWatcher = true;
63
+ this.ttl = DEFAULT_TTL_MS;
64
+ }
65
+ /**
66
+ * Check if a cache entry is still valid
67
+ */
68
+ isValid(entry) {
69
+ if (!entry) return false;
70
+ return Date.now() - entry.timestamp < this.ttl;
71
+ }
72
+ // ==================== Collections Cache ====================
73
+ /**
74
+ * Get cached collections if valid
75
+ *
76
+ * @returns Cached collections or null if expired/not cached
77
+ */
78
+ getCollections() {
79
+ if (this.isValid(this.collectionsCache)) {
80
+ return this.collectionsCache.data;
81
+ }
82
+ return null;
83
+ }
84
+ /**
85
+ * Set collections cache
86
+ *
87
+ * @param collections - Collections to cache
88
+ */
89
+ setCollections(collections) {
90
+ this.collectionsCache = {
91
+ data: collections,
92
+ timestamp: Date.now()
93
+ };
94
+ }
95
+ /**
96
+ * Invalidate collections cache
97
+ */
98
+ invalidateCollections() {
99
+ this.collectionsCache = null;
100
+ }
101
+ // ==================== Content Cache ====================
102
+ /**
103
+ * Get cached content summaries for a collection if valid
104
+ *
105
+ * @param collection - Collection name
106
+ * @returns Cached content summaries or null if expired/not cached
107
+ */
108
+ getContent(collection) {
109
+ const entry = this.contentCache.get(collection);
110
+ if (this.isValid(entry)) {
111
+ return entry.data;
112
+ }
113
+ return null;
114
+ }
115
+ /**
116
+ * Set content cache for a collection
117
+ *
118
+ * @param collection - Collection name
119
+ * @param items - Content summaries to cache
120
+ */
121
+ setContent(collection, items) {
122
+ this.contentCache.set(collection, {
123
+ data: items,
124
+ timestamp: Date.now()
125
+ });
126
+ }
127
+ /**
128
+ * Invalidate content cache for a specific collection
129
+ *
130
+ * @param collection - Collection name to invalidate
131
+ */
132
+ invalidateContent(collection) {
133
+ this.contentCache.delete(collection);
134
+ }
135
+ /**
136
+ * Invalidate all content caches
137
+ */
138
+ invalidateAllContent() {
139
+ this.contentCache.clear();
140
+ }
141
+ // ==================== Images Cache ====================
142
+ /**
143
+ * Generate cache key for image cache
144
+ *
145
+ * @param collection - Collection name
146
+ * @param contentId - Content ID
147
+ * @returns Cache key string
148
+ */
149
+ getImagesCacheKey(collection, contentId) {
150
+ return `${collection}:${contentId}`;
151
+ }
152
+ /**
153
+ * Get cached images for a content item if valid
154
+ *
155
+ * @param collection - Collection name
156
+ * @param contentId - Content ID
157
+ * @returns Cached images or null if expired/not cached
158
+ */
159
+ getImages(collection, contentId) {
160
+ const key = this.getImagesCacheKey(collection, contentId);
161
+ const entry = this.imagesCache.get(key);
162
+ if (this.isValid(entry)) {
163
+ return entry.data;
164
+ }
165
+ return null;
166
+ }
167
+ /**
168
+ * Set images cache for a content item
169
+ *
170
+ * @param collection - Collection name
171
+ * @param contentId - Content ID
172
+ * @param images - Discovered images to cache
173
+ */
174
+ setImages(collection, contentId, images) {
175
+ const key = this.getImagesCacheKey(collection, contentId);
176
+ this.imagesCache.set(key, {
177
+ data: images,
178
+ timestamp: Date.now()
179
+ });
180
+ }
181
+ /**
182
+ * Invalidate image cache for a specific content item
183
+ *
184
+ * @param collection - Collection name
185
+ * @param contentId - Content ID
186
+ */
187
+ invalidateImages(collection, contentId) {
188
+ const key = this.getImagesCacheKey(collection, contentId);
189
+ this.imagesCache.delete(key);
190
+ }
191
+ /**
192
+ * Invalidate all image caches for a collection
193
+ *
194
+ * @param collection - Collection name
195
+ */
196
+ invalidateCollectionImages(collection) {
197
+ const prefix = `${collection}:`;
198
+ for (const key of this.imagesCache.keys()) {
199
+ if (key.startsWith(prefix)) {
200
+ this.imagesCache.delete(key);
201
+ }
202
+ }
203
+ }
204
+ /**
205
+ * Invalidate all image caches
206
+ */
207
+ invalidateAllImages() {
208
+ this.imagesCache.clear();
209
+ }
210
+ // ==================== Bulk Invalidation ====================
211
+ /**
212
+ * Invalidate all caches
213
+ *
214
+ * Called when a major change occurs that affects everything.
215
+ */
216
+ invalidateAll() {
217
+ this.collectionsCache = null;
218
+ this.contentCache.clear();
219
+ this.imagesCache.clear();
220
+ }
221
+ /**
222
+ * Handle file change event from watcher
223
+ *
224
+ * Intelligently invalidates only the affected caches.
225
+ *
226
+ * @param type - Type of file change (add, change, unlink)
227
+ * @param collection - Collection that was affected
228
+ * @param contentId - Optional content ID for targeted image cache invalidation
229
+ */
230
+ handleFileChange(type, collection, contentId) {
231
+ this.invalidateContent(collection);
232
+ if (contentId) {
233
+ this.invalidateImages(collection, contentId);
234
+ } else {
235
+ this.invalidateCollectionImages(collection);
236
+ }
237
+ if (type === "add" || type === "unlink") {
238
+ this.invalidateCollections();
239
+ }
240
+ }
241
+ // ==================== Stats ====================
242
+ /**
243
+ * Get cache statistics for debugging
244
+ *
245
+ * @returns Object with cache stats
246
+ */
247
+ getStats() {
248
+ return {
249
+ collectionsValid: this.isValid(this.collectionsCache),
250
+ contentCollections: Array.from(this.contentCache.keys()).filter(
251
+ (key) => this.isValid(this.contentCache.get(key))
252
+ ),
253
+ cachedImages: Array.from(this.imagesCache.keys()).filter(
254
+ (key) => this.isValid(this.imagesCache.get(key))
255
+ ),
256
+ ttl: this.ttl,
257
+ hasWatcher: this.hasWatcher
258
+ };
259
+ }
260
+ };
261
+ var globalCache = null;
262
+ function getCache(options) {
263
+ if (!globalCache) {
264
+ globalCache = new ServerCache(options);
265
+ }
266
+ return globalCache;
267
+ }
268
+ function resetCache() {
269
+ globalCache = null;
270
+ }
271
+
272
+ // src/server/routes.ts
273
+ import { createReadStream, existsSync as existsSync2, statSync } from "fs";
274
+ import { join as join2, extname as extname2 } from "path";
275
+
276
+ // src/server/assets.ts
277
+ import { readFile } from "fs/promises";
278
+ import { existsSync } from "fs";
279
+ import { join, extname } from "path";
280
+ import { fileURLToPath } from "url";
281
+ function getPackageRoot() {
282
+ const currentFile = fileURLToPath(import.meta.url);
283
+ const currentDir = fileURLToPath(new URL(".", import.meta.url));
284
+ if (currentFile.endsWith("dist/index.js") || currentDir.endsWith("dist/")) {
285
+ return join(currentDir, "..");
286
+ }
287
+ if (currentDir.includes("/src/")) {
288
+ return join(currentDir, "..", "..");
289
+ }
290
+ return join(currentDir, "..");
291
+ }
292
+ var PACKAGE_ROOT = getPackageRoot();
293
+ var MIME_TYPES = {
294
+ ".js": "application/javascript",
295
+ ".mjs": "application/javascript",
296
+ ".css": "text/css",
297
+ ".json": "application/json",
298
+ ".svg": "image/svg+xml",
299
+ ".png": "image/png",
300
+ ".jpg": "image/jpeg",
301
+ ".jpeg": "image/jpeg",
302
+ ".gif": "image/gif",
303
+ ".woff": "font/woff",
304
+ ".woff2": "font/woff2",
305
+ ".ttf": "font/ttf"
306
+ };
307
+ async function serveEditorHtml(_req, res, context) {
308
+ const { basePath } = context;
309
+ const html = generateEditorHtml(basePath);
310
+ res.statusCode = 200;
311
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
312
+ res.setHeader("Cache-Control", "no-cache");
313
+ res.end(html);
314
+ }
315
+ async function serveAsset(_req, res, assetPath, _context) {
316
+ const distPath = join(PACKAGE_ROOT, "dist", "client", assetPath);
317
+ if (!existsSync(distPath)) {
318
+ console.error("[writenex] Asset not found:", distPath);
319
+ res.statusCode = 404;
320
+ res.setHeader("Content-Type", "text/plain");
321
+ res.end(`Asset not found: ${assetPath}`);
322
+ return;
323
+ }
324
+ const filePath = distPath;
325
+ try {
326
+ const content = await readFile(filePath);
327
+ const ext = extname(assetPath).toLowerCase();
328
+ const mimeType = MIME_TYPES[ext] ?? "application/octet-stream";
329
+ res.statusCode = 200;
330
+ res.setHeader("Content-Type", mimeType);
331
+ res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
332
+ res.end(content);
333
+ } catch (error) {
334
+ console.error(`[writenex] Failed to serve asset: ${assetPath}`, error);
335
+ res.statusCode = 500;
336
+ res.setHeader("Content-Type", "text/plain");
337
+ res.end("Failed to read asset");
338
+ }
339
+ }
340
+ function generateEditorHtml(basePath) {
341
+ return `<!DOCTYPE html>
342
+ <html lang="en">
343
+ <head>
344
+ <meta charset="UTF-8">
345
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
346
+ <meta name="robots" content="noindex, nofollow">
347
+ <title>Writenex - Content Editor</title>
348
+
349
+ <!-- Editor styles -->
350
+ <link rel="stylesheet" href="${basePath}/assets/index.css">
351
+ <link rel="stylesheet" href="${basePath}/assets/styles.css">
352
+
353
+ <style>
354
+ /* Critical CSS for initial load */
355
+ * {
356
+ margin: 0;
357
+ padding: 0;
358
+ box-sizing: border-box;
359
+ }
360
+
361
+ html, body, #root {
362
+ height: 100%;
363
+ width: 100%;
364
+ }
365
+
366
+ body {
367
+ font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
368
+ background-color: #0a0a0a;
369
+ color: #fafafa;
370
+ }
371
+
372
+ /* Loading state */
373
+ .writenex-loading {
374
+ display: flex;
375
+ flex-direction: column;
376
+ align-items: center;
377
+ justify-content: center;
378
+ height: 100%;
379
+ gap: 1rem;
380
+ }
381
+
382
+ .writenex-loading-spinner {
383
+ width: 40px;
384
+ height: 40px;
385
+ border: 3px solid rgba(59, 130, 246, 0.2);
386
+ border-top-color: #3b82f6;
387
+ border-radius: 50%;
388
+ animation: spin 1s linear infinite;
389
+ }
390
+
391
+ @keyframes spin {
392
+ to { transform: rotate(360deg); }
393
+ }
394
+
395
+ .writenex-loading-text {
396
+ color: #71717a;
397
+ font-size: 0.875rem;
398
+ }
399
+ </style>
400
+ </head>
401
+ <body>
402
+ <div id="root">
403
+ <!-- Loading state shown while React loads -->
404
+ <div class="writenex-loading">
405
+ <div class="writenex-loading-spinner"></div>
406
+ <div class="writenex-loading-text">Loading Writenex Editor...</div>
407
+ </div>
408
+ </div>
409
+
410
+ <!-- Configuration for the client app -->
411
+ <script>
412
+ window.__WRITENEX_CONFIG__ = {
413
+ basePath: "${basePath}",
414
+ apiBase: "${basePath}/api",
415
+ };
416
+ </script>
417
+
418
+ <!-- Editor application -->
419
+ <script type="module" src="${basePath}/assets/index.js"></script>
420
+ </body>
421
+ </html>`;
422
+ }
423
+ function getClientDistPath() {
424
+ return join(PACKAGE_ROOT, "dist", "client");
425
+ }
426
+ function hasClientBundle() {
427
+ const indexPath = join(getClientDistPath(), "index.js");
428
+ return existsSync(indexPath);
429
+ }
430
+
431
+ // src/server/middleware.ts
432
+ function createMiddleware(context) {
433
+ const { basePath } = context;
434
+ const apiRouter = createApiRouter(context);
435
+ return async (req, res, next) => {
436
+ const url = req.url ?? "";
437
+ if (!url.startsWith(basePath)) {
438
+ return next();
439
+ }
440
+ const path = url.slice(basePath.length) || "/";
441
+ try {
442
+ if (path.startsWith("/api/")) {
443
+ return await apiRouter(req, res, path.slice(4));
444
+ }
445
+ if (path.startsWith("/assets/")) {
446
+ return await serveAsset(req, res, path.slice(8), context);
447
+ }
448
+ return await serveEditorHtml(req, res, context);
449
+ } catch (error) {
450
+ const writenexError = isWritenexError(error) ? error : wrapError(error, "API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */);
451
+ console.error(
452
+ `[writenex] Middleware error [${writenexError.code}]: ${writenexError.message}`
453
+ );
454
+ res.statusCode = writenexError.httpStatus;
455
+ res.setHeader("Content-Type", "application/json");
456
+ res.end(JSON.stringify(writenexError.toJSON()));
457
+ }
458
+ };
459
+ }
460
+ function parseQueryParams(url) {
461
+ const queryIndex = url.indexOf("?");
462
+ if (queryIndex === -1) return {};
463
+ const queryString = url.slice(queryIndex + 1);
464
+ const params = {};
465
+ for (const pair of queryString.split("&")) {
466
+ const [key, value] = pair.split("=");
467
+ if (key) {
468
+ params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : "";
469
+ }
470
+ }
471
+ return params;
472
+ }
473
+ async function parseJsonBody(req) {
474
+ return new Promise((resolve) => {
475
+ let body = "";
476
+ req.on("data", (chunk) => {
477
+ body += chunk.toString();
478
+ });
479
+ req.on("end", () => {
480
+ if (!body) {
481
+ resolve(null);
482
+ return;
483
+ }
484
+ try {
485
+ resolve(JSON.parse(body));
486
+ } catch {
487
+ resolve(null);
488
+ }
489
+ });
490
+ req.on("error", () => {
491
+ resolve(null);
492
+ });
493
+ });
494
+ }
495
+ function sendJson(res, data, statusCode = 200) {
496
+ res.statusCode = statusCode;
497
+ res.setHeader("Content-Type", "application/json");
498
+ res.end(JSON.stringify(data));
499
+ }
500
+ function sendError(res, message, statusCode = 400) {
501
+ sendJson(res, { error: message }, statusCode);
502
+ }
503
+ function sendWritenexError(res, error) {
504
+ sendJson(res, error.toJSON(), error.httpStatus);
505
+ }
506
+
507
+ // src/server/routes.ts
508
+ function createApiRouter(context) {
509
+ return async (req, res, path) => {
510
+ const method = req.method?.toUpperCase() ?? "GET";
511
+ const query = parseQueryParams(req.url ?? "");
512
+ const pathWithoutQuery = path.split("?")[0] ?? path;
513
+ const segments = pathWithoutQuery.split("/").filter(Boolean);
514
+ const params = { query };
515
+ if (segments[0] === "collections") {
516
+ if (method === "GET") {
517
+ return handleGetCollections(req, res, params, context);
518
+ }
519
+ return sendWritenexError(
520
+ res,
521
+ new ApiMethodNotAllowedError(method, ["GET"])
522
+ );
523
+ }
524
+ if (segments[0] === "config") {
525
+ if (method === "GET") {
526
+ if (segments[1] === "path") {
527
+ return handleGetConfigPath(req, res, params, context);
528
+ }
529
+ return handleGetConfig(req, res, params, context);
530
+ }
531
+ return sendWritenexError(
532
+ res,
533
+ new ApiMethodNotAllowedError(method, ["GET"])
534
+ );
535
+ }
536
+ if (segments[0] === "content") {
537
+ params.collection = segments[1];
538
+ params.id = segments[2];
539
+ switch (method) {
540
+ case "GET":
541
+ if (params.id) {
542
+ return handleGetContent(req, res, params, context);
543
+ }
544
+ return handleListContent(req, res, params, context);
545
+ case "POST":
546
+ return handleCreateContent(req, res, params, context);
547
+ case "PUT":
548
+ return handleUpdateContent(req, res, params, context);
549
+ case "DELETE":
550
+ return handleDeleteContent(req, res, params, context);
551
+ default:
552
+ return sendWritenexError(
553
+ res,
554
+ new ApiMethodNotAllowedError(method, [
555
+ "GET",
556
+ "POST",
557
+ "PUT",
558
+ "DELETE"
559
+ ])
560
+ );
561
+ }
562
+ }
563
+ if (segments[0] === "images") {
564
+ params.collection = segments[1];
565
+ params.id = segments[2];
566
+ if (method === "GET" && params.collection && params.id && segments.length > 3) {
567
+ const imagePath = segments.slice(3).join("/");
568
+ return handleServeImage(req, res, params, imagePath, context);
569
+ }
570
+ if (method === "GET" && params.collection && params.id) {
571
+ return handleImageDiscovery(req, res, params, context);
572
+ }
573
+ if (method === "POST") {
574
+ return handleImageUpload(req, res, params, context);
575
+ }
576
+ return sendWritenexError(
577
+ res,
578
+ new ApiMethodNotAllowedError(method, ["GET", "POST"])
579
+ );
580
+ }
581
+ if (segments[0] === "versions") {
582
+ params.collection = segments[1];
583
+ params.id = segments[2];
584
+ params.versionId = segments[3];
585
+ const action = segments[4];
586
+ switch (method) {
587
+ case "GET":
588
+ if (params.versionId) {
589
+ if (action === "diff") {
590
+ return handleGetVersionDiff(req, res, params, context);
591
+ }
592
+ return handleGetVersion(req, res, params, context);
593
+ }
594
+ return handleListVersions(req, res, params, context);
595
+ case "POST":
596
+ if (params.versionId && action === "restore") {
597
+ return handleRestoreVersion(req, res, params, context);
598
+ }
599
+ if (!params.versionId) {
600
+ return handleCreateVersion(req, res, params, context);
601
+ }
602
+ return sendWritenexError(
603
+ res,
604
+ new ApiMethodNotAllowedError(method, ["GET", "POST", "DELETE"])
605
+ );
606
+ case "DELETE":
607
+ if (params.versionId) {
608
+ return handleDeleteVersion(req, res, params, context);
609
+ }
610
+ return handleClearVersions(req, res, params, context);
611
+ default:
612
+ return sendWritenexError(
613
+ res,
614
+ new ApiMethodNotAllowedError(method, ["GET", "POST", "DELETE"])
615
+ );
616
+ }
617
+ }
618
+ if (segments[0] === "health") {
619
+ return sendJson(res, {
620
+ status: "ok",
621
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
622
+ });
623
+ }
624
+ return sendError(res, "Not found", 404);
625
+ };
626
+ }
627
+ var handleGetConfig = async (_req, res, _params, context) => {
628
+ const { config, trailingSlash } = context;
629
+ sendJson(res, {
630
+ images: config.images,
631
+ editor: config.editor,
632
+ trailingSlash
633
+ });
634
+ };
635
+ var handleGetConfigPath = async (_req, res, _params, context) => {
636
+ const { projectRoot } = context;
637
+ const { findConfigFile } = await import("./loader-55LWCXHA.js");
638
+ const configPath = findConfigFile(projectRoot);
639
+ sendJson(res, {
640
+ configPath,
641
+ projectRoot,
642
+ hasConfigFile: configPath !== null
643
+ });
644
+ };
645
+ var handleGetCollections = async (_req, res, _params, context) => {
646
+ const { config, projectRoot } = context;
647
+ const cache = getCache();
648
+ try {
649
+ let collections = cache.getCollections();
650
+ if (!collections) {
651
+ const discovered = await discoverCollections(projectRoot);
652
+ collections = mergeCollections(discovered, config.collections);
653
+ cache.setCollections(collections);
654
+ }
655
+ sendJson(res, { collections });
656
+ } catch (error) {
657
+ const wrappedError = isWritenexError(error) ? error : new CollectionDiscoveryError(
658
+ join2(projectRoot, "src/content"),
659
+ error instanceof Error ? error : void 0
660
+ );
661
+ sendWritenexError(res, wrappedError);
662
+ }
663
+ };
664
+ var handleListContent = async (_req, res, params, context) => {
665
+ const { collection, query } = params;
666
+ const { projectRoot } = context;
667
+ if (!collection) {
668
+ return sendWritenexError(
669
+ res,
670
+ new ApiBadRequestError("Collection name required")
671
+ );
672
+ }
673
+ const cache = getCache();
674
+ try {
675
+ const collectionPath = join2(projectRoot, "src/content", collection);
676
+ if (!existsSync2(collectionPath)) {
677
+ return sendWritenexError(res, new CollectionNotFoundError(collection));
678
+ }
679
+ const includeDrafts = query.draft === "true";
680
+ const sortBy = query.sort ?? "pubDate";
681
+ const sortOrder = query.order ?? "desc";
682
+ const isDefaultQuery = includeDrafts && sortBy === "pubDate" && sortOrder === "desc";
683
+ let items = isDefaultQuery ? cache.getContent(collection) : null;
684
+ if (!items) {
685
+ items = await getCollectionSummaries(collectionPath, {
686
+ includeDrafts,
687
+ sortBy,
688
+ sortOrder
689
+ });
690
+ if (isDefaultQuery) {
691
+ cache.setContent(collection, items);
692
+ }
693
+ }
694
+ sendJson(res, {
695
+ items,
696
+ total: items.length
697
+ });
698
+ } catch (error) {
699
+ console.error("[writenex] List content error:", error);
700
+ const wrappedError = isWritenexError(error) ? error : wrapError(error, "API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */);
701
+ sendWritenexError(res, wrappedError);
702
+ }
703
+ };
704
+ var handleGetContent = async (_req, res, params, context) => {
705
+ const { collection, id } = params;
706
+ const { projectRoot } = context;
707
+ if (!collection || !id) {
708
+ return sendWritenexError(
709
+ res,
710
+ new ApiBadRequestError("Collection and content ID required")
711
+ );
712
+ }
713
+ try {
714
+ const collectionPath = join2(projectRoot, "src/content", collection);
715
+ const filePath = getContentFilePath(collectionPath, id);
716
+ if (!filePath) {
717
+ return sendWritenexError(res, new ContentNotFoundError(collection, id));
718
+ }
719
+ const result = await readContentFile(filePath, collectionPath);
720
+ if (!result.success || !result.content) {
721
+ return sendError(res, result.error ?? "Failed to read content", 500);
722
+ }
723
+ sendJson(res, result.content);
724
+ } catch (error) {
725
+ const wrappedError = isWritenexError(error) ? error : wrapError(error, "API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */);
726
+ sendWritenexError(res, wrappedError);
727
+ }
728
+ };
729
+ var handleCreateContent = async (req, res, params, context) => {
730
+ const { collection } = params;
731
+ const { projectRoot, config } = context;
732
+ if (!collection) {
733
+ return sendError(res, "Collection name required", 400);
734
+ }
735
+ try {
736
+ const body = await parseJsonBody(req);
737
+ if (!body || typeof body !== "object") {
738
+ return sendError(res, "Invalid request body", 400);
739
+ }
740
+ const {
741
+ frontmatter,
742
+ body: contentBody,
743
+ slug
744
+ } = body;
745
+ if (!frontmatter) {
746
+ return sendError(res, "Frontmatter is required", 400);
747
+ }
748
+ const collectionPath = join2(projectRoot, "src/content", collection);
749
+ const cache = getCache();
750
+ let filePattern;
751
+ const configuredCollection = config.collections.find(
752
+ (c) => c.name === collection
753
+ );
754
+ if (configuredCollection?.filePattern) {
755
+ filePattern = configuredCollection.filePattern;
756
+ } else {
757
+ let collections = cache.getCollections();
758
+ if (!collections) {
759
+ const discovered = await discoverCollections(projectRoot);
760
+ collections = mergeCollections(discovered, config.collections);
761
+ cache.setCollections(collections);
762
+ }
763
+ const discoveredCollection = collections.find(
764
+ (c) => c.name === collection
765
+ );
766
+ if (discoveredCollection?.filePattern) {
767
+ filePattern = discoveredCollection.filePattern;
768
+ }
769
+ }
770
+ const result = await createContent(collectionPath, {
771
+ frontmatter,
772
+ body: contentBody ?? "",
773
+ slug,
774
+ filePattern
775
+ });
776
+ if (!result.success) {
777
+ return sendError(res, result.error ?? "Failed to create content", 500);
778
+ }
779
+ cache.handleFileChange("add", collection);
780
+ sendJson(res, {
781
+ success: true,
782
+ id: result.id,
783
+ path: result.path
784
+ });
785
+ } catch (error) {
786
+ const message = error instanceof Error ? error.message : "Unknown error";
787
+ sendError(res, `Failed to create content: ${message}`, 500);
788
+ }
789
+ };
790
+ var handleUpdateContent = async (req, res, params, context) => {
791
+ const { collection, id } = params;
792
+ const { projectRoot, config } = context;
793
+ if (!collection || !id) {
794
+ return sendWritenexError(
795
+ res,
796
+ new ApiBadRequestError("Collection and content ID required")
797
+ );
798
+ }
799
+ try {
800
+ const body = await parseJsonBody(req);
801
+ if (!body || typeof body !== "object") {
802
+ return sendWritenexError(
803
+ res,
804
+ new ApiBadRequestError("Invalid request body")
805
+ );
806
+ }
807
+ const {
808
+ frontmatter,
809
+ body: contentBody,
810
+ expectedMtime,
811
+ forceOverwrite
812
+ } = body;
813
+ const collectionPath = join2(projectRoot, "src/content", collection);
814
+ const filePath = getContentFilePath(collectionPath, id);
815
+ if (!filePath) {
816
+ return sendWritenexError(res, new ContentNotFoundError(collection, id));
817
+ }
818
+ const result = await updateContent(filePath, collectionPath, {
819
+ frontmatter,
820
+ body: contentBody,
821
+ projectRoot,
822
+ collection,
823
+ versionHistoryConfig: config.versionHistory,
824
+ // Only check mtime if not forcing overwrite
825
+ expectedMtime: forceOverwrite ? void 0 : expectedMtime
826
+ });
827
+ if (!result.success && result.conflict) {
828
+ return sendWritenexError(res, result.conflict);
829
+ }
830
+ if (!result.success) {
831
+ return sendError(res, result.error ?? "Failed to update content", 500);
832
+ }
833
+ const cache = getCache();
834
+ cache.handleFileChange("change", collection);
835
+ sendJson(res, {
836
+ success: true,
837
+ id: result.id,
838
+ path: result.path,
839
+ mtime: result.mtime
840
+ });
841
+ } catch (error) {
842
+ const wrappedError = isWritenexError(error) ? error : wrapError(error, "API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */);
843
+ sendWritenexError(res, wrappedError);
844
+ }
845
+ };
846
+ var handleDeleteContent = async (_req, res, params, context) => {
847
+ const { collection, id } = params;
848
+ const { projectRoot } = context;
849
+ if (!collection || !id) {
850
+ return sendError(res, "Collection and content ID required", 400);
851
+ }
852
+ try {
853
+ const collectionPath = join2(projectRoot, "src/content", collection);
854
+ const filePath = getContentFilePath(collectionPath, id);
855
+ if (!filePath) {
856
+ return sendError(
857
+ res,
858
+ `Content '${id}' not found in '${collection}'`,
859
+ 404
860
+ );
861
+ }
862
+ const result = await deleteContent(filePath);
863
+ if (!result.success) {
864
+ return sendError(res, result.error ?? "Failed to delete content", 500);
865
+ }
866
+ const cache = getCache();
867
+ cache.handleFileChange("unlink", collection);
868
+ sendJson(res, {
869
+ success: true,
870
+ path: result.path
871
+ });
872
+ } catch (error) {
873
+ const message = error instanceof Error ? error.message : "Unknown error";
874
+ sendError(res, `Failed to delete content: ${message}`, 500);
875
+ }
876
+ };
877
+ var handleImageUpload = async (req, res, _params, context) => {
878
+ const { projectRoot, config } = context;
879
+ try {
880
+ const chunks = [];
881
+ for await (const chunk of req) {
882
+ chunks.push(chunk);
883
+ }
884
+ const body = Buffer.concat(chunks);
885
+ const contentType = req.headers["content-type"] ?? "";
886
+ if (!contentType.includes("multipart/form-data")) {
887
+ return sendWritenexError(
888
+ res,
889
+ new ApiBadRequestError("Content-Type must be multipart/form-data")
890
+ );
891
+ }
892
+ const { file, fields } = parseMultipartFormData(body, contentType);
893
+ if (!file) {
894
+ return sendWritenexError(res, new ApiBadRequestError("No file uploaded"));
895
+ }
896
+ if (!fields.collection || !fields.contentId) {
897
+ return sendWritenexError(
898
+ res,
899
+ new ApiBadRequestError("collection and contentId are required")
900
+ );
901
+ }
902
+ if (!isValidImageFile(file.filename)) {
903
+ return sendWritenexError(
904
+ res,
905
+ new ImageInvalidTypeError(file.filename, [
906
+ ".jpg",
907
+ ".jpeg",
908
+ ".png",
909
+ ".gif",
910
+ ".webp",
911
+ ".avif",
912
+ ".svg"
913
+ ])
914
+ );
915
+ }
916
+ const result = await uploadImage({
917
+ filename: file.filename,
918
+ data: file.data,
919
+ collection: fields.collection,
920
+ contentId: fields.contentId,
921
+ projectRoot,
922
+ config: config.images
923
+ });
924
+ if (!result.success) {
925
+ return sendError(res, result.error ?? "Failed to upload image", 500);
926
+ }
927
+ sendJson(res, {
928
+ success: true,
929
+ path: result.path,
930
+ url: result.url
931
+ });
932
+ } catch (error) {
933
+ const wrappedError = isWritenexError(error) ? error : wrapError(error, "IMAGE_UPLOAD_ERROR" /* IMAGE_UPLOAD_ERROR */);
934
+ sendWritenexError(res, wrappedError);
935
+ }
936
+ };
937
+ var handleImageDiscovery = async (_req, res, params, context) => {
938
+ const { collection, id: contentId } = params;
939
+ const { projectRoot } = context;
940
+ if (!collection || !contentId) {
941
+ return sendError(res, "Collection and content ID required", 400);
942
+ }
943
+ const cache = getCache();
944
+ try {
945
+ const collectionPath = join2(projectRoot, "src/content", collection);
946
+ let collections = cache.getCollections();
947
+ if (!collections) {
948
+ collections = await discoverCollections(projectRoot);
949
+ cache.setCollections(collections);
950
+ }
951
+ if (!collections.some((c) => c.name === collection)) {
952
+ return sendError(res, `Collection '${collection}' not found`, 404);
953
+ }
954
+ const contentFilePath = getContentFilePath(collectionPath, contentId);
955
+ if (!contentFilePath) {
956
+ return sendError(
957
+ res,
958
+ `Content '${contentId}' not found in '${collection}'`,
959
+ 404
960
+ );
961
+ }
962
+ let images = cache.getImages(collection, contentId);
963
+ if (!images) {
964
+ const result = await discoverContentImages(collectionPath, contentId);
965
+ if (!result.success) {
966
+ return sendError(res, result.error ?? "Failed to discover images", 500);
967
+ }
968
+ images = result.images;
969
+ cache.setImages(collection, contentId, images);
970
+ }
971
+ sendJson(res, {
972
+ success: true,
973
+ images,
974
+ contentPath: contentFilePath
975
+ });
976
+ } catch (error) {
977
+ const message = error instanceof Error ? error.message : "Unknown error";
978
+ sendError(res, `Failed to discover images: ${message}`, 500);
979
+ }
980
+ };
981
+ var IMAGE_MIME_TYPES = {
982
+ ".jpg": "image/jpeg",
983
+ ".jpeg": "image/jpeg",
984
+ ".png": "image/png",
985
+ ".gif": "image/gif",
986
+ ".webp": "image/webp",
987
+ ".avif": "image/avif",
988
+ ".svg": "image/svg+xml"
989
+ };
990
+ var handleServeImage = async (_req, res, params, imagePath, context) => {
991
+ const { collection, id: contentId } = params;
992
+ const { projectRoot } = context;
993
+ if (!collection || !contentId) {
994
+ return sendWritenexError(
995
+ res,
996
+ new ApiBadRequestError("Collection and content ID required")
997
+ );
998
+ }
999
+ try {
1000
+ const collectionPath = join2(projectRoot, "src/content", collection);
1001
+ const contentFilePath = getContentFilePath(collectionPath, contentId);
1002
+ if (!contentFilePath) {
1003
+ return sendWritenexError(
1004
+ res,
1005
+ new ContentNotFoundError(collection, contentId)
1006
+ );
1007
+ }
1008
+ let fullImagePath;
1009
+ if (contentFilePath.endsWith("/index.md") || contentFilePath.endsWith("/index.mdx")) {
1010
+ const contentFolder = contentFilePath.replace(/\/index\.mdx?$/, "");
1011
+ fullImagePath = join2(contentFolder, imagePath);
1012
+ } else {
1013
+ fullImagePath = join2(collectionPath, contentId, imagePath);
1014
+ }
1015
+ const normalizedPath = join2(fullImagePath);
1016
+ if (!normalizedPath.startsWith(collectionPath)) {
1017
+ return sendWritenexError(
1018
+ res,
1019
+ new PathTraversalError(imagePath, collectionPath)
1020
+ );
1021
+ }
1022
+ if (!existsSync2(fullImagePath)) {
1023
+ return sendWritenexError(res, new ImageNotFoundError(imagePath));
1024
+ }
1025
+ const stats = statSync(fullImagePath);
1026
+ if (!stats.isFile()) {
1027
+ return sendWritenexError(
1028
+ res,
1029
+ new ApiBadRequestError("Requested path is not a file")
1030
+ );
1031
+ }
1032
+ const ext = extname2(fullImagePath).toLowerCase();
1033
+ const mimeType = IMAGE_MIME_TYPES[ext] ?? "application/octet-stream";
1034
+ res.setHeader("Content-Type", mimeType);
1035
+ res.setHeader("Content-Length", stats.size);
1036
+ res.setHeader("Cache-Control", "public, max-age=3600");
1037
+ const stream = createReadStream(fullImagePath);
1038
+ stream.pipe(res);
1039
+ } catch (error) {
1040
+ const wrappedError = isWritenexError(error) ? error : wrapError(error, "API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */);
1041
+ sendWritenexError(res, wrappedError);
1042
+ }
1043
+ };
1044
+ function getResolvedVersionConfig(config) {
1045
+ return {
1046
+ enabled: config?.enabled ?? true,
1047
+ maxVersions: config?.maxVersions ?? 20,
1048
+ storagePath: config?.storagePath ?? ".writenex/versions"
1049
+ };
1050
+ }
1051
+ var handleListVersions = async (_req, res, params, context) => {
1052
+ const { collection, id } = params;
1053
+ const { projectRoot, config } = context;
1054
+ if (!collection || !id) {
1055
+ return sendError(res, "Collection and content ID required", 400);
1056
+ }
1057
+ try {
1058
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1059
+ const versions = await getVersions(
1060
+ projectRoot,
1061
+ collection,
1062
+ id,
1063
+ versionConfig
1064
+ );
1065
+ sendJson(res, {
1066
+ success: true,
1067
+ versions,
1068
+ total: versions.length
1069
+ });
1070
+ } catch (error) {
1071
+ const message = error instanceof Error ? error.message : "Unknown error";
1072
+ sendError(res, `Failed to list versions: ${message}`, 500);
1073
+ }
1074
+ };
1075
+ var handleGetVersion = async (_req, res, params, context) => {
1076
+ const { collection, id, versionId } = params;
1077
+ const { projectRoot, config } = context;
1078
+ if (!collection || !id || !versionId) {
1079
+ return sendWritenexError(
1080
+ res,
1081
+ new ApiBadRequestError("Collection, content ID, and version ID required")
1082
+ );
1083
+ }
1084
+ try {
1085
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1086
+ const version = await getVersion(
1087
+ projectRoot,
1088
+ collection,
1089
+ id,
1090
+ versionId,
1091
+ versionConfig
1092
+ );
1093
+ if (!version) {
1094
+ return sendWritenexError(
1095
+ res,
1096
+ new VersionNotFoundError(collection, id, versionId)
1097
+ );
1098
+ }
1099
+ sendJson(res, {
1100
+ success: true,
1101
+ version
1102
+ });
1103
+ } catch (error) {
1104
+ const wrappedError = isWritenexError(error) ? error : wrapError(error, "API_INTERNAL_ERROR" /* API_INTERNAL_ERROR */);
1105
+ sendWritenexError(res, wrappedError);
1106
+ }
1107
+ };
1108
+ var handleCreateVersion = async (req, res, params, context) => {
1109
+ const { collection, id } = params;
1110
+ const { projectRoot, config } = context;
1111
+ if (!collection || !id) {
1112
+ return sendError(res, "Collection and content ID required", 400);
1113
+ }
1114
+ try {
1115
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1116
+ if (!versionConfig.enabled) {
1117
+ return sendError(res, "Version history is disabled", 400);
1118
+ }
1119
+ const body = await parseJsonBody(req);
1120
+ const label = body && typeof body === "object" && "label" in body ? String(body.label) : void 0;
1121
+ const collectionPath = join2(projectRoot, "src/content", collection);
1122
+ const filePath = getContentFilePath(collectionPath, id);
1123
+ if (!filePath) {
1124
+ return sendError(
1125
+ res,
1126
+ `Content '${id}' not found in '${collection}'`,
1127
+ 404
1128
+ );
1129
+ }
1130
+ const readResult = await readContentFile(filePath, collectionPath);
1131
+ if (!readResult.success || !readResult.content) {
1132
+ return sendError(res, readResult.error ?? "Failed to read content", 500);
1133
+ }
1134
+ const result = await saveVersion(
1135
+ projectRoot,
1136
+ collection,
1137
+ id,
1138
+ readResult.content.raw,
1139
+ versionConfig,
1140
+ { label }
1141
+ );
1142
+ if (!result.success) {
1143
+ return sendError(res, result.error ?? "Failed to create version", 500);
1144
+ }
1145
+ sendJson(res, {
1146
+ success: true,
1147
+ version: result.version
1148
+ });
1149
+ } catch (error) {
1150
+ const message = error instanceof Error ? error.message : "Unknown error";
1151
+ sendError(res, `Failed to create version: ${message}`, 500);
1152
+ }
1153
+ };
1154
+ var handleRestoreVersion = async (_req, res, params, context) => {
1155
+ const { collection, id, versionId } = params;
1156
+ const { projectRoot, config } = context;
1157
+ if (!collection || !id || !versionId) {
1158
+ return sendError(
1159
+ res,
1160
+ "Collection, content ID, and version ID required",
1161
+ 400
1162
+ );
1163
+ }
1164
+ try {
1165
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1166
+ const collectionPath = join2(projectRoot, "src/content", collection);
1167
+ const filePath = getContentFilePath(collectionPath, id);
1168
+ if (!filePath) {
1169
+ return sendError(
1170
+ res,
1171
+ `Content '${id}' not found in '${collection}'`,
1172
+ 404
1173
+ );
1174
+ }
1175
+ const result = await restoreVersion(
1176
+ projectRoot,
1177
+ collection,
1178
+ id,
1179
+ versionId,
1180
+ filePath,
1181
+ versionConfig
1182
+ );
1183
+ if (!result.success) {
1184
+ if (result.error?.includes("not found")) {
1185
+ return sendError(res, result.error, 404);
1186
+ }
1187
+ return sendError(res, result.error ?? "Failed to restore version", 500);
1188
+ }
1189
+ const cache = getCache();
1190
+ cache.handleFileChange("change", collection);
1191
+ sendJson(res, {
1192
+ success: true,
1193
+ version: result.version,
1194
+ content: result.content,
1195
+ safetySnapshot: result.safetySnapshot
1196
+ });
1197
+ } catch (error) {
1198
+ const message = error instanceof Error ? error.message : "Unknown error";
1199
+ sendError(res, `Failed to restore version: ${message}`, 500);
1200
+ }
1201
+ };
1202
+ var handleGetVersionDiff = async (_req, res, params, context) => {
1203
+ const { collection, id, versionId } = params;
1204
+ const { projectRoot, config } = context;
1205
+ if (!collection || !id || !versionId) {
1206
+ return sendError(
1207
+ res,
1208
+ "Collection, content ID, and version ID required",
1209
+ 400
1210
+ );
1211
+ }
1212
+ try {
1213
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1214
+ const version = await getVersion(
1215
+ projectRoot,
1216
+ collection,
1217
+ id,
1218
+ versionId,
1219
+ versionConfig
1220
+ );
1221
+ if (!version) {
1222
+ return sendError(res, `Version '${versionId}' not found`, 404);
1223
+ }
1224
+ const collectionPath = join2(projectRoot, "src/content", collection);
1225
+ const filePath = getContentFilePath(collectionPath, id);
1226
+ if (!filePath) {
1227
+ return sendError(
1228
+ res,
1229
+ `Content '${id}' not found in '${collection}'`,
1230
+ 404
1231
+ );
1232
+ }
1233
+ const readResult = await readContentFile(filePath, collectionPath);
1234
+ if (!readResult.success || !readResult.content) {
1235
+ return sendError(
1236
+ res,
1237
+ readResult.error ?? "Failed to read current content",
1238
+ 500
1239
+ );
1240
+ }
1241
+ sendJson(res, {
1242
+ success: true,
1243
+ version,
1244
+ current: {
1245
+ content: readResult.content.raw,
1246
+ frontmatter: readResult.content.frontmatter,
1247
+ body: readResult.content.body
1248
+ }
1249
+ });
1250
+ } catch (error) {
1251
+ const message = error instanceof Error ? error.message : "Unknown error";
1252
+ sendError(res, `Failed to get diff data: ${message}`, 500);
1253
+ }
1254
+ };
1255
+ var handleDeleteVersion = async (_req, res, params, context) => {
1256
+ const { collection, id, versionId } = params;
1257
+ const { projectRoot, config } = context;
1258
+ if (!collection || !id || !versionId) {
1259
+ return sendError(
1260
+ res,
1261
+ "Collection, content ID, and version ID required",
1262
+ 400
1263
+ );
1264
+ }
1265
+ try {
1266
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1267
+ const result = await deleteVersion(
1268
+ projectRoot,
1269
+ collection,
1270
+ id,
1271
+ versionId,
1272
+ versionConfig
1273
+ );
1274
+ if (!result.success) {
1275
+ if (result.error?.includes("not found")) {
1276
+ return sendError(res, result.error, 404);
1277
+ }
1278
+ return sendError(res, result.error ?? "Failed to delete version", 500);
1279
+ }
1280
+ sendJson(res, {
1281
+ success: true,
1282
+ version: result.version
1283
+ });
1284
+ } catch (error) {
1285
+ const message = error instanceof Error ? error.message : "Unknown error";
1286
+ sendError(res, `Failed to delete version: ${message}`, 500);
1287
+ }
1288
+ };
1289
+ var handleClearVersions = async (_req, res, params, context) => {
1290
+ const { collection, id } = params;
1291
+ const { projectRoot, config } = context;
1292
+ if (!collection || !id) {
1293
+ return sendError(res, "Collection and content ID required", 400);
1294
+ }
1295
+ try {
1296
+ const versionConfig = getResolvedVersionConfig(config.versionHistory);
1297
+ const result = await clearVersions(
1298
+ projectRoot,
1299
+ collection,
1300
+ id,
1301
+ versionConfig
1302
+ );
1303
+ if (!result.success) {
1304
+ return sendError(res, result.error ?? "Failed to clear versions", 500);
1305
+ }
1306
+ sendJson(res, {
1307
+ success: true
1308
+ });
1309
+ } catch (error) {
1310
+ const message = error instanceof Error ? error.message : "Unknown error";
1311
+ sendError(res, `Failed to clear versions: ${message}`, 500);
1312
+ }
1313
+ };
1314
+
1315
+ export {
1316
+ ServerCache,
1317
+ getCache,
1318
+ resetCache,
1319
+ createApiRouter,
1320
+ serveEditorHtml,
1321
+ serveAsset,
1322
+ getClientDistPath,
1323
+ hasClientBundle,
1324
+ createMiddleware,
1325
+ parseQueryParams,
1326
+ parseJsonBody,
1327
+ sendJson,
1328
+ sendError,
1329
+ sendWritenexError
1330
+ };
1331
+ //# sourceMappingURL=chunk-7XU5X6CW.js.map