@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,540 @@
1
+ /**
2
+ * @fileoverview Filesystem writer for content operations
3
+ *
4
+ * This module provides functions for creating, updating, and deleting
5
+ * content files in Astro content collections.
6
+ *
7
+ * ## Features:
8
+ * - Create new content files with frontmatter
9
+ * - Update existing content files
10
+ * - Delete content files
11
+ * - Generate unique slugs to avoid collisions
12
+ * - Support for different file patterns (flat, folder-based, date-prefixed)
13
+ * - Automatic version history creation before updates
14
+ *
15
+ * @module @writenex/astro/filesystem/writer
16
+ */
17
+
18
+ import { writeFile, unlink, mkdir, readFile, stat } from "node:fs/promises";
19
+ import { existsSync } from "node:fs";
20
+ import { join, dirname, basename } from "node:path";
21
+ import slugify from "slugify";
22
+ import { readContentFile } from "./reader";
23
+ import { saveVersion } from "./versions";
24
+ import {
25
+ generatePathFromPattern,
26
+ resolvePatternTokens,
27
+ isValidPattern,
28
+ } from "@/discovery/patterns";
29
+ import type { VersionHistoryConfig } from "@/types";
30
+ import { ContentConflictError } from "@/core/errors";
31
+
32
+ /**
33
+ * Options for creating content
34
+ */
35
+ export interface CreateContentOptions {
36
+ /** Frontmatter data */
37
+ frontmatter: Record<string, unknown>;
38
+ /** Markdown body content */
39
+ body: string;
40
+ /** Custom slug (optional, generated from title if not provided) */
41
+ slug?: string;
42
+ /** File pattern for the collection (e.g., "{slug}/index.md", "{date}-{slug}.md") */
43
+ filePattern?: string;
44
+ /** Custom token values to override automatic resolution */
45
+ customTokens?: Record<string, string>;
46
+ }
47
+
48
+ /**
49
+ * Options for updating content
50
+ */
51
+ export interface UpdateContentOptions {
52
+ /** Updated frontmatter data */
53
+ frontmatter?: Record<string, unknown>;
54
+ /** Updated markdown body content */
55
+ body?: string;
56
+ /** Project root for version history (required for version creation) */
57
+ projectRoot?: string;
58
+ /** Collection name for version history */
59
+ collection?: string;
60
+ /** Version history configuration */
61
+ versionHistoryConfig?: Required<VersionHistoryConfig>;
62
+ /**
63
+ * Expected modification time for conflict detection.
64
+ * If provided and the file's mtime differs, the update will fail with a conflict error.
65
+ */
66
+ expectedMtime?: number;
67
+ }
68
+
69
+ /**
70
+ * Result of a write operation
71
+ */
72
+ export interface WriteResult {
73
+ /** Whether the operation was successful */
74
+ success: boolean;
75
+ /** The content ID (slug) */
76
+ id?: string;
77
+ /** The file path */
78
+ path?: string;
79
+ /** Error message if failed */
80
+ error?: string;
81
+ /** New modification time after write (for conflict detection) */
82
+ mtime?: number;
83
+ /** Conflict error if update failed due to external modification */
84
+ conflict?: ContentConflictError;
85
+ }
86
+
87
+ /**
88
+ * Generate a URL-safe slug from a string
89
+ *
90
+ * @param text - Text to slugify
91
+ * @returns URL-safe slug
92
+ */
93
+ export function generateSlug(text: string): string {
94
+ return slugify(text, {
95
+ lower: true,
96
+ strict: true,
97
+ trim: true,
98
+ });
99
+ }
100
+
101
+ /**
102
+ * Check if a content file already exists for a given slug and pattern
103
+ *
104
+ * @param slug - The slug to check
105
+ * @param collectionPath - Path to the collection directory
106
+ * @param filePattern - File pattern (e.g., "{slug}.md", "{slug}/index.md")
107
+ * @returns True if content already exists
108
+ */
109
+ function contentExists(
110
+ slug: string,
111
+ collectionPath: string,
112
+ filePattern: string
113
+ ): boolean {
114
+ const relativePath = generatePathFromPattern(filePattern, { slug });
115
+ const fullPath = join(collectionPath, relativePath);
116
+
117
+ // For folder-based patterns, check if the folder exists
118
+ if (filePattern.includes("/index.")) {
119
+ const folderPath = join(collectionPath, slug);
120
+ return existsSync(folderPath);
121
+ }
122
+
123
+ return existsSync(fullPath);
124
+ }
125
+
126
+ /**
127
+ * Generate a unique slug that doesn't conflict with existing files
128
+ *
129
+ * @param baseSlug - The base slug to start with
130
+ * @param collectionPath - Path to the collection directory
131
+ * @param filePattern - File pattern (default: "{slug}.md")
132
+ * @returns A unique slug
133
+ */
134
+ export async function generateUniqueSlug(
135
+ baseSlug: string,
136
+ collectionPath: string,
137
+ filePattern: string = "{slug}.md"
138
+ ): Promise<string> {
139
+ let slug = baseSlug;
140
+ let counter = 2;
141
+
142
+ while (contentExists(slug, collectionPath, filePattern)) {
143
+ slug = `${baseSlug}-${counter}`;
144
+ counter++;
145
+ }
146
+
147
+ return slug;
148
+ }
149
+
150
+ /**
151
+ * Convert frontmatter object to YAML string
152
+ *
153
+ * @param frontmatter - Frontmatter data
154
+ * @returns YAML string
155
+ */
156
+ function frontmatterToYaml(frontmatter: Record<string, unknown>): string {
157
+ const lines: string[] = [];
158
+
159
+ for (const [key, value] of Object.entries(frontmatter)) {
160
+ if (value === undefined || value === null) {
161
+ continue;
162
+ }
163
+
164
+ if (typeof value === "string") {
165
+ // Quote strings that contain special characters
166
+ if (value.includes(":") || value.includes("#") || value.includes("\n")) {
167
+ lines.push(`${key}: "${value.replace(/"/g, '\\"')}"`);
168
+ } else {
169
+ lines.push(`${key}: ${value}`);
170
+ }
171
+ } else if (typeof value === "number" || typeof value === "boolean") {
172
+ lines.push(`${key}: ${value}`);
173
+ } else if (value instanceof Date) {
174
+ lines.push(`${key}: ${value.toISOString().split("T")[0]}`);
175
+ } else if (Array.isArray(value)) {
176
+ if (value.length === 0) {
177
+ lines.push(`${key}: []`);
178
+ } else {
179
+ lines.push(`${key}:`);
180
+ for (const item of value) {
181
+ lines.push(` - ${item}`);
182
+ }
183
+ }
184
+ } else if (typeof value === "object") {
185
+ // Simple object serialization
186
+ lines.push(`${key}:`);
187
+ for (const [subKey, subValue] of Object.entries(value)) {
188
+ lines.push(` ${subKey}: ${subValue}`);
189
+ }
190
+ }
191
+ }
192
+
193
+ return lines.join("\n");
194
+ }
195
+
196
+ /**
197
+ * Create a content file with frontmatter and body
198
+ *
199
+ * @param frontmatter - Frontmatter data
200
+ * @param body - Markdown body content
201
+ * @returns Complete file content
202
+ */
203
+ function createFileContent(
204
+ frontmatter: Record<string, unknown>,
205
+ body: string
206
+ ): string {
207
+ const yaml = frontmatterToYaml(frontmatter);
208
+ return `---\n${yaml}\n---\n\n${body}`;
209
+ }
210
+
211
+ /**
212
+ * Create a new content file in a collection
213
+ *
214
+ * Supports various file patterns with automatic token resolution:
215
+ * - `{slug}.md` - Simple flat structure (default)
216
+ * - `{slug}/index.md` - Folder-based content
217
+ * - `{date}-{slug}.md` - Date-prefixed naming
218
+ * - `{year}/{slug}.md` - Year folder structure
219
+ * - `{year}/{month}/{slug}.md` - Year/month folder structure
220
+ * - `{year}/{month}/{day}/{slug}.md` - Full date folder structure
221
+ * - `{lang}/{slug}.md` - Language-prefixed (i18n)
222
+ * - `{category}/{slug}.md` - Category folder structure
223
+ * - `{author}/{slug}.md` - Author folder structure
224
+ * - Any custom pattern with tokens from frontmatter
225
+ *
226
+ * Token resolution priority:
227
+ * 1. Custom tokens (explicitly provided via customTokens)
228
+ * 2. Known token resolvers (date, year, month, lang, category, etc.)
229
+ * 3. Frontmatter values (for custom tokens)
230
+ * 4. Default values
231
+ *
232
+ * @param collectionPath - Absolute path to the collection directory
233
+ * @param options - Content creation options
234
+ * @returns WriteResult with success status and file info
235
+ *
236
+ * @example
237
+ * ```typescript
238
+ * // Flat structure
239
+ * const result = await createContent('/project/src/content/blog', {
240
+ * frontmatter: { title: 'My New Post', pubDate: new Date() },
241
+ * body: '# Hello World',
242
+ * });
243
+ *
244
+ * // Folder-based structure
245
+ * const result = await createContent('/project/src/content/blog', {
246
+ * frontmatter: { title: 'My New Post', pubDate: new Date() },
247
+ * body: '# Hello World',
248
+ * filePattern: '{slug}/index.md',
249
+ * });
250
+ *
251
+ * // i18n structure with custom token
252
+ * const result = await createContent('/project/src/content/blog', {
253
+ * frontmatter: { title: 'My New Post', lang: 'id' },
254
+ * body: '# Hello World',
255
+ * filePattern: '{lang}/{slug}.md',
256
+ * });
257
+ *
258
+ * // Custom pattern with explicit token
259
+ * const result = await createContent('/project/src/content/blog', {
260
+ * frontmatter: { title: 'My New Post' },
261
+ * body: '# Hello World',
262
+ * filePattern: '{category}/{slug}.md',
263
+ * customTokens: { category: 'tutorials' },
264
+ * });
265
+ * ```
266
+ */
267
+ export async function createContent(
268
+ collectionPath: string,
269
+ options: CreateContentOptions
270
+ ): Promise<WriteResult> {
271
+ const {
272
+ frontmatter,
273
+ body,
274
+ slug: customSlug,
275
+ filePattern = "{slug}.md",
276
+ customTokens = {},
277
+ } = options;
278
+
279
+ try {
280
+ // Validate pattern
281
+ const validation = isValidPattern(filePattern);
282
+ if (!validation.valid) {
283
+ return {
284
+ success: false,
285
+ error: `Invalid file pattern: ${validation.error}`,
286
+ };
287
+ }
288
+
289
+ // Generate slug from title or use custom slug
290
+ const title = frontmatter.title as string | undefined;
291
+ const baseSlug = customSlug ?? (title ? generateSlug(title) : "untitled");
292
+
293
+ // Ensure unique slug using the file pattern
294
+ const slug = await generateUniqueSlug(
295
+ baseSlug,
296
+ collectionPath,
297
+ filePattern
298
+ );
299
+
300
+ // Resolve all tokens using the flexible token resolver
301
+ const tokens = resolvePatternTokens(filePattern, {
302
+ slug,
303
+ frontmatter,
304
+ customTokens,
305
+ });
306
+
307
+ // Generate the relative file path from the pattern
308
+ const relativePath = generatePathFromPattern(filePattern, tokens);
309
+ const filePath = join(collectionPath, relativePath);
310
+
311
+ // Ensure parent directory exists (important for folder-based patterns)
312
+ const parentDir = dirname(filePath);
313
+ if (!existsSync(parentDir)) {
314
+ await mkdir(parentDir, { recursive: true });
315
+ }
316
+
317
+ // Create file content
318
+ const content = createFileContent(frontmatter, body);
319
+
320
+ // Write file
321
+ await writeFile(filePath, content, "utf-8");
322
+
323
+ return {
324
+ success: true,
325
+ id: slug,
326
+ path: filePath,
327
+ };
328
+ } catch (error) {
329
+ const message = error instanceof Error ? error.message : String(error);
330
+ return {
331
+ success: false,
332
+ error: `Failed to create content: ${message}`,
333
+ };
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Update an existing content file
339
+ *
340
+ * Creates a version snapshot of the current content before updating
341
+ * when version history is configured. Skips both version creation and
342
+ * file write if the new content is identical to the current file content.
343
+ * Version creation errors are logged but do not fail the save operation.
344
+ *
345
+ * @param filePath - Absolute path to the content file
346
+ * @param collectionPath - Path to the collection directory
347
+ * @param options - Update options including version history config
348
+ * @returns WriteResult with success status
349
+ *
350
+ * @example
351
+ * ```typescript
352
+ * const result = await updateContent(
353
+ * '/project/src/content/blog/my-post.md',
354
+ * '/project/src/content/blog',
355
+ * {
356
+ * frontmatter: { title: 'Updated Title' },
357
+ * body: '# Updated Content',
358
+ * projectRoot: '/project',
359
+ * collection: 'blog',
360
+ * versionHistoryConfig: { enabled: true, maxVersions: 20, storagePath: '.writenex/versions' },
361
+ * }
362
+ * );
363
+ * ```
364
+ */
365
+ export async function updateContent(
366
+ filePath: string,
367
+ collectionPath: string,
368
+ options: UpdateContentOptions
369
+ ): Promise<WriteResult> {
370
+ const { projectRoot, collection, versionHistoryConfig, expectedMtime } =
371
+ options;
372
+
373
+ try {
374
+ // Read existing content
375
+ const existing = await readContentFile(filePath, collectionPath);
376
+
377
+ if (!existing.success || !existing.content) {
378
+ return {
379
+ success: false,
380
+ error: existing.error ?? "Content not found",
381
+ };
382
+ }
383
+
384
+ // Conflict detection: check if file was modified externally
385
+ if (expectedMtime !== undefined && existing.content.mtime !== undefined) {
386
+ // Allow small tolerance (1ms) for filesystem precision differences
387
+ const mtimeDiff = Math.abs(existing.content.mtime - expectedMtime);
388
+ if (mtimeDiff > 1) {
389
+ // File was modified externally - return conflict error
390
+ const conflictError = new ContentConflictError(
391
+ collection ?? "unknown",
392
+ existing.content.id,
393
+ existing.content.raw,
394
+ existing.content.mtime,
395
+ expectedMtime
396
+ );
397
+
398
+ return {
399
+ success: false,
400
+ error: conflictError.message,
401
+ conflict: conflictError,
402
+ };
403
+ }
404
+ }
405
+
406
+ // Merge frontmatter
407
+ const frontmatter = options.frontmatter
408
+ ? { ...existing.content.frontmatter, ...options.frontmatter }
409
+ : existing.content.frontmatter;
410
+
411
+ // Use new body or existing
412
+ const body = options.body ?? existing.content.body;
413
+
414
+ // Create updated content
415
+ const newContent = createFileContent(frontmatter, body);
416
+
417
+ // Read current file content for comparison
418
+ const currentContent = existsSync(filePath)
419
+ ? await readFile(filePath, "utf-8")
420
+ : "";
421
+
422
+ // Skip if content is identical (no changes to save)
423
+ if (newContent === currentContent) {
424
+ return {
425
+ success: true,
426
+ id: existing.content.id,
427
+ path: filePath,
428
+ mtime: existing.content.mtime,
429
+ };
430
+ }
431
+
432
+ // Create version snapshot before updating (if version history is configured)
433
+ // Only create version if content actually changed
434
+ if (
435
+ projectRoot &&
436
+ collection &&
437
+ versionHistoryConfig &&
438
+ versionHistoryConfig.enabled &&
439
+ currentContent
440
+ ) {
441
+ try {
442
+ // Extract content ID from file path
443
+ const fileName = basename(filePath);
444
+ const contentId =
445
+ fileName === "index.md" || fileName === "index.mdx"
446
+ ? basename(dirname(filePath))
447
+ : fileName.replace(/\.(md|mdx)$/, "");
448
+
449
+ // Save version of the current content before overwriting
450
+ // skipIfIdentical compares with last version in history
451
+ const versionResult = await saveVersion(
452
+ projectRoot,
453
+ collection,
454
+ contentId,
455
+ currentContent,
456
+ versionHistoryConfig,
457
+ { skipIfIdentical: true }
458
+ );
459
+
460
+ if (!versionResult.success) {
461
+ console.warn(
462
+ `[writenex] Failed to create version snapshot: ${versionResult.error}`
463
+ );
464
+ }
465
+ } catch (versionError) {
466
+ // Log version creation error but continue with save
467
+ console.warn(
468
+ `[writenex] Version creation error (save will continue):`,
469
+ versionError
470
+ );
471
+ }
472
+ }
473
+
474
+ // Write file with new content
475
+ await writeFile(filePath, newContent, "utf-8");
476
+
477
+ // Get new mtime after write
478
+ const newStats = await stat(filePath);
479
+
480
+ return {
481
+ success: true,
482
+ id: existing.content.id,
483
+ path: filePath,
484
+ mtime: newStats.mtimeMs,
485
+ };
486
+ } catch (error) {
487
+ // Re-throw ContentConflictError as-is
488
+ if (error instanceof ContentConflictError) {
489
+ return {
490
+ success: false,
491
+ error: error.message,
492
+ conflict: error,
493
+ };
494
+ }
495
+
496
+ const message = error instanceof Error ? error.message : String(error);
497
+ return {
498
+ success: false,
499
+ error: `Failed to update content: ${message}`,
500
+ };
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Delete a content file
506
+ *
507
+ * @param filePath - Absolute path to the content file
508
+ * @returns WriteResult with success status
509
+ *
510
+ * @example
511
+ * ```typescript
512
+ * const result = await deleteContent('/project/src/content/blog/my-post.md');
513
+ * ```
514
+ */
515
+ export async function deleteContent(filePath: string): Promise<WriteResult> {
516
+ try {
517
+ if (!existsSync(filePath)) {
518
+ return {
519
+ success: false,
520
+ error: "Content file not found",
521
+ };
522
+ }
523
+
524
+ await unlink(filePath);
525
+
526
+ return {
527
+ success: true,
528
+ path: filePath,
529
+ };
530
+ } catch (error) {
531
+ const message = error instanceof Error ? error.message : String(error);
532
+ return {
533
+ success: false,
534
+ error: `Failed to delete content: ${message}`,
535
+ };
536
+ }
537
+ }
538
+
539
+ // Re-export getContentFilePath from reader for backward compatibility
540
+ export { getContentFilePath } from "./reader";
package/src/index.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * @fileoverview Main entry point for @writenex/astro
3
+ *
4
+ * This file exports the minimal public API for the Astro integration.
5
+ * For advanced usage, import from sub-modules:
6
+ * - @writenex/astro/config
7
+ * - @writenex/astro/discovery
8
+ * - @writenex/astro/filesystem
9
+ * - @writenex/astro/server
10
+ *
11
+ * @module @writenex/astro
12
+ * @example
13
+ * ```typescript
14
+ * // astro.config.mjs
15
+ * import { defineConfig } from 'astro/config';
16
+ * import writenex from '@writenex/astro';
17
+ *
18
+ * export default defineConfig({
19
+ * integrations: [writenex()],
20
+ * });
21
+ * ```
22
+ */
23
+
24
+ // =============================================================================
25
+ // Main Integration
26
+ // =============================================================================
27
+
28
+ export { default, default as writenex } from "./integration";
29
+
30
+ // =============================================================================
31
+ // Configuration Utilities (most commonly used)
32
+ // =============================================================================
33
+
34
+ export { defineConfig, validateConfig, loadConfig } from "@/config";
35
+
36
+ // =============================================================================
37
+ // Core Types (essential for consumers)
38
+ // =============================================================================
39
+
40
+ export type {
41
+ WritenexConfig,
42
+ WritenexOptions,
43
+ CollectionConfig,
44
+ CollectionSchema,
45
+ SchemaField,
46
+ FieldType,
47
+ ImageConfig,
48
+ ImageStrategy,
49
+ DiscoveryConfig,
50
+ EditorConfig,
51
+ ContentItem,
52
+ ContentSummary,
53
+ DiscoveredCollection,
54
+ VersionHistoryConfig,
55
+ } from "@/types";
56
+
57
+ // =============================================================================
58
+ // Error Handling (commonly needed)
59
+ // =============================================================================
60
+
61
+ export { WritenexError, WritenexErrorCode, isWritenexError } from "@/core";