diff --git a/packages/ono/example/barrels/blog.ts b/packages/ono/example/barrels/blog.ts
new file mode 100644
index 0000000..f44dce6
--- /dev/null
+++ b/packages/ono/example/barrels/blog.ts
@@ -0,0 +1,20 @@
+// Auto-generated barrel file - DO NOT EDIT
+// Generated by Ono SSG
+
+export type Meta = {
+ title: string;
+ date: string;
+ author: string;
+ tags?: string[];
+};
+
+export { default as gettingStarted, meta as gettingStartedMeta } from './blog/getting-started.tsx';
+export { default as helloWorld, meta as helloWorldMeta } from './blog/hello-world.tsx';
+
+export const entries = ['getting-started', 'hello-world'] as const;
+export type EntryId = typeof entries[number];
+
+export const posts = {
+ 'getting-started': { component: gettingStarted, meta: gettingStartedMeta },
+ 'hello-world': { component: helloWorld, meta: helloWorldMeta }
+};
diff --git a/packages/ono/example/barrels/blog/getting-started.tsx b/packages/ono/example/barrels/blog/getting-started.tsx
new file mode 100644
index 0000000..305514f
--- /dev/null
+++ b/packages/ono/example/barrels/blog/getting-started.tsx
@@ -0,0 +1,24 @@
+export const meta = {
+ title: "Getting Started with Ono",
+ date: "2025-01-03",
+ author: "hashrock",
+ tags: ["tutorial", "ono"],
+};
+
+export default function GettingStarted() {
+ return (
+
+ {meta.title}
+ {meta.date} by {meta.author}
+
+ {meta.tags.map((tag) => (
+ {tag}
+ ))}
+
+ Installation
+ npm install @hashrock/ono
+ Usage
+ Create a pages directory and add JSX files.
+
+ );
+}
diff --git a/packages/ono/example/barrels/blog/hello-world.tsx b/packages/ono/example/barrels/blog/hello-world.tsx
new file mode 100644
index 0000000..2ae48e5
--- /dev/null
+++ b/packages/ono/example/barrels/blog/hello-world.tsx
@@ -0,0 +1,16 @@
+export const meta = {
+ title: "Hello World",
+ date: "2025-01-04",
+ author: "hashrock",
+};
+
+export default function HelloWorld() {
+ return (
+
+ {meta.title}
+ {meta.date} by {meta.author}
+ Welcome to my first blog post built with Ono SSG!
+ This is a simple example of using barrels for content management.
+
+ );
+}
diff --git a/packages/ono/example/content.config.js b/packages/ono/example/content.config.js
deleted file mode 100644
index 64582a2..0000000
--- a/packages/ono/example/content.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export const collections = {
- blog: {
- schema: {
- title: { type: "string", required: true },
- date: { type: "date", required: true },
- author: { type: "string", required: true },
- tags: { type: "array", items: "string" },
- draft: { type: "boolean", default: false },
- },
- },
-};
diff --git a/packages/ono/example/content/blog/first-post.md b/packages/ono/example/content/blog/first-post.md
deleted file mode 100644
index 8995e53..0000000
--- a/packages/ono/example/content/blog/first-post.md
+++ /dev/null
@@ -1,37 +0,0 @@
----
-title: My First Blog Post
-date: 2024-01-15
-author: John Doe
-tags:
- - javascript
- - web
- - tutorial
-draft: false
----
-
-# Welcome to My Blog
-
-This is my first blog post using **Ono's Content Collections**!
-
-## Features
-
-Content Collections in Ono provide:
-
-- Frontmatter parsing
-- Markdown to HTML conversion
-- Type validation
-- Easy querying with `getCollection()` and `getEntry()`
-
-## Code Example
-
-Here's a simple example:
-
-```javascript
-import { getCollection } from '@hashrock/ono/content';
-
-const posts = await getCollection('blog');
-```
-
-## Conclusion
-
-Building static sites with Ono is simple and lightweight!
diff --git a/packages/ono/example/content/blog/second-post.md b/packages/ono/example/content/blog/second-post.md
deleted file mode 100644
index 57022a6..0000000
--- a/packages/ono/example/content/blog/second-post.md
+++ /dev/null
@@ -1,33 +0,0 @@
----
-title: Building with JSX
-date: 2024-01-20
-author: Jane Smith
-tags: [jsx, react, components]
-draft: false
----
-
-# Building Static Sites with JSX
-
-JSX provides a powerful way to build static sites with component-based architecture.
-
-## Why JSX?
-
-1. **Familiar syntax** - If you know React, you know JSX
-2. **Component reusability** - Build once, use everywhere
-3. **Type safety** - Works great with TypeScript
-
-## Example Component
-
-```jsx
-function BlogPost({ title, date, children }) {
- return (
-
- {title}
-
- {children}
-
- );
-}
-```
-
-That's all for today!
diff --git a/packages/ono/example/pages/blog.jsx b/packages/ono/example/pages/blog.jsx
deleted file mode 100644
index 29b3382..0000000
--- a/packages/ono/example/pages/blog.jsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { getCollection } from "@hashrock/ono/content";
-
-export default async function BlogIndex() {
- const posts = await getCollection("blog", (post) => !post.data.draft);
-
- return (
-
-
-
-
- Blog - Ono Content Collections Example
-
-
-
-
-
-
- {posts.map((post) => (
-
-
-
-
-
- {" ยท "}
- {post.data.author}
-
- {post.data.tags && post.data.tags.length > 0 && (
-
- {post.data.tags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
-
-
-
- ))}
-
-
-
-
-
- );
-}
diff --git a/packages/ono/example/pages/blog/[slug].jsx b/packages/ono/example/pages/blog/[slug].jsx
deleted file mode 100644
index 3994cb8..0000000
--- a/packages/ono/example/pages/blog/[slug].jsx
+++ /dev/null
@@ -1,81 +0,0 @@
-import { getCollection, getEntry } from "@hashrock/ono/content";
-
-export async function getStaticPaths() {
- const posts = await getCollection("blog");
- return posts.map((post) => ({
- params: { slug: post.slug },
- }));
-}
-
-export default async function BlogPost({ params }) {
- const post = await getEntry("blog", params.slug);
-
- if (!post) {
- return (
-
-
- Post Not Found
-
-
- 404 - Post Not Found
-
-
- );
- }
-
- return (
-
-
-
-
- {post.data.title} - Blog
-
-
-
-
-
-
-
- {post.data.title}
-
-
- {" ยท "}
- {post.data.author}
-
- {post.data.tags && post.data.tags.length > 0 && (
-
- {post.data.tags.map((tag) => (
-
- {tag}
-
- ))}
-
- )}
-
-
-
-
-
-
-
-
- );
-}
diff --git a/packages/ono/package.json b/packages/ono/package.json
index 8089551..ecbec6b 100644
--- a/packages/ono/package.json
+++ b/packages/ono/package.json
@@ -10,7 +10,7 @@
"./renderer": "./src/renderer.js",
"./transformer": "./src/transformer.js",
"./bundler": "./src/bundler.js",
- "./content": "./src/content.js",
+ "./barrels": "./src/barrels.js",
"./browser/compiler": "./src/browser/compiler.js",
"./browser/unocss": "./src/browser/unocss.js"
},
@@ -54,7 +54,6 @@
"dependencies": {
"@unocss/preset-uno": "^66.5.4",
"h3": "^2.0.1-rc.5",
- "marked": "^16.4.1",
"typescript": "^5.9.3",
"unocss": "^66.5.4",
"ws": "^8.18.3"
diff --git a/packages/ono/src/barrels.js b/packages/ono/src/barrels.js
new file mode 100644
index 0000000..aff2837
--- /dev/null
+++ b/packages/ono/src/barrels.js
@@ -0,0 +1,234 @@
+/**
+ * Barrels - Auto-generated barrel files with type inference
+ */
+import { readdir, writeFile, mkdir } from "node:fs/promises";
+import { join, basename, dirname, relative } from "node:path";
+import { bundle } from "./bundler.js";
+
+/**
+ * Convert kebab-case or snake_case to camelCase
+ */
+function toCamelCase(str) {
+ return str.replace(/[-_]([a-z])/g, (_, c) => c.toUpperCase());
+}
+
+/**
+ * Infer TypeScript type from a JavaScript value
+ */
+function inferType(value) {
+ if (value === null) return "null";
+ if (value === undefined) return "undefined";
+ if (Array.isArray(value)) {
+ if (value.length === 0) return "unknown[]";
+ // Infer from first element
+ return `${inferType(value[0])}[]`;
+ }
+ if (value instanceof Date) return "Date";
+ if (typeof value === "object") {
+ // For nested objects, use Record type
+ return "Record";
+ }
+ return typeof value; // 'string' | 'number' | 'boolean'
+}
+
+/**
+ * Generate Meta type definition from collected metas
+ */
+function generateMetaType(metas) {
+ if (metas.length === 0) return "export type Meta = Record;";
+
+ // Collect all keys and their occurrence count
+ const keyCounts = {};
+ const keyTypes = {};
+
+ for (const meta of metas) {
+ if (!meta) continue;
+ for (const [key, value] of Object.entries(meta)) {
+ keyCounts[key] = (keyCounts[key] || 0) + 1;
+ // Store the first non-undefined value's type
+ if (!keyTypes[key] && value !== undefined) {
+ keyTypes[key] = inferType(value);
+ }
+ }
+ }
+
+ // Generate type fields
+ const fields = Object.entries(keyCounts)
+ .map(([key, count]) => {
+ const type = keyTypes[key] || "unknown";
+ const optional = count < metas.length;
+ return ` ${key}${optional ? "?" : ""}: ${type};`;
+ })
+ .join("\n");
+
+ return `export type Meta = {\n${fields}\n};`;
+}
+
+/**
+ * Get all entry files from a barrel directory
+ */
+async function getBarrelEntries(barrelDir) {
+ const entries = [];
+
+ try {
+ const files = await readdir(barrelDir, { withFileTypes: true });
+
+ for (const file of files) {
+ if (file.isFile() && (file.name.endsWith(".tsx") || file.name.endsWith(".jsx"))) {
+ const id = basename(file.name, file.name.endsWith(".tsx") ? ".tsx" : ".jsx");
+ entries.push({
+ id,
+ file: file.name,
+ path: join(barrelDir, file.name),
+ });
+ }
+ }
+ } catch (error) {
+ if (error.code !== "ENOENT") throw error;
+ }
+
+ return entries.sort((a, b) => a.id.localeCompare(b.id));
+}
+
+/**
+ * Load meta from an entry file by bundling and evaluating
+ */
+async function loadMeta(entryPath) {
+ try {
+ // Bundle the file to resolve imports
+ const bundledCode = await bundle(entryPath);
+
+ // Create a temporary module to extract meta
+ const tempDir = dirname(entryPath);
+ const tempFile = join(tempDir, `_temp_meta_${Date.now()}.js`);
+
+ // Add JSX runtime and extract meta
+ const code = `
+function h(tag, props, ...children) {
+ return { tag, props: props || {}, children };
+}
+${bundledCode}
+`;
+
+ await writeFile(tempFile, code);
+
+ try {
+ const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
+ const module = await import(moduleUrl.href);
+ return module.meta || null;
+ } finally {
+ // Clean up
+ await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
+ }
+ } catch (error) {
+ console.warn(`Warning: Could not load meta from ${entryPath}:`, error.message);
+ return null;
+ }
+}
+
+/**
+ * Generate a barrel file for a directory
+ */
+export async function generateBarrel(barrelDir, options = {}) {
+ const { silent = false } = options;
+
+ const entries = await getBarrelEntries(barrelDir);
+
+ if (entries.length === 0) {
+ return null;
+ }
+
+ // Load all metas
+ const metas = await Promise.all(
+ entries.map((entry) => loadMeta(entry.path))
+ );
+
+ // Generate type definition
+ const metaType = generateMetaType(metas);
+
+ // Generate imports with camelCase identifiers
+ const imports = entries
+ .map((entry, idx) => {
+ const camelId = toCamelCase(entry.id);
+ const hasExportedMeta = metas[idx] !== null;
+ if (hasExportedMeta) {
+ return `export { default as ${camelId}, meta as ${camelId}Meta } from './${basename(barrelDir)}/${entry.file}';`;
+ }
+ return `export { default as ${camelId} } from './${basename(barrelDir)}/${entry.file}';`;
+ })
+ .join("\n");
+
+ // Generate entries array (keep original IDs for URL paths)
+ const entriesArray = `export const entries = [${entries.map((e) => `'${e.id}'`).join(", ")}] as const;`;
+
+ // Generate ID to component mapping
+ const mapping = entries
+ .map((entry, idx) => {
+ const camelId = toCamelCase(entry.id);
+ const hasExportedMeta = metas[idx] !== null;
+ if (hasExportedMeta) {
+ return ` '${entry.id}': { component: ${camelId}, meta: ${camelId}Meta }`;
+ }
+ return ` '${entry.id}': { component: ${camelId}, meta: null }`;
+ })
+ .join(",\n");
+ const mappingExport = `export const posts = {\n${mapping}\n};`;
+ const entryIdType = "export type EntryId = typeof entries[number];";
+
+ // Generate barrel content
+ const barrelContent = `// Auto-generated barrel file - DO NOT EDIT
+// Generated by Ono SSG
+
+${metaType}
+
+${imports}
+
+${entriesArray}
+${entryIdType}
+
+${mappingExport}
+`;
+
+ // Write barrel file
+ const barrelPath = `${barrelDir}.ts`;
+ await writeFile(barrelPath, barrelContent);
+
+ if (!silent) {
+ console.log(` Generated: ${relative(process.cwd(), barrelPath)}`);
+ }
+
+ return barrelPath;
+}
+
+/**
+ * Generate all barrel files in a directory
+ */
+export async function generateBarrels(barrelsRoot, options = {}) {
+ const { silent = false } = options;
+
+ try {
+ const entries = await readdir(barrelsRoot, { withFileTypes: true });
+ const results = [];
+
+ for (const entry of entries) {
+ if (entry.isDirectory()) {
+ const barrelDir = join(barrelsRoot, entry.name);
+ const result = await generateBarrel(barrelDir, { silent: true });
+ if (result) {
+ results.push(result);
+ }
+ }
+ }
+
+ if (!silent && results.length > 0) {
+ console.log(`Generated ${results.length} barrel file(s)`);
+ }
+
+ return results;
+ } catch (error) {
+ if (error.code === "ENOENT") {
+ return [];
+ }
+ throw error;
+ }
+}
diff --git a/packages/ono/src/builder.js b/packages/ono/src/builder.js
index 8ecf5e2..748fd78 100644
--- a/packages/ono/src/builder.js
+++ b/packages/ono/src/builder.js
@@ -1,20 +1,12 @@
/**
* Build utilities for Ono SSG
*/
-import { readFile, writeFile, mkdir, readdir, stat } from "node:fs/promises";
-import { resolve, join, dirname, basename, relative, extname } from "node:path";
-import { transformJSX } from "./transformer.js";
+import { writeFile, mkdir, readdir } from "node:fs/promises";
+import { resolve, join, dirname, basename, relative } from "node:path";
import { renderToString } from "./renderer.js";
import { generateCSSFromFiles } from "./unocss.js";
import { bundle } from "./bundler.js";
-/**
- * Check if a route is dynamic (contains [param])
- */
-export function isDynamicRoute(filePath) {
- return /\[([^\]]+)\]/.test(filePath);
-}
-
// Inline JSX runtime for bundled output
const INLINE_JSX_RUNTIME = `
function flattenChildren(children) {
@@ -88,88 +80,6 @@ export async function buildFile(inputFile, options = {}) {
return { outputPath, html };
}
-/**
- * Build a dynamic route (e.g., [slug].jsx)
- */
-export async function buildDynamicRoute(inputFile, options = {}) {
- const { outputDir = "dist", silent = false } = options;
-
- const outDir = resolve(process.cwd(), outputDir);
- const resolvedInput = resolve(process.cwd(), inputFile);
-
- // Bundle the file with all its dependencies
- const bundledCode = await bundle(resolvedInput);
-
- // Add inline JSX runtime
- const codeWithRuntime = INLINE_JSX_RUNTIME + bundledCode;
-
- // Write transformed JS temporarily
- const tempFile = join(outDir, `_temp_${Date.now()}.js`);
- await mkdir(dirname(tempFile), { recursive: true });
- await writeFile(tempFile, codeWithRuntime);
-
- // Import module
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
- const module = await import(moduleUrl.href);
-
- if (!module.getStaticPaths) {
- throw new Error(
- `Dynamic route ${inputFile} must export a getStaticPaths function`
- );
- }
-
- const pathsData = await module.getStaticPaths();
- const paths = Array.isArray(pathsData) ? pathsData : pathsData.paths || [];
-
- const App = module.default;
- if (!App) {
- throw new Error(`No default export found in ${inputFile}`);
- }
-
- const outputs = [];
-
- for (const pathData of paths) {
- const params = pathData.params || {};
-
- let vnode = typeof App === "function" ? App({ params }) : App;
- if (vnode instanceof Promise) {
- vnode = await vnode;
- }
-
- const html = renderToString(vnode);
-
- // Determine output path from params
- const routeDir = dirname(relative(join(process.cwd(), "pages"), resolvedInput));
- const fileName = basename(resolvedInput, ".jsx");
-
- // Replace [param] with actual value
- let outputPath = fileName;
- for (const [key, value] of Object.entries(params)) {
- outputPath = outputPath.replace(`[${key}]`, value);
- }
-
- const fullOutputPath = join(
- outDir,
- routeDir === "." ? "" : routeDir,
- `${outputPath}.html`
- );
-
- await mkdir(dirname(fullOutputPath), { recursive: true });
- await writeFile(fullOutputPath, html);
-
- outputs.push({ outputPath: fullOutputPath, html, params });
-
- if (!silent) {
- console.log(` โ ${relative(process.cwd(), fullOutputPath)}`);
- }
- }
-
- // Clean up temp file
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
-
- return outputs;
-}
-
/**
* Build multiple JSX files
*/
@@ -186,22 +96,11 @@ export async function buildFiles(inputPattern, options = {}) {
const results = [];
for (const file of files) {
- if (isDynamicRoute(file)) {
- if (!silent) {
- const relativePath = relative(process.cwd(), file);
- const pathsData = await getDynamicRoutePaths(file);
- const count = Array.isArray(pathsData) ? pathsData.length : pathsData.paths?.length || 0;
- console.log(`Building dynamic route ${relativePath} (${count} pages)...`);
- }
- const outputs = await buildDynamicRoute(file, { outputDir, silent: true });
- results.push(...outputs);
- } else {
- if (!silent) {
- console.log(`Building ${relative(process.cwd(), file)}...`);
- }
- const result = await buildFile(file, { outputDir, unocssConfig, silent: true });
- results.push(result);
+ if (!silent) {
+ console.log(`Building ${relative(process.cwd(), file)}...`);
}
+ const result = await buildFile(file, { outputDir, unocssConfig, silent: true });
+ results.push(result);
}
return results;
@@ -228,35 +127,6 @@ async function getAllJSXFiles(dir) {
return files;
}
-/**
- * Helper to get paths from a dynamic route
- */
-/**
- * Helper to get paths from a dynamic route
- */
-export async function getDynamicRoutePaths(file) {
- const outDir = resolve(process.cwd(), "dist");
-
- // Bundle the file with all its dependencies
- const bundledCode = await bundle(file);
-
- // Add inline JSX runtime
- const codeWithRuntime = INLINE_JSX_RUNTIME + bundledCode;
-
- const tempFile = join(outDir, `_temp_paths_${Date.now()}.js`);
- await mkdir(dirname(tempFile), { recursive: true });
- await writeFile(tempFile, codeWithRuntime);
-
- const moduleUrl = new URL(`file://${tempFile}?t=${Date.now()}`);
- const module = await import(moduleUrl.href);
-
- const pathsData = module.getStaticPaths ? await module.getStaticPaths() : [];
-
- await import("node:fs/promises").then((fs) => fs.unlink(tempFile).catch(() => {}));
-
- return pathsData;
-}
-
/**
* Generate UnoCSS file
*/
diff --git a/packages/ono/src/cli.js b/packages/ono/src/cli.js
index 316b91c..37ce7f3 100755
--- a/packages/ono/src/cli.js
+++ b/packages/ono/src/cli.js
@@ -11,6 +11,7 @@ import { loadUnoConfig } from "./unocss.js";
import { createDevServer } from "./server.js";
import { buildFile, buildFiles, generateUnoCSS } from "./builder.js";
import { watchFile, watchFiles, createWebSocketServer } from "./watcher.js";
+import { generateBarrels } from "./barrels.js";
const args = process.argv.slice(2);
const command = args[0];
@@ -75,6 +76,13 @@ Examples:
const unocssConfig = await loadUnoConfig();
+ // Generate barrel files if barrels directory exists
+ const barrelsDir = resolve(process.cwd(), "barrels");
+ if (existsSync(barrelsDir)) {
+ console.log("Generating barrel files...");
+ await generateBarrels(barrelsDir);
+ }
+
// Check if input is a directory or a file
const inputPath = resolve(process.cwd(), input);
const inputStat = await stat(inputPath);
@@ -109,6 +117,13 @@ async function runDevCommand() {
const unocssConfig = await loadUnoConfig();
+ // Generate barrel files if barrels directory exists
+ const barrelsDir = resolve(process.cwd(), "barrels");
+ if (existsSync(barrelsDir)) {
+ console.log("Generating barrel files...");
+ await generateBarrels(barrelsDir);
+ }
+
// Initial build
const inputPath = resolve(process.cwd(), input);
const inputStat = await stat(inputPath);
diff --git a/packages/ono/src/content.js b/packages/ono/src/content.js
deleted file mode 100644
index 231bc5f..0000000
--- a/packages/ono/src/content.js
+++ /dev/null
@@ -1,272 +0,0 @@
-import { readdir, readFile } from "node:fs/promises";
-import { join, relative, sep } from "node:path";
-import { marked } from "marked";
-
-/**
- * Parse frontmatter from markdown content
- * @param {string} content - Raw markdown content
- * @returns {{ data: Object, content: string }} Parsed frontmatter and content
- */
-function parseFrontmatter(content) {
- const frontmatterRegex = /^---\s*\n([\s\S]*?)\n---\s*\n([\s\S]*)$/;
- const match = content.match(frontmatterRegex);
-
- if (!match) {
- return { data: {}, content };
- }
-
- const [, frontmatterStr, markdownContent] = match;
- const data = {};
-
- // Simple YAML-like parser (supports basic key: value pairs)
- const lines = frontmatterStr.split("\n");
- let currentKey = null;
- let arrayItems = [];
-
- for (const line of lines) {
- const trimmed = line.trim();
- if (!trimmed) continue;
-
- // Array item
- if (trimmed.startsWith("- ")) {
- if (currentKey) {
- arrayItems.push(trimmed.slice(2).trim());
- }
- continue;
- }
-
- // If we were collecting array items, save them
- if (currentKey && arrayItems.length > 0) {
- data[currentKey] = arrayItems;
- arrayItems = [];
- currentKey = null;
- }
-
- // Key-value pair
- const colonIndex = trimmed.indexOf(":");
- if (colonIndex > 0) {
- const key = trimmed.slice(0, colonIndex).trim();
- let value = trimmed.slice(colonIndex + 1).trim();
-
- // Parse value types
- if (value === "") {
- // Empty value might indicate an array follows
- currentKey = key;
- continue;
- } else if (value === "true") {
- value = true;
- } else if (value === "false") {
- value = false;
- } else if (value.startsWith("[") && value.endsWith("]")) {
- // JSON array - parse with proper quoting for unquoted strings
- try {
- // First try parsing as-is (for properly quoted JSON)
- value = JSON.parse(value);
- } catch {
- // If that fails, try parsing as YAML-style array [item1, item2]
- try {
- const items = value
- .slice(1, -1) // Remove [ ]
- .split(",")
- .map((item) => item.trim())
- .filter((item) => item.length > 0);
- value = items;
- } catch {
- // Keep as string if all parsing fails
- }
- }
- } else if (value.startsWith("{") && value.endsWith("}")) {
- // JSON object
- try {
- value = JSON.parse(value);
- } catch {
- // Keep as string if parsing fails
- }
- } else if (/^\d{4}-\d{2}-\d{2}/.test(value)) {
- // Date format
- value = new Date(value);
- } else if (/^\d+$/.test(value)) {
- value = parseInt(value, 10);
- } else if (/^\d+\.\d+$/.test(value)) {
- value = parseFloat(value);
- }
-
- data[key] = value;
- currentKey = null;
- }
- }
-
- // Handle final array if exists
- if (currentKey && arrayItems.length > 0) {
- data[currentKey] = arrayItems;
- }
-
- return { data, content: markdownContent };
-}
-
-/**
- * Validate data against schema
- * @param {Object} data - Data to validate
- * @param {Object} schema - Schema definition
- * @returns {{ valid: boolean, errors: string[] }}
- */
-function validateSchema(data, schema) {
- const errors = [];
-
- for (const [key, definition] of Object.entries(schema)) {
- const value = data[key];
-
- // Check required fields
- if (definition.required && (value === undefined || value === null)) {
- errors.push(`Missing required field: ${key}`);
- continue;
- }
-
- // Skip validation if field is not present and not required
- if (value === undefined || value === null) {
- // Apply default if specified
- if (definition.default !== undefined) {
- data[key] = definition.default;
- }
- continue;
- }
-
- // Type validation
- const actualType = Array.isArray(value) ? "array" : typeof value === "object" && value instanceof Date ? "date" : typeof value;
-
- if (definition.type !== actualType) {
- errors.push(
- `Invalid type for ${key}: expected ${definition.type}, got ${actualType}`,
- );
- continue;
- }
-
- // Array item type validation
- if (definition.type === "array" && definition.items) {
- for (let i = 0; i < value.length; i++) {
- const itemType = typeof value[i];
- if (itemType !== definition.items) {
- errors.push(
- `Invalid array item type for ${key}[${i}]: expected ${definition.items}, got ${itemType}`,
- );
- }
- }
- }
- }
-
- return { valid: errors.length === 0, errors };
-}
-
-/**
- * Generate slug from file path
- * @param {string} filePath - File path relative to content directory
- * @returns {string} Generated slug
- */
-function generateSlug(filePath) {
- return filePath
- .replace(/\.md$/, "")
- .split(sep)
- .join("/");
-}
-
-/**
- * Read all markdown files from a directory recursively
- * @param {string} dir - Directory path
- * @returns {Promise} Array of file paths
- */
-async function readMarkdownFiles(dir) {
- const files = [];
- const entries = await readdir(dir, { withFileTypes: true });
-
- for (const entry of entries) {
- const fullPath = join(dir, entry.name);
- if (entry.isDirectory()) {
- files.push(...(await readMarkdownFiles(fullPath)));
- } else if (entry.name.endsWith(".md")) {
- files.push(fullPath);
- }
- }
-
- return files;
-}
-
-/**
- * Load content collection configuration
- * @param {string} configPath - Path to content.config.js
- * @returns {Promise