@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.
- package/README.md +539 -0
- package/dist/chunk-5PM6EQE5.js +151 -0
- package/dist/chunk-5PM6EQE5.js.map +1 -0
- package/dist/chunk-7XU5X6CW.js +1331 -0
- package/dist/chunk-7XU5X6CW.js.map +1 -0
- package/dist/chunk-AAOQHQPU.js +574 -0
- package/dist/chunk-AAOQHQPU.js.map +1 -0
- package/dist/chunk-CF2XXJFF.js +1410 -0
- package/dist/chunk-CF2XXJFF.js.map +1 -0
- package/dist/chunk-CRPZUUDU.js +52 -0
- package/dist/chunk-CRPZUUDU.js.map +1 -0
- package/dist/chunk-CYLDJ3HZ.js +310 -0
- package/dist/chunk-CYLDJ3HZ.js.map +1 -0
- package/dist/chunk-KIKIPIFA.js +1 -0
- package/dist/chunk-KIKIPIFA.js.map +1 -0
- package/dist/chunk-XNTQTTJU.js +145 -0
- package/dist/chunk-XNTQTTJU.js.map +1 -0
- package/dist/client/index.css +2 -0
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.js +375 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/styles.css +584 -0
- package/dist/client/variables.css +304 -0
- package/dist/config/index.d.ts +54 -0
- package/dist/config/index.js +38 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config-BmEdBDo_.d.ts +220 -0
- package/dist/content-BWR52vD-.d.ts +64 -0
- package/dist/discovery/index.d.ts +310 -0
- package/dist/discovery/index.js +38 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/errors-C0iYiDTv.d.ts +107 -0
- package/dist/filesystem/index.d.ts +1292 -0
- package/dist/filesystem/index.js +203 -0
- package/dist/filesystem/index.js.map +1 -0
- package/dist/image-FP7w5ZIs.d.ts +47 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +151 -0
- package/dist/index.js.map +1 -0
- package/dist/loader-55LWCXHA.js +12 -0
- package/dist/loader-55LWCXHA.js.map +1 -0
- package/dist/loader-CrdnaAWR.d.ts +327 -0
- package/dist/server/index.d.ts +357 -0
- package/dist/server/index.js +37 -0
- package/dist/server/index.js.map +1 -0
- package/package.json +94 -0
- package/src/client/App.tsx +900 -0
- package/src/client/components/ConfigPanel/ConfigPanel.css +553 -0
- package/src/client/components/ConfigPanel/ConfigPanel.tsx +396 -0
- package/src/client/components/ConfigPanel/index.ts +6 -0
- package/src/client/components/CreateContentModal/CreateContentModal.css +327 -0
- package/src/client/components/CreateContentModal/CreateContentModal.tsx +216 -0
- package/src/client/components/CreateContentModal/index.ts +7 -0
- package/src/client/components/Editor/Editor.css +885 -0
- package/src/client/components/Editor/Editor.tsx +484 -0
- package/src/client/components/Editor/ImageDialog.css +344 -0
- package/src/client/components/Editor/ImageDialog.tsx +367 -0
- package/src/client/components/Editor/LinkDialog.css +326 -0
- package/src/client/components/Editor/LinkDialog.tsx +332 -0
- package/src/client/components/Editor/index.ts +6 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.css +468 -0
- package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +914 -0
- package/src/client/components/FrontmatterForm/index.ts +7 -0
- package/src/client/components/Header/Header.css +300 -0
- package/src/client/components/Header/Header.tsx +300 -0
- package/src/client/components/Header/index.ts +7 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.css +239 -0
- package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +151 -0
- package/src/client/components/KeyboardShortcuts/index.ts +6 -0
- package/src/client/components/LazyEditor.tsx +75 -0
- package/src/client/components/LiveRegion/LiveRegion.css +19 -0
- package/src/client/components/LiveRegion/LiveRegion.tsx +60 -0
- package/src/client/components/LiveRegion/index.ts +7 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.css +300 -0
- package/src/client/components/SearchReplace/SearchReplacePanel.tsx +332 -0
- package/src/client/components/SearchReplace/index.ts +7 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.css +308 -0
- package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +223 -0
- package/src/client/components/SelectCollectionModal/index.ts +7 -0
- package/src/client/components/Sidebar/Sidebar.css +570 -0
- package/src/client/components/Sidebar/Sidebar.tsx +617 -0
- package/src/client/components/Sidebar/index.ts +7 -0
- package/src/client/components/SkipLink/SkipLink.css +51 -0
- package/src/client/components/SkipLink/SkipLink.tsx +67 -0
- package/src/client/components/SkipLink/index.ts +7 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.css +233 -0
- package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +160 -0
- package/src/client/components/UnsavedChangesModal/index.ts +1 -0
- package/src/client/components/VersionHistory/DiffViewer.css +430 -0
- package/src/client/components/VersionHistory/DiffViewer.tsx +383 -0
- package/src/client/components/VersionHistory/VersionActions.css +318 -0
- package/src/client/components/VersionHistory/VersionActions.tsx +277 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.css +369 -0
- package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +469 -0
- package/src/client/components/VersionHistory/index.ts +9 -0
- package/src/client/context/ApiContext.tsx +154 -0
- package/src/client/context/ThemeContext.tsx +172 -0
- package/src/client/hooks/useAnnounce.ts +201 -0
- package/src/client/hooks/useApi.ts +374 -0
- package/src/client/hooks/useArrowNavigation.ts +286 -0
- package/src/client/hooks/useAutosave.ts +241 -0
- package/src/client/hooks/useFocusTrap.ts +178 -0
- package/src/client/hooks/useKeyboardShortcuts.ts +203 -0
- package/src/client/hooks/useSearch.ts +206 -0
- package/src/client/hooks/useVersionHistory.ts +451 -0
- package/src/client/index.tsx +70 -0
- package/src/client/styles.css +584 -0
- package/src/client/utils/focus.ts +57 -0
- package/src/client/utils/openInEditor.ts +130 -0
- package/src/client/variables.css +304 -0
- package/src/config/defaults.ts +109 -0
- package/src/config/index.ts +32 -0
- package/src/config/loader.ts +174 -0
- package/src/config/schema.ts +161 -0
- package/src/core/constants.ts +39 -0
- package/src/core/errors.ts +739 -0
- package/src/core/index.ts +11 -0
- package/src/discovery/collections.ts +216 -0
- package/src/discovery/index.ts +33 -0
- package/src/discovery/patterns.ts +702 -0
- package/src/discovery/schema.ts +453 -0
- package/src/filesystem/images.ts +798 -0
- package/src/filesystem/index.ts +107 -0
- package/src/filesystem/reader.ts +452 -0
- package/src/filesystem/version-config.ts +390 -0
- package/src/filesystem/versions.ts +1339 -0
- package/src/filesystem/watcher.ts +226 -0
- package/src/filesystem/writer.ts +540 -0
- package/src/index.ts +61 -0
- package/src/integration.ts +228 -0
- package/src/server/assets.ts +254 -0
- package/src/server/cache.ts +355 -0
- package/src/server/index.ts +33 -0
- package/src/server/middleware.ts +209 -0
- package/src/server/routes.ts +1428 -0
- package/src/types/api.ts +61 -0
- package/src/types/config.ts +134 -0
- package/src/types/content.ts +64 -0
- package/src/types/image.ts +48 -0
- package/src/types/index.ts +58 -0
- 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
|