@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,355 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Server-side caching for Writenex
|
|
3
|
+
*
|
|
4
|
+
* Provides in-memory caching for collection discovery and content summaries
|
|
5
|
+
* to improve performance by avoiding repeated filesystem operations.
|
|
6
|
+
*
|
|
7
|
+
* ## Features:
|
|
8
|
+
* - TTL-based cache expiration
|
|
9
|
+
* - Per-collection content caching
|
|
10
|
+
* - Image discovery caching
|
|
11
|
+
* - Manual cache invalidation
|
|
12
|
+
* - Integration with file watcher for automatic invalidation
|
|
13
|
+
*
|
|
14
|
+
* @module @writenex/astro/server/cache
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type {
|
|
18
|
+
DiscoveredCollection,
|
|
19
|
+
ContentSummary,
|
|
20
|
+
DiscoveredImage,
|
|
21
|
+
} from "@/types";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Cache entry with timestamp and data
|
|
25
|
+
*/
|
|
26
|
+
interface CacheEntry<T> {
|
|
27
|
+
data: T;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Default TTL for cache entries (5 minutes)
|
|
33
|
+
*/
|
|
34
|
+
const DEFAULT_TTL_MS = 5 * 60 * 1000;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Short TTL for development (30 seconds)
|
|
38
|
+
* Used when file watcher is not active
|
|
39
|
+
*/
|
|
40
|
+
const DEV_TTL_MS = 30 * 1000;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Server-side cache for collection and content data
|
|
44
|
+
*
|
|
45
|
+
* This cache stores:
|
|
46
|
+
* - Collection discovery results (list of collections with metadata)
|
|
47
|
+
* - Content summaries per collection (list of content items)
|
|
48
|
+
* - Discovered images per content item
|
|
49
|
+
*
|
|
50
|
+
* Cache invalidation happens:
|
|
51
|
+
* - Automatically when TTL expires
|
|
52
|
+
* - Manually via invalidate methods
|
|
53
|
+
* - Via file watcher integration
|
|
54
|
+
*/
|
|
55
|
+
export class ServerCache {
|
|
56
|
+
/** Cache for collection discovery results */
|
|
57
|
+
private collectionsCache: CacheEntry<DiscoveredCollection[]> | null = null;
|
|
58
|
+
|
|
59
|
+
/** Cache for content summaries, keyed by collection name */
|
|
60
|
+
private contentCache: Map<string, CacheEntry<ContentSummary[]>> = new Map();
|
|
61
|
+
|
|
62
|
+
/** Cache for discovered images, keyed by "collection:contentId" */
|
|
63
|
+
private imagesCache: Map<string, CacheEntry<DiscoveredImage[]>> = new Map();
|
|
64
|
+
|
|
65
|
+
/** Time-to-live for cache entries in milliseconds */
|
|
66
|
+
private ttl: number;
|
|
67
|
+
|
|
68
|
+
/** Whether file watcher is integrated (allows longer TTL) */
|
|
69
|
+
private hasWatcher: boolean = false;
|
|
70
|
+
|
|
71
|
+
constructor(options: { ttl?: number; hasWatcher?: boolean } = {}) {
|
|
72
|
+
this.ttl =
|
|
73
|
+
options.ttl ?? (options.hasWatcher ? DEFAULT_TTL_MS : DEV_TTL_MS);
|
|
74
|
+
this.hasWatcher = options.hasWatcher ?? false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Enable file watcher integration
|
|
79
|
+
*
|
|
80
|
+
* When watcher is enabled, cache can use longer TTL since
|
|
81
|
+
* invalidation happens via watcher events.
|
|
82
|
+
*/
|
|
83
|
+
enableWatcher(): void {
|
|
84
|
+
this.hasWatcher = true;
|
|
85
|
+
this.ttl = DEFAULT_TTL_MS;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Check if a cache entry is still valid
|
|
90
|
+
*/
|
|
91
|
+
private isValid<T>(entry: CacheEntry<T> | null | undefined): boolean {
|
|
92
|
+
if (!entry) return false;
|
|
93
|
+
return Date.now() - entry.timestamp < this.ttl;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ==================== Collections Cache ====================
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Get cached collections if valid
|
|
100
|
+
*
|
|
101
|
+
* @returns Cached collections or null if expired/not cached
|
|
102
|
+
*/
|
|
103
|
+
getCollections(): DiscoveredCollection[] | null {
|
|
104
|
+
if (this.isValid(this.collectionsCache)) {
|
|
105
|
+
return this.collectionsCache!.data;
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Set collections cache
|
|
112
|
+
*
|
|
113
|
+
* @param collections - Collections to cache
|
|
114
|
+
*/
|
|
115
|
+
setCollections(collections: DiscoveredCollection[]): void {
|
|
116
|
+
this.collectionsCache = {
|
|
117
|
+
data: collections,
|
|
118
|
+
timestamp: Date.now(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Invalidate collections cache
|
|
124
|
+
*/
|
|
125
|
+
invalidateCollections(): void {
|
|
126
|
+
this.collectionsCache = null;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ==================== Content Cache ====================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get cached content summaries for a collection if valid
|
|
133
|
+
*
|
|
134
|
+
* @param collection - Collection name
|
|
135
|
+
* @returns Cached content summaries or null if expired/not cached
|
|
136
|
+
*/
|
|
137
|
+
getContent(collection: string): ContentSummary[] | null {
|
|
138
|
+
const entry = this.contentCache.get(collection);
|
|
139
|
+
if (this.isValid(entry)) {
|
|
140
|
+
return entry!.data;
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Set content cache for a collection
|
|
147
|
+
*
|
|
148
|
+
* @param collection - Collection name
|
|
149
|
+
* @param items - Content summaries to cache
|
|
150
|
+
*/
|
|
151
|
+
setContent(collection: string, items: ContentSummary[]): void {
|
|
152
|
+
this.contentCache.set(collection, {
|
|
153
|
+
data: items,
|
|
154
|
+
timestamp: Date.now(),
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Invalidate content cache for a specific collection
|
|
160
|
+
*
|
|
161
|
+
* @param collection - Collection name to invalidate
|
|
162
|
+
*/
|
|
163
|
+
invalidateContent(collection: string): void {
|
|
164
|
+
this.contentCache.delete(collection);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Invalidate all content caches
|
|
169
|
+
*/
|
|
170
|
+
invalidateAllContent(): void {
|
|
171
|
+
this.contentCache.clear();
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// ==================== Images Cache ====================
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Generate cache key for image cache
|
|
178
|
+
*
|
|
179
|
+
* @param collection - Collection name
|
|
180
|
+
* @param contentId - Content ID
|
|
181
|
+
* @returns Cache key string
|
|
182
|
+
*/
|
|
183
|
+
private getImagesCacheKey(collection: string, contentId: string): string {
|
|
184
|
+
return `${collection}:${contentId}`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Get cached images for a content item if valid
|
|
189
|
+
*
|
|
190
|
+
* @param collection - Collection name
|
|
191
|
+
* @param contentId - Content ID
|
|
192
|
+
* @returns Cached images or null if expired/not cached
|
|
193
|
+
*/
|
|
194
|
+
getImages(collection: string, contentId: string): DiscoveredImage[] | null {
|
|
195
|
+
const key = this.getImagesCacheKey(collection, contentId);
|
|
196
|
+
const entry = this.imagesCache.get(key);
|
|
197
|
+
if (this.isValid(entry)) {
|
|
198
|
+
return entry!.data;
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Set images cache for a content item
|
|
205
|
+
*
|
|
206
|
+
* @param collection - Collection name
|
|
207
|
+
* @param contentId - Content ID
|
|
208
|
+
* @param images - Discovered images to cache
|
|
209
|
+
*/
|
|
210
|
+
setImages(
|
|
211
|
+
collection: string,
|
|
212
|
+
contentId: string,
|
|
213
|
+
images: DiscoveredImage[]
|
|
214
|
+
): void {
|
|
215
|
+
const key = this.getImagesCacheKey(collection, contentId);
|
|
216
|
+
this.imagesCache.set(key, {
|
|
217
|
+
data: images,
|
|
218
|
+
timestamp: Date.now(),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Invalidate image cache for a specific content item
|
|
224
|
+
*
|
|
225
|
+
* @param collection - Collection name
|
|
226
|
+
* @param contentId - Content ID
|
|
227
|
+
*/
|
|
228
|
+
invalidateImages(collection: string, contentId: string): void {
|
|
229
|
+
const key = this.getImagesCacheKey(collection, contentId);
|
|
230
|
+
this.imagesCache.delete(key);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Invalidate all image caches for a collection
|
|
235
|
+
*
|
|
236
|
+
* @param collection - Collection name
|
|
237
|
+
*/
|
|
238
|
+
invalidateCollectionImages(collection: string): void {
|
|
239
|
+
const prefix = `${collection}:`;
|
|
240
|
+
for (const key of this.imagesCache.keys()) {
|
|
241
|
+
if (key.startsWith(prefix)) {
|
|
242
|
+
this.imagesCache.delete(key);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Invalidate all image caches
|
|
249
|
+
*/
|
|
250
|
+
invalidateAllImages(): void {
|
|
251
|
+
this.imagesCache.clear();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ==================== Bulk Invalidation ====================
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Invalidate all caches
|
|
258
|
+
*
|
|
259
|
+
* Called when a major change occurs that affects everything.
|
|
260
|
+
*/
|
|
261
|
+
invalidateAll(): void {
|
|
262
|
+
this.collectionsCache = null;
|
|
263
|
+
this.contentCache.clear();
|
|
264
|
+
this.imagesCache.clear();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Handle file change event from watcher
|
|
269
|
+
*
|
|
270
|
+
* Intelligently invalidates only the affected caches.
|
|
271
|
+
*
|
|
272
|
+
* @param type - Type of file change (add, change, unlink)
|
|
273
|
+
* @param collection - Collection that was affected
|
|
274
|
+
* @param contentId - Optional content ID for targeted image cache invalidation
|
|
275
|
+
*/
|
|
276
|
+
handleFileChange(
|
|
277
|
+
type: "add" | "change" | "unlink",
|
|
278
|
+
collection: string,
|
|
279
|
+
contentId?: string
|
|
280
|
+
): void {
|
|
281
|
+
// Always invalidate the affected collection's content cache
|
|
282
|
+
this.invalidateContent(collection);
|
|
283
|
+
|
|
284
|
+
// Invalidate image cache for the specific content item or entire collection
|
|
285
|
+
if (contentId) {
|
|
286
|
+
this.invalidateImages(collection, contentId);
|
|
287
|
+
} else {
|
|
288
|
+
this.invalidateCollectionImages(collection);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// For add/unlink, also invalidate collections cache (count changed)
|
|
292
|
+
if (type === "add" || type === "unlink") {
|
|
293
|
+
this.invalidateCollections();
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ==================== Stats ====================
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get cache statistics for debugging
|
|
301
|
+
*
|
|
302
|
+
* @returns Object with cache stats
|
|
303
|
+
*/
|
|
304
|
+
getStats(): {
|
|
305
|
+
collectionsValid: boolean;
|
|
306
|
+
contentCollections: string[];
|
|
307
|
+
cachedImages: string[];
|
|
308
|
+
ttl: number;
|
|
309
|
+
hasWatcher: boolean;
|
|
310
|
+
} {
|
|
311
|
+
return {
|
|
312
|
+
collectionsValid: this.isValid(this.collectionsCache),
|
|
313
|
+
contentCollections: Array.from(this.contentCache.keys()).filter((key) =>
|
|
314
|
+
this.isValid(this.contentCache.get(key))
|
|
315
|
+
),
|
|
316
|
+
cachedImages: Array.from(this.imagesCache.keys()).filter((key) =>
|
|
317
|
+
this.isValid(this.imagesCache.get(key))
|
|
318
|
+
),
|
|
319
|
+
ttl: this.ttl,
|
|
320
|
+
hasWatcher: this.hasWatcher,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Global cache instance
|
|
327
|
+
*
|
|
328
|
+
* Shared across the server for consistent caching.
|
|
329
|
+
*/
|
|
330
|
+
let globalCache: ServerCache | null = null;
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Get or create the global cache instance
|
|
334
|
+
*
|
|
335
|
+
* @param options - Cache options (only used on first call)
|
|
336
|
+
* @returns The global cache instance
|
|
337
|
+
*/
|
|
338
|
+
export function getCache(options?: {
|
|
339
|
+
ttl?: number;
|
|
340
|
+
hasWatcher?: boolean;
|
|
341
|
+
}): ServerCache {
|
|
342
|
+
if (!globalCache) {
|
|
343
|
+
globalCache = new ServerCache(options);
|
|
344
|
+
}
|
|
345
|
+
return globalCache;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Reset the global cache instance
|
|
350
|
+
*
|
|
351
|
+
* Useful for testing or when configuration changes.
|
|
352
|
+
*/
|
|
353
|
+
export function resetCache(): void {
|
|
354
|
+
globalCache = null;
|
|
355
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Server module exports for @writenex/astro
|
|
3
|
+
*
|
|
4
|
+
* This module provides the public API for server-side functionality,
|
|
5
|
+
* including middleware, API routes, static assets, and caching.
|
|
6
|
+
*
|
|
7
|
+
* @module @writenex/astro/server
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Middleware functions and types
|
|
11
|
+
export {
|
|
12
|
+
createMiddleware,
|
|
13
|
+
parseQueryParams,
|
|
14
|
+
parseJsonBody,
|
|
15
|
+
sendJson,
|
|
16
|
+
sendError,
|
|
17
|
+
sendWritenexError,
|
|
18
|
+
} from "./middleware";
|
|
19
|
+
export type { MiddlewareContext } from "./middleware";
|
|
20
|
+
|
|
21
|
+
// Routes
|
|
22
|
+
export { createApiRouter } from "./routes";
|
|
23
|
+
|
|
24
|
+
// Assets
|
|
25
|
+
export {
|
|
26
|
+
serveEditorHtml,
|
|
27
|
+
serveAsset,
|
|
28
|
+
getClientDistPath,
|
|
29
|
+
hasClientBundle,
|
|
30
|
+
} from "./assets";
|
|
31
|
+
|
|
32
|
+
// Cache
|
|
33
|
+
export { ServerCache, getCache, resetCache } from "./cache";
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Vite middleware for Writenex routes
|
|
3
|
+
*
|
|
4
|
+
* This module provides the main middleware handler that intercepts requests
|
|
5
|
+
* to Writenex routes (/_writenex/*) and delegates to appropriate handlers.
|
|
6
|
+
*
|
|
7
|
+
* ## Route Structure:
|
|
8
|
+
* - `/_writenex` - Editor UI (HTML page with React app)
|
|
9
|
+
* - `/_writenex/api/*` - API endpoints for CRUD operations
|
|
10
|
+
* - `/_writenex/assets/*` - Static assets (JS, CSS)
|
|
11
|
+
*
|
|
12
|
+
* @module @writenex/astro/server/middleware
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
16
|
+
import type { Connect } from "vite";
|
|
17
|
+
import type { WritenexConfig } from "@/types";
|
|
18
|
+
import { createApiRouter } from "./routes";
|
|
19
|
+
import { serveEditorHtml, serveAsset } from "./assets";
|
|
20
|
+
import type { WritenexError } from "@/core/errors";
|
|
21
|
+
import { WritenexErrorCode, isWritenexError, wrapError } from "@/core/errors";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Middleware context passed to handlers
|
|
25
|
+
*/
|
|
26
|
+
export interface MiddlewareContext {
|
|
27
|
+
/** Base path for Writenex routes */
|
|
28
|
+
basePath: string;
|
|
29
|
+
/** Project root directory */
|
|
30
|
+
projectRoot: string;
|
|
31
|
+
/** Resolved Writenex configuration */
|
|
32
|
+
config: Required<WritenexConfig>;
|
|
33
|
+
/** Astro trailingSlash setting for preview URLs */
|
|
34
|
+
trailingSlash: "always" | "never" | "ignore";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Create the Writenex middleware handler
|
|
39
|
+
*
|
|
40
|
+
* @param context - Middleware context with configuration
|
|
41
|
+
* @returns Connect middleware function
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* ```typescript
|
|
45
|
+
* const middleware = createMiddleware({
|
|
46
|
+
* basePath: '/_writenex',
|
|
47
|
+
* projectRoot: '/path/to/project',
|
|
48
|
+
* config: resolvedConfig,
|
|
49
|
+
* });
|
|
50
|
+
*
|
|
51
|
+
* server.middlewares.use(middleware);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export function createMiddleware(
|
|
55
|
+
context: MiddlewareContext
|
|
56
|
+
): Connect.NextHandleFunction {
|
|
57
|
+
const { basePath } = context;
|
|
58
|
+
const apiRouter = createApiRouter(context);
|
|
59
|
+
|
|
60
|
+
return async (
|
|
61
|
+
req: IncomingMessage,
|
|
62
|
+
res: ServerResponse,
|
|
63
|
+
next: Connect.NextFunction
|
|
64
|
+
) => {
|
|
65
|
+
const url = req.url ?? "";
|
|
66
|
+
|
|
67
|
+
// Only handle requests to our base path
|
|
68
|
+
if (!url.startsWith(basePath)) {
|
|
69
|
+
return next();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Extract the path after base path
|
|
73
|
+
const path = url.slice(basePath.length) || "/";
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Handle API routes
|
|
77
|
+
if (path.startsWith("/api/")) {
|
|
78
|
+
return await apiRouter(req, res, path.slice(4)); // Remove '/api' prefix
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Handle static assets
|
|
82
|
+
if (path.startsWith("/assets/")) {
|
|
83
|
+
return await serveAsset(req, res, path.slice(8), context); // Remove '/assets' prefix
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Handle editor UI (root and any sub-routes for client-side routing)
|
|
87
|
+
return await serveEditorHtml(req, res, context);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
// Handle errors gracefully using WritenexError
|
|
90
|
+
const writenexError = isWritenexError(error)
|
|
91
|
+
? error
|
|
92
|
+
: wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);
|
|
93
|
+
|
|
94
|
+
console.error(
|
|
95
|
+
`[writenex] Middleware error [${writenexError.code}]: ${writenexError.message}`
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
res.statusCode = writenexError.httpStatus;
|
|
99
|
+
res.setHeader("Content-Type", "application/json");
|
|
100
|
+
res.end(JSON.stringify(writenexError.toJSON()));
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Parse URL query parameters
|
|
107
|
+
*
|
|
108
|
+
* @param url - The URL string to parse
|
|
109
|
+
* @returns Object with query parameters
|
|
110
|
+
*/
|
|
111
|
+
export function parseQueryParams(url: string): Record<string, string> {
|
|
112
|
+
const queryIndex = url.indexOf("?");
|
|
113
|
+
if (queryIndex === -1) return {};
|
|
114
|
+
|
|
115
|
+
const queryString = url.slice(queryIndex + 1);
|
|
116
|
+
const params: Record<string, string> = {};
|
|
117
|
+
|
|
118
|
+
for (const pair of queryString.split("&")) {
|
|
119
|
+
const [key, value] = pair.split("=");
|
|
120
|
+
if (key) {
|
|
121
|
+
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return params;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse request body as JSON
|
|
130
|
+
*
|
|
131
|
+
* @param req - The incoming request
|
|
132
|
+
* @returns Parsed JSON body or null
|
|
133
|
+
*/
|
|
134
|
+
export async function parseJsonBody(
|
|
135
|
+
req: IncomingMessage
|
|
136
|
+
): Promise<unknown | null> {
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
let body = "";
|
|
139
|
+
|
|
140
|
+
req.on("data", (chunk: Buffer) => {
|
|
141
|
+
body += chunk.toString();
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
req.on("end", () => {
|
|
145
|
+
if (!body) {
|
|
146
|
+
resolve(null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
resolve(JSON.parse(body));
|
|
152
|
+
} catch {
|
|
153
|
+
resolve(null);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
req.on("error", () => {
|
|
158
|
+
resolve(null);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Send JSON response
|
|
165
|
+
*
|
|
166
|
+
* @param res - The server response
|
|
167
|
+
* @param data - Data to send as JSON
|
|
168
|
+
* @param statusCode - HTTP status code (default: 200)
|
|
169
|
+
*/
|
|
170
|
+
export function sendJson(
|
|
171
|
+
res: ServerResponse,
|
|
172
|
+
data: unknown,
|
|
173
|
+
statusCode: number = 200
|
|
174
|
+
): void {
|
|
175
|
+
res.statusCode = statusCode;
|
|
176
|
+
res.setHeader("Content-Type", "application/json");
|
|
177
|
+
res.end(JSON.stringify(data));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Send error response
|
|
182
|
+
*
|
|
183
|
+
* @param res - The server response
|
|
184
|
+
* @param message - Error message
|
|
185
|
+
* @param statusCode - HTTP status code (default: 400)
|
|
186
|
+
*/
|
|
187
|
+
export function sendError(
|
|
188
|
+
res: ServerResponse,
|
|
189
|
+
message: string,
|
|
190
|
+
statusCode: number = 400
|
|
191
|
+
): void {
|
|
192
|
+
sendJson(res, { error: message }, statusCode);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Send WritenexError response
|
|
197
|
+
*
|
|
198
|
+
* Automatically uses the error's HTTP status code and formats
|
|
199
|
+
* the response using the error's toJSON method.
|
|
200
|
+
*
|
|
201
|
+
* @param res - The server response
|
|
202
|
+
* @param error - WritenexError instance
|
|
203
|
+
*/
|
|
204
|
+
export function sendWritenexError(
|
|
205
|
+
res: ServerResponse,
|
|
206
|
+
error: WritenexError
|
|
207
|
+
): void {
|
|
208
|
+
sendJson(res, error.toJSON(), error.httpStatus);
|
|
209
|
+
}
|