Skip to content

Add config generation and extends support for layered configs#2597

Draft
mosch wants to merge 3 commits into
mainfrom
claude/intelligent-cori-daex1h
Draft

Add config generation and extends support for layered configs#2597
mosch wants to merge 3 commits into
mainfrom
claude/intelligent-cori-daex1h

Conversation

@mosch

@mosch mosch commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Summary

This PR adds support for composing Zudoku configurations from multiple layers via extends, and introduces zudoku generate and zudoku schema CLI commands. Together they enable a workflow where a tool (e.g. the Zuplo portal) emits a serializable config spec that is turned into a typed config layer with real plugin imports just before zudoku dev/zudoku build runs — the committed config references it lazily and never needs the file to exist.

Key Changes

  • Lazy config layering via extends: entries are module specifiers, tsconfig-style — extends: ["./zudoku.base"] — resolved at config-load time relative to the declaring file (extension optional; bare package specifiers supported; inline config objects can be mixed in). A missing layer fails with a hint to run zudoku generate. Merge semantics: layers fold left-to-right with the config on top; scalars/objects later-wins, plugins concatenate, other arrays replace; chains resolve depth-first with cycle detection.

  • Layer resolution across all runtimes:

    • The config loader walks the extends chain depth-first, importing each layer through the same Vite pipeline as the config itself, and watches layer files so regenerating one reloads dev.
    • A new virtual:zudoku-raw-config module emits a static import per layer and replays the same walk, so layer plugin instances are created in the config module's realm (build-time virtual modules resolve). It feeds virtual:zudoku-config (routing) and replaces the config file as the SSR build's zudoku.config entry, covering dev, SSG prerender, and SSR runtime.
  • Config spec validation and generation:

    • spec.ts defines the serializable projection of ZudokuConfig and validateSpec() with $schema version checking (https://schemas.zudoku.dev/config-spec/v1.json)
    • generate.ts implements generateConfig(): validated spec → typed TypeScript config module with real plugin factory imports
    • Strict top-level validation catches typos; plugin options are validated per-plugin for better error messages
  • JSON Schema generation: buildSpecJsonSchema() derives the publishable schema from the Zod source — non-serializable fields (functions, React nodes) are omitted unless annotated via .meta(), plugin refs compose as a union keyed by id, unused $defs are pruned.

  • Preset plugin registry: preset-registry.ts curates the available plugins (graphql, monetization) with their option schemas. The registry lives in core because the CLI cannot import plugin packages at runtime; type tests in each plugin package (preset-descriptor.test-d.ts) guard against drift.

  • CLI commands:

    • zudoku generate <spec> -o zudoku.base.ts reads a JSON spec, validates it, and writes a typed config module
    • zudoku schema emits the JSON Schema for spec validation in editors/UIs
  • Example project: examples/config-spec/ demonstrates the lazy flow: zudoku.base.ts is gitignored and regenerated by the dev/build scripts from spec.json; the hand-written config composes it via extends: ["./zudoku.base"]. Built in CI with a smoke test asserting that routes contributed solely by the generated layer prerender.

https://claude.ai/code/session_01AZzEMtJTS9L4azMgsBfPH6

Adds a UI-driven config workflow: a serializable spec (emitted e.g. by the
Zuplo portal) is turned into a typed Zudoku config layer with real static
plugin imports, which the user's config composes via a new `extends` field.

- `extends` on ZudokuConfig: layers fold left to right with the config on
  top; scalars/objects later-wins, `plugins` concatenate. Resolved in the
  loader, `virtual:zudoku-config` (routing), and the prerender worker so
  layer-contributed plugins reach the route tree in dev, SSR, and build.
- `zudoku generate spec.json -o zudoku.base.ts`: validates the spec
  (`$schema`-versioned, strict top level, per-plugin option schemas) and
  emits a typed config module; also exported programmatically from
  `zudoku/codegen` as `generateConfig`.
- `zudoku schema`: emits the publishable JSON Schema for the spec, derived
  from the Zod config schema with non-serializable (code-holding) fields
  omitted and preset plugin options composed as a union keyed by `id`.
- Curated preset registry (graphql, monetization) in core; drift guarded by
  type tests in each plugin package (vitest typecheck now enabled there).
- `examples/config-spec`: generated layer + extends wiring, built in CI
  with a smoke test asserting layer-contributed routes prerender.

https://claude.ai/code/session_01AZzEMtJTS9L4azMgsBfPH6
@vercel

vercel Bot commented Jun 11, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zudoku-cosmo-cargo Ready Ready Preview, Comment Jun 12, 2026 8:32am
zudoku-dev Ready Ready Preview, Comment Jun 12, 2026 8:32am

Request Review

@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 68.79%
⬆️ +0.80%
4969 / 7223
🔵 Statements 68%
⬆️ +0.76%
5348 / 7864
🔵 Functions 59.68%
⬆️ +0.94%
1186 / 1987
🔵 Branches 58.35%
⬆️ +0.75%
3571 / 6119
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/zudoku/src/codegen/generate.ts 89.13% 70% 100% 95.23% 14, 21, 28, 39, 47
packages/zudoku/src/codegen/json-schema.ts 93.61% 90.41% 100% 95.29% 74, 84, 96, 123, 146, 149
packages/zudoku/src/codegen/spec.ts 97.22% 95.65% 100% 97.14% 81
packages/zudoku/src/config/loader.ts 82.4%
⬇️ -2.60%
62.68%
⬆️ +2.68%
80%
⬆️ +10.00%
83.05%
⬇️ -3.43%
79, 82, 124-128, 133-135, 173-180, 190-192, 200-204, 207-209, 297, 326, 350-352, 356, 371, 420-430
packages/zudoku/src/config/preset-registry.ts 100% 100% 100% 100%
packages/zudoku/src/config/validators/InputNavigationSchema.ts 88.46%
⬆️ +3.85%
100%
🟰 ±0%
40%
⬆️ +20.00%
88.46%
⬆️ +0.46%
10, 41, 78
packages/zudoku/src/config/validators/ZudokuConfig.ts 83.33%
⬆️ +1.11%
76.47%
🟰 ±0%
48%
⬆️ +4.00%
84.09%
⬆️ +1.14%
110-113, 160-192, 229, 290, 404, 413, 643, 680, 696, 865
packages/zudoku/src/lib/core/plugins.ts 91.66%
🟰 ±0%
85%
🟰 ±0%
83.33%
🟰 ±0%
91.66%
🟰 ±0%
39, 43
packages/zudoku/src/lib/core/transform-config.ts 85.36%
⬆️ +10.36%
93.75%
⬆️ +7.09%
87.5%
⬆️ +12.50%
87.5%
⬆️ +9.24%
113-122
packages/zudoku/src/vite/prerender/prerender.ts 8.47%
🟰 ±0%
13.33%
🟰 ±0%
16.66%
🟰 ±0%
8.1%
🟰 ±0%
64-266, 293-326, 340-356
Generated in workflow #5851 for commit e9b8431 by the Vitest Coverage Report Action

`extends` entries are now module specifiers (tsconfig-style) resolved at
config-load time instead of statically imported config objects. This makes
generated layers lazy: `zudoku generate` can emit the base config just
before `zudoku dev`/`zudoku build` runs, and the committed config never
needs the file to exist.

- Specifiers resolve relative to the declaring config file, extension
  optional; bare package specifiers go through module resolution. A missing
  layer fails with a hint to run `zudoku generate`. Inline config objects
  still work and can be mixed with strings.
- The loader walks the extends chain depth-first, importing each layer
  through the same Vite pipeline as the config itself, watches layer files
  for dev reload, and records the ordered specifiers in `__meta`.
- New `virtual:zudoku-raw-config` emits a static import per layer and
  replays the same walk, so layer plugin instances are created in the
  config module's realm. It feeds `virtual:zudoku-config` and replaces the
  config file as the SSR build's `zudoku.config` entry, covering dev, SSG
  prerender, and SSR runtime.
- The example now demonstrates the lazy flow: `zudoku.base.ts` is
  gitignored and regenerated by the dev/build scripts; the config uses
  `extends: ["./zudoku.base"]`.

https://claude.ai/code/session_01AZzEMtJTS9L4azMgsBfPH6
The config-spec smoke step was inserted mid-step, orphaning the
favicon/asset-prefix checks (which reference the unset `$nested`) inside
the new step and failing the job.

https://claude.ai/code/session_01AZzEMtJTS9L4azMgsBfPH6
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.

2 participants