Skip to content

feat: add content/preset support in chronicle.yaml with Zod validation#27

Open
rsbh wants to merge 1 commit intomainfrom
feat_add_preset_to_config
Open

feat: add content/preset support in chronicle.yaml with Zod validation#27
rsbh wants to merge 1 commit intomainfrom
feat_add_preset_to_config

Conversation

@rsbh
Copy link
Copy Markdown
Member

@rsbh rsbh commented Apr 1, 2026

Summary

  • Add content and preset fields to chronicle.yaml config schema
  • Replace manual TypeScript interfaces with Zod schemas for runtime YAML validation
  • CLI flags (--content, --preset) override YAML values (flag > yaml > default)
  • Split config loading into readConfig (I/O) and validateConfig (Zod safeParse) with user-friendly error messages

Test plan

  • Type check passes (no new errors)
  • Dev server smoke test returns 200
  • Verify content field in YAML resolves content directory correctly
  • Verify preset field in YAML is used by build/serve commands
  • Verify CLI flags override YAML values
  • Verify invalid YAML shows Zod validation errors

🤖 Generated with Claude Code

Add `content` and `preset` fields to chronicle.yaml config. CLI flags
override YAML values (flag > yaml > default). Replace manual TypeScript
interfaces with Zod schemas for runtime config validation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

📝 Walkthrough

Summary by CodeRabbit

  • Configuration Updates

    • Changed configuration field name from contentDir to content in Chronicle YAML configuration files.
  • Improvements

    • Enhanced configuration validation with stricter schema enforcement.

Walkthrough

This PR refactors the CLI configuration system to use centralized async validation with Zod schemas. It introduces loadCLIConfig as the primary config loader across all commands, replaces TypeScript interfaces with Zod schema definitions for runtime validation, adds a new preset configuration field, and renames the contentDir config field to content.

Changes

Cohort / File(s) Summary
Configuration Schema & Types
packages/chronicle/src/types/config.ts
Replaced TypeScript interfaces with Zod schema definitions. Added chronicleConfigSchema and converted all exported types (ChronicleConfig, LogoConfig, ThemeConfig, etc.) to z.infer types. Introduced new fields content and preset at the top level.
CLI Config Resolution & Validation
packages/chronicle/src/cli/utils/config.ts
Refactored loadCLIConfig signature to accept configPath and options object with content and preset fields. Added new validateConfig function using Zod schema validation. Updated resolveContentDir to accept ChronicleConfig as first parameter. Added resolvePreset utility function.
CLI Commands
packages/chronicle/src/cli/commands/build.ts, dev.ts, serve.ts, start.ts
Updated all command actions to use async loadCLIConfig instead of synchronous resolveContentDir/resolveConfigPath. Commands now pass derived preset value to createViteConfig. Added configPath parameter to Vite configuration in start.ts.
Example Configuration
examples/basic/chronicle.yaml
Renamed configuration field from contentDir: . to content: ..
Dependencies
packages/chronicle/package.json
Added zod dependency at version ^4.3.6. Reordered remark-parse within dependencies list.

Sequence Diagram

sequenceDiagram
    participant Cmd as CLI Command
    participant Load as loadCLIConfig
    participant Read as readConfig
    participant Validate as validateConfig
    participant Resolve as resolveContentDir/<br/>resolvePreset
    participant Vite as createViteConfig

    Cmd->>Load: loadCLIConfig(configPath, options)
    Load->>Read: readConfig(configPath)
    Read-->>Load: raw YAML string
    Load->>Validate: validateConfig(raw, configPath)
    Validate-->>Load: ChronicleConfig (typed)
    Load->>Resolve: resolveContentDir(config, options.content)
    Resolve-->>Load: contentDir
    Load->>Resolve: resolvePreset(config, options.preset)
    Resolve-->>Load: preset
    Load-->>Cmd: CLIConfig {contentDir, configPath, preset}
    Cmd->>Vite: createViteConfig(..., preset, configPath)
    Vite-->>Cmd: Vite config
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • rohilsurana
  • rohanchkrabrty
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main feature: adding content/preset support to chronicle.yaml configuration with Zod validation, which aligns with the primary changes across the codebase.
Description check ✅ Passed The description is directly related to the changeset, detailing the addition of content/preset fields, the replacement of TypeScript interfaces with Zod schemas, and the flag override precedence logic implemented throughout the PR.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat_add_preset_to_config

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/chronicle/src/cli/commands/start.ts (1)

7-12: ⚠️ Potential issue | 🟡 Minor

Missing --config option for consistency with other commands.

The start command is missing the --config <path> option that dev, build, and serve all define. This means users cannot specify a custom config path when using chronicle start, while they can for all other commands.

Proposed fix to add --config option
 export const startCommand = new Command('start')
   .description('Start production server')
   .option('-p, --port <port>', 'Port number', '3000')
   .option('--content <path>', 'Content directory')
+  .option('--config <path>', 'Path to chronicle.yaml')
   .action(async options => {
-    const { contentDir, configPath } = await loadCLIConfig(undefined, { content: options.content });
+    const { contentDir, configPath } = await loadCLIConfig(options.config, { content: options.content });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/cli/commands/start.ts` around lines 7 - 12, The
startCommand is missing the --config option; add .option('--config <path>',
'Config path') to the startCommand definition and pass the value into
loadCLIConfig like the other CLI commands (i.e., call loadCLIConfig(undefined, {
content: options.content, config: options.config }) so the existing destructure
of { contentDir, configPath } receives the user-provided config path; reference
startCommand and loadCLIConfig when making this change.
🧹 Nitpick comments (3)
packages/chronicle/src/cli/utils/config.ts (1)

19-30: Consider explicit never return type annotation for clarity.

The .catch() handler calls process.exit(1) which never returns, but TypeScript may infer the return type as Promise<string | never>. While the runtime behavior is correct, adding an explicit return type annotation or using throw after logging would make the control flow clearer.

Alternative pattern using throw
 async function readConfig(configPath: string): Promise<string> {
-  return fs.readFile(configPath, 'utf-8').catch((error: NodeJS.ErrnoException) => {
+  try {
+    return await fs.readFile(configPath, 'utf-8');
+  } catch (err) {
+    const error = err as NodeJS.ErrnoException;
     if (error.code === 'ENOENT') {
       console.log(chalk.red(`Error: chronicle.yaml not found at '${configPath}'`));
       console.log(chalk.gray("Run 'chronicle init' to create one"));
     } else {
       console.log(chalk.red(`Error: Failed to read '${configPath}'`));
       console.log(chalk.gray(error.message));
     }
     process.exit(1);
-  });
+  }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/cli/utils/config.ts` around lines 19 - 30, The
readConfig function's Promise return type is ambiguous because the .catch
callback calls process.exit(1); fix by making control flow explicit: update the
readConfig signature to Promise<string> and annotate the .catch handler callback
to return never (e.g., (error: NodeJS.ErrnoException): never => { ...
process.exit(1) }), or alternatively rewrite readConfig to async/await with a
try/catch that logs and then throws or calls process.exit, ensuring the catch
block has an explicit never/throw so TypeScript infers Promise<string> for
readConfig; reference symbols: readConfig, fs.readFile, .catch, process.exit.
packages/chronicle/src/types/config.ts (2)

8-11: Consider making theme.name optional with a default.

When theme is provided in YAML, name is required. This means a user who only wants to customize colors must also specify name:

theme:
  colors:
    primary: "#ff0000"
# This will fail validation because `name` is missing
Option 1: Make name optional
 const themeSchema = z.object({
-  name: z.enum(['default', 'paper']),
+  name: z.enum(['default', 'paper']).optional(),
   colors: z.record(z.string(), z.string()).optional(),
 })
Option 2: Use default value (Zod 4 syntax)
 const themeSchema = z.object({
-  name: z.enum(['default', 'paper']),
+  name: z.enum(['default', 'paper']).default('default'),
   colors: z.record(z.string(), z.string()).optional(),
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/types/config.ts` around lines 8 - 11, The current
themeSchema requires theme.name so users who only set theme.colors fail
validation; update themeSchema so name is optional with a sensible default
(e.g., "default") by changing the name field on themeSchema (the
z.enum(['default','paper']) entry) to use Zod's optional/default API (e.g., make
it optional and .default('default') or directly .default('default')), so that
when an object includes theme.colors but omits name it validates and the
resulting parsed value has name set to "default".

44-50: Consider making apiSchema.server optional.

The server field is required when defining an API entry, but users might not always need to override server settings if the spec file contains server definitions. This could be overly restrictive.

Proposed change
 const apiSchema = z.object({
   name: z.string(),
   spec: z.string(),
   basePath: z.string(),
-  server: apiServerSchema,
+  server: apiServerSchema.optional(),
   auth: apiAuthSchema.optional(),
 })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/chronicle/src/types/config.ts` around lines 44 - 50, The apiSchema
currently requires a server field which can be too strict; update apiSchema to
make server optional by replacing server: apiServerSchema with server:
apiServerSchema.optional(), and adjust any derived TypeScript types or usages
that assume server is always present (e.g., references to apiSchema, Api types,
or code that accesses api.server) to handle undefined/null accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@packages/chronicle/src/cli/commands/start.ts`:
- Around line 7-12: The startCommand is missing the --config option; add
.option('--config <path>', 'Config path') to the startCommand definition and
pass the value into loadCLIConfig like the other CLI commands (i.e., call
loadCLIConfig(undefined, { content: options.content, config: options.config })
so the existing destructure of { contentDir, configPath } receives the
user-provided config path; reference startCommand and loadCLIConfig when making
this change.

---

Nitpick comments:
In `@packages/chronicle/src/cli/utils/config.ts`:
- Around line 19-30: The readConfig function's Promise return type is ambiguous
because the .catch callback calls process.exit(1); fix by making control flow
explicit: update the readConfig signature to Promise<string> and annotate the
.catch handler callback to return never (e.g., (error: NodeJS.ErrnoException):
never => { ... process.exit(1) }), or alternatively rewrite readConfig to
async/await with a try/catch that logs and then throws or calls process.exit,
ensuring the catch block has an explicit never/throw so TypeScript infers
Promise<string> for readConfig; reference symbols: readConfig, fs.readFile,
.catch, process.exit.

In `@packages/chronicle/src/types/config.ts`:
- Around line 8-11: The current themeSchema requires theme.name so users who
only set theme.colors fail validation; update themeSchema so name is optional
with a sensible default (e.g., "default") by changing the name field on
themeSchema (the z.enum(['default','paper']) entry) to use Zod's
optional/default API (e.g., make it optional and .default('default') or directly
.default('default')), so that when an object includes theme.colors but omits
name it validates and the resulting parsed value has name set to "default".
- Around line 44-50: The apiSchema currently requires a server field which can
be too strict; update apiSchema to make server optional by replacing server:
apiServerSchema with server: apiServerSchema.optional(), and adjust any derived
TypeScript types or usages that assume server is always present (e.g.,
references to apiSchema, Api types, or code that accesses api.server) to handle
undefined/null accordingly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 47eca8b8-1c3e-4d7e-8cc6-d58a0909a2b1

📥 Commits

Reviewing files that changed from the base of the PR and between ec1599a and 1aca8be.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • examples/basic/chronicle.yaml
  • packages/chronicle/package.json
  • packages/chronicle/src/cli/commands/build.ts
  • packages/chronicle/src/cli/commands/dev.ts
  • packages/chronicle/src/cli/commands/serve.ts
  • packages/chronicle/src/cli/commands/start.ts
  • packages/chronicle/src/cli/utils/config.ts
  • packages/chronicle/src/types/config.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant