@imjp/writenex-astro 1.3.0 → 1.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-OWYFIQFK.js → chunk-4H63L4YO.js} +2 -2
- package/dist/{chunk-TQAYIZOA.js → chunk-GYAFIVVI.js} +2 -2
- package/dist/chunk-GYAFIVVI.js.map +1 -0
- package/dist/{chunk-GIS7XEJF.js → chunk-JMNCPNQX.js} +2 -2
- package/dist/{chunk-S2OUQLMK.js → chunk-N37EPLKG.js} +7 -1
- package/dist/chunk-N37EPLKG.js.map +1 -0
- package/dist/{chunk-GUUSVFBP.js → chunk-NSW7AIVF.js} +5 -5
- package/dist/config/index.js +1 -1
- package/dist/discovery/index.js +2 -2
- package/dist/filesystem/index.js +2 -2
- package/dist/index.js +6 -6
- package/dist/index.js.map +1 -1
- package/dist/{loader-VGNXC2XJ.js → loader-53VVP2IN.js} +2 -2
- package/dist/server/index.js +4 -4
- package/package.json +1 -1
- package/src/config/loader.ts +18 -0
- package/src/filesystem/reader.ts +3 -1
- package/src/integration.ts +4 -1
- package/dist/chunk-S2OUQLMK.js.map +0 -1
- package/dist/chunk-TQAYIZOA.js.map +0 -1
- /package/dist/{chunk-OWYFIQFK.js.map → chunk-4H63L4YO.js.map} +0 -0
- /package/dist/{chunk-GIS7XEJF.js.map → chunk-JMNCPNQX.js.map} +0 -0
- /package/dist/{chunk-GUUSVFBP.js.map → chunk-NSW7AIVF.js.map} +0 -0
- /package/dist/{loader-VGNXC2XJ.js.map → loader-53VVP2IN.js.map} +0 -0
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
isValidPattern,
|
|
5
5
|
readContentFile,
|
|
6
6
|
resolvePatternTokens
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-GYAFIVVI.js";
|
|
8
8
|
|
|
9
9
|
// src/core/errors.ts
|
|
10
10
|
var WritenexErrorCode = /* @__PURE__ */ ((WritenexErrorCode2) => {
|
|
@@ -1407,4 +1407,4 @@ export {
|
|
|
1407
1407
|
updateContent,
|
|
1408
1408
|
deleteContent
|
|
1409
1409
|
};
|
|
1410
|
-
//# sourceMappingURL=chunk-
|
|
1410
|
+
//# sourceMappingURL=chunk-4H63L4YO.js.map
|
|
@@ -14,7 +14,7 @@ function extractSlug(filePath, collectionPath) {
|
|
|
14
14
|
const filename = basename(relativePath);
|
|
15
15
|
const ext = extname(filename);
|
|
16
16
|
if (filename === "index.md" || filename === "index.mdx") {
|
|
17
|
-
const parts = relativePath.split("/");
|
|
17
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
18
18
|
if (parts.length >= 2) {
|
|
19
19
|
const slug = parts[parts.length - 2];
|
|
20
20
|
if (slug) return slug;
|
|
@@ -571,4 +571,4 @@ export {
|
|
|
571
571
|
isValidPattern,
|
|
572
572
|
getSupportedTokens
|
|
573
573
|
};
|
|
574
|
-
//# sourceMappingURL=chunk-
|
|
574
|
+
//# sourceMappingURL=chunk-GYAFIVVI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/filesystem/reader.ts","../src/discovery/patterns.ts"],"sourcesContent":["/**\n * @fileoverview Filesystem reader for content collections\n *\n * This module provides functions for reading content files from the filesystem,\n * parsing frontmatter, and extracting content metadata.\n *\n * ## Features:\n * - Read individual content files with frontmatter parsing\n * - List all content files in a collection\n * - Generate content summaries for listing\n * - Support for .md and .mdx files\n *\n * @module @writenex/astro/filesystem/reader\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { basename, extname, join, relative } from \"node:path\";\nimport matter from \"gray-matter\";\nimport type { ContentItem, ContentSummary } from \"@/types\";\n\n/**\n * Supported content file extensions\n */\nconst CONTENT_EXTENSIONS = [\".md\", \".mdx\"];\n\n/**\n * Maximum excerpt length in characters\n */\nconst EXCERPT_LENGTH = 150;\n\n/**\n * Options for reading content\n */\nexport interface ReadContentOptions {\n /** Include draft content in listings */\n includeDrafts?: boolean;\n /** Sort field for listings */\n sortBy?: string;\n /** Sort order */\n sortOrder?: \"asc\" | \"desc\";\n}\n\n/**\n * Result of reading a content file\n */\nexport interface ReadFileResult {\n /** Whether the read was successful */\n success: boolean;\n /** The content item (if successful) */\n content?: ContentItem;\n /** Error message (if failed) */\n error?: string;\n}\n\n/**\n * Check if a file is a content file based on extension\n *\n * @param filename - The filename to check\n * @returns True if the file is a content file\n */\nexport function isContentFile(filename: string): boolean {\n const ext = extname(filename).toLowerCase();\n return CONTENT_EXTENSIONS.includes(ext);\n}\n\n/**\n * Extract slug from a content file path\n *\n * Handles various file patterns:\n * - `my-post.md` -> `my-post`\n * - `2024-01-15-my-post.md` -> `2024-01-15-my-post`\n * - `my-post/index.md` -> `my-post`\n *\n * @param filePath - Path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns The extracted slug\n */\nexport function extractSlug(filePath: string, collectionPath: string): string {\n const relativePath = relative(collectionPath, filePath);\n const filename = basename(relativePath);\n const ext = extname(filename);\n\n // Handle index files (folder-based content)\n // On Windows, path.relative() uses backslashes — normalise to forward slashes\n // so the split works on both platforms.\n if (filename === \"index.md\" || filename === \"index.mdx\") {\n const parts = relativePath.replace(/\\\\/g, \"/\").split(\"/\");\n if (parts.length >= 2) {\n const slug = parts[parts.length - 2];\n if (slug) return slug;\n }\n }\n\n // Remove extension to get slug\n return filename.slice(0, -ext.length);\n}\n\n/**\n * Generate an excerpt from markdown content\n *\n * @param body - The markdown body content\n * @param maxLength - Maximum excerpt length\n * @returns The generated excerpt\n */\nexport function generateExcerpt(\n body: string,\n maxLength: number = EXCERPT_LENGTH\n): string {\n // Remove markdown formatting for cleaner excerpt\n const cleaned = body\n // Remove headers\n .replace(/^#{1,6}\\s+/gm, \"\")\n // Remove bold/italic\n .replace(/\\*\\*([^*]+)\\*\\*/g, \"$1\")\n .replace(/\\*([^*]+)\\*/g, \"$1\")\n .replace(/__([^_]+)__/g, \"$1\")\n .replace(/_([^_]+)_/g, \"$1\")\n // Remove links but keep text\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n // Remove images\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, \"\")\n // Remove code blocks\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`([^`]+)`/g, \"$1\")\n // Remove blockquotes\n .replace(/^>\\s+/gm, \"\")\n // Remove horizontal rules\n .replace(/^[-*_]{3,}$/gm, \"\")\n // Collapse whitespace\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (cleaned.length <= maxLength) {\n return cleaned;\n }\n\n // Truncate at word boundary\n const truncated = cleaned.slice(0, maxLength);\n const lastSpace = truncated.lastIndexOf(\" \");\n\n if (lastSpace > maxLength * 0.7) {\n return truncated.slice(0, lastSpace) + \"...\";\n }\n\n return truncated + \"...\";\n}\n\n/**\n * Read and parse a single content file\n *\n * @param filePath - Absolute path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns ReadFileResult with the parsed content or error\n *\n * @example\n * ```typescript\n * const result = await readContentFile(\n * '/project/src/content/blog/my-post.md',\n * '/project/src/content/blog'\n * );\n *\n * if (result.success) {\n * console.log(result.content.frontmatter.title);\n * }\n * ```\n */\nexport async function readContentFile(\n filePath: string,\n collectionPath: string\n): Promise<ReadFileResult> {\n try {\n // Check if file exists\n if (!existsSync(filePath)) {\n return {\n success: false,\n error: `File not found: ${filePath}`,\n };\n }\n\n // Read file content and stats in parallel\n const [raw, stats] = await Promise.all([\n readFile(filePath, \"utf-8\"),\n stat(filePath),\n ]);\n\n // Parse frontmatter\n const { data: frontmatter, content: body } = matter(raw);\n\n // Extract slug\n const id = extractSlug(filePath, collectionPath);\n\n return {\n success: true,\n content: {\n id,\n path: filePath,\n frontmatter,\n body: body.trim(),\n raw,\n mtime: stats.mtimeMs,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to read content file: ${message}`,\n };\n }\n}\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Path to the directory to scan\n * @returns Array of absolute file paths\n */\nasync function listFilesRecursive(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n const entries = await readdir(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Recursively scan subdirectories\n const subFiles = await listFilesRecursive(fullPath);\n files.push(...subFiles);\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n/**\n * Read all content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content items\n *\n * @example\n * ```typescript\n * const items = await readCollection('/project/src/content/blog', {\n * includeDrafts: false,\n * sortBy: 'pubDate',\n * sortOrder: 'desc',\n * });\n * ```\n */\nexport async function readCollection(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentItem[]> {\n const { includeDrafts = true, sortBy, sortOrder = \"desc\" } = options;\n\n // Get all content files\n const filePaths = await listFilesRecursive(collectionPath);\n\n // Read and parse all files\n const results = await Promise.all(\n filePaths.map((fp) => readContentFile(fp, collectionPath))\n );\n\n // Filter successful reads and optionally filter drafts\n let items = results\n .filter(\n (r): r is { success: true; content: ContentItem } =>\n r.success && !!r.content\n )\n .map((r) => r.content)\n .filter((item) => {\n if (!includeDrafts && item.frontmatter.draft === true) {\n return false;\n }\n return true;\n });\n\n // Sort if requested\n if (sortBy) {\n items = items.sort((a, b) => {\n const aVal = a.frontmatter[sortBy];\n const bVal = b.frontmatter[sortBy];\n\n // Handle undefined values\n if (aVal === undefined && bVal === undefined) return 0;\n if (aVal === undefined) return sortOrder === \"asc\" ? -1 : 1;\n if (bVal === undefined) return sortOrder === \"asc\" ? 1 : -1;\n\n // Compare values (convert to string for comparison)\n const aStr = String(aVal);\n const bStr = String(bVal);\n if (aStr < bStr) return sortOrder === \"asc\" ? -1 : 1;\n if (aStr > bStr) return sortOrder === \"asc\" ? 1 : -1;\n return 0;\n });\n }\n\n return items;\n}\n\n/**\n * Convert a content item to a summary for listing\n *\n * @param item - The full content item\n * @returns Content summary with essential fields\n */\nexport function toContentSummary(item: ContentItem): ContentSummary {\n const { id, path, frontmatter, body } = item;\n\n // Support both pubDate and publishDate naming conventions\n const dateValue =\n frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;\n\n return {\n id,\n path,\n title: String(frontmatter.title ?? id),\n pubDate: dateValue ? String(dateValue) : undefined,\n draft: frontmatter.draft === true,\n excerpt: generateExcerpt(body),\n };\n}\n\n/**\n * Get content summaries for a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content summaries\n */\nexport async function getCollectionSummaries(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentSummary[]> {\n const items = await readCollection(collectionPath, options);\n return items.map(toContentSummary);\n}\n\n/**\n * Get the count of content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Number of content files\n */\nexport async function getCollectionCount(\n collectionPath: string\n): Promise<number> {\n const filePaths = await listFilesRecursive(collectionPath);\n return filePaths.length;\n}\n\n/**\n * Check if a collection directory exists and contains content\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Object with exists and hasContent flags\n */\nexport async function checkCollection(collectionPath: string): Promise<{\n exists: boolean;\n hasContent: boolean;\n count: number;\n}> {\n if (!existsSync(collectionPath)) {\n return { exists: false, hasContent: false, count: 0 };\n }\n\n const count = await getCollectionCount(collectionPath);\n\n return {\n exists: true,\n hasContent: count > 0,\n count,\n };\n}\n\n/**\n * Get file stats for a content file\n *\n * @param filePath - Path to the content file\n * @returns File stats or null if file doesn't exist\n */\nexport async function getFileStats(filePath: string): Promise<{\n size: number;\n mtime: Date;\n ctime: Date;\n} | null> {\n try {\n const stats = await stat(filePath);\n return {\n size: stats.size,\n mtime: stats.mtime,\n ctime: stats.birthtime,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Get the file path for a content item by ID\n *\n * Searches for the content file in the collection directory,\n * handling different content structures:\n * - Folder-based: `slug/index.md` or `slug/index.mdx`\n * - Flat file: `slug.md` or `slug.mdx`\n *\n * @param collectionPath - Path to the collection directory\n * @param contentId - Content ID (slug)\n * @returns File path if found, null otherwise\n *\n * @example\n * ```typescript\n * const filePath = getContentFilePath('/project/src/content/blog', 'my-post');\n * // Returns: '/project/src/content/blog/my-post.md' or\n * // '/project/src/content/blog/my-post/index.md'\n * ```\n */\nexport function getContentFilePath(\n collectionPath: string,\n contentId: string\n): string | null {\n // Try folder-based structure first (slug/index.md or slug/index.mdx)\n const indexMdPath = join(collectionPath, contentId, \"index.md\");\n if (existsSync(indexMdPath)) {\n return indexMdPath;\n }\n\n const indexMdxPath = join(collectionPath, contentId, \"index.mdx\");\n if (existsSync(indexMdxPath)) {\n return indexMdxPath;\n }\n\n // Try flat file structure (slug.md or slug.mdx)\n const flatMdPath = join(collectionPath, `${contentId}.md`);\n if (existsSync(flatMdPath)) {\n return flatMdPath;\n }\n\n const flatMdxPath = join(collectionPath, `${contentId}.mdx`);\n if (existsSync(flatMdxPath)) {\n return flatMdxPath;\n }\n\n return null;\n}\n","/**\n * @fileoverview File pattern detection for content collections\n *\n * This module provides functions to detect and work with file naming patterns\n * in Astro content collections.\n *\n * ## Supported Patterns:\n * - `{slug}.md` - Simple slug-based naming\n * - `{date}-{slug}.md` - Date-prefixed naming (2024-01-15-my-post.md)\n * - `{year}/{slug}.md` - Year folder structure\n * - `{year}/{month}/{slug}.md` - Year/month folder structure\n * - `{year}/{month}/{day}/{slug}.md` - Full date folder structure\n * - `{slug}/index.md` - Folder-based with index file\n * - `{category}/{slug}.md` - Category folder structure\n * - `{category}/{slug}/index.md` - Category with folder-based content\n * - `{lang}/{slug}.md` - Language-prefixed content (i18n)\n * - `{lang}/{slug}/index.md` - Language with folder-based content\n *\n * ## Custom Patterns:\n * Developers can configure custom patterns in their collection config.\n * Custom tokens are resolved from frontmatter data or use default values.\n *\n * ## Detection Process:\n * 1. Scan collection directory for all content files\n * 2. Analyze file paths and names for common patterns\n * 3. Score each pattern based on match frequency\n * 4. Return the best matching pattern\n *\n * @module @writenex/astro/discovery/patterns\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { extname, join, relative } from \"node:path\";\nimport { isContentFile } from \"@/filesystem/reader\";\n\n/**\n * Pattern definition with regex and template\n */\ninterface PatternDefinition {\n /** Pattern name for identification */\n name: string;\n /** Template string with tokens */\n template: string;\n /** Regex to match against file paths */\n regex: RegExp;\n /** Function to extract tokens from a match */\n extract: (match: RegExpMatchArray, ext: string) => Record<string, string>;\n /** Priority when multiple patterns match (higher = preferred) */\n priority: number;\n}\n\n/**\n * Result of pattern detection\n */\nexport interface PatternDetectionResult {\n /** The detected pattern template */\n pattern: string;\n /** Confidence score (0-1) */\n confidence: number;\n /** Number of files that matched this pattern */\n matchCount: number;\n /** Total files analyzed */\n totalFiles: number;\n /** Sample matches for debugging */\n samples: Array<{\n filePath: string;\n extracted: Record<string, string>;\n }>;\n}\n\n/**\n * All supported pattern definitions\n *\n * Order matters - more specific patterns should come first.\n * Higher priority patterns are preferred when multiple patterns match.\n */\nconst PATTERN_DEFINITIONS: PatternDefinition[] = [\n // {year}/{month}/{day}/{slug}.md - Full date folder structure\n {\n name: \"year-month-day-slug\",\n template: \"{year}/{month}/{day}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n day: match[3] ?? \"\",\n slug: match[4] ?? \"\",\n extension: ext,\n }),\n priority: 95,\n },\n\n // {year}/{month}/{slug}.md - Year/month nested date structure\n {\n name: \"year-month-slug\",\n template: \"{year}/{month}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n slug: match[3] ?? \"\",\n extension: ext,\n }),\n priority: 90,\n },\n\n // {year}/{slug}.md - Year folder structure\n {\n name: \"year-slug\",\n template: \"{year}/{slug}.md\",\n regex: /^(\\d{4})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 85,\n },\n\n // {lang}/{slug}/index.md - Language with folder-based content (i18n)\n {\n name: \"lang-folder-index\",\n template: \"{lang}/{slug}/index.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 82,\n },\n\n // {category}/{slug}/index.md - Category with folder-based content\n {\n name: \"category-folder-index\",\n template: \"{category}/{slug}/index.md\",\n regex: /^([^/]+)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 80,\n },\n\n // {slug}/index.md - Folder-based content\n {\n name: \"folder-index\",\n template: \"{slug}/index.md\",\n regex: /^([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 75,\n },\n\n // {date}-{slug}.md - Date-prefixed (ISO format)\n {\n name: \"date-slug\",\n template: \"{date}-{slug}.md\",\n regex: /^(\\d{4}-\\d{2}-\\d{2})-(.+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n date: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 70,\n },\n\n // {lang}/{slug}.md - Language-prefixed content (i18n)\n // Matches: en/my-post.md, pt-BR/my-post.md\n {\n name: \"lang-slug\",\n template: \"{lang}/{slug}.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 60,\n },\n\n // {category}/{slug}.md - Category folder (catch-all for non-date/non-lang folders)\n {\n name: \"category-slug\",\n template: \"{category}/{slug}.md\",\n regex: /^([^/]+)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 50,\n },\n\n // {slug}.md - Simple flat structure (default fallback)\n {\n name: \"simple-slug\",\n template: \"{slug}.md\",\n regex: /^([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 10,\n },\n];\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Directory to scan\n * @returns Array of relative file paths\n */\nasync function listContentFiles(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n async function scan(currentPath: string, relativeTo: string): Promise<void> {\n const entries = await readdir(currentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(currentPath, entry.name);\n const relativePath = relative(relativeTo, fullPath);\n\n if (entry.isDirectory()) {\n // Skip hidden and special directories\n if (!entry.name.startsWith(\".\") && !entry.name.startsWith(\"_\")) {\n await scan(fullPath, relativeTo);\n }\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(relativePath);\n }\n }\n }\n\n await scan(dirPath, dirPath);\n return files;\n}\n\n/**\n * Try to match a file path against all pattern definitions\n *\n * @param relativePath - Relative path to the content file\n * @returns Matched pattern and extracted tokens, or null\n */\nfunction matchPattern(\n relativePath: string\n): { pattern: PatternDefinition; match: RegExpMatchArray } | null {\n // Normalize path separators\n const normalizedPath = relativePath.replace(/\\\\/g, \"/\");\n\n for (const pattern of PATTERN_DEFINITIONS) {\n const match = normalizedPath.match(pattern.regex);\n if (match) {\n return { pattern, match };\n }\n }\n\n return null;\n}\n\n/**\n * Detect the file naming pattern used in a collection\n *\n * Analyzes all content files in the collection directory and determines\n * the most likely pattern based on file names and structure.\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Pattern detection result with confidence score\n *\n * @example\n * ```typescript\n * const result = await detectFilePattern('/project/src/content/blog');\n * console.log(result.pattern); // \"{date}-{slug}.md\"\n * console.log(result.confidence); // 0.95\n * ```\n */\nexport async function detectFilePattern(\n collectionPath: string\n): Promise<PatternDetectionResult> {\n const files = await listContentFiles(collectionPath);\n\n if (files.length === 0) {\n return {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: 0,\n samples: [],\n };\n }\n\n // Count matches for each pattern\n const patternCounts = new Map<\n string,\n {\n pattern: PatternDefinition;\n count: number;\n samples: Array<{ filePath: string; extracted: Record<string, string> }>;\n extension: string;\n }\n >();\n\n for (const pattern of PATTERN_DEFINITIONS) {\n patternCounts.set(pattern.name, {\n pattern,\n count: 0,\n samples: [],\n extension: \".md\",\n });\n }\n\n // Analyze each file\n for (const filePath of files) {\n const result = matchPattern(filePath);\n\n if (result) {\n const { pattern, match } = result;\n const entry = patternCounts.get(pattern.name);\n\n if (entry) {\n const ext = extname(filePath);\n const extracted = pattern.extract(match, ext);\n\n entry.count++;\n entry.extension = ext;\n\n // Keep up to 3 samples\n if (entry.samples.length < 3) {\n entry.samples.push({ filePath, extracted });\n }\n }\n }\n }\n\n // Find the best matching pattern\n // Consider both match count and pattern priority\n let bestPattern: PatternDetectionResult | null = null;\n let bestScore = -1;\n\n for (const [, entry] of patternCounts) {\n if (entry.count === 0) continue;\n\n // Score = (match ratio * 100) + priority\n // This ensures high match ratio wins, but priority breaks ties\n const matchRatio = entry.count / files.length;\n const score = matchRatio * 100 + entry.pattern.priority;\n\n if (score > bestScore) {\n bestScore = score;\n\n // Adjust template for actual extension used\n let template = entry.pattern.template;\n if (entry.extension === \".mdx\") {\n template = template.replace(\".md\", \".mdx\");\n }\n\n bestPattern = {\n pattern: template,\n confidence: matchRatio,\n matchCount: entry.count,\n totalFiles: files.length,\n samples: entry.samples,\n };\n }\n }\n\n // Return best pattern or default\n return (\n bestPattern ?? {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: files.length,\n samples: [],\n }\n );\n}\n\n/**\n * Generate a file path from a pattern and tokens\n *\n * @param pattern - Pattern template (e.g., \"{date}-{slug}.md\")\n * @param tokens - Token values to substitute\n * @returns Generated file path\n *\n * @example\n * ```typescript\n * const path = generatePathFromPattern(\n * \"{date}-{slug}.md\",\n * { date: \"2024-01-15\", slug: \"my-post\" }\n * );\n * // Returns: \"2024-01-15-my-post.md\"\n * ```\n */\nexport function generatePathFromPattern(\n pattern: string,\n tokens: Record<string, string>\n): string {\n let result = pattern;\n\n for (const [key, value] of Object.entries(tokens)) {\n result = result.replace(`{${key}}`, value);\n }\n\n return result;\n}\n\n/**\n * Parse a pattern template to extract token names\n *\n * @param pattern - Pattern template\n * @returns Array of token names\n *\n * @example\n * ```typescript\n * const tokens = parsePatternTokens(\"{year}/{month}/{slug}.md\");\n * // Returns: [\"year\", \"month\", \"slug\"]\n * ```\n */\nexport function parsePatternTokens(pattern: string): string[] {\n const tokenRegex = /\\{([^}]+)\\}/g;\n const tokens: string[] = [];\n let match;\n\n while ((match = tokenRegex.exec(pattern)) !== null) {\n if (match[1]) {\n tokens.push(match[1]);\n }\n }\n\n return tokens;\n}\n\n/**\n * Validate that a pattern has all required tokens\n *\n * @param pattern - Pattern template\n * @param requiredTokens - Required token names\n * @returns True if all required tokens are present\n */\nexport function validatePattern(\n pattern: string,\n requiredTokens: string[] = [\"slug\"]\n): boolean {\n const tokens = parsePatternTokens(pattern);\n return requiredTokens.every((req) => tokens.includes(req));\n}\n\n/**\n * Get the default extension for a pattern\n *\n * @param pattern - Pattern template\n * @returns The file extension (.md or .mdx)\n */\nexport function getPatternExtension(pattern: string): string {\n if (pattern.endsWith(\".mdx\")) {\n return \".mdx\";\n }\n return \".md\";\n}\n\n/**\n * Known token types and their default value generators\n */\ntype TokenResolver = (\n frontmatter: Record<string, unknown>,\n slug: string\n) => string;\n\nconst TOKEN_RESOLVERS: Record<string, TokenResolver> = {\n // Core tokens\n slug: (_fm, slug) => slug,\n\n // Date tokens - from pubDate or current date\n date: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.toISOString().split(\"T\")[0] ?? \"\";\n },\n year: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getFullYear().toString();\n },\n month: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return (pubDate.getMonth() + 1).toString().padStart(2, \"0\");\n },\n day: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getDate().toString().padStart(2, \"0\");\n },\n\n // i18n tokens\n lang: (fm) => {\n if (typeof fm.lang === \"string\") return fm.lang;\n if (typeof fm.language === \"string\") return fm.language;\n if (typeof fm.locale === \"string\") return fm.locale;\n return \"en\"; // Default to English\n },\n\n // Organization tokens\n category: (fm) => {\n if (typeof fm.category === \"string\") return fm.category;\n if (Array.isArray(fm.categories) && typeof fm.categories[0] === \"string\") {\n return fm.categories[0];\n }\n return \"uncategorized\";\n },\n author: (fm) => {\n if (typeof fm.author === \"string\") return slugifyValue(fm.author);\n if (\n typeof fm.author === \"object\" &&\n fm.author !== null &&\n \"name\" in fm.author\n ) {\n return slugifyValue(String(fm.author.name));\n }\n return \"anonymous\";\n },\n type: (fm) => {\n if (typeof fm.type === \"string\") return fm.type;\n if (typeof fm.contentType === \"string\") return fm.contentType;\n return \"post\";\n },\n status: (fm) => {\n if (typeof fm.status === \"string\") return fm.status;\n if (fm.draft === true) return \"draft\";\n return \"published\";\n },\n series: (fm) => {\n if (typeof fm.series === \"string\") return slugifyValue(fm.series);\n return \"\";\n },\n collection: (fm) => {\n if (typeof fm.collection === \"string\") return fm.collection;\n return \"\";\n },\n};\n\n/**\n * Resolve a date from frontmatter\n *\n * Checks common date field names: pubDate, date, publishDate, createdAt\n *\n * @param frontmatter - Frontmatter data\n * @returns Resolved Date object\n */\nfunction resolveDateFromFrontmatter(\n frontmatter: Record<string, unknown>\n): Date {\n const dateFields = [\"pubDate\", \"date\", \"publishDate\", \"createdAt\", \"created\"];\n\n for (const field of dateFields) {\n const value = frontmatter[field];\n if (value instanceof Date) return value;\n if (typeof value === \"string\") {\n const parsed = new Date(value);\n if (!isNaN(parsed.getTime())) return parsed;\n }\n }\n\n return new Date();\n}\n\n/**\n * Convert a string to a URL-safe slug\n *\n * @param value - String to slugify\n * @returns URL-safe slug\n */\nfunction slugifyValue(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/[\\s_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Options for resolving pattern tokens\n */\nexport interface ResolveTokensOptions {\n /** The content slug */\n slug: string;\n /** Frontmatter data for resolving dynamic tokens */\n frontmatter?: Record<string, unknown>;\n /** Custom token values (override automatic resolution) */\n customTokens?: Record<string, string>;\n}\n\n/**\n * Resolve all tokens in a pattern to their values\n *\n * Token resolution priority:\n * 1. Custom tokens (explicitly provided)\n * 2. Known token resolvers (date, year, month, etc.)\n * 3. Frontmatter values (for custom tokens)\n * 4. Empty string (fallback)\n *\n * @param pattern - Pattern template with tokens\n * @param options - Resolution options\n * @returns Record of token names to resolved values\n *\n * @example\n * ```typescript\n * const tokens = resolvePatternTokens(\"{year}/{month}/{slug}.md\", {\n * slug: \"my-post\",\n * frontmatter: { pubDate: new Date(\"2024-06-15\") }\n * });\n * // Returns: { year: \"2024\", month: \"06\", slug: \"my-post\" }\n * ```\n */\nexport function resolvePatternTokens(\n pattern: string,\n options: ResolveTokensOptions\n): Record<string, string> {\n const { slug, frontmatter = {}, customTokens = {} } = options;\n const tokenNames = parsePatternTokens(pattern);\n const resolved: Record<string, string> = {};\n\n for (const tokenName of tokenNames) {\n // Priority 1: Custom tokens\n if (tokenName in customTokens) {\n resolved[tokenName] = customTokens[tokenName] ?? \"\";\n continue;\n }\n\n // Priority 2: Known token resolvers\n const resolver = TOKEN_RESOLVERS[tokenName];\n if (resolver) {\n resolved[tokenName] = resolver(frontmatter, slug);\n continue;\n }\n\n // Priority 3: Direct frontmatter value\n const fmValue = frontmatter[tokenName];\n if (typeof fmValue === \"string\") {\n resolved[tokenName] = slugifyValue(fmValue);\n continue;\n }\n if (typeof fmValue === \"number\") {\n resolved[tokenName] = fmValue.toString();\n continue;\n }\n\n // Priority 4: Fallback to empty string\n resolved[tokenName] = \"\";\n }\n\n return resolved;\n}\n\n/**\n * Check if a pattern is valid for content creation\n *\n * A pattern is valid if:\n * - It contains the {slug} token (required)\n * - It ends with .md or .mdx\n * - All tokens can be resolved\n *\n * @param pattern - Pattern template to validate\n * @returns Validation result with error message if invalid\n */\nexport function isValidPattern(pattern: string): {\n valid: boolean;\n error?: string;\n} {\n // Must contain slug token\n if (!pattern.includes(\"{slug}\")) {\n return { valid: false, error: \"Pattern must contain {slug} token\" };\n }\n\n // Must end with .md or .mdx\n if (!pattern.endsWith(\".md\") && !pattern.endsWith(\".mdx\")) {\n return { valid: false, error: \"Pattern must end with .md or .mdx\" };\n }\n\n // Check for unclosed tokens\n const unclosed = pattern.match(/\\{[^}]*$/);\n if (unclosed) {\n return { valid: false, error: \"Pattern contains unclosed token\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Get list of all supported token names\n *\n * @returns Array of supported token names\n */\nexport function getSupportedTokens(): string[] {\n return Object.keys(TOKEN_RESOLVERS);\n}\n"],"mappings":"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","names":["existsSync","readdir","extname","join","relative","existsSync","readdir","join","relative","extname"]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
detectFilePattern,
|
|
3
3
|
getCollectionCount,
|
|
4
4
|
readCollection
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-GYAFIVVI.js";
|
|
6
6
|
import {
|
|
7
7
|
DEFAULT_FILE_PATTERN
|
|
8
8
|
} from "./chunk-YBCPOLMY.js";
|
|
@@ -307,4 +307,4 @@ export {
|
|
|
307
307
|
getCollection,
|
|
308
308
|
collectionExists
|
|
309
309
|
};
|
|
310
|
-
//# sourceMappingURL=chunk-
|
|
310
|
+
//# sourceMappingURL=chunk-JMNCPNQX.js.map
|
|
@@ -77,6 +77,9 @@ function validateConfig(config) {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// src/config/loader.ts
|
|
80
|
+
function normalizeProjectRoot(projectRoot) {
|
|
81
|
+
return projectRoot.replace(/^\/([A-Za-z]:)/, "$1");
|
|
82
|
+
}
|
|
80
83
|
var CONFIG_FILE_NAMES = [
|
|
81
84
|
"writenex.config.ts",
|
|
82
85
|
"writenex.config.mts",
|
|
@@ -84,6 +87,7 @@ var CONFIG_FILE_NAMES = [
|
|
|
84
87
|
"writenex.config.mjs"
|
|
85
88
|
];
|
|
86
89
|
function findConfigFile(projectRoot) {
|
|
90
|
+
projectRoot = normalizeProjectRoot(projectRoot);
|
|
87
91
|
for (const fileName of CONFIG_FILE_NAMES) {
|
|
88
92
|
const filePath = join(projectRoot, fileName);
|
|
89
93
|
if (existsSync(filePath)) {
|
|
@@ -106,6 +110,7 @@ async function loadConfigFile(configPath) {
|
|
|
106
110
|
}
|
|
107
111
|
}
|
|
108
112
|
async function loadConfig(projectRoot) {
|
|
113
|
+
projectRoot = normalizeProjectRoot(projectRoot);
|
|
109
114
|
const warnings = [];
|
|
110
115
|
let userConfig = {};
|
|
111
116
|
let configPath = null;
|
|
@@ -137,6 +142,7 @@ async function loadConfig(projectRoot) {
|
|
|
137
142
|
};
|
|
138
143
|
}
|
|
139
144
|
function contentDirectoryExists(projectRoot, contentPath = "src/content") {
|
|
145
|
+
projectRoot = normalizeProjectRoot(projectRoot);
|
|
140
146
|
const fullPath = join(projectRoot, contentPath);
|
|
141
147
|
return existsSync(fullPath);
|
|
142
148
|
}
|
|
@@ -150,4 +156,4 @@ export {
|
|
|
150
156
|
loadConfig,
|
|
151
157
|
contentDirectoryExists
|
|
152
158
|
};
|
|
153
|
-
//# sourceMappingURL=chunk-
|
|
159
|
+
//# sourceMappingURL=chunk-N37EPLKG.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/config/loader.ts","../src/config/schema.ts"],"sourcesContent":["/**\n * @fileoverview Configuration loader for @writenex/astro\n *\n * This module handles loading Writenex configuration from various sources:\n * - writenex.config.ts (TypeScript)\n * - writenex.config.js (JavaScript)\n * - writenex.config.mjs (ES Module)\n *\n * The loader searches for configuration files in the project root and\n * applies default values for any missing options.\n *\n * @module @writenex/astro/config/loader\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { WritenexConfig } from \"@/types\";\nimport { applyConfigDefaults } from \"./defaults\";\nimport { validateConfig } from \"./schema\";\n\n/**\n * Normalize a project root path that may have come from URL.pathname.\n *\n * On Windows, `config.root.pathname` returns `/C:/Users/...` — a leading slash\n * before the drive letter that makes every subsequent `path.join` and\n * `fs.existsSync` call fail silently. Strip it so downstream code gets a\n * valid Windows path like `C:\\Users\\...`.\n *\n * On macOS/Linux the path starts with a real `/` so the regex won't match\n * and the string is returned unchanged.\n */\nfunction normalizeProjectRoot(projectRoot: string): string {\n return projectRoot.replace(/^\\/([A-Za-z]:)/, \"$1\");\n}\n\n/**\n * Supported configuration file names in order of priority\n */\nconst CONFIG_FILE_NAMES = [\n \"writenex.config.ts\",\n \"writenex.config.mts\",\n \"writenex.config.js\",\n \"writenex.config.mjs\",\n];\n\n/**\n * Result of loading configuration\n */\nexport interface LoadConfigResult {\n /** The loaded and validated configuration with defaults applied */\n config: Required<WritenexConfig>;\n /** Path to the configuration file (if found) */\n configPath: string | null;\n /** Whether a configuration file was found */\n hasConfigFile: boolean;\n /** Any warnings generated during loading */\n warnings: string[];\n}\n\n/**\n * Find the configuration file in the project root\n *\n * @param projectRoot - The root directory of the Astro project\n * @returns Path to the configuration file, or null if not found\n */\nexport function findConfigFile(projectRoot: string): string | null {\n projectRoot = normalizeProjectRoot(projectRoot);\n for (const fileName of CONFIG_FILE_NAMES) {\n const filePath = join(projectRoot, fileName);\n if (existsSync(filePath)) {\n return filePath;\n }\n }\n return null;\n}\n\n/**\n * Load configuration from a file\n *\n * @param configPath - Path to the configuration file\n * @returns The loaded configuration object\n * @throws Error if the file cannot be loaded or parsed\n */\nasync function loadConfigFile(configPath: string): Promise<WritenexConfig> {\n try {\n // Convert to file URL for dynamic import (required for Windows compatibility)\n const fileUrl = pathToFileURL(resolve(configPath)).href;\n\n // Dynamic import the configuration file\n const module = await import(fileUrl);\n\n // Support both default export and named export\n const config = module.default ?? module.config ?? module;\n\n return config as WritenexConfig;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `Failed to load configuration from ${configPath}: ${message}`\n );\n }\n}\n\n/**\n * Load Writenex configuration from the project root\n *\n * This function:\n * 1. Searches for a configuration file in the project root\n * 2. Loads and validates the configuration if found\n * 3. Applies default values for any missing options\n * 4. Returns the resolved configuration\n *\n * If no configuration file is found, default configuration is returned\n * with auto-discovery enabled.\n *\n * @param projectRoot - The root directory of the Astro project\n * @returns LoadConfigResult with the resolved configuration\n *\n * @example\n * ```typescript\n * const { config, hasConfigFile, warnings } = await loadConfig('/path/to/project');\n *\n * if (!hasConfigFile) {\n * console.log('Using auto-discovery mode');\n * }\n *\n * if (warnings.length > 0) {\n * warnings.forEach(w => console.warn(w));\n * }\n * ```\n */\nexport async function loadConfig(\n projectRoot: string\n): Promise<LoadConfigResult> {\n projectRoot = normalizeProjectRoot(projectRoot);\n const warnings: string[] = [];\n let userConfig: WritenexConfig = {};\n let configPath: string | null = null;\n let hasConfigFile = false;\n\n // Try to find and load configuration file\n configPath = findConfigFile(projectRoot);\n\n if (configPath) {\n hasConfigFile = true;\n\n try {\n userConfig = await loadConfigFile(configPath);\n\n // Validate the loaded configuration\n const validationResult = validateConfig(userConfig);\n\n if (!validationResult.success) {\n const errors = validationResult.error.issues\n .map(\n (e: import(\"zod\").ZodIssue) => `${e.path.join(\".\")}: ${e.message}`\n )\n .join(\", \");\n warnings.push(`Configuration validation warnings: ${errors}`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n warnings.push(`Failed to load config file: ${message}. Using defaults.`);\n userConfig = {};\n }\n }\n\n // Apply defaults to the configuration\n const config = applyConfigDefaults(userConfig);\n\n return {\n config,\n configPath,\n hasConfigFile,\n warnings,\n };\n}\n\n/**\n * Check if a content directory exists in the project\n *\n * @param projectRoot - The root directory of the Astro project\n * @param contentPath - Relative path to the content directory\n * @returns True if the content directory exists\n */\nexport function contentDirectoryExists(\n projectRoot: string,\n contentPath: string = \"src/content\"\n): boolean {\n projectRoot = normalizeProjectRoot(projectRoot);\n const fullPath = join(projectRoot, contentPath);\n return existsSync(fullPath);\n}\n","/**\n * @fileoverview Configuration schema and validation for @writenex/astro\n *\n * This module provides Zod schemas for validating Writenex configuration\n * and a helper function for defining type-safe configurations.\n *\n * @module @writenex/astro/config/schema\n */\n\nimport { z } from \"zod\";\nimport type { WritenexConfig } from \"@/types\";\n\n/**\n * Schema for field type definitions\n */\nconst fieldTypeSchema = z.enum([\n \"string\",\n \"number\",\n \"boolean\",\n \"date\",\n \"array\",\n \"image\",\n \"object\",\n]);\n\n/**\n * Schema for individual schema field definition\n */\nconst schemaFieldSchema = z.object({\n type: fieldTypeSchema,\n required: z.boolean().optional(),\n default: z.unknown().optional(),\n items: z.string().optional(),\n description: z.string().optional(),\n});\n\n/**\n * Schema for collection schema (record of field definitions)\n */\nconst collectionSchemaSchema = z.record(z.string(), schemaFieldSchema);\n\n/**\n * Schema for image strategy\n */\nconst imageStrategySchema = z.enum([\"colocated\", \"public\", \"custom\"]);\n\n/**\n * Schema for image configuration\n */\nconst imageConfigSchema = z.object({\n strategy: imageStrategySchema,\n publicPath: z.string().optional(),\n storagePath: z.string().optional(),\n});\n\n/**\n * Schema for collection configuration\n */\nconst collectionConfigSchema = z.object({\n name: z.string().min(1, \"Collection name is required\"),\n path: z.string().min(1, \"Collection path is required\"),\n filePattern: z.string().optional(),\n previewUrl: z.string().optional(),\n schema: collectionSchemaSchema.optional(),\n images: imageConfigSchema.optional(),\n});\n\n/**\n * Schema for discovery configuration\n */\nconst discoveryConfigSchema = z.object({\n enabled: z.boolean(),\n ignore: z.array(z.string()).optional(),\n});\n\n/**\n * Schema for editor configuration\n */\nconst editorConfigSchema = z.object({\n autosave: z.boolean().optional(),\n autosaveInterval: z.number().positive().optional(),\n});\n\n/**\n * Schema for version history configuration\n */\nconst versionHistoryConfigSchema = z.object({\n enabled: z.boolean().optional(),\n maxVersions: z.number().int().positive().optional(),\n storagePath: z.string().optional(),\n});\n\n/**\n * Main Writenex configuration schema\n */\nexport const writenexConfigSchema = z.object({\n collections: z.array(collectionConfigSchema).optional(),\n images: imageConfigSchema.optional(),\n editor: editorConfigSchema.optional(),\n discovery: discoveryConfigSchema.optional(),\n versionHistory: versionHistoryConfigSchema.optional(),\n});\n\n/**\n * Schema for integration options\n */\nexport const writenexOptionsSchema = z.object({\n allowProduction: z.boolean().optional(),\n});\n\n/**\n * Helper function for defining type-safe Writenex configuration.\n *\n * This function provides IDE autocompletion and type checking for\n * the configuration object. It's the recommended way to create\n * a writenex.config.ts file.\n *\n * @param config - The Writenex configuration object\n * @returns The same configuration object (identity function for type safety)\n *\n * @example\n * ```typescript\n * // writenex.config.ts\n * import { defineConfig } from '@writenex/astro';\n *\n * export default defineConfig({\n * collections: [\n * {\n * name: 'blog',\n * path: 'src/content/blog',\n * filePattern: '{slug}.md',\n * },\n * ],\n * });\n * ```\n */\nexport function defineConfig(config: WritenexConfig): WritenexConfig {\n // Validate the configuration\n const result = writenexConfigSchema.safeParse(config);\n\n if (!result.success) {\n const errors = result.error.issues\n .map((e: z.ZodIssue) => ` - ${e.path.join(\".\")}: ${e.message}`)\n .join(\"\\n\");\n console.warn(`[writenex] Invalid configuration:\\n${errors}`);\n }\n\n return config;\n}\n\n/**\n * Validate a Writenex configuration object\n *\n * @param config - The configuration to validate\n * @returns Validation result with success status and parsed data or errors\n */\nexport function validateConfig(\n config: unknown\n):\n | { success: true; data: WritenexConfig }\n | { success: false; error: z.ZodError } {\n return writenexConfigSchema.safeParse(config) as\n | { success: true; data: WritenexConfig }\n | { success: false; error: z.ZodError };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
discoverCollections,
|
|
3
3
|
mergeCollections
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-JMNCPNQX.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-
|
|
30
|
+
} from "./chunk-4H63L4YO.js";
|
|
31
31
|
import {
|
|
32
32
|
getCollectionSummaries,
|
|
33
33
|
getContentFilePath,
|
|
34
34
|
readContentFile
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-GYAFIVVI.js";
|
|
36
36
|
|
|
37
37
|
// src/server/cache.ts
|
|
38
38
|
var DEFAULT_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -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-
|
|
637
|
+
const { findConfigFile } = await import("./loader-53VVP2IN.js");
|
|
638
638
|
const configPath = findConfigFile(projectRoot);
|
|
639
639
|
sendJson(res, {
|
|
640
640
|
configPath,
|
|
@@ -1328,4 +1328,4 @@ export {
|
|
|
1328
1328
|
sendError,
|
|
1329
1329
|
sendWritenexError
|
|
1330
1330
|
};
|
|
1331
|
-
//# sourceMappingURL=chunk-
|
|
1331
|
+
//# sourceMappingURL=chunk-NSW7AIVF.js.map
|
package/dist/config/index.js
CHANGED
package/dist/discovery/index.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getCollection,
|
|
7
7
|
mergeCollections,
|
|
8
8
|
mergeSchema
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-JMNCPNQX.js";
|
|
10
10
|
import {
|
|
11
11
|
detectFilePattern,
|
|
12
12
|
generatePathFromPattern,
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
parsePatternTokens,
|
|
17
17
|
resolvePatternTokens,
|
|
18
18
|
validatePattern
|
|
19
|
-
} from "../chunk-
|
|
19
|
+
} from "../chunk-GYAFIVVI.js";
|
|
20
20
|
import "../chunk-YBCPOLMY.js";
|
|
21
21
|
export {
|
|
22
22
|
collectionExists,
|
package/dist/filesystem/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import {
|
|
|
38
38
|
updateContent,
|
|
39
39
|
uploadImage,
|
|
40
40
|
writeManifest
|
|
41
|
-
} from "../chunk-
|
|
41
|
+
} from "../chunk-4H63L4YO.js";
|
|
42
42
|
import {
|
|
43
43
|
checkCollection,
|
|
44
44
|
extractSlug,
|
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
readCollection,
|
|
52
52
|
readContentFile,
|
|
53
53
|
toContentSummary
|
|
54
|
-
} from "../chunk-
|
|
54
|
+
} from "../chunk-GYAFIVVI.js";
|
|
55
55
|
import {
|
|
56
56
|
DEFAULT_VERSION_HISTORY_CONFIG
|
|
57
57
|
} from "../chunk-YBCPOLMY.js";
|
package/dist/index.js
CHANGED
|
@@ -5,19 +5,19 @@ import {
|
|
|
5
5
|
import {
|
|
6
6
|
createMiddleware,
|
|
7
7
|
getCache
|
|
8
|
-
} from "./chunk-
|
|
9
|
-
import "./chunk-
|
|
8
|
+
} from "./chunk-NSW7AIVF.js";
|
|
9
|
+
import "./chunk-JMNCPNQX.js";
|
|
10
10
|
import {
|
|
11
11
|
WritenexError,
|
|
12
12
|
WritenexErrorCode,
|
|
13
13
|
isWritenexError
|
|
14
|
-
} from "./chunk-
|
|
15
|
-
import "./chunk-
|
|
14
|
+
} from "./chunk-4H63L4YO.js";
|
|
15
|
+
import "./chunk-GYAFIVVI.js";
|
|
16
16
|
import {
|
|
17
17
|
defineConfig,
|
|
18
18
|
loadConfig,
|
|
19
19
|
validateConfig
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-N37EPLKG.js";
|
|
21
21
|
import "./chunk-YBCPOLMY.js";
|
|
22
22
|
|
|
23
23
|
// src/integration.ts
|
|
@@ -52,7 +52,7 @@ function writenex(options) {
|
|
|
52
52
|
isActive = false;
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
-
projectRoot = config.root.pathname;
|
|
55
|
+
projectRoot = config.root.pathname.replace(/^\/([A-Za-z]:)/, "$1");
|
|
56
56
|
astroTrailingSlash = config.trailingSlash ?? "ignore";
|
|
57
57
|
const { config: loadedConfig, warnings } = await loadConfig(projectRoot);
|
|
58
58
|
resolvedConfig = loadedConfig;
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/integration.ts","../src/core/constants.ts"],"sourcesContent":["/**\n * @fileoverview Astro integration for Writenex visual editor\n *\n * This module provides the main Astro integration that injects the Writenex\n * editor UI and API routes into an Astro project.\n *\n * ## Features:\n * - Injects editor UI at /_writenex\n * - Provides API routes for content CRUD operations\n * - Auto-discovers content collections\n * - Production guard to prevent accidental exposure\n *\n * ## Usage:\n * ```typescript\n * // astro.config.mjs\n * import { defineConfig } from 'astro/config';\n * import writenex from '@writenex/astro';\n *\n * export default defineConfig({\n * integrations: [writenex()],\n * });\n * ```\n *\n * @module @writenex/astro/integration\n */\n\nimport type { AstroIntegration } from \"astro\";\nimport { loadConfig } from \"@/config/loader\";\nimport { ContentWatcher } from \"@/filesystem/watcher\";\nimport { getCache } from \"@/server/cache\";\nimport { createMiddleware } from \"@/server/middleware\";\nimport type { WritenexConfig, WritenexOptions } from \"@/types\";\n\n/**\n * Default base path for the Writenex editor UI\n */\nconst DEFAULT_BASE_PATH = \"/_writenex\";\n\n/**\n * Package name for logging\n */\nconst PACKAGE_NAME = \"@writenex/astro\";\n\n/**\n * Creates the Writenex Astro integration.\n *\n * This integration injects the Writenex visual editor into your Astro project,\n * providing a WYSIWYG interface for editing content collections.\n *\n * @param options - Integration options\n * @param options.allowProduction - Allow running in production (default: false)\n * @returns Astro integration object\n *\n * @example\n * ```typescript\n * // Basic usage\n * export default defineConfig({\n * integrations: [writenex()],\n * });\n *\n * // With options\n * export default defineConfig({\n * integrations: [\n * writenex({\n * allowProduction: true, // Enable in production (use with caution)\n * }),\n * ],\n * });\n * ```\n */\nexport default function writenex(options?: WritenexOptions): AstroIntegration {\n const { allowProduction = false } = options ?? {};\n\n // Use fixed base path for consistency and branding\n const basePath = DEFAULT_BASE_PATH;\n\n // Track if we should be active\n let isActive = true;\n\n // Store loaded configuration\n let resolvedConfig: Required<WritenexConfig> | null = null;\n\n // Store project root\n let projectRoot = \"\";\n\n // Store Astro's trailingSlash setting\n let astroTrailingSlash: \"always\" | \"never\" | \"ignore\" = \"ignore\";\n\n // File watcher instance\n let watcher: ContentWatcher | null = null;\n\n // Track if editor URL has been logged (to avoid duplicate logs)\n let hasLoggedEditorUrl = false;\n\n return {\n name: PACKAGE_NAME,\n hooks: {\n /**\n * Configuration setup hook\n *\n * This hook runs during Astro's config resolution phase.\n * We use it to:\n * 1. Check if we should run (production guard)\n * 2. Load Writenex configuration\n * 3. Register any necessary Vite plugins\n */\n \"astro:config:setup\": async ({ command, logger, config }) => {\n // Production guard: disable in production unless explicitly allowed\n if (command === \"build\" && !allowProduction) {\n logger.warn(\n \"Disabled in production build. Use allowProduction: true to override.\"\n );\n isActive = false;\n return;\n }\n\n // Store project root\n projectRoot = config.root.pathname;\n\n // Capture Astro's trailingSlash setting for preview URLs\n astroTrailingSlash = config.trailingSlash ?? \"ignore\";\n\n // Load Writenex configuration\n const { config: loadedConfig, warnings } =\n await loadConfig(projectRoot);\n resolvedConfig = loadedConfig;\n\n // Log any configuration warnings\n for (const warning of warnings) {\n logger.warn(warning);\n }\n },\n\n /**\n * Server setup hook\n *\n * This hook runs when the Astro dev server starts.\n * We use it to:\n * 1. Inject middleware for API routes\n * 2. Serve the editor UI\n * 3. Start file watcher for cache invalidation\n */\n \"astro:server:setup\": ({ server }) => {\n // Skip if disabled (production guard triggered)\n if (!isActive || !resolvedConfig) {\n return;\n }\n\n // Create and register the middleware\n const middleware = createMiddleware({\n basePath,\n projectRoot,\n config: resolvedConfig,\n trailingSlash: astroTrailingSlash,\n });\n\n server.middlewares.use(middleware);\n\n // Setup cache with file watcher integration\n const cache = getCache({ hasWatcher: true });\n\n // Start file watcher for cache invalidation\n watcher = new ContentWatcher(projectRoot, \"src/content\", {\n onChange: (event) => {\n cache.handleFileChange(event.type, event.collection);\n },\n });\n\n watcher.start();\n },\n\n /**\n * Server start hook\n *\n * This hook runs after the dev server has started and is listening.\n * We use it to log the full editor URL with the actual server address.\n */\n \"astro:server:start\": ({ address, logger }) => {\n if (!isActive || hasLoggedEditorUrl) {\n return;\n }\n\n // Build the full URL from the server address\n // Normalize loopback addresses to \"localhost\" for better readability\n const protocol = \"http\";\n const rawHost = address.address;\n const isLoopback =\n rawHost === \"\" ||\n rawHost === \"::\" ||\n rawHost === \"127.0.0.1\" ||\n rawHost === \"::1\";\n const host = isLoopback ? \"localhost\" : rawHost;\n const port = address.port;\n const editorUrl = `${protocol}://${host}:${port}${basePath}`;\n\n logger.info(`Writenex editor running at: ${editorUrl}`);\n hasLoggedEditorUrl = true;\n },\n\n /**\n * Server done hook\n *\n * This hook runs when the server is shutting down.\n * We use it to clean up the file watcher.\n */\n \"astro:server:done\": async () => {\n if (watcher) {\n await watcher.stop();\n watcher = null;\n }\n },\n\n /**\n * Build done hook\n *\n * This hook runs after the build completes.\n * Currently just logs a warning if production mode is enabled.\n */\n \"astro:build:done\": ({ logger }) => {\n if (allowProduction) {\n logger.warn(\n \"Production mode enabled. Ensure your deployment is secured.\"\n );\n }\n },\n },\n };\n}\n","/**\n * @fileoverview Shared constants for @writenex/astro\n *\n * This module provides centralized constants used across the integration,\n * including version information, default paths, and configuration limits.\n *\n * @module @writenex/astro/core/constants\n */\n\n/**\n * Current version of the @writenex/astro package\n */\nexport const WRITENEX_VERSION = \"1.0.0\";\n\n/**\n * Default base path for the Writenex editor UI\n */\nexport const DEFAULT_BASE_PATH = \"/_writenex\";\n\n/**\n * Default API path for Writenex API endpoints\n */\nexport const DEFAULT_API_PATH = \"/_writenex/api\";\n\n/**\n * Supported image MIME types for upload\n */\nexport const SUPPORTED_IMAGE_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n] as const;\n\n/**\n * Maximum allowed image file size in bytes (10MB)\n */\nexport const MAX_IMAGE_SIZE = 10 * 1024 * 1024;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,oBAAoB;AAK1B,IAAM,eAAe;AA6BN,SAAR,SAA0B,SAA6C;AAC5E,QAAM,EAAE,kBAAkB,MAAM,IAAI,WAAW,CAAC;AAGhD,QAAM,WAAW;AAGjB,MAAI,WAAW;AAGf,MAAI,iBAAkD;AAGtD,MAAI,cAAc;AAGlB,MAAI,qBAAoD;AAGxD,MAAI,UAAiC;AAGrC,MAAI,qBAAqB;AAEzB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAUL,sBAAsB,OAAO,EAAE,SAAS,QAAQ,OAAO,MAAM;AAE3D,YAAI,YAAY,WAAW,CAAC,iBAAiB;AAC3C,iBAAO;AAAA,YACL;AAAA,UACF;AACA,qBAAW;AACX;AAAA,QACF;
|
|
1
|
+
{"version":3,"sources":["../src/integration.ts","../src/core/constants.ts"],"sourcesContent":["/**\n * @fileoverview Astro integration for Writenex visual editor\n *\n * This module provides the main Astro integration that injects the Writenex\n * editor UI and API routes into an Astro project.\n *\n * ## Features:\n * - Injects editor UI at /_writenex\n * - Provides API routes for content CRUD operations\n * - Auto-discovers content collections\n * - Production guard to prevent accidental exposure\n *\n * ## Usage:\n * ```typescript\n * // astro.config.mjs\n * import { defineConfig } from 'astro/config';\n * import writenex from '@writenex/astro';\n *\n * export default defineConfig({\n * integrations: [writenex()],\n * });\n * ```\n *\n * @module @writenex/astro/integration\n */\n\nimport type { AstroIntegration } from \"astro\";\nimport { loadConfig } from \"@/config/loader\";\nimport { ContentWatcher } from \"@/filesystem/watcher\";\nimport { getCache } from \"@/server/cache\";\nimport { createMiddleware } from \"@/server/middleware\";\nimport type { WritenexConfig, WritenexOptions } from \"@/types\";\n\n/**\n * Default base path for the Writenex editor UI\n */\nconst DEFAULT_BASE_PATH = \"/_writenex\";\n\n/**\n * Package name for logging\n */\nconst PACKAGE_NAME = \"@writenex/astro\";\n\n/**\n * Creates the Writenex Astro integration.\n *\n * This integration injects the Writenex visual editor into your Astro project,\n * providing a WYSIWYG interface for editing content collections.\n *\n * @param options - Integration options\n * @param options.allowProduction - Allow running in production (default: false)\n * @returns Astro integration object\n *\n * @example\n * ```typescript\n * // Basic usage\n * export default defineConfig({\n * integrations: [writenex()],\n * });\n *\n * // With options\n * export default defineConfig({\n * integrations: [\n * writenex({\n * allowProduction: true, // Enable in production (use with caution)\n * }),\n * ],\n * });\n * ```\n */\nexport default function writenex(options?: WritenexOptions): AstroIntegration {\n const { allowProduction = false } = options ?? {};\n\n // Use fixed base path for consistency and branding\n const basePath = DEFAULT_BASE_PATH;\n\n // Track if we should be active\n let isActive = true;\n\n // Store loaded configuration\n let resolvedConfig: Required<WritenexConfig> | null = null;\n\n // Store project root\n let projectRoot = \"\";\n\n // Store Astro's trailingSlash setting\n let astroTrailingSlash: \"always\" | \"never\" | \"ignore\" = \"ignore\";\n\n // File watcher instance\n let watcher: ContentWatcher | null = null;\n\n // Track if editor URL has been logged (to avoid duplicate logs)\n let hasLoggedEditorUrl = false;\n\n return {\n name: PACKAGE_NAME,\n hooks: {\n /**\n * Configuration setup hook\n *\n * This hook runs during Astro's config resolution phase.\n * We use it to:\n * 1. Check if we should run (production guard)\n * 2. Load Writenex configuration\n * 3. Register any necessary Vite plugins\n */\n \"astro:config:setup\": async ({ command, logger, config }) => {\n // Production guard: disable in production unless explicitly allowed\n if (command === \"build\" && !allowProduction) {\n logger.warn(\n \"Disabled in production build. Use allowProduction: true to override.\"\n );\n isActive = false;\n return;\n }\n\n // Store project root\n // On Windows, URL.pathname produces /C:/Users/... with a leading slash\n // before the drive letter which breaks path.join and fs.existsSync.\n // Strip that leading slash so we get a valid Windows path (C:\\Users\\...).\n projectRoot = config.root.pathname.replace(/^\\/([A-Za-z]:)/, \"$1\");\n\n // Capture Astro's trailingSlash setting for preview URLs\n astroTrailingSlash = config.trailingSlash ?? \"ignore\";\n\n // Load Writenex configuration\n const { config: loadedConfig, warnings } =\n await loadConfig(projectRoot);\n resolvedConfig = loadedConfig;\n\n // Log any configuration warnings\n for (const warning of warnings) {\n logger.warn(warning);\n }\n },\n\n /**\n * Server setup hook\n *\n * This hook runs when the Astro dev server starts.\n * We use it to:\n * 1. Inject middleware for API routes\n * 2. Serve the editor UI\n * 3. Start file watcher for cache invalidation\n */\n \"astro:server:setup\": ({ server }) => {\n // Skip if disabled (production guard triggered)\n if (!isActive || !resolvedConfig) {\n return;\n }\n\n // Create and register the middleware\n const middleware = createMiddleware({\n basePath,\n projectRoot,\n config: resolvedConfig,\n trailingSlash: astroTrailingSlash,\n });\n\n server.middlewares.use(middleware);\n\n // Setup cache with file watcher integration\n const cache = getCache({ hasWatcher: true });\n\n // Start file watcher for cache invalidation\n watcher = new ContentWatcher(projectRoot, \"src/content\", {\n onChange: (event) => {\n cache.handleFileChange(event.type, event.collection);\n },\n });\n\n watcher.start();\n },\n\n /**\n * Server start hook\n *\n * This hook runs after the dev server has started and is listening.\n * We use it to log the full editor URL with the actual server address.\n */\n \"astro:server:start\": ({ address, logger }) => {\n if (!isActive || hasLoggedEditorUrl) {\n return;\n }\n\n // Build the full URL from the server address\n // Normalize loopback addresses to \"localhost\" for better readability\n const protocol = \"http\";\n const rawHost = address.address;\n const isLoopback =\n rawHost === \"\" ||\n rawHost === \"::\" ||\n rawHost === \"127.0.0.1\" ||\n rawHost === \"::1\";\n const host = isLoopback ? \"localhost\" : rawHost;\n const port = address.port;\n const editorUrl = `${protocol}://${host}:${port}${basePath}`;\n\n logger.info(`Writenex editor running at: ${editorUrl}`);\n hasLoggedEditorUrl = true;\n },\n\n /**\n * Server done hook\n *\n * This hook runs when the server is shutting down.\n * We use it to clean up the file watcher.\n */\n \"astro:server:done\": async () => {\n if (watcher) {\n await watcher.stop();\n watcher = null;\n }\n },\n\n /**\n * Build done hook\n *\n * This hook runs after the build completes.\n * Currently just logs a warning if production mode is enabled.\n */\n \"astro:build:done\": ({ logger }) => {\n if (allowProduction) {\n logger.warn(\n \"Production mode enabled. Ensure your deployment is secured.\"\n );\n }\n },\n },\n };\n}\n","/**\n * @fileoverview Shared constants for @writenex/astro\n *\n * This module provides centralized constants used across the integration,\n * including version information, default paths, and configuration limits.\n *\n * @module @writenex/astro/core/constants\n */\n\n/**\n * Current version of the @writenex/astro package\n */\nexport const WRITENEX_VERSION = \"1.0.0\";\n\n/**\n * Default base path for the Writenex editor UI\n */\nexport const DEFAULT_BASE_PATH = \"/_writenex\";\n\n/**\n * Default API path for Writenex API endpoints\n */\nexport const DEFAULT_API_PATH = \"/_writenex/api\";\n\n/**\n * Supported image MIME types for upload\n */\nexport const SUPPORTED_IMAGE_TYPES = [\n \"image/jpeg\",\n \"image/png\",\n \"image/gif\",\n \"image/webp\",\n \"image/svg+xml\",\n] as const;\n\n/**\n * Maximum allowed image file size in bytes (10MB)\n */\nexport const MAX_IMAGE_SIZE = 10 * 1024 * 1024;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;AAoCA,IAAM,oBAAoB;AAK1B,IAAM,eAAe;AA6BN,SAAR,SAA0B,SAA6C;AAC5E,QAAM,EAAE,kBAAkB,MAAM,IAAI,WAAW,CAAC;AAGhD,QAAM,WAAW;AAGjB,MAAI,WAAW;AAGf,MAAI,iBAAkD;AAGtD,MAAI,cAAc;AAGlB,MAAI,qBAAoD;AAGxD,MAAI,UAAiC;AAGrC,MAAI,qBAAqB;AAEzB,SAAO;AAAA,IACL,MAAM;AAAA,IACN,OAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAUL,sBAAsB,OAAO,EAAE,SAAS,QAAQ,OAAO,MAAM;AAE3D,YAAI,YAAY,WAAW,CAAC,iBAAiB;AAC3C,iBAAO;AAAA,YACL;AAAA,UACF;AACA,qBAAW;AACX;AAAA,QACF;AAMA,sBAAc,OAAO,KAAK,SAAS,QAAQ,kBAAkB,IAAI;AAGjE,6BAAqB,OAAO,iBAAiB;AAG7C,cAAM,EAAE,QAAQ,cAAc,SAAS,IACrC,MAAM,WAAW,WAAW;AAC9B,yBAAiB;AAGjB,mBAAW,WAAW,UAAU;AAC9B,iBAAO,KAAK,OAAO;AAAA,QACrB;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAWA,sBAAsB,CAAC,EAAE,OAAO,MAAM;AAEpC,YAAI,CAAC,YAAY,CAAC,gBAAgB;AAChC;AAAA,QACF;AAGA,cAAM,aAAa,iBAAiB;AAAA,UAClC;AAAA,UACA;AAAA,UACA,QAAQ;AAAA,UACR,eAAe;AAAA,QACjB,CAAC;AAED,eAAO,YAAY,IAAI,UAAU;AAGjC,cAAM,QAAQ,SAAS,EAAE,YAAY,KAAK,CAAC;AAG3C,kBAAU,IAAI,eAAe,aAAa,eAAe;AAAA,UACvD,UAAU,CAAC,UAAU;AACnB,kBAAM,iBAAiB,MAAM,MAAM,MAAM,UAAU;AAAA,UACrD;AAAA,QACF,CAAC;AAED,gBAAQ,MAAM;AAAA,MAChB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,sBAAsB,CAAC,EAAE,SAAS,OAAO,MAAM;AAC7C,YAAI,CAAC,YAAY,oBAAoB;AACnC;AAAA,QACF;AAIA,cAAM,WAAW;AACjB,cAAM,UAAU,QAAQ;AACxB,cAAM,aACJ,YAAY,MACZ,YAAY,QACZ,YAAY,eACZ,YAAY;AACd,cAAM,OAAO,aAAa,cAAc;AACxC,cAAM,OAAO,QAAQ;AACrB,cAAM,YAAY,GAAG,QAAQ,MAAM,IAAI,IAAI,IAAI,GAAG,QAAQ;AAE1D,eAAO,KAAK,+BAA+B,SAAS,EAAE;AACtD,6BAAqB;AAAA,MACvB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,qBAAqB,YAAY;AAC/B,YAAI,SAAS;AACX,gBAAM,QAAQ,KAAK;AACnB,oBAAU;AAAA,QACZ;AAAA,MACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,MAQA,oBAAoB,CAAC,EAAE,OAAO,MAAM;AAClC,YAAI,iBAAiB;AACnB,iBAAO;AAAA,YACL;AAAA,UACF;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;;;AChMO,IAAM,iBAAiB,KAAK,OAAO;","names":[]}
|
|
@@ -2,11 +2,11 @@ import {
|
|
|
2
2
|
contentDirectoryExists,
|
|
3
3
|
findConfigFile,
|
|
4
4
|
loadConfig
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-N37EPLKG.js";
|
|
6
6
|
import "./chunk-YBCPOLMY.js";
|
|
7
7
|
export {
|
|
8
8
|
contentDirectoryExists,
|
|
9
9
|
findConfigFile,
|
|
10
10
|
loadConfig
|
|
11
11
|
};
|
|
12
|
-
//# sourceMappingURL=loader-
|
|
12
|
+
//# sourceMappingURL=loader-53VVP2IN.js.map
|
package/dist/server/index.js
CHANGED
|
@@ -13,10 +13,10 @@ import {
|
|
|
13
13
|
sendWritenexError,
|
|
14
14
|
serveAsset,
|
|
15
15
|
serveEditorHtml
|
|
16
|
-
} from "../chunk-
|
|
17
|
-
import "../chunk-
|
|
18
|
-
import "../chunk-
|
|
19
|
-
import "../chunk-
|
|
16
|
+
} from "../chunk-NSW7AIVF.js";
|
|
17
|
+
import "../chunk-JMNCPNQX.js";
|
|
18
|
+
import "../chunk-4H63L4YO.js";
|
|
19
|
+
import "../chunk-GYAFIVVI.js";
|
|
20
20
|
import "../chunk-YBCPOLMY.js";
|
|
21
21
|
export {
|
|
22
22
|
ServerCache,
|
package/package.json
CHANGED
package/src/config/loader.ts
CHANGED
|
@@ -19,6 +19,21 @@ import type { WritenexConfig } from "@/types";
|
|
|
19
19
|
import { applyConfigDefaults } from "./defaults";
|
|
20
20
|
import { validateConfig } from "./schema";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a project root path that may have come from URL.pathname.
|
|
24
|
+
*
|
|
25
|
+
* On Windows, `config.root.pathname` returns `/C:/Users/...` — a leading slash
|
|
26
|
+
* before the drive letter that makes every subsequent `path.join` and
|
|
27
|
+
* `fs.existsSync` call fail silently. Strip it so downstream code gets a
|
|
28
|
+
* valid Windows path like `C:\Users\...`.
|
|
29
|
+
*
|
|
30
|
+
* On macOS/Linux the path starts with a real `/` so the regex won't match
|
|
31
|
+
* and the string is returned unchanged.
|
|
32
|
+
*/
|
|
33
|
+
function normalizeProjectRoot(projectRoot: string): string {
|
|
34
|
+
return projectRoot.replace(/^\/([A-Za-z]:)/, "$1");
|
|
35
|
+
}
|
|
36
|
+
|
|
22
37
|
/**
|
|
23
38
|
* Supported configuration file names in order of priority
|
|
24
39
|
*/
|
|
@@ -50,6 +65,7 @@ export interface LoadConfigResult {
|
|
|
50
65
|
* @returns Path to the configuration file, or null if not found
|
|
51
66
|
*/
|
|
52
67
|
export function findConfigFile(projectRoot: string): string | null {
|
|
68
|
+
projectRoot = normalizeProjectRoot(projectRoot);
|
|
53
69
|
for (const fileName of CONFIG_FILE_NAMES) {
|
|
54
70
|
const filePath = join(projectRoot, fileName);
|
|
55
71
|
if (existsSync(filePath)) {
|
|
@@ -117,6 +133,7 @@ async function loadConfigFile(configPath: string): Promise<WritenexConfig> {
|
|
|
117
133
|
export async function loadConfig(
|
|
118
134
|
projectRoot: string
|
|
119
135
|
): Promise<LoadConfigResult> {
|
|
136
|
+
projectRoot = normalizeProjectRoot(projectRoot);
|
|
120
137
|
const warnings: string[] = [];
|
|
121
138
|
let userConfig: WritenexConfig = {};
|
|
122
139
|
let configPath: string | null = null;
|
|
@@ -171,6 +188,7 @@ export function contentDirectoryExists(
|
|
|
171
188
|
projectRoot: string,
|
|
172
189
|
contentPath: string = "src/content"
|
|
173
190
|
): boolean {
|
|
191
|
+
projectRoot = normalizeProjectRoot(projectRoot);
|
|
174
192
|
const fullPath = join(projectRoot, contentPath);
|
|
175
193
|
return existsSync(fullPath);
|
|
176
194
|
}
|
package/src/filesystem/reader.ts
CHANGED
|
@@ -82,8 +82,10 @@ export function extractSlug(filePath: string, collectionPath: string): string {
|
|
|
82
82
|
const ext = extname(filename);
|
|
83
83
|
|
|
84
84
|
// Handle index files (folder-based content)
|
|
85
|
+
// On Windows, path.relative() uses backslashes — normalise to forward slashes
|
|
86
|
+
// so the split works on both platforms.
|
|
85
87
|
if (filename === "index.md" || filename === "index.mdx") {
|
|
86
|
-
const parts = relativePath.split("/");
|
|
88
|
+
const parts = relativePath.replace(/\\/g, "/").split("/");
|
|
87
89
|
if (parts.length >= 2) {
|
|
88
90
|
const slug = parts[parts.length - 2];
|
|
89
91
|
if (slug) return slug;
|
package/src/integration.ts
CHANGED
|
@@ -115,7 +115,10 @@ export default function writenex(options?: WritenexOptions): AstroIntegration {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
// Store project root
|
|
118
|
-
|
|
118
|
+
// On Windows, URL.pathname produces /C:/Users/... with a leading slash
|
|
119
|
+
// before the drive letter which breaks path.join and fs.existsSync.
|
|
120
|
+
// Strip that leading slash so we get a valid Windows path (C:\Users\...).
|
|
121
|
+
projectRoot = config.root.pathname.replace(/^\/([A-Za-z]:)/, "$1");
|
|
119
122
|
|
|
120
123
|
// Capture Astro's trailingSlash setting for preview URLs
|
|
121
124
|
astroTrailingSlash = config.trailingSlash ?? "ignore";
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/config/loader.ts","../src/config/schema.ts"],"sourcesContent":["/**\n * @fileoverview Configuration loader for @writenex/astro\n *\n * This module handles loading Writenex configuration from various sources:\n * - writenex.config.ts (TypeScript)\n * - writenex.config.js (JavaScript)\n * - writenex.config.mjs (ES Module)\n *\n * The loader searches for configuration files in the project root and\n * applies default values for any missing options.\n *\n * @module @writenex/astro/config/loader\n */\n\nimport { existsSync } from \"node:fs\";\nimport { join, resolve } from \"node:path\";\nimport { pathToFileURL } from \"node:url\";\nimport type { WritenexConfig } from \"@/types\";\nimport { applyConfigDefaults } from \"./defaults\";\nimport { validateConfig } from \"./schema\";\n\n/**\n * Supported configuration file names in order of priority\n */\nconst CONFIG_FILE_NAMES = [\n \"writenex.config.ts\",\n \"writenex.config.mts\",\n \"writenex.config.js\",\n \"writenex.config.mjs\",\n];\n\n/**\n * Result of loading configuration\n */\nexport interface LoadConfigResult {\n /** The loaded and validated configuration with defaults applied */\n config: Required<WritenexConfig>;\n /** Path to the configuration file (if found) */\n configPath: string | null;\n /** Whether a configuration file was found */\n hasConfigFile: boolean;\n /** Any warnings generated during loading */\n warnings: string[];\n}\n\n/**\n * Find the configuration file in the project root\n *\n * @param projectRoot - The root directory of the Astro project\n * @returns Path to the configuration file, or null if not found\n */\nexport function findConfigFile(projectRoot: string): string | null {\n for (const fileName of CONFIG_FILE_NAMES) {\n const filePath = join(projectRoot, fileName);\n if (existsSync(filePath)) {\n return filePath;\n }\n }\n return null;\n}\n\n/**\n * Load configuration from a file\n *\n * @param configPath - Path to the configuration file\n * @returns The loaded configuration object\n * @throws Error if the file cannot be loaded or parsed\n */\nasync function loadConfigFile(configPath: string): Promise<WritenexConfig> {\n try {\n // Convert to file URL for dynamic import (required for Windows compatibility)\n const fileUrl = pathToFileURL(resolve(configPath)).href;\n\n // Dynamic import the configuration file\n const module = await import(fileUrl);\n\n // Support both default export and named export\n const config = module.default ?? module.config ?? module;\n\n return config as WritenexConfig;\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(\n `Failed to load configuration from ${configPath}: ${message}`\n );\n }\n}\n\n/**\n * Load Writenex configuration from the project root\n *\n * This function:\n * 1. Searches for a configuration file in the project root\n * 2. Loads and validates the configuration if found\n * 3. Applies default values for any missing options\n * 4. Returns the resolved configuration\n *\n * If no configuration file is found, default configuration is returned\n * with auto-discovery enabled.\n *\n * @param projectRoot - The root directory of the Astro project\n * @returns LoadConfigResult with the resolved configuration\n *\n * @example\n * ```typescript\n * const { config, hasConfigFile, warnings } = await loadConfig('/path/to/project');\n *\n * if (!hasConfigFile) {\n * console.log('Using auto-discovery mode');\n * }\n *\n * if (warnings.length > 0) {\n * warnings.forEach(w => console.warn(w));\n * }\n * ```\n */\nexport async function loadConfig(\n projectRoot: string\n): Promise<LoadConfigResult> {\n const warnings: string[] = [];\n let userConfig: WritenexConfig = {};\n let configPath: string | null = null;\n let hasConfigFile = false;\n\n // Try to find and load configuration file\n configPath = findConfigFile(projectRoot);\n\n if (configPath) {\n hasConfigFile = true;\n\n try {\n userConfig = await loadConfigFile(configPath);\n\n // Validate the loaded configuration\n const validationResult = validateConfig(userConfig);\n\n if (!validationResult.success) {\n const errors = validationResult.error.issues\n .map(\n (e: import(\"zod\").ZodIssue) => `${e.path.join(\".\")}: ${e.message}`\n )\n .join(\", \");\n warnings.push(`Configuration validation warnings: ${errors}`);\n }\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n warnings.push(`Failed to load config file: ${message}. Using defaults.`);\n userConfig = {};\n }\n }\n\n // Apply defaults to the configuration\n const config = applyConfigDefaults(userConfig);\n\n return {\n config,\n configPath,\n hasConfigFile,\n warnings,\n };\n}\n\n/**\n * Check if a content directory exists in the project\n *\n * @param projectRoot - The root directory of the Astro project\n * @param contentPath - Relative path to the content directory\n * @returns True if the content directory exists\n */\nexport function contentDirectoryExists(\n projectRoot: string,\n contentPath: string = \"src/content\"\n): boolean {\n const fullPath = join(projectRoot, contentPath);\n return existsSync(fullPath);\n}\n","/**\n * @fileoverview Configuration schema and validation for @writenex/astro\n *\n * This module provides Zod schemas for validating Writenex configuration\n * and a helper function for defining type-safe configurations.\n *\n * @module @writenex/astro/config/schema\n */\n\nimport { z } from \"zod\";\nimport type { WritenexConfig } from \"@/types\";\n\n/**\n * Schema for field type definitions\n */\nconst fieldTypeSchema = z.enum([\n \"string\",\n \"number\",\n \"boolean\",\n \"date\",\n \"array\",\n \"image\",\n \"object\",\n]);\n\n/**\n * Schema for individual schema field definition\n */\nconst schemaFieldSchema = z.object({\n type: fieldTypeSchema,\n required: z.boolean().optional(),\n default: z.unknown().optional(),\n items: z.string().optional(),\n description: z.string().optional(),\n});\n\n/**\n * Schema for collection schema (record of field definitions)\n */\nconst collectionSchemaSchema = z.record(z.string(), schemaFieldSchema);\n\n/**\n * Schema for image strategy\n */\nconst imageStrategySchema = z.enum([\"colocated\", \"public\", \"custom\"]);\n\n/**\n * Schema for image configuration\n */\nconst imageConfigSchema = z.object({\n strategy: imageStrategySchema,\n publicPath: z.string().optional(),\n storagePath: z.string().optional(),\n});\n\n/**\n * Schema for collection configuration\n */\nconst collectionConfigSchema = z.object({\n name: z.string().min(1, \"Collection name is required\"),\n path: z.string().min(1, \"Collection path is required\"),\n filePattern: z.string().optional(),\n previewUrl: z.string().optional(),\n schema: collectionSchemaSchema.optional(),\n images: imageConfigSchema.optional(),\n});\n\n/**\n * Schema for discovery configuration\n */\nconst discoveryConfigSchema = z.object({\n enabled: z.boolean(),\n ignore: z.array(z.string()).optional(),\n});\n\n/**\n * Schema for editor configuration\n */\nconst editorConfigSchema = z.object({\n autosave: z.boolean().optional(),\n autosaveInterval: z.number().positive().optional(),\n});\n\n/**\n * Schema for version history configuration\n */\nconst versionHistoryConfigSchema = z.object({\n enabled: z.boolean().optional(),\n maxVersions: z.number().int().positive().optional(),\n storagePath: z.string().optional(),\n});\n\n/**\n * Main Writenex configuration schema\n */\nexport const writenexConfigSchema = z.object({\n collections: z.array(collectionConfigSchema).optional(),\n images: imageConfigSchema.optional(),\n editor: editorConfigSchema.optional(),\n discovery: discoveryConfigSchema.optional(),\n versionHistory: versionHistoryConfigSchema.optional(),\n});\n\n/**\n * Schema for integration options\n */\nexport const writenexOptionsSchema = z.object({\n allowProduction: z.boolean().optional(),\n});\n\n/**\n * Helper function for defining type-safe Writenex configuration.\n *\n * This function provides IDE autocompletion and type checking for\n * the configuration object. It's the recommended way to create\n * a writenex.config.ts file.\n *\n * @param config - The Writenex configuration object\n * @returns The same configuration object (identity function for type safety)\n *\n * @example\n * ```typescript\n * // writenex.config.ts\n * import { defineConfig } from '@writenex/astro';\n *\n * export default defineConfig({\n * collections: [\n * {\n * name: 'blog',\n * path: 'src/content/blog',\n * filePattern: '{slug}.md',\n * },\n * ],\n * });\n * ```\n */\nexport function defineConfig(config: WritenexConfig): WritenexConfig {\n // Validate the configuration\n const result = writenexConfigSchema.safeParse(config);\n\n if (!result.success) {\n const errors = result.error.issues\n .map((e: z.ZodIssue) => ` - ${e.path.join(\".\")}: ${e.message}`)\n .join(\"\\n\");\n console.warn(`[writenex] Invalid configuration:\\n${errors}`);\n }\n\n return config;\n}\n\n/**\n * Validate a Writenex configuration object\n *\n * @param config - The configuration to validate\n * @returns Validation result with success status and parsed data or errors\n */\nexport function validateConfig(\n config: unknown\n):\n | { success: true; data: WritenexConfig }\n | { success: false; error: z.ZodError } {\n return writenexConfigSchema.safeParse(config) as\n | { success: true; data: WritenexConfig }\n | { success: false; error: z.ZodError };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/filesystem/reader.ts","../src/discovery/patterns.ts"],"sourcesContent":["/**\n * @fileoverview Filesystem reader for content collections\n *\n * This module provides functions for reading content files from the filesystem,\n * parsing frontmatter, and extracting content metadata.\n *\n * ## Features:\n * - Read individual content files with frontmatter parsing\n * - List all content files in a collection\n * - Generate content summaries for listing\n * - Support for .md and .mdx files\n *\n * @module @writenex/astro/filesystem/reader\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir, readFile, stat } from \"node:fs/promises\";\nimport { basename, extname, join, relative } from \"node:path\";\nimport matter from \"gray-matter\";\nimport type { ContentItem, ContentSummary } from \"@/types\";\n\n/**\n * Supported content file extensions\n */\nconst CONTENT_EXTENSIONS = [\".md\", \".mdx\"];\n\n/**\n * Maximum excerpt length in characters\n */\nconst EXCERPT_LENGTH = 150;\n\n/**\n * Options for reading content\n */\nexport interface ReadContentOptions {\n /** Include draft content in listings */\n includeDrafts?: boolean;\n /** Sort field for listings */\n sortBy?: string;\n /** Sort order */\n sortOrder?: \"asc\" | \"desc\";\n}\n\n/**\n * Result of reading a content file\n */\nexport interface ReadFileResult {\n /** Whether the read was successful */\n success: boolean;\n /** The content item (if successful) */\n content?: ContentItem;\n /** Error message (if failed) */\n error?: string;\n}\n\n/**\n * Check if a file is a content file based on extension\n *\n * @param filename - The filename to check\n * @returns True if the file is a content file\n */\nexport function isContentFile(filename: string): boolean {\n const ext = extname(filename).toLowerCase();\n return CONTENT_EXTENSIONS.includes(ext);\n}\n\n/**\n * Extract slug from a content file path\n *\n * Handles various file patterns:\n * - `my-post.md` -> `my-post`\n * - `2024-01-15-my-post.md` -> `2024-01-15-my-post`\n * - `my-post/index.md` -> `my-post`\n *\n * @param filePath - Path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns The extracted slug\n */\nexport function extractSlug(filePath: string, collectionPath: string): string {\n const relativePath = relative(collectionPath, filePath);\n const filename = basename(relativePath);\n const ext = extname(filename);\n\n // Handle index files (folder-based content)\n if (filename === \"index.md\" || filename === \"index.mdx\") {\n const parts = relativePath.split(\"/\");\n if (parts.length >= 2) {\n const slug = parts[parts.length - 2];\n if (slug) return slug;\n }\n }\n\n // Remove extension to get slug\n return filename.slice(0, -ext.length);\n}\n\n/**\n * Generate an excerpt from markdown content\n *\n * @param body - The markdown body content\n * @param maxLength - Maximum excerpt length\n * @returns The generated excerpt\n */\nexport function generateExcerpt(\n body: string,\n maxLength: number = EXCERPT_LENGTH\n): string {\n // Remove markdown formatting for cleaner excerpt\n const cleaned = body\n // Remove headers\n .replace(/^#{1,6}\\s+/gm, \"\")\n // Remove bold/italic\n .replace(/\\*\\*([^*]+)\\*\\*/g, \"$1\")\n .replace(/\\*([^*]+)\\*/g, \"$1\")\n .replace(/__([^_]+)__/g, \"$1\")\n .replace(/_([^_]+)_/g, \"$1\")\n // Remove links but keep text\n .replace(/\\[([^\\]]+)\\]\\([^)]+\\)/g, \"$1\")\n // Remove images\n .replace(/!\\[([^\\]]*)\\]\\([^)]+\\)/g, \"\")\n // Remove code blocks\n .replace(/```[\\s\\S]*?```/g, \"\")\n .replace(/`([^`]+)`/g, \"$1\")\n // Remove blockquotes\n .replace(/^>\\s+/gm, \"\")\n // Remove horizontal rules\n .replace(/^[-*_]{3,}$/gm, \"\")\n // Collapse whitespace\n .replace(/\\s+/g, \" \")\n .trim();\n\n if (cleaned.length <= maxLength) {\n return cleaned;\n }\n\n // Truncate at word boundary\n const truncated = cleaned.slice(0, maxLength);\n const lastSpace = truncated.lastIndexOf(\" \");\n\n if (lastSpace > maxLength * 0.7) {\n return truncated.slice(0, lastSpace) + \"...\";\n }\n\n return truncated + \"...\";\n}\n\n/**\n * Read and parse a single content file\n *\n * @param filePath - Absolute path to the content file\n * @param collectionPath - Path to the collection directory\n * @returns ReadFileResult with the parsed content or error\n *\n * @example\n * ```typescript\n * const result = await readContentFile(\n * '/project/src/content/blog/my-post.md',\n * '/project/src/content/blog'\n * );\n *\n * if (result.success) {\n * console.log(result.content.frontmatter.title);\n * }\n * ```\n */\nexport async function readContentFile(\n filePath: string,\n collectionPath: string\n): Promise<ReadFileResult> {\n try {\n // Check if file exists\n if (!existsSync(filePath)) {\n return {\n success: false,\n error: `File not found: ${filePath}`,\n };\n }\n\n // Read file content and stats in parallel\n const [raw, stats] = await Promise.all([\n readFile(filePath, \"utf-8\"),\n stat(filePath),\n ]);\n\n // Parse frontmatter\n const { data: frontmatter, content: body } = matter(raw);\n\n // Extract slug\n const id = extractSlug(filePath, collectionPath);\n\n return {\n success: true,\n content: {\n id,\n path: filePath,\n frontmatter,\n body: body.trim(),\n raw,\n mtime: stats.mtimeMs,\n },\n };\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n return {\n success: false,\n error: `Failed to read content file: ${message}`,\n };\n }\n}\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Path to the directory to scan\n * @returns Array of absolute file paths\n */\nasync function listFilesRecursive(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n const entries = await readdir(dirPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(dirPath, entry.name);\n\n if (entry.isDirectory()) {\n // Recursively scan subdirectories\n const subFiles = await listFilesRecursive(fullPath);\n files.push(...subFiles);\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(fullPath);\n }\n }\n\n return files;\n}\n\n/**\n * Read all content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content items\n *\n * @example\n * ```typescript\n * const items = await readCollection('/project/src/content/blog', {\n * includeDrafts: false,\n * sortBy: 'pubDate',\n * sortOrder: 'desc',\n * });\n * ```\n */\nexport async function readCollection(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentItem[]> {\n const { includeDrafts = true, sortBy, sortOrder = \"desc\" } = options;\n\n // Get all content files\n const filePaths = await listFilesRecursive(collectionPath);\n\n // Read and parse all files\n const results = await Promise.all(\n filePaths.map((fp) => readContentFile(fp, collectionPath))\n );\n\n // Filter successful reads and optionally filter drafts\n let items = results\n .filter(\n (r): r is { success: true; content: ContentItem } =>\n r.success && !!r.content\n )\n .map((r) => r.content)\n .filter((item) => {\n if (!includeDrafts && item.frontmatter.draft === true) {\n return false;\n }\n return true;\n });\n\n // Sort if requested\n if (sortBy) {\n items = items.sort((a, b) => {\n const aVal = a.frontmatter[sortBy];\n const bVal = b.frontmatter[sortBy];\n\n // Handle undefined values\n if (aVal === undefined && bVal === undefined) return 0;\n if (aVal === undefined) return sortOrder === \"asc\" ? -1 : 1;\n if (bVal === undefined) return sortOrder === \"asc\" ? 1 : -1;\n\n // Compare values (convert to string for comparison)\n const aStr = String(aVal);\n const bStr = String(bVal);\n if (aStr < bStr) return sortOrder === \"asc\" ? -1 : 1;\n if (aStr > bStr) return sortOrder === \"asc\" ? 1 : -1;\n return 0;\n });\n }\n\n return items;\n}\n\n/**\n * Convert a content item to a summary for listing\n *\n * @param item - The full content item\n * @returns Content summary with essential fields\n */\nexport function toContentSummary(item: ContentItem): ContentSummary {\n const { id, path, frontmatter, body } = item;\n\n // Support both pubDate and publishDate naming conventions\n const dateValue =\n frontmatter.pubDate ?? frontmatter.publishDate ?? frontmatter.date;\n\n return {\n id,\n path,\n title: String(frontmatter.title ?? id),\n pubDate: dateValue ? String(dateValue) : undefined,\n draft: frontmatter.draft === true,\n excerpt: generateExcerpt(body),\n };\n}\n\n/**\n * Get content summaries for a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @param options - Read options\n * @returns Array of content summaries\n */\nexport async function getCollectionSummaries(\n collectionPath: string,\n options: ReadContentOptions = {}\n): Promise<ContentSummary[]> {\n const items = await readCollection(collectionPath, options);\n return items.map(toContentSummary);\n}\n\n/**\n * Get the count of content files in a collection\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Number of content files\n */\nexport async function getCollectionCount(\n collectionPath: string\n): Promise<number> {\n const filePaths = await listFilesRecursive(collectionPath);\n return filePaths.length;\n}\n\n/**\n * Check if a collection directory exists and contains content\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Object with exists and hasContent flags\n */\nexport async function checkCollection(collectionPath: string): Promise<{\n exists: boolean;\n hasContent: boolean;\n count: number;\n}> {\n if (!existsSync(collectionPath)) {\n return { exists: false, hasContent: false, count: 0 };\n }\n\n const count = await getCollectionCount(collectionPath);\n\n return {\n exists: true,\n hasContent: count > 0,\n count,\n };\n}\n\n/**\n * Get file stats for a content file\n *\n * @param filePath - Path to the content file\n * @returns File stats or null if file doesn't exist\n */\nexport async function getFileStats(filePath: string): Promise<{\n size: number;\n mtime: Date;\n ctime: Date;\n} | null> {\n try {\n const stats = await stat(filePath);\n return {\n size: stats.size,\n mtime: stats.mtime,\n ctime: stats.birthtime,\n };\n } catch {\n return null;\n }\n}\n\n/**\n * Get the file path for a content item by ID\n *\n * Searches for the content file in the collection directory,\n * handling different content structures:\n * - Folder-based: `slug/index.md` or `slug/index.mdx`\n * - Flat file: `slug.md` or `slug.mdx`\n *\n * @param collectionPath - Path to the collection directory\n * @param contentId - Content ID (slug)\n * @returns File path if found, null otherwise\n *\n * @example\n * ```typescript\n * const filePath = getContentFilePath('/project/src/content/blog', 'my-post');\n * // Returns: '/project/src/content/blog/my-post.md' or\n * // '/project/src/content/blog/my-post/index.md'\n * ```\n */\nexport function getContentFilePath(\n collectionPath: string,\n contentId: string\n): string | null {\n // Try folder-based structure first (slug/index.md or slug/index.mdx)\n const indexMdPath = join(collectionPath, contentId, \"index.md\");\n if (existsSync(indexMdPath)) {\n return indexMdPath;\n }\n\n const indexMdxPath = join(collectionPath, contentId, \"index.mdx\");\n if (existsSync(indexMdxPath)) {\n return indexMdxPath;\n }\n\n // Try flat file structure (slug.md or slug.mdx)\n const flatMdPath = join(collectionPath, `${contentId}.md`);\n if (existsSync(flatMdPath)) {\n return flatMdPath;\n }\n\n const flatMdxPath = join(collectionPath, `${contentId}.mdx`);\n if (existsSync(flatMdxPath)) {\n return flatMdxPath;\n }\n\n return null;\n}\n","/**\n * @fileoverview File pattern detection for content collections\n *\n * This module provides functions to detect and work with file naming patterns\n * in Astro content collections.\n *\n * ## Supported Patterns:\n * - `{slug}.md` - Simple slug-based naming\n * - `{date}-{slug}.md` - Date-prefixed naming (2024-01-15-my-post.md)\n * - `{year}/{slug}.md` - Year folder structure\n * - `{year}/{month}/{slug}.md` - Year/month folder structure\n * - `{year}/{month}/{day}/{slug}.md` - Full date folder structure\n * - `{slug}/index.md` - Folder-based with index file\n * - `{category}/{slug}.md` - Category folder structure\n * - `{category}/{slug}/index.md` - Category with folder-based content\n * - `{lang}/{slug}.md` - Language-prefixed content (i18n)\n * - `{lang}/{slug}/index.md` - Language with folder-based content\n *\n * ## Custom Patterns:\n * Developers can configure custom patterns in their collection config.\n * Custom tokens are resolved from frontmatter data or use default values.\n *\n * ## Detection Process:\n * 1. Scan collection directory for all content files\n * 2. Analyze file paths and names for common patterns\n * 3. Score each pattern based on match frequency\n * 4. Return the best matching pattern\n *\n * @module @writenex/astro/discovery/patterns\n */\n\nimport { existsSync } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { extname, join, relative } from \"node:path\";\nimport { isContentFile } from \"@/filesystem/reader\";\n\n/**\n * Pattern definition with regex and template\n */\ninterface PatternDefinition {\n /** Pattern name for identification */\n name: string;\n /** Template string with tokens */\n template: string;\n /** Regex to match against file paths */\n regex: RegExp;\n /** Function to extract tokens from a match */\n extract: (match: RegExpMatchArray, ext: string) => Record<string, string>;\n /** Priority when multiple patterns match (higher = preferred) */\n priority: number;\n}\n\n/**\n * Result of pattern detection\n */\nexport interface PatternDetectionResult {\n /** The detected pattern template */\n pattern: string;\n /** Confidence score (0-1) */\n confidence: number;\n /** Number of files that matched this pattern */\n matchCount: number;\n /** Total files analyzed */\n totalFiles: number;\n /** Sample matches for debugging */\n samples: Array<{\n filePath: string;\n extracted: Record<string, string>;\n }>;\n}\n\n/**\n * All supported pattern definitions\n *\n * Order matters - more specific patterns should come first.\n * Higher priority patterns are preferred when multiple patterns match.\n */\nconst PATTERN_DEFINITIONS: PatternDefinition[] = [\n // {year}/{month}/{day}/{slug}.md - Full date folder structure\n {\n name: \"year-month-day-slug\",\n template: \"{year}/{month}/{day}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n day: match[3] ?? \"\",\n slug: match[4] ?? \"\",\n extension: ext,\n }),\n priority: 95,\n },\n\n // {year}/{month}/{slug}.md - Year/month nested date structure\n {\n name: \"year-month-slug\",\n template: \"{year}/{month}/{slug}.md\",\n regex: /^(\\d{4})\\/(\\d{2})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n month: match[2] ?? \"\",\n slug: match[3] ?? \"\",\n extension: ext,\n }),\n priority: 90,\n },\n\n // {year}/{slug}.md - Year folder structure\n {\n name: \"year-slug\",\n template: \"{year}/{slug}.md\",\n regex: /^(\\d{4})\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n year: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 85,\n },\n\n // {lang}/{slug}/index.md - Language with folder-based content (i18n)\n {\n name: \"lang-folder-index\",\n template: \"{lang}/{slug}/index.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 82,\n },\n\n // {category}/{slug}/index.md - Category with folder-based content\n {\n name: \"category-folder-index\",\n template: \"{category}/{slug}/index.md\",\n regex: /^([^/]+)\\/([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 80,\n },\n\n // {slug}/index.md - Folder-based content\n {\n name: \"folder-index\",\n template: \"{slug}/index.md\",\n regex: /^([^/]+)\\/index\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 75,\n },\n\n // {date}-{slug}.md - Date-prefixed (ISO format)\n {\n name: \"date-slug\",\n template: \"{date}-{slug}.md\",\n regex: /^(\\d{4}-\\d{2}-\\d{2})-(.+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n date: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 70,\n },\n\n // {lang}/{slug}.md - Language-prefixed content (i18n)\n // Matches: en/my-post.md, pt-BR/my-post.md\n {\n name: \"lang-slug\",\n template: \"{lang}/{slug}.md\",\n regex: /^([a-z]{2}(?:-[A-Z]{2})?)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n lang: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 60,\n },\n\n // {category}/{slug}.md - Category folder (catch-all for non-date/non-lang folders)\n {\n name: \"category-slug\",\n template: \"{category}/{slug}.md\",\n regex: /^([^/]+)\\/([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n category: match[1] ?? \"\",\n slug: match[2] ?? \"\",\n extension: ext,\n }),\n priority: 50,\n },\n\n // {slug}.md - Simple flat structure (default fallback)\n {\n name: \"simple-slug\",\n template: \"{slug}.md\",\n regex: /^([^/]+)\\.(md|mdx)$/,\n extract: (match, ext) => ({\n slug: match[1] ?? \"\",\n extension: ext,\n }),\n priority: 10,\n },\n];\n\n/**\n * List all content files in a directory recursively\n *\n * @param dirPath - Directory to scan\n * @returns Array of relative file paths\n */\nasync function listContentFiles(dirPath: string): Promise<string[]> {\n const files: string[] = [];\n\n if (!existsSync(dirPath)) {\n return files;\n }\n\n async function scan(currentPath: string, relativeTo: string): Promise<void> {\n const entries = await readdir(currentPath, { withFileTypes: true });\n\n for (const entry of entries) {\n const fullPath = join(currentPath, entry.name);\n const relativePath = relative(relativeTo, fullPath);\n\n if (entry.isDirectory()) {\n // Skip hidden and special directories\n if (!entry.name.startsWith(\".\") && !entry.name.startsWith(\"_\")) {\n await scan(fullPath, relativeTo);\n }\n } else if (entry.isFile() && isContentFile(entry.name)) {\n files.push(relativePath);\n }\n }\n }\n\n await scan(dirPath, dirPath);\n return files;\n}\n\n/**\n * Try to match a file path against all pattern definitions\n *\n * @param relativePath - Relative path to the content file\n * @returns Matched pattern and extracted tokens, or null\n */\nfunction matchPattern(\n relativePath: string\n): { pattern: PatternDefinition; match: RegExpMatchArray } | null {\n // Normalize path separators\n const normalizedPath = relativePath.replace(/\\\\/g, \"/\");\n\n for (const pattern of PATTERN_DEFINITIONS) {\n const match = normalizedPath.match(pattern.regex);\n if (match) {\n return { pattern, match };\n }\n }\n\n return null;\n}\n\n/**\n * Detect the file naming pattern used in a collection\n *\n * Analyzes all content files in the collection directory and determines\n * the most likely pattern based on file names and structure.\n *\n * @param collectionPath - Absolute path to the collection directory\n * @returns Pattern detection result with confidence score\n *\n * @example\n * ```typescript\n * const result = await detectFilePattern('/project/src/content/blog');\n * console.log(result.pattern); // \"{date}-{slug}.md\"\n * console.log(result.confidence); // 0.95\n * ```\n */\nexport async function detectFilePattern(\n collectionPath: string\n): Promise<PatternDetectionResult> {\n const files = await listContentFiles(collectionPath);\n\n if (files.length === 0) {\n return {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: 0,\n samples: [],\n };\n }\n\n // Count matches for each pattern\n const patternCounts = new Map<\n string,\n {\n pattern: PatternDefinition;\n count: number;\n samples: Array<{ filePath: string; extracted: Record<string, string> }>;\n extension: string;\n }\n >();\n\n for (const pattern of PATTERN_DEFINITIONS) {\n patternCounts.set(pattern.name, {\n pattern,\n count: 0,\n samples: [],\n extension: \".md\",\n });\n }\n\n // Analyze each file\n for (const filePath of files) {\n const result = matchPattern(filePath);\n\n if (result) {\n const { pattern, match } = result;\n const entry = patternCounts.get(pattern.name);\n\n if (entry) {\n const ext = extname(filePath);\n const extracted = pattern.extract(match, ext);\n\n entry.count++;\n entry.extension = ext;\n\n // Keep up to 3 samples\n if (entry.samples.length < 3) {\n entry.samples.push({ filePath, extracted });\n }\n }\n }\n }\n\n // Find the best matching pattern\n // Consider both match count and pattern priority\n let bestPattern: PatternDetectionResult | null = null;\n let bestScore = -1;\n\n for (const [, entry] of patternCounts) {\n if (entry.count === 0) continue;\n\n // Score = (match ratio * 100) + priority\n // This ensures high match ratio wins, but priority breaks ties\n const matchRatio = entry.count / files.length;\n const score = matchRatio * 100 + entry.pattern.priority;\n\n if (score > bestScore) {\n bestScore = score;\n\n // Adjust template for actual extension used\n let template = entry.pattern.template;\n if (entry.extension === \".mdx\") {\n template = template.replace(\".md\", \".mdx\");\n }\n\n bestPattern = {\n pattern: template,\n confidence: matchRatio,\n matchCount: entry.count,\n totalFiles: files.length,\n samples: entry.samples,\n };\n }\n }\n\n // Return best pattern or default\n return (\n bestPattern ?? {\n pattern: \"{slug}.md\",\n confidence: 0,\n matchCount: 0,\n totalFiles: files.length,\n samples: [],\n }\n );\n}\n\n/**\n * Generate a file path from a pattern and tokens\n *\n * @param pattern - Pattern template (e.g., \"{date}-{slug}.md\")\n * @param tokens - Token values to substitute\n * @returns Generated file path\n *\n * @example\n * ```typescript\n * const path = generatePathFromPattern(\n * \"{date}-{slug}.md\",\n * { date: \"2024-01-15\", slug: \"my-post\" }\n * );\n * // Returns: \"2024-01-15-my-post.md\"\n * ```\n */\nexport function generatePathFromPattern(\n pattern: string,\n tokens: Record<string, string>\n): string {\n let result = pattern;\n\n for (const [key, value] of Object.entries(tokens)) {\n result = result.replace(`{${key}}`, value);\n }\n\n return result;\n}\n\n/**\n * Parse a pattern template to extract token names\n *\n * @param pattern - Pattern template\n * @returns Array of token names\n *\n * @example\n * ```typescript\n * const tokens = parsePatternTokens(\"{year}/{month}/{slug}.md\");\n * // Returns: [\"year\", \"month\", \"slug\"]\n * ```\n */\nexport function parsePatternTokens(pattern: string): string[] {\n const tokenRegex = /\\{([^}]+)\\}/g;\n const tokens: string[] = [];\n let match;\n\n while ((match = tokenRegex.exec(pattern)) !== null) {\n if (match[1]) {\n tokens.push(match[1]);\n }\n }\n\n return tokens;\n}\n\n/**\n * Validate that a pattern has all required tokens\n *\n * @param pattern - Pattern template\n * @param requiredTokens - Required token names\n * @returns True if all required tokens are present\n */\nexport function validatePattern(\n pattern: string,\n requiredTokens: string[] = [\"slug\"]\n): boolean {\n const tokens = parsePatternTokens(pattern);\n return requiredTokens.every((req) => tokens.includes(req));\n}\n\n/**\n * Get the default extension for a pattern\n *\n * @param pattern - Pattern template\n * @returns The file extension (.md or .mdx)\n */\nexport function getPatternExtension(pattern: string): string {\n if (pattern.endsWith(\".mdx\")) {\n return \".mdx\";\n }\n return \".md\";\n}\n\n/**\n * Known token types and their default value generators\n */\ntype TokenResolver = (\n frontmatter: Record<string, unknown>,\n slug: string\n) => string;\n\nconst TOKEN_RESOLVERS: Record<string, TokenResolver> = {\n // Core tokens\n slug: (_fm, slug) => slug,\n\n // Date tokens - from pubDate or current date\n date: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.toISOString().split(\"T\")[0] ?? \"\";\n },\n year: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getFullYear().toString();\n },\n month: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return (pubDate.getMonth() + 1).toString().padStart(2, \"0\");\n },\n day: (fm) => {\n const pubDate = resolveDateFromFrontmatter(fm);\n return pubDate.getDate().toString().padStart(2, \"0\");\n },\n\n // i18n tokens\n lang: (fm) => {\n if (typeof fm.lang === \"string\") return fm.lang;\n if (typeof fm.language === \"string\") return fm.language;\n if (typeof fm.locale === \"string\") return fm.locale;\n return \"en\"; // Default to English\n },\n\n // Organization tokens\n category: (fm) => {\n if (typeof fm.category === \"string\") return fm.category;\n if (Array.isArray(fm.categories) && typeof fm.categories[0] === \"string\") {\n return fm.categories[0];\n }\n return \"uncategorized\";\n },\n author: (fm) => {\n if (typeof fm.author === \"string\") return slugifyValue(fm.author);\n if (\n typeof fm.author === \"object\" &&\n fm.author !== null &&\n \"name\" in fm.author\n ) {\n return slugifyValue(String(fm.author.name));\n }\n return \"anonymous\";\n },\n type: (fm) => {\n if (typeof fm.type === \"string\") return fm.type;\n if (typeof fm.contentType === \"string\") return fm.contentType;\n return \"post\";\n },\n status: (fm) => {\n if (typeof fm.status === \"string\") return fm.status;\n if (fm.draft === true) return \"draft\";\n return \"published\";\n },\n series: (fm) => {\n if (typeof fm.series === \"string\") return slugifyValue(fm.series);\n return \"\";\n },\n collection: (fm) => {\n if (typeof fm.collection === \"string\") return fm.collection;\n return \"\";\n },\n};\n\n/**\n * Resolve a date from frontmatter\n *\n * Checks common date field names: pubDate, date, publishDate, createdAt\n *\n * @param frontmatter - Frontmatter data\n * @returns Resolved Date object\n */\nfunction resolveDateFromFrontmatter(\n frontmatter: Record<string, unknown>\n): Date {\n const dateFields = [\"pubDate\", \"date\", \"publishDate\", \"createdAt\", \"created\"];\n\n for (const field of dateFields) {\n const value = frontmatter[field];\n if (value instanceof Date) return value;\n if (typeof value === \"string\") {\n const parsed = new Date(value);\n if (!isNaN(parsed.getTime())) return parsed;\n }\n }\n\n return new Date();\n}\n\n/**\n * Convert a string to a URL-safe slug\n *\n * @param value - String to slugify\n * @returns URL-safe slug\n */\nfunction slugifyValue(value: string): string {\n return value\n .toLowerCase()\n .trim()\n .replace(/[^\\w\\s-]/g, \"\")\n .replace(/[\\s_-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\");\n}\n\n/**\n * Options for resolving pattern tokens\n */\nexport interface ResolveTokensOptions {\n /** The content slug */\n slug: string;\n /** Frontmatter data for resolving dynamic tokens */\n frontmatter?: Record<string, unknown>;\n /** Custom token values (override automatic resolution) */\n customTokens?: Record<string, string>;\n}\n\n/**\n * Resolve all tokens in a pattern to their values\n *\n * Token resolution priority:\n * 1. Custom tokens (explicitly provided)\n * 2. Known token resolvers (date, year, month, etc.)\n * 3. Frontmatter values (for custom tokens)\n * 4. Empty string (fallback)\n *\n * @param pattern - Pattern template with tokens\n * @param options - Resolution options\n * @returns Record of token names to resolved values\n *\n * @example\n * ```typescript\n * const tokens = resolvePatternTokens(\"{year}/{month}/{slug}.md\", {\n * slug: \"my-post\",\n * frontmatter: { pubDate: new Date(\"2024-06-15\") }\n * });\n * // Returns: { year: \"2024\", month: \"06\", slug: \"my-post\" }\n * ```\n */\nexport function resolvePatternTokens(\n pattern: string,\n options: ResolveTokensOptions\n): Record<string, string> {\n const { slug, frontmatter = {}, customTokens = {} } = options;\n const tokenNames = parsePatternTokens(pattern);\n const resolved: Record<string, string> = {};\n\n for (const tokenName of tokenNames) {\n // Priority 1: Custom tokens\n if (tokenName in customTokens) {\n resolved[tokenName] = customTokens[tokenName] ?? \"\";\n continue;\n }\n\n // Priority 2: Known token resolvers\n const resolver = TOKEN_RESOLVERS[tokenName];\n if (resolver) {\n resolved[tokenName] = resolver(frontmatter, slug);\n continue;\n }\n\n // Priority 3: Direct frontmatter value\n const fmValue = frontmatter[tokenName];\n if (typeof fmValue === \"string\") {\n resolved[tokenName] = slugifyValue(fmValue);\n continue;\n }\n if (typeof fmValue === \"number\") {\n resolved[tokenName] = fmValue.toString();\n continue;\n }\n\n // Priority 4: Fallback to empty string\n resolved[tokenName] = \"\";\n }\n\n return resolved;\n}\n\n/**\n * Check if a pattern is valid for content creation\n *\n * A pattern is valid if:\n * - It contains the {slug} token (required)\n * - It ends with .md or .mdx\n * - All tokens can be resolved\n *\n * @param pattern - Pattern template to validate\n * @returns Validation result with error message if invalid\n */\nexport function isValidPattern(pattern: string): {\n valid: boolean;\n error?: string;\n} {\n // Must contain slug token\n if (!pattern.includes(\"{slug}\")) {\n return { valid: false, error: \"Pattern must contain {slug} token\" };\n }\n\n // Must end with .md or .mdx\n if (!pattern.endsWith(\".md\") && !pattern.endsWith(\".mdx\")) {\n return { valid: false, error: \"Pattern must end with .md or .mdx\" };\n }\n\n // Check for unclosed tokens\n const unclosed = pattern.match(/\\{[^}]*$/);\n if (unclosed) {\n return { valid: false, error: \"Pattern contains unclosed token\" };\n }\n\n return { valid: true };\n}\n\n/**\n * Get list of all supported token names\n *\n * @returns Array of supported token names\n */\nexport function getSupportedTokens(): string[] {\n return Object.keys(TOKEN_RESOLVERS);\n}\n"],"mappings":"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","names":["existsSync","readdir","extname","join","relative","existsSync","readdir","join","relative","extname"]}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|