@imjp/writenex-astro 0.1.0 → 1.3.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 (99) hide show
  1. package/README.md +13 -13
  2. package/dist/{chunk-CYLDJ3HZ.js → chunk-GIS7XEJF.js} +4 -4
  3. package/dist/{chunk-CYLDJ3HZ.js.map → chunk-GIS7XEJF.js.map} +1 -1
  4. package/dist/{chunk-7XU5X6CW.js → chunk-GUUSVFBP.js} +12 -12
  5. package/dist/chunk-GUUSVFBP.js.map +1 -0
  6. package/dist/{chunk-XNTQTTJU.js → chunk-JFQQJPDF.js} +2 -2
  7. package/dist/{chunk-XNTQTTJU.js.map → chunk-JFQQJPDF.js.map} +1 -1
  8. package/dist/{chunk-CF2XXJFF.js → chunk-OWYFIQFK.js} +436 -436
  9. package/dist/chunk-OWYFIQFK.js.map +1 -0
  10. package/dist/{chunk-5PM6EQE5.js → chunk-S2OUQLMK.js} +7 -5
  11. package/dist/chunk-S2OUQLMK.js.map +1 -0
  12. package/dist/{chunk-AAOQHQPU.js → chunk-TQAYIZOA.js} +5 -5
  13. package/dist/{chunk-AAOQHQPU.js.map → chunk-TQAYIZOA.js.map} +1 -1
  14. package/dist/{chunk-CRPZUUDU.js → chunk-YBCPOLMY.js} +1 -1
  15. package/dist/{chunk-CRPZUUDU.js.map → chunk-YBCPOLMY.js.map} +1 -1
  16. package/dist/client/index.css +1 -1
  17. package/dist/client/index.css.map +1 -1
  18. package/dist/client/index.d.ts +19 -0
  19. package/dist/client/index.js +159 -147
  20. package/dist/client/index.js.map +1 -1
  21. package/dist/client/styles.css +2 -8
  22. package/dist/config/index.d.ts +2 -2
  23. package/dist/config/index.js +2 -2
  24. package/dist/{config-BmEdBDo_.d.ts → config-CliL0CoN.d.ts} +1 -1
  25. package/dist/{content-BWR52vD-.d.ts → content-TuL3GT66.d.ts} +1 -1
  26. package/dist/discovery/index.d.ts +2 -2
  27. package/dist/discovery/index.js +3 -3
  28. package/dist/filesystem/index.d.ts +703 -703
  29. package/dist/filesystem/index.js +4 -4
  30. package/dist/filesystem/index.js.map +1 -1
  31. package/dist/index.d.ts +4 -4
  32. package/dist/index.js +7 -7
  33. package/dist/index.js.map +1 -1
  34. package/dist/{loader-55LWCXHA.js → loader-VGNXC2XJ.js} +3 -3
  35. package/dist/schema-DDJyoVkj.d.ts +189 -0
  36. package/dist/server/index.d.ts +37 -37
  37. package/dist/server/index.js +5 -5
  38. package/package.json +17 -18
  39. package/src/client/App.tsx +18 -18
  40. package/src/client/components/ConfigPanel/ConfigPanel.tsx +14 -13
  41. package/src/client/components/CreateContentModal/CreateContentModal.tsx +1 -1
  42. package/src/client/components/Editor/Editor.tsx +27 -27
  43. package/src/client/components/Editor/ImageDialog.tsx +4 -3
  44. package/src/client/components/Editor/LinkDialog.tsx +7 -6
  45. package/src/client/components/Editor/index.ts +1 -1
  46. package/src/client/components/FrontmatterForm/FrontmatterForm.css +1 -1
  47. package/src/client/components/FrontmatterForm/FrontmatterForm.tsx +1 -1
  48. package/src/client/components/Header/Header.tsx +8 -8
  49. package/src/client/components/KeyboardShortcuts/KeyboardShortcuts.tsx +1 -1
  50. package/src/client/components/LazyEditor.tsx +1 -1
  51. package/src/client/components/LiveRegion/index.ts +1 -1
  52. package/src/client/components/SearchReplace/SearchReplacePanel.tsx +5 -5
  53. package/src/client/components/SearchReplace/index.ts +1 -1
  54. package/src/client/components/SelectCollectionModal/SelectCollectionModal.tsx +2 -2
  55. package/src/client/components/Sidebar/Sidebar.tsx +6 -6
  56. package/src/client/components/SkipLink/index.ts +1 -1
  57. package/src/client/components/UnsavedChangesModal/UnsavedChangesModal.tsx +1 -1
  58. package/src/client/components/VersionHistory/DiffViewer.tsx +18 -11
  59. package/src/client/components/VersionHistory/VersionActions.tsx +6 -6
  60. package/src/client/components/VersionHistory/VersionHistoryPanel.tsx +10 -10
  61. package/src/client/components/VersionHistory/index.ts +2 -2
  62. package/src/client/context/ApiContext.tsx +2 -2
  63. package/src/client/context/ThemeContext.tsx +2 -2
  64. package/src/client/hooks/useApi.ts +1 -1
  65. package/src/client/hooks/useFocusTrap.ts +1 -1
  66. package/src/client/hooks/useSearch.ts +1 -1
  67. package/src/client/hooks/useVersionHistory.ts +2 -2
  68. package/src/client/index.tsx +1 -1
  69. package/src/client/styles.css +2 -8
  70. package/src/config/defaults.ts +4 -4
  71. package/src/config/index.ts +14 -16
  72. package/src/config/loader.ts +6 -4
  73. package/src/config/schema.ts +8 -4
  74. package/src/core/index.ts +1 -1
  75. package/src/discovery/collections.ts +3 -3
  76. package/src/discovery/index.ts +9 -11
  77. package/src/discovery/patterns.ts +2 -2
  78. package/src/discovery/schema.ts +1 -1
  79. package/src/filesystem/images.ts +3 -3
  80. package/src/filesystem/index.ts +74 -79
  81. package/src/filesystem/reader.ts +2 -2
  82. package/src/filesystem/version-config.ts +10 -10
  83. package/src/filesystem/versions.ts +9 -9
  84. package/src/filesystem/watcher.ts +1 -1
  85. package/src/filesystem/writer.ts +6 -6
  86. package/src/global.d.ts +39 -0
  87. package/src/index.ts +10 -10
  88. package/src/integration.ts +2 -2
  89. package/src/server/assets.ts +3 -3
  90. package/src/server/cache.ts +1 -1
  91. package/src/server/index.ts +12 -15
  92. package/src/server/middleware.ts +3 -3
  93. package/src/server/routes.ts +28 -28
  94. package/src/types/index.ts +24 -28
  95. package/dist/chunk-5PM6EQE5.js.map +0 -1
  96. package/dist/chunk-7XU5X6CW.js.map +0 -1
  97. package/dist/chunk-CF2XXJFF.js.map +0 -1
  98. package/dist/loader-CrdnaAWR.d.ts +0 -327
  99. /package/dist/{loader-55LWCXHA.js.map → loader-VGNXC2XJ.js.map} +0 -0
package/README.md CHANGED
@@ -1,10 +1,10 @@
1
- # @writenex/astro
1
+ # @imjp/writenex-astro
2
2
 
3
3
  Visual editor for Astro content collections - WYSIWYG editing for your Astro site.
4
4
 
5
5
  ## Overview
6
6
 
7
- **@writenex/astro** is an Astro integration that provides a WYSIWYG editor interface for managing your content collections. It runs alongside your Astro dev server and provides direct filesystem access to your content.
7
+ **@imjp/writenex-astro** is an Astro integration that provides a WYSIWYG editor interface for managing your content collections. It runs alongside your Astro dev server and provides direct filesystem access to your content.
8
8
 
9
9
  ### Key Features
10
10
 
@@ -27,7 +27,7 @@ Visual editor for Astro content collections - WYSIWYG editing for your Astro sit
27
27
  ### 1. Install the integration
28
28
 
29
29
  ```bash
30
- npx astro add @writenex/astro
30
+ npx astro add @imjp/writenex-astro
31
31
  ```
32
32
 
33
33
  This will install the package and automatically configure your `astro.config.mjs`.
@@ -50,13 +50,13 @@ If you prefer to install manually:
50
50
 
51
51
  ```bash
52
52
  # npm
53
- npm install @writenex/astro
53
+ npm install @imjp/writenex-astro
54
54
 
55
55
  # pnpm
56
- pnpm add @writenex/astro
56
+ pnpm add @imjp/writenex-astro
57
57
 
58
58
  # yarn
59
- yarn add @writenex/astro
59
+ yarn add @imjp/writenex-astro
60
60
  ```
61
61
 
62
62
  Then add the integration to your config:
@@ -64,7 +64,7 @@ Then add the integration to your config:
64
64
  ```typescript
65
65
  // astro.config.mjs
66
66
  import { defineConfig } from "astro/config";
67
- import writenex from "@writenex/astro";
67
+ import writenex from "@imjp/writenex-astro";
68
68
 
69
69
  export default defineConfig({
70
70
  integrations: [writenex()],
@@ -83,7 +83,7 @@ Create `writenex.config.ts` in your project root for full control:
83
83
 
84
84
  ```typescript
85
85
  // writenex.config.ts
86
- import { defineConfig } from "@writenex/astro";
86
+ import { defineConfig } from "@imjp/writenex-astro";
87
87
 
88
88
  export default defineConfig({
89
89
  // Define collections explicitly
@@ -240,7 +240,7 @@ Writenex automatically creates shadow copies of your content before each save, p
240
240
 
241
241
  ```typescript
242
242
  // writenex.config.ts
243
- import { defineConfig } from "@writenex/astro";
243
+ import { defineConfig } from "@imjp/writenex-astro";
244
244
 
245
245
  export default defineConfig({
246
246
  versionHistory: {
@@ -327,7 +327,7 @@ import {
327
327
  saveVersionWithConfig,
328
328
  getVersionsWithConfig,
329
329
  restoreVersionWithConfig,
330
- } from "@writenex/astro";
330
+ } from "@imjp/writenex-astro";
331
331
 
332
332
  // Save a version with label
333
333
  await saveVersionWithConfig(
@@ -518,14 +518,14 @@ writenex({
518
518
 
519
519
  ## Requirements
520
520
 
521
- - Astro 4.x or 5.x
521
+ - Astro 4.x, 5.x, or 6.x
522
522
  - React 18.x or 19.x
523
- - Node.js 18+
523
+ - Node.js 22.12.0+ (Node 18 and 20 are no longer supported)
524
524
 
525
525
  ### Future Plans
526
526
 
527
527
  - MDX full support (components, imports)
528
- - CLI wrapper (`npx @writenex/astro`)
528
+ - CLI wrapper (`npx @imjp/writenex-astro`)
529
529
  - Git integration (auto-commit on save)
530
530
  - Media library management
531
531
 
@@ -2,10 +2,10 @@ import {
2
2
  detectFilePattern,
3
3
  getCollectionCount,
4
4
  readCollection
5
- } from "./chunk-AAOQHQPU.js";
5
+ } from "./chunk-TQAYIZOA.js";
6
6
  import {
7
7
  DEFAULT_FILE_PATTERN
8
- } from "./chunk-CRPZUUDU.js";
8
+ } from "./chunk-YBCPOLMY.js";
9
9
 
10
10
  // src/discovery/schema.ts
11
11
  var MAX_SAMPLE_FILES = 20;
@@ -218,8 +218,8 @@ function describeSchema(schema) {
218
218
  }
219
219
 
220
220
  // src/discovery/collections.ts
221
- import { readdir, stat } from "fs/promises";
222
221
  import { existsSync } from "fs";
222
+ import { readdir, stat } from "fs/promises";
223
223
  import { join } from "path";
224
224
  var DEFAULT_CONTENT_DIR = "src/content";
225
225
  var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set(["node_modules", ".git", "_", "."]);
@@ -307,4 +307,4 @@ export {
307
307
  getCollection,
308
308
  collectionExists
309
309
  };
310
- //# sourceMappingURL=chunk-CYLDJ3HZ.js.map
310
+ //# sourceMappingURL=chunk-GIS7XEJF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/discovery/schema.ts","../src/discovery/collections.ts"],"sourcesContent":["/**\n * @fileoverview Schema auto-detection for content collections\n *\n * This module analyzes frontmatter from sample content files to automatically\n * infer the schema (field types, required status, enums, etc.).\n *\n * ## Detection Process:\n * 1. Read sample files from the collection (up to 20)\n * 2. Parse frontmatter from each file\n * 3. Analyze field patterns across all samples\n * 4. Infer field types and constraints\n * 5. Generate schema definition\n *\n * ## Detected Types:\n * - string: Plain text values\n * - number: Numeric values\n * - boolean: True/false values\n * - date: ISO date strings or Date objects\n * - array: Arrays (with item type detection)\n * - image: Paths ending with image extensions\n *\n * @module @writenex/astro/discovery/schema\n */\n\nimport type { CollectionSchema, FieldType, SchemaField } from \"@/types\";\nimport { readCollection } from \"@/filesystem/reader\";\n\n/**\n * Maximum number of files to sample for schema detection\n */\nconst MAX_SAMPLE_FILES = 20;\n\n/**\n * Minimum presence ratio to consider a field required\n * (field must appear in at least 90% of files)\n */\nconst REQUIRED_THRESHOLD = 0.9;\n\n/**\n * Maximum unique values to consider a field as enum\n */\nconst ENUM_MAX_VALUES = 10;\n\n/**\n * Minimum ratio of unique values to total to NOT be an enum\n * (if uniqueValues / total < 0.3, it's likely an enum)\n */\nconst ENUM_RATIO_THRESHOLD = 0.3;\n\n/**\n * Image file extensions for detection\n */\nconst IMAGE_EXTENSIONS = [\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".svg\",\n];\n\n/**\n * Field analysis data collected from samples\n */\ninterface FieldAnalysis {\n /** Number of files where this field appears */\n presentCount: number;\n /** Detected types for this field across samples */\n types: Set<string>;\n /** Sample values for enum detection */\n values: unknown[];\n /** Whether values look like image paths */\n hasImagePaths: boolean;\n /** Whether values look like dates */\n hasDateValues: boolean;\n /** For arrays, analysis of item types */\n arrayItemTypes: Set<string>;\n}\n\n/**\n * Result of schema detection\n */\nexport interface SchemaDetectionResult {\n /** The detected schema */\n schema: CollectionSchema;\n /** Number of files analyzed */\n samplesAnalyzed: number;\n /** Confidence score (0-1) based on sample consistency */\n confidence: number;\n /** Fields that had inconsistent types across samples */\n warnings: string[];\n}\n\n/**\n * Check if a string looks like an image path\n *\n * @param value - Value to check\n * @returns True if it looks like an image path\n */\nfunction isImagePath(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n\n const lowered = value.toLowerCase();\n return IMAGE_EXTENSIONS.some((ext) => lowered.endsWith(ext));\n}\n\n/**\n * Check if a value looks like a date\n *\n * @param value - Value to check\n * @returns True if it looks like a date\n */\nfunction isDateValue(value: unknown): boolean {\n // Already a Date object\n if (value instanceof Date) return true;\n\n // ISO date string (YYYY-MM-DD or full ISO)\n if (typeof value === \"string\") {\n // Full ISO format\n if (/^\\d{4}-\\d{2}-\\d{2}(T|\\s)/.test(value)) return true;\n // Simple date format\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return true;\n }\n\n return false;\n}\n\n/**\n * Detect the JavaScript type of a value\n *\n * @param value - Value to analyze\n * @returns Detected type string\n */\nfunction detectValueType(value: unknown): string {\n if (value === null || value === undefined) return \"null\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"string\") return \"string\";\n if (Array.isArray(value)) return \"array\";\n if (value instanceof Date) return \"date\";\n if (typeof value === \"object\") return \"object\";\n return \"unknown\";\n}\n\n/**\n * Convert detected type to schema field type\n *\n * @param analysis - Field analysis data\n * @returns The appropriate FieldType\n */\nfunction inferFieldType(analysis: FieldAnalysis): FieldType {\n // If it has image paths, it's an image field\n if (analysis.hasImagePaths) return \"image\";\n\n // If it has date values, it's a date field\n if (analysis.hasDateValues) return \"date\";\n\n // Check detected types (excluding null)\n const nonNullTypes = new Set([...analysis.types].filter((t) => t !== \"null\"));\n\n // Single type is easy\n if (nonNullTypes.size === 1) {\n const type = [...nonNullTypes][0];\n switch (type) {\n case \"boolean\":\n return \"boolean\";\n case \"number\":\n return \"number\";\n case \"array\":\n return \"array\";\n case \"object\":\n return \"object\";\n case \"date\":\n return \"date\";\n default:\n return \"string\";\n }\n }\n\n // Mixed types - default to string (most flexible)\n return \"string\";\n}\n\n/**\n * Detect if a field should be treated as an enum\n *\n * @param values - All values seen for this field\n * @param totalSamples - Total number of samples\n * @returns Array of enum values, or undefined if not an enum\n */\nfunction detectEnum(\n values: unknown[],\n totalSamples: number\n): string[] | undefined {\n // Filter to string values only\n const stringValues = values.filter(\n (v): v is string => typeof v === \"string\" && v.length > 0\n );\n\n if (stringValues.length === 0) return undefined;\n\n // Get unique values\n const uniqueValues = [...new Set(stringValues)];\n\n // Check if it's a good candidate for enum\n if (uniqueValues.length > ENUM_MAX_VALUES) return undefined;\n\n // Check ratio of unique to total\n const ratio = uniqueValues.length / totalSamples;\n if (ratio > ENUM_RATIO_THRESHOLD) return undefined;\n\n // Must have at least 2 unique values and appear multiple times\n if (uniqueValues.length < 2) return undefined;\n if (stringValues.length < totalSamples * 0.5) return undefined;\n\n return uniqueValues.sort();\n}\n\n/**\n * Detect item type for array fields\n *\n * @param analysis - Field analysis data\n * @returns The detected item type, or undefined\n */\nfunction detectArrayItemType(analysis: FieldAnalysis): string | undefined {\n if (analysis.arrayItemTypes.size === 0) return undefined;\n\n // Filter out null\n const types = [...analysis.arrayItemTypes].filter((t) => t !== \"null\");\n\n if (types.length === 0) return undefined;\n if (types.length === 1) return types[0];\n\n // Mixed types - default to string\n return \"string\";\n}\n\n/**\n * Analyze frontmatter from content items to detect schema\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Schema detection result\n *\n * @example\n * ```typescript\n * const result = await detectSchema('/project/src/content/blog');\n * console.log(result.schema);\n * // {\n * // title: { type: 'string', required: true },\n * // pubDate: { type: 'date', required: true },\n * // draft: { type: 'boolean', required: false, default: false },\n * // tags: { type: 'array', required: false, items: 'string' },\n * // }\n * ```\n */\nexport async function detectSchema(\n collectionPath: string\n): Promise<SchemaDetectionResult> {\n const warnings: string[] = [];\n\n // Read sample content files\n const items = await readCollection(collectionPath, {\n includeDrafts: true,\n });\n\n // Limit to max samples\n const samples = items.slice(0, MAX_SAMPLE_FILES);\n\n if (samples.length === 0) {\n return {\n schema: {},\n samplesAnalyzed: 0,\n confidence: 0,\n warnings: [\"No content files found in collection\"],\n };\n }\n\n // Analyze each field across all samples\n const fieldAnalyses = new Map<string, FieldAnalysis>();\n\n for (const item of samples) {\n for (const [fieldName, value] of Object.entries(item.frontmatter)) {\n // Get or create field analysis\n let analysis = fieldAnalyses.get(fieldName);\n if (!analysis) {\n analysis = {\n presentCount: 0,\n types: new Set(),\n values: [],\n hasImagePaths: false,\n hasDateValues: false,\n arrayItemTypes: new Set(),\n };\n fieldAnalyses.set(fieldName, analysis);\n }\n\n // Update analysis\n analysis.presentCount++;\n analysis.types.add(detectValueType(value));\n analysis.values.push(value);\n\n // Check for special types\n if (isImagePath(value)) {\n analysis.hasImagePaths = true;\n }\n if (isDateValue(value)) {\n analysis.hasDateValues = true;\n }\n\n // Analyze array items\n if (Array.isArray(value)) {\n for (const item of value) {\n analysis.arrayItemTypes.add(detectValueType(item));\n }\n }\n }\n }\n\n // Generate schema from analysis\n const schema: CollectionSchema = {};\n const totalSamples = samples.length;\n\n for (const [fieldName, analysis] of fieldAnalyses) {\n const fieldType = inferFieldType(analysis);\n const isRequired =\n analysis.presentCount / totalSamples >= REQUIRED_THRESHOLD;\n\n const field: SchemaField = {\n type: fieldType,\n required: isRequired,\n };\n\n // Add array item type if applicable\n if (fieldType === \"array\") {\n const itemType = detectArrayItemType(analysis);\n if (itemType) {\n field.items = itemType;\n }\n }\n\n // Detect enum for string fields\n if (fieldType === \"string\") {\n const enumValues = detectEnum(analysis.values, totalSamples);\n if (enumValues) {\n // Store enum values in the field\n // Note: We use 'default' to store enum options since SchemaField\n // doesn't have an 'enum' property - this can be enhanced later\n field.description = `Options: ${enumValues.join(\", \")}`;\n }\n }\n\n // Detect default value for boolean fields\n if (fieldType === \"boolean\") {\n const boolValues = analysis.values.filter(\n (v): v is boolean => typeof v === \"boolean\"\n );\n if (boolValues.length > 0) {\n // Use most common value as default\n const trueCount = boolValues.filter((v) => v === true).length;\n const falseCount = boolValues.filter((v) => v === false).length;\n field.default = trueCount > falseCount ? true : false;\n }\n }\n\n // Check for type inconsistencies\n const nonNullTypes = [...analysis.types].filter((t) => t !== \"null\");\n if (nonNullTypes.length > 1) {\n warnings.push(\n `Field \"${fieldName}\" has inconsistent types: ${nonNullTypes.join(\", \")}`\n );\n }\n\n schema[fieldName] = field;\n }\n\n // Calculate confidence based on consistency\n const inconsistentFields = warnings.filter((w) =>\n w.includes(\"inconsistent\")\n ).length;\n const confidence = Math.max(\n 0,\n 1 - inconsistentFields / Math.max(1, fieldAnalyses.size)\n );\n\n return {\n schema,\n samplesAnalyzed: totalSamples,\n confidence,\n warnings,\n };\n}\n\n/**\n * Merge detected schema with user-provided schema\n *\n * User schema takes precedence over detected schema.\n *\n * @param detected - Auto-detected schema\n * @param userSchema - User-provided schema overrides\n * @returns Merged schema\n */\nexport function mergeSchema(\n detected: CollectionSchema,\n userSchema?: CollectionSchema\n): CollectionSchema {\n if (!userSchema) return detected;\n\n const merged: CollectionSchema = { ...detected };\n\n for (const [fieldName, userField] of Object.entries(userSchema)) {\n merged[fieldName] = {\n ...detected[fieldName],\n ...userField,\n };\n }\n\n return merged;\n}\n\n/**\n * Convert schema to a human-readable description\n *\n * @param schema - The schema to describe\n * @returns Human-readable description\n */\nexport function describeSchema(schema: CollectionSchema): string {\n const lines: string[] = [];\n\n for (const [fieldName, field] of Object.entries(schema)) {\n let desc = `- ${fieldName}: ${field.type}`;\n\n if (field.required) {\n desc += \" (required)\";\n }\n\n if (field.items) {\n desc += ` of ${field.items}`;\n }\n\n if (field.default !== undefined) {\n desc += ` [default: ${JSON.stringify(field.default)}]`;\n }\n\n if (field.description) {\n desc += ` - ${field.description}`;\n }\n\n lines.push(desc);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * @fileoverview Collection discovery for Astro content collections\n *\n * This module provides functions to auto-discover content collections\n * from an Astro project's src/content directory.\n *\n * ## Discovery Process:\n * 1. Scan src/content/ for subdirectories\n * 2. Each subdirectory is treated as a collection\n * 3. Count content files in each collection\n * 4. Detect file patterns from existing files\n * 5. Auto-detect frontmatter schema from sample files\n *\n * @module @writenex/astro/discovery/collections\n */\n\nimport { readdir, stat } from \"node:fs/promises\";\nimport { existsSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport type { DiscoveredCollection, CollectionConfig } from \"@/types\";\nimport { getCollectionCount } from \"@/filesystem/reader\";\nimport { DEFAULT_FILE_PATTERN } from \"@/config/defaults\";\nimport { detectFilePattern as detectPattern } from \"./patterns\";\nimport { detectSchema } from \"./schema\";\n\n/**\n * Default content directory path relative to project root\n */\nconst DEFAULT_CONTENT_DIR = \"src/content\";\n\n/**\n * Directories to ignore during discovery\n */\nconst IGNORED_DIRECTORIES = new Set([\"node_modules\", \".git\", \"_\", \".\"]);\n\n/**\n * Check if a directory should be ignored\n *\n * @param name - Directory name\n * @returns True if should be ignored\n */\nfunction shouldIgnore(name: string): boolean {\n return (\n IGNORED_DIRECTORIES.has(name) ||\n name.startsWith(\"_\") ||\n name.startsWith(\".\")\n );\n}\n\n/**\n * Discover all content collections in a project\n *\n * Scans the src/content directory for subdirectories and treats\n * each as a content collection.\n *\n * @param projectRoot - Absolute path to the project root\n * @param contentDir - Relative path to content directory (default: src/content)\n * @returns Array of discovered collections\n *\n * @example\n * ```typescript\n * const collections = await discoverCollections('/path/to/project');\n * // Returns: [\n * // { name: 'blog', path: 'src/content/blog', count: 10, ... },\n * // { name: 'docs', path: 'src/content/docs', count: 5, ... },\n * // ]\n * ```\n */\nexport async function discoverCollections(\n projectRoot: string,\n contentDir: string = DEFAULT_CONTENT_DIR\n): Promise<DiscoveredCollection[]> {\n const contentPath = join(projectRoot, contentDir);\n\n // Check if content directory exists\n if (!existsSync(contentPath)) {\n return [];\n }\n\n const collections: DiscoveredCollection[] = [];\n\n try {\n const entries = await readdir(contentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip non-directories and ignored directories\n if (!entry.isDirectory() || shouldIgnore(entry.name)) {\n continue;\n }\n\n const collectionPath = join(contentPath, entry.name);\n const relativePath = join(contentDir, entry.name);\n\n // Count content files in this collection\n const count = await getCollectionCount(collectionPath);\n\n // Detect file pattern using pattern detection module\n const patternResult = await detectPattern(collectionPath);\n const filePattern = patternResult.pattern;\n\n // Auto-detect schema from sample files\n const schemaResult = await detectSchema(collectionPath);\n const schema =\n Object.keys(schemaResult.schema).length > 0\n ? schemaResult.schema\n : undefined;\n\n // Generate default preview URL pattern\n const previewUrl = `/${entry.name}/{slug}`;\n\n collections.push({\n name: entry.name,\n path: relativePath,\n filePattern,\n count,\n schema,\n previewUrl,\n });\n }\n } catch (error) {\n console.error(`[writenex] Failed to discover collections: ${error}`);\n }\n\n return collections;\n}\n\n/**\n * Merge discovered collections with configured collections\n *\n * Configured collections take precedence over discovered ones.\n * This allows users to override auto-discovered settings.\n *\n * @param discovered - Auto-discovered collections\n * @param configured - User-configured collections\n * @returns Merged collection list\n */\nexport function mergeCollections(\n discovered: DiscoveredCollection[],\n configured: CollectionConfig[]\n): DiscoveredCollection[] {\n const configuredNames = new Set(configured.map((c) => c.name));\n const result: DiscoveredCollection[] = [];\n\n // Add configured collections first (they take precedence)\n for (const config of configured) {\n const discoveredMatch = discovered.find((d) => d.name === config.name);\n\n result.push({\n name: config.name,\n path: config.path,\n filePattern:\n config.filePattern ??\n discoveredMatch?.filePattern ??\n DEFAULT_FILE_PATTERN,\n count: discoveredMatch?.count ?? 0,\n schema: config.schema ?? discoveredMatch?.schema,\n previewUrl:\n config.previewUrl ??\n discoveredMatch?.previewUrl ??\n `/${config.name}/{slug}`,\n });\n }\n\n // Add discovered collections that weren't configured\n for (const disc of discovered) {\n if (!configuredNames.has(disc.name)) {\n result.push(disc);\n }\n }\n\n return result;\n}\n\n/**\n * Get a single collection by name\n *\n * @param projectRoot - Absolute path to the project root\n * @param collectionName - Name of the collection\n * @param contentDir - Relative path to content directory\n * @returns The collection if found, undefined otherwise\n */\nexport async function getCollection(\n projectRoot: string,\n collectionName: string,\n contentDir: string = DEFAULT_CONTENT_DIR\n): Promise<DiscoveredCollection | undefined> {\n const collections = await discoverCollections(projectRoot, contentDir);\n return collections.find((c) => c.name === collectionName);\n}\n\n/**\n * Check if a collection exists\n *\n * @param projectRoot - Absolute path to the project root\n * @param collectionName - Name of the collection\n * @param contentDir - Relative path to content directory\n * @returns True if the collection exists\n */\nexport async function collectionExists(\n projectRoot: string,\n collectionName: string,\n contentDir: string = DEFAULT_CONTENT_DIR\n): Promise<boolean> {\n const collectionPath = join(projectRoot, contentDir, collectionName);\n\n if (!existsSync(collectionPath)) {\n return false;\n }\n\n try {\n const stats = await stat(collectionPath);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["item"]}
1
+ {"version":3,"sources":["../src/discovery/schema.ts","../src/discovery/collections.ts"],"sourcesContent":["/**\n * @fileoverview Schema auto-detection for content collections\n *\n * This module analyzes frontmatter from sample content files to automatically\n * infer the schema (field types, required status, enums, etc.).\n *\n * ## Detection Process:\n * 1. Read sample files from the collection (up to 20)\n * 2. Parse frontmatter from each file\n * 3. Analyze field patterns across all samples\n * 4. Infer field types and constraints\n * 5. Generate schema definition\n *\n * ## Detected Types:\n * - string: Plain text values\n * - number: Numeric values\n * - boolean: True/false values\n * - date: ISO date strings or Date objects\n * - array: Arrays (with item type detection)\n * - image: Paths ending with image extensions\n *\n * @module @writenex/astro/discovery/schema\n */\n\nimport { readCollection } from \"@/filesystem/reader\";\nimport type { CollectionSchema, FieldType, SchemaField } from \"@/types\";\n\n/**\n * Maximum number of files to sample for schema detection\n */\nconst MAX_SAMPLE_FILES = 20;\n\n/**\n * Minimum presence ratio to consider a field required\n * (field must appear in at least 90% of files)\n */\nconst REQUIRED_THRESHOLD = 0.9;\n\n/**\n * Maximum unique values to consider a field as enum\n */\nconst ENUM_MAX_VALUES = 10;\n\n/**\n * Minimum ratio of unique values to total to NOT be an enum\n * (if uniqueValues / total < 0.3, it's likely an enum)\n */\nconst ENUM_RATIO_THRESHOLD = 0.3;\n\n/**\n * Image file extensions for detection\n */\nconst IMAGE_EXTENSIONS = [\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".svg\",\n];\n\n/**\n * Field analysis data collected from samples\n */\ninterface FieldAnalysis {\n /** Number of files where this field appears */\n presentCount: number;\n /** Detected types for this field across samples */\n types: Set<string>;\n /** Sample values for enum detection */\n values: unknown[];\n /** Whether values look like image paths */\n hasImagePaths: boolean;\n /** Whether values look like dates */\n hasDateValues: boolean;\n /** For arrays, analysis of item types */\n arrayItemTypes: Set<string>;\n}\n\n/**\n * Result of schema detection\n */\nexport interface SchemaDetectionResult {\n /** The detected schema */\n schema: CollectionSchema;\n /** Number of files analyzed */\n samplesAnalyzed: number;\n /** Confidence score (0-1) based on sample consistency */\n confidence: number;\n /** Fields that had inconsistent types across samples */\n warnings: string[];\n}\n\n/**\n * Check if a string looks like an image path\n *\n * @param value - Value to check\n * @returns True if it looks like an image path\n */\nfunction isImagePath(value: unknown): boolean {\n if (typeof value !== \"string\") return false;\n\n const lowered = value.toLowerCase();\n return IMAGE_EXTENSIONS.some((ext) => lowered.endsWith(ext));\n}\n\n/**\n * Check if a value looks like a date\n *\n * @param value - Value to check\n * @returns True if it looks like a date\n */\nfunction isDateValue(value: unknown): boolean {\n // Already a Date object\n if (value instanceof Date) return true;\n\n // ISO date string (YYYY-MM-DD or full ISO)\n if (typeof value === \"string\") {\n // Full ISO format\n if (/^\\d{4}-\\d{2}-\\d{2}(T|\\s)/.test(value)) return true;\n // Simple date format\n if (/^\\d{4}-\\d{2}-\\d{2}$/.test(value)) return true;\n }\n\n return false;\n}\n\n/**\n * Detect the JavaScript type of a value\n *\n * @param value - Value to analyze\n * @returns Detected type string\n */\nfunction detectValueType(value: unknown): string {\n if (value === null || value === undefined) return \"null\";\n if (typeof value === \"boolean\") return \"boolean\";\n if (typeof value === \"number\") return \"number\";\n if (typeof value === \"string\") return \"string\";\n if (Array.isArray(value)) return \"array\";\n if (value instanceof Date) return \"date\";\n if (typeof value === \"object\") return \"object\";\n return \"unknown\";\n}\n\n/**\n * Convert detected type to schema field type\n *\n * @param analysis - Field analysis data\n * @returns The appropriate FieldType\n */\nfunction inferFieldType(analysis: FieldAnalysis): FieldType {\n // If it has image paths, it's an image field\n if (analysis.hasImagePaths) return \"image\";\n\n // If it has date values, it's a date field\n if (analysis.hasDateValues) return \"date\";\n\n // Check detected types (excluding null)\n const nonNullTypes = new Set([...analysis.types].filter((t) => t !== \"null\"));\n\n // Single type is easy\n if (nonNullTypes.size === 1) {\n const type = [...nonNullTypes][0];\n switch (type) {\n case \"boolean\":\n return \"boolean\";\n case \"number\":\n return \"number\";\n case \"array\":\n return \"array\";\n case \"object\":\n return \"object\";\n case \"date\":\n return \"date\";\n default:\n return \"string\";\n }\n }\n\n // Mixed types - default to string (most flexible)\n return \"string\";\n}\n\n/**\n * Detect if a field should be treated as an enum\n *\n * @param values - All values seen for this field\n * @param totalSamples - Total number of samples\n * @returns Array of enum values, or undefined if not an enum\n */\nfunction detectEnum(\n values: unknown[],\n totalSamples: number\n): string[] | undefined {\n // Filter to string values only\n const stringValues = values.filter(\n (v): v is string => typeof v === \"string\" && v.length > 0\n );\n\n if (stringValues.length === 0) return undefined;\n\n // Get unique values\n const uniqueValues = [...new Set(stringValues)];\n\n // Check if it's a good candidate for enum\n if (uniqueValues.length > ENUM_MAX_VALUES) return undefined;\n\n // Check ratio of unique to total\n const ratio = uniqueValues.length / totalSamples;\n if (ratio > ENUM_RATIO_THRESHOLD) return undefined;\n\n // Must have at least 2 unique values and appear multiple times\n if (uniqueValues.length < 2) return undefined;\n if (stringValues.length < totalSamples * 0.5) return undefined;\n\n return uniqueValues.sort();\n}\n\n/**\n * Detect item type for array fields\n *\n * @param analysis - Field analysis data\n * @returns The detected item type, or undefined\n */\nfunction detectArrayItemType(analysis: FieldAnalysis): string | undefined {\n if (analysis.arrayItemTypes.size === 0) return undefined;\n\n // Filter out null\n const types = [...analysis.arrayItemTypes].filter((t) => t !== \"null\");\n\n if (types.length === 0) return undefined;\n if (types.length === 1) return types[0];\n\n // Mixed types - default to string\n return \"string\";\n}\n\n/**\n * Analyze frontmatter from content items to detect schema\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Schema detection result\n *\n * @example\n * ```typescript\n * const result = await detectSchema('/project/src/content/blog');\n * console.log(result.schema);\n * // {\n * // title: { type: 'string', required: true },\n * // pubDate: { type: 'date', required: true },\n * // draft: { type: 'boolean', required: false, default: false },\n * // tags: { type: 'array', required: false, items: 'string' },\n * // }\n * ```\n */\nexport async function detectSchema(\n collectionPath: string\n): Promise<SchemaDetectionResult> {\n const warnings: string[] = [];\n\n // Read sample content files\n const items = await readCollection(collectionPath, {\n includeDrafts: true,\n });\n\n // Limit to max samples\n const samples = items.slice(0, MAX_SAMPLE_FILES);\n\n if (samples.length === 0) {\n return {\n schema: {},\n samplesAnalyzed: 0,\n confidence: 0,\n warnings: [\"No content files found in collection\"],\n };\n }\n\n // Analyze each field across all samples\n const fieldAnalyses = new Map<string, FieldAnalysis>();\n\n for (const item of samples) {\n for (const [fieldName, value] of Object.entries(item.frontmatter)) {\n // Get or create field analysis\n let analysis = fieldAnalyses.get(fieldName);\n if (!analysis) {\n analysis = {\n presentCount: 0,\n types: new Set(),\n values: [],\n hasImagePaths: false,\n hasDateValues: false,\n arrayItemTypes: new Set(),\n };\n fieldAnalyses.set(fieldName, analysis);\n }\n\n // Update analysis\n analysis.presentCount++;\n analysis.types.add(detectValueType(value));\n analysis.values.push(value);\n\n // Check for special types\n if (isImagePath(value)) {\n analysis.hasImagePaths = true;\n }\n if (isDateValue(value)) {\n analysis.hasDateValues = true;\n }\n\n // Analyze array items\n if (Array.isArray(value)) {\n for (const item of value) {\n analysis.arrayItemTypes.add(detectValueType(item));\n }\n }\n }\n }\n\n // Generate schema from analysis\n const schema: CollectionSchema = {};\n const totalSamples = samples.length;\n\n for (const [fieldName, analysis] of fieldAnalyses) {\n const fieldType = inferFieldType(analysis);\n const isRequired =\n analysis.presentCount / totalSamples >= REQUIRED_THRESHOLD;\n\n const field: SchemaField = {\n type: fieldType,\n required: isRequired,\n };\n\n // Add array item type if applicable\n if (fieldType === \"array\") {\n const itemType = detectArrayItemType(analysis);\n if (itemType) {\n field.items = itemType;\n }\n }\n\n // Detect enum for string fields\n if (fieldType === \"string\") {\n const enumValues = detectEnum(analysis.values, totalSamples);\n if (enumValues) {\n // Store enum values in the field\n // Note: We use 'default' to store enum options since SchemaField\n // doesn't have an 'enum' property - this can be enhanced later\n field.description = `Options: ${enumValues.join(\", \")}`;\n }\n }\n\n // Detect default value for boolean fields\n if (fieldType === \"boolean\") {\n const boolValues = analysis.values.filter(\n (v): v is boolean => typeof v === \"boolean\"\n );\n if (boolValues.length > 0) {\n // Use most common value as default\n const trueCount = boolValues.filter((v) => v === true).length;\n const falseCount = boolValues.filter((v) => v === false).length;\n field.default = trueCount > falseCount ? true : false;\n }\n }\n\n // Check for type inconsistencies\n const nonNullTypes = [...analysis.types].filter((t) => t !== \"null\");\n if (nonNullTypes.length > 1) {\n warnings.push(\n `Field \"${fieldName}\" has inconsistent types: ${nonNullTypes.join(\", \")}`\n );\n }\n\n schema[fieldName] = field;\n }\n\n // Calculate confidence based on consistency\n const inconsistentFields = warnings.filter((w) =>\n w.includes(\"inconsistent\")\n ).length;\n const confidence = Math.max(\n 0,\n 1 - inconsistentFields / Math.max(1, fieldAnalyses.size)\n );\n\n return {\n schema,\n samplesAnalyzed: totalSamples,\n confidence,\n warnings,\n };\n}\n\n/**\n * Merge detected schema with user-provided schema\n *\n * User schema takes precedence over detected schema.\n *\n * @param detected - Auto-detected schema\n * @param userSchema - User-provided schema overrides\n * @returns Merged schema\n */\nexport function mergeSchema(\n detected: CollectionSchema,\n userSchema?: CollectionSchema\n): CollectionSchema {\n if (!userSchema) return detected;\n\n const merged: CollectionSchema = { ...detected };\n\n for (const [fieldName, userField] of Object.entries(userSchema)) {\n merged[fieldName] = {\n ...detected[fieldName],\n ...userField,\n };\n }\n\n return merged;\n}\n\n/**\n * Convert schema to a human-readable description\n *\n * @param schema - The schema to describe\n * @returns Human-readable description\n */\nexport function describeSchema(schema: CollectionSchema): string {\n const lines: string[] = [];\n\n for (const [fieldName, field] of Object.entries(schema)) {\n let desc = `- ${fieldName}: ${field.type}`;\n\n if (field.required) {\n desc += \" (required)\";\n }\n\n if (field.items) {\n desc += ` of ${field.items}`;\n }\n\n if (field.default !== undefined) {\n desc += ` [default: ${JSON.stringify(field.default)}]`;\n }\n\n if (field.description) {\n desc += ` - ${field.description}`;\n }\n\n lines.push(desc);\n }\n\n return lines.join(\"\\n\");\n}\n","/**\n * @fileoverview Collection discovery for Astro content collections\n *\n * This module provides functions to auto-discover content collections\n * from an Astro project's src/content directory.\n *\n * ## Discovery Process:\n * 1. Scan src/content/ for subdirectories\n * 2. Each subdirectory is treated as a collection\n * 3. Count content files in each collection\n * 4. Detect file patterns from existing files\n * 5. Auto-detect frontmatter schema from sample files\n *\n * @module @writenex/astro/discovery/collections\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir, stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { DEFAULT_FILE_PATTERN } from \"@/config/defaults\";\nimport { getCollectionCount } from \"@/filesystem/reader\";\nimport type { CollectionConfig, DiscoveredCollection } from \"@/types\";\nimport { detectFilePattern as detectPattern } from \"./patterns\";\nimport { detectSchema } from \"./schema\";\n\n/**\n * Default content directory path relative to project root\n */\nconst DEFAULT_CONTENT_DIR = \"src/content\";\n\n/**\n * Directories to ignore during discovery\n */\nconst IGNORED_DIRECTORIES = new Set([\"node_modules\", \".git\", \"_\", \".\"]);\n\n/**\n * Check if a directory should be ignored\n *\n * @param name - Directory name\n * @returns True if should be ignored\n */\nfunction shouldIgnore(name: string): boolean {\n return (\n IGNORED_DIRECTORIES.has(name) ||\n name.startsWith(\"_\") ||\n name.startsWith(\".\")\n );\n}\n\n/**\n * Discover all content collections in a project\n *\n * Scans the src/content directory for subdirectories and treats\n * each as a content collection.\n *\n * @param projectRoot - Absolute path to the project root\n * @param contentDir - Relative path to content directory (default: src/content)\n * @returns Array of discovered collections\n *\n * @example\n * ```typescript\n * const collections = await discoverCollections('/path/to/project');\n * // Returns: [\n * // { name: 'blog', path: 'src/content/blog', count: 10, ... },\n * // { name: 'docs', path: 'src/content/docs', count: 5, ... },\n * // ]\n * ```\n */\nexport async function discoverCollections(\n projectRoot: string,\n contentDir: string = DEFAULT_CONTENT_DIR\n): Promise<DiscoveredCollection[]> {\n const contentPath = join(projectRoot, contentDir);\n\n // Check if content directory exists\n if (!existsSync(contentPath)) {\n return [];\n }\n\n const collections: DiscoveredCollection[] = [];\n\n try {\n const entries = await readdir(contentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n // Skip non-directories and ignored directories\n if (!entry.isDirectory() || shouldIgnore(entry.name)) {\n continue;\n }\n\n const collectionPath = join(contentPath, entry.name);\n const relativePath = join(contentDir, entry.name);\n\n // Count content files in this collection\n const count = await getCollectionCount(collectionPath);\n\n // Detect file pattern using pattern detection module\n const patternResult = await detectPattern(collectionPath);\n const filePattern = patternResult.pattern;\n\n // Auto-detect schema from sample files\n const schemaResult = await detectSchema(collectionPath);\n const schema =\n Object.keys(schemaResult.schema).length > 0\n ? schemaResult.schema\n : undefined;\n\n // Generate default preview URL pattern\n const previewUrl = `/${entry.name}/{slug}`;\n\n collections.push({\n name: entry.name,\n path: relativePath,\n filePattern,\n count,\n schema,\n previewUrl,\n });\n }\n } catch (error) {\n console.error(`[writenex] Failed to discover collections: ${error}`);\n }\n\n return collections;\n}\n\n/**\n * Merge discovered collections with configured collections\n *\n * Configured collections take precedence over discovered ones.\n * This allows users to override auto-discovered settings.\n *\n * @param discovered - Auto-discovered collections\n * @param configured - User-configured collections\n * @returns Merged collection list\n */\nexport function mergeCollections(\n discovered: DiscoveredCollection[],\n configured: CollectionConfig[]\n): DiscoveredCollection[] {\n const configuredNames = new Set(configured.map((c) => c.name));\n const result: DiscoveredCollection[] = [];\n\n // Add configured collections first (they take precedence)\n for (const config of configured) {\n const discoveredMatch = discovered.find((d) => d.name === config.name);\n\n result.push({\n name: config.name,\n path: config.path,\n filePattern:\n config.filePattern ??\n discoveredMatch?.filePattern ??\n DEFAULT_FILE_PATTERN,\n count: discoveredMatch?.count ?? 0,\n schema: config.schema ?? discoveredMatch?.schema,\n previewUrl:\n config.previewUrl ??\n discoveredMatch?.previewUrl ??\n `/${config.name}/{slug}`,\n });\n }\n\n // Add discovered collections that weren't configured\n for (const disc of discovered) {\n if (!configuredNames.has(disc.name)) {\n result.push(disc);\n }\n }\n\n return result;\n}\n\n/**\n * Get a single collection by name\n *\n * @param projectRoot - Absolute path to the project root\n * @param collectionName - Name of the collection\n * @param contentDir - Relative path to content directory\n * @returns The collection if found, undefined otherwise\n */\nexport async function getCollection(\n projectRoot: string,\n collectionName: string,\n contentDir: string = DEFAULT_CONTENT_DIR\n): Promise<DiscoveredCollection | undefined> {\n const collections = await discoverCollections(projectRoot, contentDir);\n return collections.find((c) => c.name === collectionName);\n}\n\n/**\n * Check if a collection exists\n *\n * @param projectRoot - Absolute path to the project root\n * @param collectionName - Name of the collection\n * @param contentDir - Relative path to content directory\n * @returns True if the collection exists\n */\nexport async function collectionExists(\n projectRoot: string,\n collectionName: string,\n contentDir: string = DEFAULT_CONTENT_DIR\n): Promise<boolean> {\n const collectionPath = join(projectRoot, contentDir, collectionName);\n\n if (!existsSync(collectionPath)) {\n return false;\n }\n\n try {\n const stats = await stat(collectionPath);\n return stats.isDirectory();\n } catch {\n return false;\n }\n}\n"],"mappings":"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","names":["item"]}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  discoverCollections,
3
3
  mergeCollections
4
- } from "./chunk-CYLDJ3HZ.js";
4
+ } from "./chunk-GIS7XEJF.js";
5
5
  import {
6
6
  ApiBadRequestError,
7
7
  ApiMethodNotAllowedError,
@@ -27,12 +27,12 @@ import {
27
27
  updateContent,
28
28
  uploadImage,
29
29
  wrapError
30
- } from "./chunk-CF2XXJFF.js";
30
+ } from "./chunk-OWYFIQFK.js";
31
31
  import {
32
32
  getCollectionSummaries,
33
33
  getContentFilePath,
34
34
  readContentFile
35
- } from "./chunk-AAOQHQPU.js";
35
+ } from "./chunk-TQAYIZOA.js";
36
36
 
37
37
  // src/server/cache.ts
38
38
  var DEFAULT_TTL_MS = 5 * 60 * 1e3;
@@ -269,14 +269,10 @@ function resetCache() {
269
269
  globalCache = null;
270
270
  }
271
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
272
  // src/server/assets.ts
277
- import { readFile } from "fs/promises";
278
273
  import { existsSync } from "fs";
279
- import { join, extname } from "path";
274
+ import { readFile } from "fs/promises";
275
+ import { extname, join } from "path";
280
276
  import { fileURLToPath } from "url";
281
277
  function getPackageRoot() {
282
278
  const currentFile = fileURLToPath(import.meta.url);
@@ -428,6 +424,10 @@ function hasClientBundle() {
428
424
  return existsSync(indexPath);
429
425
  }
430
426
 
427
+ // src/server/routes.ts
428
+ import { createReadStream, existsSync as existsSync2, statSync } from "fs";
429
+ import { extname as extname2, join as join2 } from "path";
430
+
431
431
  // src/server/middleware.ts
432
432
  function createMiddleware(context) {
433
433
  const { basePath } = context;
@@ -634,7 +634,7 @@ var handleGetConfig = async (_req, res, _params, context) => {
634
634
  };
635
635
  var handleGetConfigPath = async (_req, res, _params, context) => {
636
636
  const { projectRoot } = context;
637
- const { findConfigFile } = await import("./loader-55LWCXHA.js");
637
+ const { findConfigFile } = await import("./loader-VGNXC2XJ.js");
638
638
  const configPath = findConfigFile(projectRoot);
639
639
  sendJson(res, {
640
640
  configPath,
@@ -1316,11 +1316,11 @@ export {
1316
1316
  ServerCache,
1317
1317
  getCache,
1318
1318
  resetCache,
1319
- createApiRouter,
1320
1319
  serveEditorHtml,
1321
1320
  serveAsset,
1322
1321
  getClientDistPath,
1323
1322
  hasClientBundle,
1323
+ createApiRouter,
1324
1324
  createMiddleware,
1325
1325
  parseQueryParams,
1326
1326
  parseJsonBody,
@@ -1328,4 +1328,4 @@ export {
1328
1328
  sendError,
1329
1329
  sendWritenexError
1330
1330
  };
1331
- //# sourceMappingURL=chunk-7XU5X6CW.js.map
1331
+ //# sourceMappingURL=chunk-GUUSVFBP.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server/cache.ts","../src/server/assets.ts","../src/server/routes.ts","../src/server/middleware.ts"],"sourcesContent":["/**\n * @fileoverview Server-side caching for Writenex\n *\n * Provides in-memory caching for collection discovery and content summaries\n * to improve performance by avoiding repeated filesystem operations.\n *\n * ## Features:\n * - TTL-based cache expiration\n * - Per-collection content caching\n * - Image discovery caching\n * - Manual cache invalidation\n * - Integration with file watcher for automatic invalidation\n *\n * @module @writenex/astro/server/cache\n */\n\nimport type {\n ContentSummary,\n DiscoveredCollection,\n DiscoveredImage,\n} from \"@/types\";\n\n/**\n * Cache entry with timestamp and data\n */\ninterface CacheEntry<T> {\n data: T;\n timestamp: number;\n}\n\n/**\n * Default TTL for cache entries (5 minutes)\n */\nconst DEFAULT_TTL_MS = 5 * 60 * 1000;\n\n/**\n * Short TTL for development (30 seconds)\n * Used when file watcher is not active\n */\nconst DEV_TTL_MS = 30 * 1000;\n\n/**\n * Server-side cache for collection and content data\n *\n * This cache stores:\n * - Collection discovery results (list of collections with metadata)\n * - Content summaries per collection (list of content items)\n * - Discovered images per content item\n *\n * Cache invalidation happens:\n * - Automatically when TTL expires\n * - Manually via invalidate methods\n * - Via file watcher integration\n */\nexport class ServerCache {\n /** Cache for collection discovery results */\n private collectionsCache: CacheEntry<DiscoveredCollection[]> | null = null;\n\n /** Cache for content summaries, keyed by collection name */\n private contentCache: Map<string, CacheEntry<ContentSummary[]>> = new Map();\n\n /** Cache for discovered images, keyed by \"collection:contentId\" */\n private imagesCache: Map<string, CacheEntry<DiscoveredImage[]>> = new Map();\n\n /** Time-to-live for cache entries in milliseconds */\n private ttl: number;\n\n /** Whether file watcher is integrated (allows longer TTL) */\n private hasWatcher: boolean = false;\n\n constructor(options: { ttl?: number; hasWatcher?: boolean } = {}) {\n this.ttl =\n options.ttl ?? (options.hasWatcher ? DEFAULT_TTL_MS : DEV_TTL_MS);\n this.hasWatcher = options.hasWatcher ?? false;\n }\n\n /**\n * Enable file watcher integration\n *\n * When watcher is enabled, cache can use longer TTL since\n * invalidation happens via watcher events.\n */\n enableWatcher(): void {\n this.hasWatcher = true;\n this.ttl = DEFAULT_TTL_MS;\n }\n\n /**\n * Check if a cache entry is still valid\n */\n private isValid<T>(entry: CacheEntry<T> | null | undefined): boolean {\n if (!entry) return false;\n return Date.now() - entry.timestamp < this.ttl;\n }\n\n // ==================== Collections Cache ====================\n\n /**\n * Get cached collections if valid\n *\n * @returns Cached collections or null if expired/not cached\n */\n getCollections(): DiscoveredCollection[] | null {\n if (this.isValid(this.collectionsCache)) {\n return this.collectionsCache!.data;\n }\n return null;\n }\n\n /**\n * Set collections cache\n *\n * @param collections - Collections to cache\n */\n setCollections(collections: DiscoveredCollection[]): void {\n this.collectionsCache = {\n data: collections,\n timestamp: Date.now(),\n };\n }\n\n /**\n * Invalidate collections cache\n */\n invalidateCollections(): void {\n this.collectionsCache = null;\n }\n\n // ==================== Content Cache ====================\n\n /**\n * Get cached content summaries for a collection if valid\n *\n * @param collection - Collection name\n * @returns Cached content summaries or null if expired/not cached\n */\n getContent(collection: string): ContentSummary[] | null {\n const entry = this.contentCache.get(collection);\n if (this.isValid(entry)) {\n return entry!.data;\n }\n return null;\n }\n\n /**\n * Set content cache for a collection\n *\n * @param collection - Collection name\n * @param items - Content summaries to cache\n */\n setContent(collection: string, items: ContentSummary[]): void {\n this.contentCache.set(collection, {\n data: items,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Invalidate content cache for a specific collection\n *\n * @param collection - Collection name to invalidate\n */\n invalidateContent(collection: string): void {\n this.contentCache.delete(collection);\n }\n\n /**\n * Invalidate all content caches\n */\n invalidateAllContent(): void {\n this.contentCache.clear();\n }\n\n // ==================== Images Cache ====================\n\n /**\n * Generate cache key for image cache\n *\n * @param collection - Collection name\n * @param contentId - Content ID\n * @returns Cache key string\n */\n private getImagesCacheKey(collection: string, contentId: string): string {\n return `${collection}:${contentId}`;\n }\n\n /**\n * Get cached images for a content item if valid\n *\n * @param collection - Collection name\n * @param contentId - Content ID\n * @returns Cached images or null if expired/not cached\n */\n getImages(collection: string, contentId: string): DiscoveredImage[] | null {\n const key = this.getImagesCacheKey(collection, contentId);\n const entry = this.imagesCache.get(key);\n if (this.isValid(entry)) {\n return entry!.data;\n }\n return null;\n }\n\n /**\n * Set images cache for a content item\n *\n * @param collection - Collection name\n * @param contentId - Content ID\n * @param images - Discovered images to cache\n */\n setImages(\n collection: string,\n contentId: string,\n images: DiscoveredImage[]\n ): void {\n const key = this.getImagesCacheKey(collection, contentId);\n this.imagesCache.set(key, {\n data: images,\n timestamp: Date.now(),\n });\n }\n\n /**\n * Invalidate image cache for a specific content item\n *\n * @param collection - Collection name\n * @param contentId - Content ID\n */\n invalidateImages(collection: string, contentId: string): void {\n const key = this.getImagesCacheKey(collection, contentId);\n this.imagesCache.delete(key);\n }\n\n /**\n * Invalidate all image caches for a collection\n *\n * @param collection - Collection name\n */\n invalidateCollectionImages(collection: string): void {\n const prefix = `${collection}:`;\n for (const key of this.imagesCache.keys()) {\n if (key.startsWith(prefix)) {\n this.imagesCache.delete(key);\n }\n }\n }\n\n /**\n * Invalidate all image caches\n */\n invalidateAllImages(): void {\n this.imagesCache.clear();\n }\n\n // ==================== Bulk Invalidation ====================\n\n /**\n * Invalidate all caches\n *\n * Called when a major change occurs that affects everything.\n */\n invalidateAll(): void {\n this.collectionsCache = null;\n this.contentCache.clear();\n this.imagesCache.clear();\n }\n\n /**\n * Handle file change event from watcher\n *\n * Intelligently invalidates only the affected caches.\n *\n * @param type - Type of file change (add, change, unlink)\n * @param collection - Collection that was affected\n * @param contentId - Optional content ID for targeted image cache invalidation\n */\n handleFileChange(\n type: \"add\" | \"change\" | \"unlink\",\n collection: string,\n contentId?: string\n ): void {\n // Always invalidate the affected collection's content cache\n this.invalidateContent(collection);\n\n // Invalidate image cache for the specific content item or entire collection\n if (contentId) {\n this.invalidateImages(collection, contentId);\n } else {\n this.invalidateCollectionImages(collection);\n }\n\n // For add/unlink, also invalidate collections cache (count changed)\n if (type === \"add\" || type === \"unlink\") {\n this.invalidateCollections();\n }\n }\n\n // ==================== Stats ====================\n\n /**\n * Get cache statistics for debugging\n *\n * @returns Object with cache stats\n */\n getStats(): {\n collectionsValid: boolean;\n contentCollections: string[];\n cachedImages: string[];\n ttl: number;\n hasWatcher: boolean;\n } {\n return {\n collectionsValid: this.isValid(this.collectionsCache),\n contentCollections: Array.from(this.contentCache.keys()).filter((key) =>\n this.isValid(this.contentCache.get(key))\n ),\n cachedImages: Array.from(this.imagesCache.keys()).filter((key) =>\n this.isValid(this.imagesCache.get(key))\n ),\n ttl: this.ttl,\n hasWatcher: this.hasWatcher,\n };\n }\n}\n\n/**\n * Global cache instance\n *\n * Shared across the server for consistent caching.\n */\nlet globalCache: ServerCache | null = null;\n\n/**\n * Get or create the global cache instance\n *\n * @param options - Cache options (only used on first call)\n * @returns The global cache instance\n */\nexport function getCache(options?: {\n ttl?: number;\n hasWatcher?: boolean;\n}): ServerCache {\n if (!globalCache) {\n globalCache = new ServerCache(options);\n }\n return globalCache;\n}\n\n/**\n * Reset the global cache instance\n *\n * Useful for testing or when configuration changes.\n */\nexport function resetCache(): void {\n globalCache = null;\n}\n","/**\n * @fileoverview Static asset serving for Writenex editor\n *\n * This module handles serving the editor UI HTML and static assets\n * (JavaScript, CSS) for the Writenex editor interface.\n *\n * ## Asset Strategy:\n * - In development: Serve from source with Vite transform\n * - In production: Serve pre-bundled assets from dist/client\n *\n * @module @writenex/astro/server/assets\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { extname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport type { MiddlewareContext } from \"./middleware\";\n\n/**\n * Get the package root directory\n *\n * This function determines the package root based on where the code is running from.\n * When installed from npm, the structure is:\n * node_modules/@writenex/astro/dist/index.js\n *\n * We need to find the package root to locate dist/client/ assets.\n */\nfunction getPackageRoot(): string {\n const currentFile = fileURLToPath(import.meta.url);\n const currentDir = fileURLToPath(new URL(\".\", import.meta.url));\n\n // When bundled, import.meta.url points to the dist/index.js file\n // We need to go up one level to get to package root\n if (currentFile.endsWith(\"dist/index.js\") || currentDir.endsWith(\"dist/\")) {\n return join(currentDir, \"..\");\n }\n\n // When running from source (development), we're in src/server/\n // Go up 2 levels to get to package root\n if (currentDir.includes(\"/src/\")) {\n return join(currentDir, \"..\", \"..\");\n }\n\n // Fallback: assume we're in dist\n return join(currentDir, \"..\");\n}\n\nconst PACKAGE_ROOT = getPackageRoot();\n\n/**\n * MIME types for static assets\n */\nconst MIME_TYPES: Record<string, string> = {\n \".js\": \"application/javascript\",\n \".mjs\": \"application/javascript\",\n \".css\": \"text/css\",\n \".json\": \"application/json\",\n \".svg\": \"image/svg+xml\",\n \".png\": \"image/png\",\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".gif\": \"image/gif\",\n \".woff\": \"font/woff\",\n \".woff2\": \"font/woff2\",\n \".ttf\": \"font/ttf\",\n};\n\n/**\n * Serve the editor HTML page\n *\n * This generates the HTML shell that loads the React editor application.\n * The actual React components will be loaded via the bundled client assets.\n *\n * @param _req - The incoming request\n * @param res - The server response\n * @param context - Middleware context\n */\nexport async function serveEditorHtml(\n _req: IncomingMessage,\n res: ServerResponse,\n context: MiddlewareContext\n): Promise<void> {\n const { basePath } = context;\n\n const html = generateEditorHtml(basePath);\n\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", \"text/html; charset=utf-8\");\n res.setHeader(\"Cache-Control\", \"no-cache\");\n res.end(html);\n}\n\n/**\n * Serve static assets (JS, CSS, etc.)\n *\n * @param _req - The incoming request\n * @param res - The server response\n * @param assetPath - Path to the asset (relative to assets directory)\n * @param _context - Middleware context\n */\nexport async function serveAsset(\n _req: IncomingMessage,\n res: ServerResponse,\n assetPath: string,\n _context: MiddlewareContext\n): Promise<void> {\n // Determine asset location\n // Assets are always in dist/client (pre-bundled by tsup)\n const distPath = join(PACKAGE_ROOT, \"dist\", \"client\", assetPath);\n\n if (!existsSync(distPath)) {\n console.error(\"[writenex] Asset not found:\", distPath);\n res.statusCode = 404;\n res.setHeader(\"Content-Type\", \"text/plain\");\n res.end(`Asset not found: ${assetPath}`);\n return;\n }\n\n const filePath = distPath;\n\n try {\n const content = await readFile(filePath);\n const ext = extname(assetPath).toLowerCase();\n const mimeType = MIME_TYPES[ext] ?? \"application/octet-stream\";\n\n res.statusCode = 200;\n res.setHeader(\"Content-Type\", mimeType);\n res.setHeader(\"Cache-Control\", \"public, max-age=31536000, immutable\");\n res.end(content);\n } catch (error) {\n console.error(`[writenex] Failed to serve asset: ${assetPath}`, error);\n res.statusCode = 500;\n res.setHeader(\"Content-Type\", \"text/plain\");\n res.end(\"Failed to read asset\");\n }\n}\n\n/**\n * Generate the editor HTML shell\n *\n * This creates the HTML page that bootstraps the React editor application.\n * It includes:\n * - Meta tags for viewport and charset\n * - CSS for the editor\n * - React mount point\n * - JavaScript bundle\n *\n * @param basePath - Base path for the editor\n * @returns HTML string\n */\nfunction generateEditorHtml(basePath: string): string {\n return `<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <meta name=\"robots\" content=\"noindex, nofollow\">\n <title>Writenex - Content Editor</title>\n \n <!-- Editor styles -->\n <link rel=\"stylesheet\" href=\"${basePath}/assets/index.css\">\n <link rel=\"stylesheet\" href=\"${basePath}/assets/styles.css\">\n \n <style>\n /* Critical CSS for initial load */\n * {\n margin: 0;\n padding: 0;\n box-sizing: border-box;\n }\n \n html, body, #root {\n height: 100%;\n width: 100%;\n }\n \n body {\n font-family: 'Plus Jakarta Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;\n background-color: #0a0a0a;\n color: #fafafa;\n }\n \n /* Loading state */\n .writenex-loading {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n height: 100%;\n gap: 1rem;\n }\n \n .writenex-loading-spinner {\n width: 40px;\n height: 40px;\n border: 3px solid rgba(59, 130, 246, 0.2);\n border-top-color: #3b82f6;\n border-radius: 50%;\n animation: spin 1s linear infinite;\n }\n \n @keyframes spin {\n to { transform: rotate(360deg); }\n }\n \n .writenex-loading-text {\n color: #71717a;\n font-size: 0.875rem;\n }\n </style>\n</head>\n<body>\n <div id=\"root\">\n <!-- Loading state shown while React loads -->\n <div class=\"writenex-loading\">\n <div class=\"writenex-loading-spinner\"></div>\n <div class=\"writenex-loading-text\">Loading Writenex Editor...</div>\n </div>\n </div>\n \n <!-- Configuration for the client app -->\n <script>\n window.__WRITENEX_CONFIG__ = {\n basePath: \"${basePath}\",\n apiBase: \"${basePath}/api\",\n };\n </script>\n \n <!-- Editor application -->\n <script type=\"module\" src=\"${basePath}/assets/index.js\"></script>\n</body>\n</html>`;\n}\n\n/**\n * Get the path to bundled client assets\n *\n * @returns Path to the client dist directory\n */\nexport function getClientDistPath(): string {\n return join(PACKAGE_ROOT, \"dist\", \"client\");\n}\n\n/**\n * Check if client assets are bundled\n *\n * @returns True if bundled assets exist\n */\nexport function hasClientBundle(): boolean {\n const indexPath = join(getClientDistPath(), \"index.js\");\n return existsSync(indexPath);\n}\n","/**\n * @fileoverview API route handlers for Writenex\n *\n * This module provides the API router that handles CRUD operations\n * for content collections.\n *\n * ## API Endpoints:\n * - GET /api/collections - List all collections\n * - GET /api/content/:collection - List content in collection\n * - GET /api/content/:collection/:id - Get single content item\n * - POST /api/content/:collection - Create new content\n * - PUT /api/content/:collection/:id - Update content\n * - DELETE /api/content/:collection/:id - Delete content\n * - GET /api/images/:collection/:contentId - Discover images for content\n * - GET /api/images/:collection/:contentId/* - Serve image file\n * - POST /api/images - Upload image\n * - GET /api/versions/:collection/:id - List versions\n * - GET /api/versions/:collection/:id/:versionId - Get version\n * - POST /api/versions/:collection/:id - Create manual version\n * - POST /api/versions/:collection/:id/:versionId/restore - Restore version\n * - GET /api/versions/:collection/:id/:versionId/diff - Get diff data\n * - DELETE /api/versions/:collection/:id/:versionId - Delete version\n * - DELETE /api/versions/:collection/:id - Clear all versions\n *\n * @module @writenex/astro/server/routes\n */\n\nimport { createReadStream, existsSync, statSync } from \"node:fs\";\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport { extname, join } from \"node:path\";\nimport {\n ApiBadRequestError,\n ApiMethodNotAllowedError,\n CollectionDiscoveryError,\n CollectionNotFoundError,\n ContentNotFoundError,\n ImageInvalidTypeError,\n ImageNotFoundError,\n isWritenexError,\n PathTraversalError,\n VersionNotFoundError,\n WritenexErrorCode,\n wrapError,\n} from \"@/core/errors\";\nimport { discoverCollections, mergeCollections } from \"@/discovery/collections\";\nimport {\n discoverContentImages,\n isValidImageFile,\n parseMultipartFormData,\n uploadImage,\n} from \"@/filesystem/images\";\nimport { getCollectionSummaries, readContentFile } from \"@/filesystem/reader\";\nimport {\n clearVersions,\n deleteVersion,\n getVersion,\n getVersions,\n restoreVersion,\n saveVersion,\n} from \"@/filesystem/versions\";\nimport {\n createContent,\n deleteContent,\n getContentFilePath,\n updateContent,\n} from \"@/filesystem/writer\";\nimport type { VersionHistoryConfig } from \"@/types\";\nimport { getCache } from \"./cache\";\nimport type { MiddlewareContext } from \"./middleware\";\nimport {\n parseJsonBody,\n parseQueryParams,\n sendError,\n sendJson,\n sendWritenexError,\n} from \"./middleware\";\n\n/**\n * API route handler function type\n */\ntype RouteHandler = (\n req: IncomingMessage,\n res: ServerResponse,\n params: RouteParams,\n context: MiddlewareContext\n) => Promise<void>;\n\n/**\n * Route parameters extracted from URL\n */\ninterface RouteParams {\n collection?: string;\n id?: string;\n versionId?: string;\n query: Record<string, string>;\n}\n\n/**\n * Create the API router\n *\n * @param context - Middleware context\n * @returns Router function that handles API requests\n */\nexport function createApiRouter(\n context: MiddlewareContext\n): (req: IncomingMessage, res: ServerResponse, path: string) => Promise<void> {\n return async (req, res, path) => {\n const method = req.method?.toUpperCase() ?? \"GET\";\n const query = parseQueryParams(req.url ?? \"\");\n\n // Strip query string from path before parsing segments\n const pathWithoutQuery = path.split(\"?\")[0] ?? path;\n\n // Parse route segments\n const segments = pathWithoutQuery.split(\"/\").filter(Boolean);\n const params: RouteParams = { query };\n\n // Route: /collections\n if (segments[0] === \"collections\") {\n if (method === \"GET\") {\n return handleGetCollections(req, res, params, context);\n }\n return sendWritenexError(\n res,\n new ApiMethodNotAllowedError(method, [\"GET\"])\n );\n }\n\n // Route: /config or /config/path\n if (segments[0] === \"config\") {\n if (method === \"GET\") {\n if (segments[1] === \"path\") {\n return handleGetConfigPath(req, res, params, context);\n }\n return handleGetConfig(req, res, params, context);\n }\n return sendWritenexError(\n res,\n new ApiMethodNotAllowedError(method, [\"GET\"])\n );\n }\n\n // Route: /content/:collection/:id?\n if (segments[0] === \"content\") {\n params.collection = segments[1];\n params.id = segments[2];\n\n switch (method) {\n case \"GET\":\n if (params.id) {\n return handleGetContent(req, res, params, context);\n }\n return handleListContent(req, res, params, context);\n case \"POST\":\n return handleCreateContent(req, res, params, context);\n case \"PUT\":\n return handleUpdateContent(req, res, params, context);\n case \"DELETE\":\n return handleDeleteContent(req, res, params, context);\n default:\n return sendWritenexError(\n res,\n new ApiMethodNotAllowedError(method, [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"DELETE\",\n ])\n );\n }\n }\n\n // Route: /images/:collection/:contentId - Image discovery\n // Route: /images/:collection/:contentId/* - Serve image file\n if (segments[0] === \"images\") {\n params.collection = segments[1];\n params.id = segments[2];\n\n // Check if this is a file request (has more segments after contentId)\n if (\n method === \"GET\" &&\n params.collection &&\n params.id &&\n segments.length > 3\n ) {\n // Serve image file: /images/:collection/:contentId/path/to/image.jpg\n const imagePath = segments.slice(3).join(\"/\");\n return handleServeImage(req, res, params, imagePath, context);\n }\n\n if (method === \"GET\" && params.collection && params.id) {\n return handleImageDiscovery(req, res, params, context);\n }\n if (method === \"POST\") {\n return handleImageUpload(req, res, params, context);\n }\n return sendWritenexError(\n res,\n new ApiMethodNotAllowedError(method, [\"GET\", \"POST\"])\n );\n }\n\n // Route: /versions/:collection/:id/:versionId?\n if (segments[0] === \"versions\") {\n params.collection = segments[1];\n params.id = segments[2];\n params.versionId = segments[3];\n\n // Check for special action routes (restore, diff)\n const action = segments[4];\n\n switch (method) {\n case \"GET\":\n if (params.versionId) {\n // Check if this is a diff request\n if (action === \"diff\") {\n return handleGetVersionDiff(req, res, params, context);\n }\n return handleGetVersion(req, res, params, context);\n }\n return handleListVersions(req, res, params, context);\n case \"POST\":\n if (params.versionId && action === \"restore\") {\n return handleRestoreVersion(req, res, params, context);\n }\n if (!params.versionId) {\n return handleCreateVersion(req, res, params, context);\n }\n return sendWritenexError(\n res,\n new ApiMethodNotAllowedError(method, [\"GET\", \"POST\", \"DELETE\"])\n );\n case \"DELETE\":\n if (params.versionId) {\n return handleDeleteVersion(req, res, params, context);\n }\n return handleClearVersions(req, res, params, context);\n default:\n return sendWritenexError(\n res,\n new ApiMethodNotAllowedError(method, [\"GET\", \"POST\", \"DELETE\"])\n );\n }\n }\n\n // Route: /health (for testing)\n if (segments[0] === \"health\") {\n return sendJson(res, {\n status: \"ok\",\n timestamp: new Date().toISOString(),\n });\n }\n\n // Unknown route\n return sendError(res, \"Not found\", 404);\n };\n}\n\n/**\n * GET /api/config - Get current configuration\n *\n * Returns the current Writenex configuration including image settings\n * and Astro's trailingSlash setting for preview URLs.\n */\nconst handleGetConfig: RouteHandler = async (_req, res, _params, context) => {\n const { config, trailingSlash } = context;\n\n sendJson(res, {\n images: config.images,\n editor: config.editor,\n trailingSlash,\n });\n};\n\n/**\n * GET /api/config/path - Get config file path\n *\n * Returns the absolute path to the configuration file for opening in editor.\n * Also returns the project root for reference.\n */\nconst handleGetConfigPath: RouteHandler = async (\n _req,\n res,\n _params,\n context\n) => {\n const { projectRoot } = context;\n\n // Import findConfigFile from config loader\n const { findConfigFile } = await import(\"@/config/loader\");\n const configPath = findConfigFile(projectRoot);\n\n sendJson(res, {\n configPath,\n projectRoot,\n hasConfigFile: configPath !== null,\n });\n};\n\n/**\n * GET /api/collections - List all collections\n *\n * Returns discovered and configured collections with metadata.\n * Results are cached for performance.\n */\nconst handleGetCollections: RouteHandler = async (\n _req,\n res,\n _params,\n context\n) => {\n const { config, projectRoot } = context;\n const cache = getCache();\n\n try {\n // Try to get from cache first\n let collections = cache.getCollections();\n\n if (!collections) {\n // Cache miss - discover and merge collections\n const discovered = await discoverCollections(projectRoot);\n collections = mergeCollections(discovered, config.collections);\n\n // Store in cache\n cache.setCollections(collections);\n }\n\n sendJson(res, { collections });\n } catch (error) {\n const wrappedError = isWritenexError(error)\n ? error\n : new CollectionDiscoveryError(\n join(projectRoot, \"src/content\"),\n error instanceof Error ? error : undefined\n );\n sendWritenexError(res, wrappedError);\n }\n};\n\n/**\n * GET /api/content/:collection - List content in collection\n *\n * Query params:\n * - draft: Include drafts (default: false)\n * - sort: Sort field (default: pubDate)\n * - order: Sort order (asc/desc, default: desc)\n *\n * Results are cached for performance.\n */\nconst handleListContent: RouteHandler = async (_req, res, params, context) => {\n const { collection, query } = params;\n const { projectRoot } = context;\n\n if (!collection) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Collection name required\")\n );\n }\n\n const cache = getCache();\n\n try {\n const collectionPath = join(projectRoot, \"src/content\", collection);\n\n // Check if collection exists\n if (!existsSync(collectionPath)) {\n return sendWritenexError(res, new CollectionNotFoundError(collection));\n }\n\n // Parse query parameters\n const includeDrafts = query.draft === \"true\";\n const sortBy = query.sort ?? \"pubDate\";\n const sortOrder = (query.order as \"asc\" | \"desc\") ?? \"desc\";\n\n // Try to get from cache first (only for default queries)\n // We cache the \"all content\" query (includeDrafts=true, default sort)\n const isDefaultQuery =\n includeDrafts && sortBy === \"pubDate\" && sortOrder === \"desc\";\n\n let items = isDefaultQuery ? cache.getContent(collection) : null;\n\n if (!items) {\n items = await getCollectionSummaries(collectionPath, {\n includeDrafts,\n sortBy,\n sortOrder,\n });\n\n // Cache only the \"all content\" query\n if (isDefaultQuery) {\n cache.setContent(collection, items);\n }\n }\n\n sendJson(res, {\n items,\n total: items.length,\n });\n } catch (error) {\n console.error(\"[writenex] List content error:\", error);\n const wrappedError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);\n sendWritenexError(res, wrappedError);\n }\n};\n\n/**\n * GET /api/content/:collection/:id - Get single content item\n */\nconst handleGetContent: RouteHandler = async (_req, res, params, context) => {\n const { collection, id } = params;\n const { projectRoot } = context;\n\n if (!collection || !id) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Collection and content ID required\")\n );\n }\n\n try {\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const filePath = getContentFilePath(collectionPath, id);\n\n if (!filePath) {\n return sendWritenexError(res, new ContentNotFoundError(collection, id));\n }\n\n const result = await readContentFile(filePath, collectionPath);\n\n if (!result.success || !result.content) {\n return sendError(res, result.error ?? \"Failed to read content\", 500);\n }\n\n sendJson(res, result.content);\n } catch (error) {\n const wrappedError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);\n sendWritenexError(res, wrappedError);\n }\n};\n\n/**\n * POST /api/content/:collection - Create new content\n *\n * Automatically detects the file pattern from existing content in the collection\n * and creates new content following the same pattern.\n */\nconst handleCreateContent: RouteHandler = async (req, res, params, context) => {\n const { collection } = params;\n const { projectRoot, config } = context;\n\n if (!collection) {\n return sendError(res, \"Collection name required\", 400);\n }\n\n try {\n const body = await parseJsonBody(req);\n\n if (!body || typeof body !== \"object\") {\n return sendError(res, \"Invalid request body\", 400);\n }\n\n const {\n frontmatter,\n body: contentBody,\n slug,\n } = body as {\n frontmatter?: Record<string, unknown>;\n body?: string;\n slug?: string;\n };\n\n if (!frontmatter) {\n return sendError(res, \"Frontmatter is required\", 400);\n }\n\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const cache = getCache();\n\n // Get the file pattern for this collection\n let filePattern: string | undefined;\n\n // First, check if there's a configured pattern for this collection\n const configuredCollection = config.collections.find(\n (c) => c.name === collection\n );\n if (configuredCollection?.filePattern) {\n filePattern = configuredCollection.filePattern;\n } else {\n // Otherwise, get the detected pattern from discovered collections\n let collections = cache.getCollections();\n if (!collections) {\n const discovered = await discoverCollections(projectRoot);\n collections = mergeCollections(discovered, config.collections);\n cache.setCollections(collections);\n }\n\n const discoveredCollection = collections.find(\n (c) => c.name === collection\n );\n if (discoveredCollection?.filePattern) {\n filePattern = discoveredCollection.filePattern;\n }\n }\n\n const result = await createContent(collectionPath, {\n frontmatter,\n body: contentBody ?? \"\",\n slug,\n filePattern,\n });\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to create content\", 500);\n }\n\n // Invalidate cache for this collection (new content added)\n cache.handleFileChange(\"add\", collection);\n\n sendJson(res, {\n success: true,\n id: result.id,\n path: result.path,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to create content: ${message}`, 500);\n }\n};\n\n/**\n * PUT /api/content/:collection/:id - Update content\n *\n * Creates a version snapshot of the current content before updating\n * when version history is enabled in configuration.\n *\n * Supports conflict detection via expectedMtime parameter:\n * - If expectedMtime is provided and differs from current file mtime,\n * returns 409 Conflict with both versions for client-side resolution.\n *\n * Request body:\n * {\n * frontmatter?: Record<string, unknown>;\n * body?: string;\n * expectedMtime?: number; // For conflict detection\n * forceOverwrite?: boolean; // Skip conflict check (use with caution)\n * }\n *\n * Response on conflict (409):\n * {\n * error: string;\n * code: \"CONTENT_CONFLICT\";\n * serverContent: string;\n * serverMtime: number;\n * clientMtime: number;\n * }\n */\nconst handleUpdateContent: RouteHandler = async (req, res, params, context) => {\n const { collection, id } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Collection and content ID required\")\n );\n }\n\n try {\n const body = await parseJsonBody(req);\n\n if (!body || typeof body !== \"object\") {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Invalid request body\")\n );\n }\n\n const {\n frontmatter,\n body: contentBody,\n expectedMtime,\n forceOverwrite,\n } = body as {\n frontmatter?: Record<string, unknown>;\n body?: string;\n expectedMtime?: number;\n forceOverwrite?: boolean;\n };\n\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const filePath = getContentFilePath(collectionPath, id);\n\n if (!filePath) {\n return sendWritenexError(res, new ContentNotFoundError(collection, id));\n }\n\n // Pass version history config to updateContent for automatic version creation\n // Note: config.versionHistory is guaranteed to have all required fields\n // because applyConfigDefaults() applies DEFAULT_VERSION_HISTORY_CONFIG\n const result = await updateContent(filePath, collectionPath, {\n frontmatter,\n body: contentBody,\n projectRoot,\n collection,\n versionHistoryConfig: config.versionHistory as Required<\n typeof config.versionHistory\n >,\n // Only check mtime if not forcing overwrite\n expectedMtime: forceOverwrite ? undefined : expectedMtime,\n });\n\n // Handle conflict error specially\n if (!result.success && result.conflict) {\n return sendWritenexError(res, result.conflict);\n }\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to update content\", 500);\n }\n\n // Invalidate cache for this collection (content modified)\n const cache = getCache();\n cache.handleFileChange(\"change\", collection);\n\n sendJson(res, {\n success: true,\n id: result.id,\n path: result.path,\n mtime: result.mtime,\n });\n } catch (error) {\n const wrappedError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);\n sendWritenexError(res, wrappedError);\n }\n};\n\n/**\n * DELETE /api/content/:collection/:id - Delete content\n */\nconst handleDeleteContent: RouteHandler = async (\n _req,\n res,\n params,\n context\n) => {\n const { collection, id } = params;\n const { projectRoot } = context;\n\n if (!collection || !id) {\n return sendError(res, \"Collection and content ID required\", 400);\n }\n\n try {\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const filePath = getContentFilePath(collectionPath, id);\n\n if (!filePath) {\n return sendError(\n res,\n `Content '${id}' not found in '${collection}'`,\n 404\n );\n }\n\n const result = await deleteContent(filePath);\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to delete content\", 500);\n }\n\n // Invalidate cache for this collection (content removed)\n const cache = getCache();\n cache.handleFileChange(\"unlink\", collection);\n\n sendJson(res, {\n success: true,\n path: result.path,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to delete content: ${message}`, 500);\n }\n};\n\n/**\n * POST /api/images - Upload image\n *\n * Expects multipart/form-data with:\n * - file: The image file\n * - collection: Collection name\n * - contentId: Content ID (slug)\n */\nconst handleImageUpload: RouteHandler = async (req, res, _params, context) => {\n const { projectRoot, config } = context;\n\n try {\n // Read raw body\n const chunks: Buffer[] = [];\n for await (const chunk of req) {\n chunks.push(chunk);\n }\n const body = Buffer.concat(chunks);\n\n // Get content type\n const contentType = req.headers[\"content-type\"] ?? \"\";\n\n if (!contentType.includes(\"multipart/form-data\")) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Content-Type must be multipart/form-data\")\n );\n }\n\n // Parse multipart data\n const { file, fields } = parseMultipartFormData(body, contentType);\n\n if (!file) {\n return sendWritenexError(res, new ApiBadRequestError(\"No file uploaded\"));\n }\n\n if (!fields.collection || !fields.contentId) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"collection and contentId are required\")\n );\n }\n\n if (!isValidImageFile(file.filename)) {\n return sendWritenexError(\n res,\n new ImageInvalidTypeError(file.filename, [\n \".jpg\",\n \".jpeg\",\n \".png\",\n \".gif\",\n \".webp\",\n \".avif\",\n \".svg\",\n ])\n );\n }\n\n // Upload image\n const result = await uploadImage({\n filename: file.filename,\n data: file.data,\n collection: fields.collection,\n contentId: fields.contentId,\n projectRoot,\n config: config.images,\n });\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to upload image\", 500);\n }\n\n sendJson(res, {\n success: true,\n path: result.path,\n url: result.url,\n });\n } catch (error) {\n const wrappedError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.IMAGE_UPLOAD_ERROR);\n sendWritenexError(res, wrappedError);\n }\n};\n\n/**\n * GET /api/images/:collection/:contentId - Discover images for content\n *\n * Returns list of discovered images for a content item.\n * Results are cached for performance.\n *\n * Response:\n * {\n * success: boolean;\n * images: DiscoveredImage[];\n * contentPath: string;\n * }\n */\nconst handleImageDiscovery: RouteHandler = async (\n _req,\n res,\n params,\n context\n) => {\n const { collection, id: contentId } = params;\n const { projectRoot } = context;\n\n if (!collection || !contentId) {\n return sendError(res, \"Collection and content ID required\", 400);\n }\n\n const cache = getCache();\n\n try {\n const collectionPath = join(projectRoot, \"src/content\", collection);\n\n // Check if collection exists by discovering collections\n let collections = cache.getCollections();\n if (!collections) {\n // Cache miss - discover collections\n collections = await discoverCollections(projectRoot);\n cache.setCollections(collections);\n }\n\n if (!collections.some((c) => c.name === collection)) {\n return sendError(res, `Collection '${collection}' not found`, 404);\n }\n\n // Check if content exists\n const contentFilePath = getContentFilePath(collectionPath, contentId);\n if (!contentFilePath) {\n return sendError(\n res,\n `Content '${contentId}' not found in '${collection}'`,\n 404\n );\n }\n\n // Try to get from cache first\n let images = cache.getImages(collection, contentId);\n\n if (!images) {\n // Cache miss - discover images\n const result = await discoverContentImages(collectionPath, contentId);\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to discover images\", 500);\n }\n\n images = result.images;\n\n // Store in cache\n cache.setImages(collection, contentId, images);\n }\n\n sendJson(res, {\n success: true,\n images,\n contentPath: contentFilePath,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to discover images: ${message}`, 500);\n }\n};\n\n/**\n * MIME types for image files\n */\nconst IMAGE_MIME_TYPES: Record<string, string> = {\n \".jpg\": \"image/jpeg\",\n \".jpeg\": \"image/jpeg\",\n \".png\": \"image/png\",\n \".gif\": \"image/gif\",\n \".webp\": \"image/webp\",\n \".avif\": \"image/avif\",\n \".svg\": \"image/svg+xml\",\n};\n\n/**\n * GET /api/images/:collection/:contentId/* - Serve image file\n *\n * Serves an image file from the content folder.\n * This allows the editor to display images with relative paths.\n */\nconst handleServeImage = async (\n _req: IncomingMessage,\n res: ServerResponse,\n params: RouteParams,\n imagePath: string,\n context: MiddlewareContext\n): Promise<void> => {\n const { collection, id: contentId } = params;\n const { projectRoot } = context;\n\n if (!collection || !contentId) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Collection and content ID required\")\n );\n }\n\n try {\n const collectionPath = join(projectRoot, \"src/content\", collection);\n\n // Check if content exists\n const contentFilePath = getContentFilePath(collectionPath, contentId);\n if (!contentFilePath) {\n return sendWritenexError(\n res,\n new ContentNotFoundError(collection, contentId)\n );\n }\n\n // Build the full image path\n // For folder-based content (index.md), images are in the same folder\n // For flat files (slug.md), images are in a sibling folder with the same name\n let fullImagePath: string;\n\n if (\n contentFilePath.endsWith(\"/index.md\") ||\n contentFilePath.endsWith(\"/index.mdx\")\n ) {\n // Folder-based: content is at slug/index.md, images are at slug/imagePath\n const contentFolder = contentFilePath.replace(/\\/index\\.mdx?$/, \"\");\n fullImagePath = join(contentFolder, imagePath);\n } else {\n // Flat file: content is at slug.md, images are at slug/imagePath\n fullImagePath = join(collectionPath, contentId, imagePath);\n }\n\n // Security check: ensure the path is within the content folder\n const normalizedPath = join(fullImagePath);\n if (!normalizedPath.startsWith(collectionPath)) {\n return sendWritenexError(\n res,\n new PathTraversalError(imagePath, collectionPath)\n );\n }\n\n // Check if file exists\n if (!existsSync(fullImagePath)) {\n return sendWritenexError(res, new ImageNotFoundError(imagePath));\n }\n\n // Get file stats\n const stats = statSync(fullImagePath);\n if (!stats.isFile()) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Requested path is not a file\")\n );\n }\n\n // Determine MIME type\n const ext = extname(fullImagePath).toLowerCase();\n const mimeType = IMAGE_MIME_TYPES[ext] ?? \"application/octet-stream\";\n\n // Set headers\n res.setHeader(\"Content-Type\", mimeType);\n res.setHeader(\"Content-Length\", stats.size);\n res.setHeader(\"Cache-Control\", \"public, max-age=3600\");\n\n // Stream the file\n const stream = createReadStream(fullImagePath);\n stream.pipe(res);\n } catch (error) {\n const wrappedError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);\n sendWritenexError(res, wrappedError);\n }\n};\n\n// =============================================================================\n// Version History Route Handlers\n// =============================================================================\n\n/**\n * Get the resolved version history config with all required fields\n *\n * @param config - The version history config from context\n * @returns Resolved config with all required fields\n */\nfunction getResolvedVersionConfig(\n config: VersionHistoryConfig | undefined\n): Required<VersionHistoryConfig> {\n return {\n enabled: config?.enabled ?? true,\n maxVersions: config?.maxVersions ?? 20,\n storagePath: config?.storagePath ?? \".writenex/versions\",\n };\n}\n\n/**\n * GET /api/versions/:collection/:id - List all versions\n *\n * Returns versions sorted by timestamp in descending order (newest first).\n *\n * Response:\n * {\n * success: boolean;\n * versions: VersionEntry[];\n * total: number;\n * }\n */\nconst handleListVersions: RouteHandler = async (_req, res, params, context) => {\n const { collection, id } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id) {\n return sendError(res, \"Collection and content ID required\", 400);\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n const versions = await getVersions(\n projectRoot,\n collection,\n id,\n versionConfig\n );\n\n sendJson(res, {\n success: true,\n versions,\n total: versions.length,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to list versions: ${message}`, 500);\n }\n};\n\n/**\n * GET /api/versions/:collection/:id/:versionId - Get specific version\n *\n * Returns the full content of a specific version.\n *\n * Response:\n * {\n * success: boolean;\n * version: Version;\n * }\n */\nconst handleGetVersion: RouteHandler = async (_req, res, params, context) => {\n const { collection, id, versionId } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id || !versionId) {\n return sendWritenexError(\n res,\n new ApiBadRequestError(\"Collection, content ID, and version ID required\")\n );\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n const version = await getVersion(\n projectRoot,\n collection,\n id,\n versionId,\n versionConfig\n );\n\n if (!version) {\n return sendWritenexError(\n res,\n new VersionNotFoundError(collection, id, versionId)\n );\n }\n\n sendJson(res, {\n success: true,\n version,\n });\n } catch (error) {\n const wrappedError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);\n sendWritenexError(res, wrappedError);\n }\n};\n\n/**\n * POST /api/versions/:collection/:id - Create manual version\n *\n * Creates a manual version snapshot with optional label.\n *\n * Request body:\n * {\n * label?: string;\n * }\n *\n * Response:\n * {\n * success: boolean;\n * version?: VersionEntry;\n * }\n */\nconst handleCreateVersion: RouteHandler = async (req, res, params, context) => {\n const { collection, id } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id) {\n return sendError(res, \"Collection and content ID required\", 400);\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n // Check if version history is enabled\n if (!versionConfig.enabled) {\n return sendError(res, \"Version history is disabled\", 400);\n }\n\n // Parse request body for optional label\n const body = await parseJsonBody(req);\n const label =\n body && typeof body === \"object\" && \"label\" in body\n ? String(body.label)\n : undefined;\n\n // Get current content\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const filePath = getContentFilePath(collectionPath, id);\n\n if (!filePath) {\n return sendError(\n res,\n `Content '${id}' not found in '${collection}'`,\n 404\n );\n }\n\n // Read current content\n const readResult = await readContentFile(filePath, collectionPath);\n\n if (!readResult.success || !readResult.content) {\n return sendError(res, readResult.error ?? \"Failed to read content\", 500);\n }\n\n // Create version\n const result = await saveVersion(\n projectRoot,\n collection,\n id,\n readResult.content.raw,\n versionConfig,\n { label }\n );\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to create version\", 500);\n }\n\n sendJson(res, {\n success: true,\n version: result.version,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to create version: ${message}`, 500);\n }\n};\n\n/**\n * POST /api/versions/:collection/:id/:versionId/restore - Restore version\n *\n * Restores a version to the current content file.\n * Creates a safety snapshot before restoring.\n *\n * Response:\n * {\n * success: boolean;\n * version?: VersionEntry;\n * content?: string;\n * safetySnapshot?: VersionEntry;\n * }\n */\nconst handleRestoreVersion: RouteHandler = async (\n _req,\n res,\n params,\n context\n) => {\n const { collection, id, versionId } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id || !versionId) {\n return sendError(\n res,\n \"Collection, content ID, and version ID required\",\n 400\n );\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n // Get content file path\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const filePath = getContentFilePath(collectionPath, id);\n\n if (!filePath) {\n return sendError(\n res,\n `Content '${id}' not found in '${collection}'`,\n 404\n );\n }\n\n // Restore version\n const result = await restoreVersion(\n projectRoot,\n collection,\n id,\n versionId,\n filePath,\n versionConfig\n );\n\n if (!result.success) {\n // Check if it's a not found error\n if (result.error?.includes(\"not found\")) {\n return sendError(res, result.error, 404);\n }\n return sendError(res, result.error ?? \"Failed to restore version\", 500);\n }\n\n // Invalidate cache for this collection (content modified)\n const cache = getCache();\n cache.handleFileChange(\"change\", collection);\n\n sendJson(res, {\n success: true,\n version: result.version,\n content: result.content,\n safetySnapshot: result.safetySnapshot,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to restore version: ${message}`, 500);\n }\n};\n\n/**\n * GET /api/versions/:collection/:id/:versionId/diff - Get diff data\n *\n * Returns both the version content and current content for comparison.\n *\n * Response:\n * {\n * success: boolean;\n * version: Version;\n * current: {\n * content: string;\n * frontmatter: Record<string, unknown>;\n * body: string;\n * };\n * }\n */\nconst handleGetVersionDiff: RouteHandler = async (\n _req,\n res,\n params,\n context\n) => {\n const { collection, id, versionId } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id || !versionId) {\n return sendError(\n res,\n \"Collection, content ID, and version ID required\",\n 400\n );\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n // Get version content\n const version = await getVersion(\n projectRoot,\n collection,\n id,\n versionId,\n versionConfig\n );\n\n if (!version) {\n return sendError(res, `Version '${versionId}' not found`, 404);\n }\n\n // Get current content\n const collectionPath = join(projectRoot, \"src/content\", collection);\n const filePath = getContentFilePath(collectionPath, id);\n\n if (!filePath) {\n return sendError(\n res,\n `Content '${id}' not found in '${collection}'`,\n 404\n );\n }\n\n const readResult = await readContentFile(filePath, collectionPath);\n\n if (!readResult.success || !readResult.content) {\n return sendError(\n res,\n readResult.error ?? \"Failed to read current content\",\n 500\n );\n }\n\n sendJson(res, {\n success: true,\n version,\n current: {\n content: readResult.content.raw,\n frontmatter: readResult.content.frontmatter,\n body: readResult.content.body,\n },\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to get diff data: ${message}`, 500);\n }\n};\n\n/**\n * DELETE /api/versions/:collection/:id/:versionId - Delete specific version\n *\n * Deletes a specific version file and removes it from the manifest.\n *\n * Response:\n * {\n * success: boolean;\n * version?: VersionEntry;\n * }\n */\nconst handleDeleteVersion: RouteHandler = async (\n _req,\n res,\n params,\n context\n) => {\n const { collection, id, versionId } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id || !versionId) {\n return sendError(\n res,\n \"Collection, content ID, and version ID required\",\n 400\n );\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n const result = await deleteVersion(\n projectRoot,\n collection,\n id,\n versionId,\n versionConfig\n );\n\n if (!result.success) {\n // Check if it's a not found error\n if (result.error?.includes(\"not found\")) {\n return sendError(res, result.error, 404);\n }\n return sendError(res, result.error ?? \"Failed to delete version\", 500);\n }\n\n sendJson(res, {\n success: true,\n version: result.version,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to delete version: ${message}`, 500);\n }\n};\n\n/**\n * DELETE /api/versions/:collection/:id - Clear all versions\n *\n * Deletes all version files for a content item and resets the manifest.\n *\n * Response:\n * {\n * success: boolean;\n * }\n */\nconst handleClearVersions: RouteHandler = async (\n _req,\n res,\n params,\n context\n) => {\n const { collection, id } = params;\n const { projectRoot, config } = context;\n\n if (!collection || !id) {\n return sendError(res, \"Collection and content ID required\", 400);\n }\n\n try {\n const versionConfig = getResolvedVersionConfig(config.versionHistory);\n\n const result = await clearVersions(\n projectRoot,\n collection,\n id,\n versionConfig\n );\n\n if (!result.success) {\n return sendError(res, result.error ?? \"Failed to clear versions\", 500);\n }\n\n sendJson(res, {\n success: true,\n });\n } catch (error) {\n const message = error instanceof Error ? error.message : \"Unknown error\";\n sendError(res, `Failed to clear versions: ${message}`, 500);\n }\n};\n","/**\n * @fileoverview Vite middleware for Writenex routes\n *\n * This module provides the main middleware handler that intercepts requests\n * to Writenex routes (/_writenex/*) and delegates to appropriate handlers.\n *\n * ## Route Structure:\n * - `/_writenex` - Editor UI (HTML page with React app)\n * - `/_writenex/api/*` - API endpoints for CRUD operations\n * - `/_writenex/assets/*` - Static assets (JS, CSS)\n *\n * @module @writenex/astro/server/middleware\n */\n\nimport type { IncomingMessage, ServerResponse } from \"node:http\";\nimport type { Connect } from \"vite\";\nimport type { WritenexError } from \"@/core/errors\";\nimport { isWritenexError, WritenexErrorCode, wrapError } from \"@/core/errors\";\nimport type { WritenexConfig } from \"@/types\";\nimport { serveAsset, serveEditorHtml } from \"./assets\";\nimport { createApiRouter } from \"./routes\";\n\n/**\n * Middleware context passed to handlers\n */\nexport interface MiddlewareContext {\n /** Base path for Writenex routes */\n basePath: string;\n /** Project root directory */\n projectRoot: string;\n /** Resolved Writenex configuration */\n config: Required<WritenexConfig>;\n /** Astro trailingSlash setting for preview URLs */\n trailingSlash: \"always\" | \"never\" | \"ignore\";\n}\n\n/**\n * Create the Writenex middleware handler\n *\n * @param context - Middleware context with configuration\n * @returns Connect middleware function\n *\n * @example\n * ```typescript\n * const middleware = createMiddleware({\n * basePath: '/_writenex',\n * projectRoot: '/path/to/project',\n * config: resolvedConfig,\n * });\n *\n * server.middlewares.use(middleware);\n * ```\n */\nexport function createMiddleware(\n context: MiddlewareContext\n): Connect.NextHandleFunction {\n const { basePath } = context;\n const apiRouter = createApiRouter(context);\n\n return async (\n req: IncomingMessage,\n res: ServerResponse,\n next: Connect.NextFunction\n ) => {\n const url = req.url ?? \"\";\n\n // Only handle requests to our base path\n if (!url.startsWith(basePath)) {\n return next();\n }\n\n // Extract the path after base path\n const path = url.slice(basePath.length) || \"/\";\n\n try {\n // Handle API routes\n if (path.startsWith(\"/api/\")) {\n return await apiRouter(req, res, path.slice(4)); // Remove '/api' prefix\n }\n\n // Handle static assets\n if (path.startsWith(\"/assets/\")) {\n return await serveAsset(req, res, path.slice(8), context); // Remove '/assets' prefix\n }\n\n // Handle editor UI (root and any sub-routes for client-side routing)\n return await serveEditorHtml(req, res, context);\n } catch (error) {\n // Handle errors gracefully using WritenexError\n const writenexError = isWritenexError(error)\n ? error\n : wrapError(error, WritenexErrorCode.API_INTERNAL_ERROR);\n\n console.error(\n `[writenex] Middleware error [${writenexError.code}]: ${writenexError.message}`\n );\n\n res.statusCode = writenexError.httpStatus;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(writenexError.toJSON()));\n }\n };\n}\n\n/**\n * Parse URL query parameters\n *\n * @param url - The URL string to parse\n * @returns Object with query parameters\n */\nexport function parseQueryParams(url: string): Record<string, string> {\n const queryIndex = url.indexOf(\"?\");\n if (queryIndex === -1) return {};\n\n const queryString = url.slice(queryIndex + 1);\n const params: Record<string, string> = {};\n\n for (const pair of queryString.split(\"&\")) {\n const [key, value] = pair.split(\"=\");\n if (key) {\n params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : \"\";\n }\n }\n\n return params;\n}\n\n/**\n * Parse request body as JSON\n *\n * @param req - The incoming request\n * @returns Parsed JSON body or null\n */\nexport async function parseJsonBody(\n req: IncomingMessage\n): Promise<unknown | null> {\n return new Promise((resolve) => {\n let body = \"\";\n\n req.on(\"data\", (chunk: Buffer) => {\n body += chunk.toString();\n });\n\n req.on(\"end\", () => {\n if (!body) {\n resolve(null);\n return;\n }\n\n try {\n resolve(JSON.parse(body));\n } catch {\n resolve(null);\n }\n });\n\n req.on(\"error\", () => {\n resolve(null);\n });\n });\n}\n\n/**\n * Send JSON response\n *\n * @param res - The server response\n * @param data - Data to send as JSON\n * @param statusCode - HTTP status code (default: 200)\n */\nexport function sendJson(\n res: ServerResponse,\n data: unknown,\n statusCode: number = 200\n): void {\n res.statusCode = statusCode;\n res.setHeader(\"Content-Type\", \"application/json\");\n res.end(JSON.stringify(data));\n}\n\n/**\n * Send error response\n *\n * @param res - The server response\n * @param message - Error message\n * @param statusCode - HTTP status code (default: 400)\n */\nexport function sendError(\n res: ServerResponse,\n message: string,\n statusCode: number = 400\n): void {\n sendJson(res, { error: message }, statusCode);\n}\n\n/**\n * Send WritenexError response\n *\n * Automatically uses the error's HTTP status code and formats\n * the response using the error's toJSON method.\n *\n * @param res - The server response\n * @param error - WritenexError instance\n */\nexport function sendWritenexError(\n res: ServerResponse,\n error: WritenexError\n): void {\n sendJson(res, error.toJSON(), error.httpStatus);\n}\n"],"mappings":"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","names":["existsSync","extname","join","join","existsSync","extname"]}
@@ -1,7 +1,7 @@
1
1
  // src/filesystem/watcher.ts
2
- import { watch } from "chokidar";
3
2
  import { stat } from "fs/promises";
4
3
  import { join } from "path";
4
+ import { watch } from "chokidar";
5
5
  var ContentWatcher = class {
6
6
  watcher = null;
7
7
  projectRoot;
@@ -142,4 +142,4 @@ export {
142
142
  FileModificationTracker,
143
143
  createContentWatcher
144
144
  };
145
- //# sourceMappingURL=chunk-XNTQTTJU.js.map
145
+ //# sourceMappingURL=chunk-JFQQJPDF.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/filesystem/watcher.ts"],"sourcesContent":["/**\n * @fileoverview File watcher for detecting external changes\n *\n * This module provides file watching capabilities to detect when\n * content files are modified outside of the Writenex editor\n * (e.g., in VS Code or another editor).\n *\n * @module @writenex/astro/filesystem/watcher\n */\n\nimport { watch, type FSWatcher } from \"chokidar\";\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\n\n/**\n * File change event types\n */\nexport type FileChangeType = \"add\" | \"change\" | \"unlink\";\n\n/**\n * File change event\n */\nexport interface FileChangeEvent {\n type: FileChangeType;\n path: string;\n collection: string;\n}\n\n/**\n * Watcher options\n */\nexport interface WatcherOptions {\n /** Callback when a file changes */\n onChange?: (event: FileChangeEvent) => void;\n /** Debounce delay in milliseconds */\n debounceMs?: number;\n /** Patterns to ignore */\n ignored?: string[];\n}\n\n/**\n * Content file watcher\n *\n * Watches the src/content directory for changes and emits events\n * when files are added, modified, or deleted.\n */\nexport class ContentWatcher {\n private watcher: FSWatcher | null = null;\n private projectRoot: string;\n private contentDir: string;\n private options: WatcherOptions;\n private debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\n constructor(\n projectRoot: string,\n contentDir: string = \"src/content\",\n options: WatcherOptions = {}\n ) {\n this.projectRoot = projectRoot;\n this.contentDir = contentDir;\n this.options = {\n debounceMs: 100,\n ignored: [\"**/node_modules/**\", \"**/.git/**\"],\n ...options,\n };\n }\n\n /**\n * Start watching for file changes\n */\n start(): void {\n if (this.watcher) {\n return; // Already watching\n }\n\n const watchPath = join(this.projectRoot, this.contentDir);\n\n this.watcher = watch(watchPath, {\n ignored: this.options.ignored,\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n });\n\n this.watcher\n .on(\"add\", (path) => this.handleChange(\"add\", path))\n .on(\"change\", (path) => this.handleChange(\"change\", path))\n .on(\"unlink\", (path) => this.handleChange(\"unlink\", path))\n .on(\"error\", (error) => {\n console.error(\"[writenex] Watcher error:\", error);\n });\n }\n\n /**\n * Stop watching for file changes\n */\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n\n // Clear all debounce timers\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n }\n\n /**\n * Handle a file change event\n */\n private handleChange(type: FileChangeType, filePath: string): void {\n // Only handle markdown files\n if (!filePath.endsWith(\".md\") && !filePath.endsWith(\".mdx\")) {\n return;\n }\n\n // Debounce rapid changes\n const existingTimer = this.debounceTimers.get(filePath);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n\n const timer = setTimeout(() => {\n this.debounceTimers.delete(filePath);\n this.emitChange(type, filePath);\n }, this.options.debounceMs);\n\n this.debounceTimers.set(filePath, timer);\n }\n\n /**\n * Emit a file change event\n */\n private emitChange(type: FileChangeType, filePath: string): void {\n if (!this.options.onChange) {\n return;\n }\n\n // Extract collection name from path\n const contentPath = join(this.projectRoot, this.contentDir);\n const relativePath = filePath\n .replace(contentPath, \"\")\n .replace(/^[/\\\\]/, \"\");\n const parts = relativePath.split(/[/\\\\]/);\n const collection = parts[0] ?? \"\";\n\n this.options.onChange({\n type,\n path: filePath,\n collection,\n });\n }\n\n /**\n * Check if the watcher is running\n */\n isWatching(): boolean {\n return this.watcher !== null;\n }\n}\n\n/**\n * Track file modification times for conflict detection\n */\nexport class FileModificationTracker {\n private mtimes: Map<string, number> = new Map();\n\n /**\n * Record the current modification time of a file\n */\n async track(filePath: string): Promise<void> {\n try {\n const stats = await stat(filePath);\n this.mtimes.set(filePath, stats.mtimeMs);\n } catch {\n // File might not exist yet\n this.mtimes.delete(filePath);\n }\n }\n\n /**\n * Check if a file has been modified externally\n */\n async hasExternalChanges(filePath: string): Promise<boolean> {\n const lastKnown = this.mtimes.get(filePath);\n if (lastKnown === undefined) {\n return false; // Not tracked, assume no changes\n }\n\n try {\n const stats = await stat(filePath);\n return stats.mtimeMs > lastKnown;\n } catch {\n return true; // File might have been deleted\n }\n }\n\n /**\n * Clear tracking for a file\n */\n untrack(filePath: string): void {\n this.mtimes.delete(filePath);\n }\n\n /**\n * Clear all tracking\n */\n clear(): void {\n this.mtimes.clear();\n }\n}\n\n/**\n * Create a content watcher instance\n */\nexport function createContentWatcher(\n projectRoot: string,\n options?: WatcherOptions\n): ContentWatcher {\n return new ContentWatcher(projectRoot, \"src/content\", options);\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/filesystem/watcher.ts"],"sourcesContent":["/**\n * @fileoverview File watcher for detecting external changes\n *\n * This module provides file watching capabilities to detect when\n * content files are modified outside of the Writenex editor\n * (e.g., in VS Code or another editor).\n *\n * @module @writenex/astro/filesystem/watcher\n */\n\nimport { stat } from \"node:fs/promises\";\nimport { join } from \"node:path\";\nimport { type FSWatcher, watch } from \"chokidar\";\n\n/**\n * File change event types\n */\nexport type FileChangeType = \"add\" | \"change\" | \"unlink\";\n\n/**\n * File change event\n */\nexport interface FileChangeEvent {\n type: FileChangeType;\n path: string;\n collection: string;\n}\n\n/**\n * Watcher options\n */\nexport interface WatcherOptions {\n /** Callback when a file changes */\n onChange?: (event: FileChangeEvent) => void;\n /** Debounce delay in milliseconds */\n debounceMs?: number;\n /** Patterns to ignore */\n ignored?: string[];\n}\n\n/**\n * Content file watcher\n *\n * Watches the src/content directory for changes and emits events\n * when files are added, modified, or deleted.\n */\nexport class ContentWatcher {\n private watcher: FSWatcher | null = null;\n private projectRoot: string;\n private contentDir: string;\n private options: WatcherOptions;\n private debounceTimers: Map<string, NodeJS.Timeout> = new Map();\n\n constructor(\n projectRoot: string,\n contentDir: string = \"src/content\",\n options: WatcherOptions = {}\n ) {\n this.projectRoot = projectRoot;\n this.contentDir = contentDir;\n this.options = {\n debounceMs: 100,\n ignored: [\"**/node_modules/**\", \"**/.git/**\"],\n ...options,\n };\n }\n\n /**\n * Start watching for file changes\n */\n start(): void {\n if (this.watcher) {\n return; // Already watching\n }\n\n const watchPath = join(this.projectRoot, this.contentDir);\n\n this.watcher = watch(watchPath, {\n ignored: this.options.ignored,\n persistent: true,\n ignoreInitial: true,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n });\n\n this.watcher\n .on(\"add\", (path) => this.handleChange(\"add\", path))\n .on(\"change\", (path) => this.handleChange(\"change\", path))\n .on(\"unlink\", (path) => this.handleChange(\"unlink\", path))\n .on(\"error\", (error) => {\n console.error(\"[writenex] Watcher error:\", error);\n });\n }\n\n /**\n * Stop watching for file changes\n */\n async stop(): Promise<void> {\n if (this.watcher) {\n await this.watcher.close();\n this.watcher = null;\n }\n\n // Clear all debounce timers\n for (const timer of this.debounceTimers.values()) {\n clearTimeout(timer);\n }\n this.debounceTimers.clear();\n }\n\n /**\n * Handle a file change event\n */\n private handleChange(type: FileChangeType, filePath: string): void {\n // Only handle markdown files\n if (!filePath.endsWith(\".md\") && !filePath.endsWith(\".mdx\")) {\n return;\n }\n\n // Debounce rapid changes\n const existingTimer = this.debounceTimers.get(filePath);\n if (existingTimer) {\n clearTimeout(existingTimer);\n }\n\n const timer = setTimeout(() => {\n this.debounceTimers.delete(filePath);\n this.emitChange(type, filePath);\n }, this.options.debounceMs);\n\n this.debounceTimers.set(filePath, timer);\n }\n\n /**\n * Emit a file change event\n */\n private emitChange(type: FileChangeType, filePath: string): void {\n if (!this.options.onChange) {\n return;\n }\n\n // Extract collection name from path\n const contentPath = join(this.projectRoot, this.contentDir);\n const relativePath = filePath\n .replace(contentPath, \"\")\n .replace(/^[/\\\\]/, \"\");\n const parts = relativePath.split(/[/\\\\]/);\n const collection = parts[0] ?? \"\";\n\n this.options.onChange({\n type,\n path: filePath,\n collection,\n });\n }\n\n /**\n * Check if the watcher is running\n */\n isWatching(): boolean {\n return this.watcher !== null;\n }\n}\n\n/**\n * Track file modification times for conflict detection\n */\nexport class FileModificationTracker {\n private mtimes: Map<string, number> = new Map();\n\n /**\n * Record the current modification time of a file\n */\n async track(filePath: string): Promise<void> {\n try {\n const stats = await stat(filePath);\n this.mtimes.set(filePath, stats.mtimeMs);\n } catch {\n // File might not exist yet\n this.mtimes.delete(filePath);\n }\n }\n\n /**\n * Check if a file has been modified externally\n */\n async hasExternalChanges(filePath: string): Promise<boolean> {\n const lastKnown = this.mtimes.get(filePath);\n if (lastKnown === undefined) {\n return false; // Not tracked, assume no changes\n }\n\n try {\n const stats = await stat(filePath);\n return stats.mtimeMs > lastKnown;\n } catch {\n return true; // File might have been deleted\n }\n }\n\n /**\n * Clear tracking for a file\n */\n untrack(filePath: string): void {\n this.mtimes.delete(filePath);\n }\n\n /**\n * Clear all tracking\n */\n clear(): void {\n this.mtimes.clear();\n }\n}\n\n/**\n * Create a content watcher instance\n */\nexport function createContentWatcher(\n projectRoot: string,\n options?: WatcherOptions\n): ContentWatcher {\n return new ContentWatcher(projectRoot, \"src/content\", options);\n}\n"],"mappings":"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","names":[]}