zudo-doc
GitHub repository

Type to search...

to open search from anywhere

l-migrate-to-preset-style

Migrate an existing generated zudo-doc project toward the preset-first shape: diff against the pinned template baseline, auto-delete files that are byte-identical to the template, pause for human conf...

/l-migrate-to-preset-style

Migrate an existing generated project from the legacy flat-config shape toward the preset-first shape (zudoDocPreset() in zfb.config.ts, no inline plugin wiring). The migration is human-gated: auto-delete is permitted only for files that are byte-identical to the pinned template baseline; any edited or ambiguous file pauses for explicit human confirmation before touching it.

Shaped by l-lessons-zfb-migration-parity: a dead island silently server-renders and the build still exits 0, so byte-comparison alone is insufficient proof that an island migration succeeded. Every island-touching step is gated behind a hydration smoke test.


Preconditions

Verify ALL of the following before proceeding. Stop and tell the user if any fails.

  1. The target project was generated by create-zudo-doc (a package.json and a src/config/settings.ts exist at the project root).

  2. The working tree is clean (git status --porcelain returns empty). If not, commit or stash first — the migration writes files and a dirty tree makes audit ambiguous.

  3. Identify the pinned @takazudo/zudo-doc version from package.json and confirm the corresponding create-zudo-doc release is tagged in this repo (needed to locate the baseline templates). If the version cannot be resolved, ask the user for the tag before continuing.

  4. --dry-run — when passed, run Steps 1–3 (diff + classify + report) but apply no mutations, perform no hydration checks, and print the plan without executing it.


Step 1 — Template-baseline diff

Use scripts/check-template-drift.sh (from the zudo-doc repo) as the canonical diff mechanism. The script compares templates/base/ and templates/features/*/files/ against the production project, respecting .template-drift-allowlist. It reports three categories:

  • [DIFF] — file differs from the template (may have local edits)

  • [MISSING IN PROD] — template file has no counterpart in the target project

  • [USE-CLIENT DRIFT]"use client" directive mismatch between template and host

Run from the target project root, pointing ROOT_DIR at it:

ROOT_DIR=<target-project-root> bash <path-to-zudo-doc>/scripts/check-template-drift.sh

Capture the full output. This is the definitive baseline: the template side is the pristine pinned source; the target project side is what the user may have customized.


Step 2 — Classify each drifted file

For every file flagged by Step 1, classify it into one of three categories:

CategoryCriterionAction
A — byte-identicaldiff -q returns 0 against the template counterpartSafe to auto-delete (no local edits)
B — editeddiff -q returns 1Pause → human confirmation → default: eject
C — missing in prodTemplate file has no counterpartUsually means it should be added; pause → confirm

Byte-identical check (the definitive test for Category A):

diff -q <template-file> <target-file>
# exit 0 → identical (Category A)
# exit 1 → differs (Category B)

Build two lists — AUTO_DELETE (Category A) and NEEDS_CONFIRMATION (B + C) — before taking any action.


Step 3 — Auto-delete byte-identical files (Category A)

Files in AUTO_DELETE are safe to remove — they contain no local edits; the preset-based project derives their content from the package instead of copying it.

for f in "${AUTO_DELETE[@]}"; do
  rm "$f"
  git rm "$f"          # stage the removal
done

Report each deleted path so the user has a clear audit trail.

Do NOT auto-delete "use client" island components even if they are byte-identical. Islands that are byte-identical to the template stub may still have been relied upon for hydration wiring. Move every island component to NEEDS_CONFIRMATION regardless of diff result — the hydration smoke gate in Step 5 is mandatory before their removal can be declared safe.


Step 4 — Human-gated loop for edited / ambiguous files (Category B + C)

For each file in NEEDS_CONFIRMATION, pause and present the following prompt to the user:

File: <relative-path>
Diff summary:
<show diff --stat or a short unified diff — keep it readable>

Action?
  [E] eject — copy local source into project via `zudo-doc eject <component>` (recommended)
  [K] keep  — keep the file as-is (project retains ownership, no package import)
  [S] skip  — leave file unchanged for now (handle later)
  [D] delete — delete this file (only if you are certain it contains no custom edits)

Default: [E] eject

Wait for the user's explicit response before proceeding to the next file. The default action on Enter (or ambiguous input) is eject. Never silently delete or overwrite an edited file.

Eject handoff

When the user chooses eject (or accepts the default), call the C1 eject CLI:

zudo-doc eject <component>

The CLI (from packages/create-zudo-doc) performs:

  1. Copies the component's TS source from the published eject/ bundle into src/components/zudo-doc/<component>/ inside the target project.

  2. Rewrites parent-relative cross-component imports to @takazudo/zudo-doc/<dir> subpath specifiers so they keep resolving against the installed package.

  3. Records the ejected component in .zudo-doc.json: { "ejected": { "<component>": "src/components/zudo-doc/<component>" } }.

  4. Is idempotent — re-running on an already-ejected component prints "already ejected at <path>" and exits 0 without clobbering local edits.

Ejectable components (12 presentational/layout components — the full allowlist):

CLI nameImport subpath
header@takazudo/zudo-doc/header
footer@takazudo/zudo-doc/footer
breadcrumb@takazudo/zudo-doc/breadcrumb
toc@takazudo/zudo-doc/toc
sidebar@takazudo/zudo-doc/sidebar
theme-toggle@takazudo/zudo-doc/theme-toggle
page-loading@takazudo/zudo-doc/page-loading
tab-item@takazudo/zudo-doc/tab-item
doc-pager@takazudo/zudo-doc/doc-pager
content-admonition@takazudo/zudo-doc/content-admonition
code-group@takazudo/zudo-doc/code-group
details@takazudo/zudo-doc/details

If the file to be ejected does not map to one of these 12 components, the eject CLI will reject it. In that case, route to keep and advise the user to handle it manually.


Step 5 — Hydration smoke gate (mandatory for every island-touching change)

Critical invariant (per l-lessons-zfb-migration-parity): a dead island server-renders silently. The zfb build exits 0 and emits valid HTML even when an island component loses its "use client" directive or its registration is broken. Byte-identical deletion and eject are both insufficient proof that islands still hydrate. This gate is not optional — skip it only when --skip-hydration-check was explicitly passed.

After completing Steps 3 and 4, run the hydration smoke check:

pnpm build

Then verify island hydration for each component that was deleted or ejected. The minimal check is a grep for the island marker in the built HTML:

# Each interactive island should have a hydration marker in dist/ HTML.
# zfb emits data-island markers for client-registered components.
grep -r 'data-island' dist/ | head -20

For any island component touched in Steps 3–4, also verify the component is reachable in the dev server and interactive — a static HTML render is not the same as a hydrated island. Use pnpm dev + a quick manual check, or invoke /verify-ui for a computed-style smoke:

pnpm dev &
# Then check the component interacts as expected.

If any island fails the hydration check:

  1. Stop immediately — do not proceed to Step 6.

  2. Report which component is dead (grep dist/ for the island marker pattern).

  3. Offer to re-eject or restore the file from git: git checkout -- <path>.


Step 6 — Verify build and types

After all mutations, run a full build + typecheck to confirm nothing is broken:

pnpm build
pnpm check

Fix any TypeScript errors before committing. The most common post-migration errors are:

  • Import path still references the old local copy after it was deleted (the preset version uses the package subpath instead).

  • A "use client" directive is missing on a newly-ejected component that the host registers as an island — add it back at line 1.


Step 7 — Emit summary report

After Steps 3–6 complete (or at any early stop), print a structured summary:

## Migration Summary

### Auto-deleted (byte-identical to template)
- src/components/foo.tsx
- ...

### Ejected (via `zudo-doc eject`)
- theme-toggle → src/components/zudo-doc/theme-toggle/ (recorded in .zudo-doc.json)
- ...

### Kept as-is (user chose to retain ownership)
- src/components/custom-header.tsx
- ...

### Skipped (deferred for later)
- src/components/bar.tsx
- ...

### Hydration smoke: PASS / FAIL
<list any failed islands>

### Build: PASS / FAIL
### Typecheck: PASS / FAIL

### Next steps
<list any remaining manual steps, e.g. "bar.tsx was skipped — revisit after ejecting foo">

Flags

FlagEffect
--dry-runRun Steps 1–3 classification only; print the plan without mutating any files.
--skip-hydration-checkSkip Step 5 (only for non-island migrations or known-safe deletions). Must be explicit — not implied by any other flag.

Key files and references

PathRole
scripts/check-template-drift.shTemplate-baseline diff mechanism (the source of truth)
.template-drift-allowlistFiles excluded from the automated drift check (still need manual review)
packages/create-zudo-doc/templates/base/Pristine base template
packages/create-zudo-doc/templates/features/*/files/Pristine feature-specific templates
packages/create-zudo-doc/docs/eject-contract.mdFull eject CLI contract (C0 #2359)
packages/create-zudo-doc/src/eject.tsEJECTABLE map (authoritative list of valid component names)
.zudo-doc.jsonProvenance marker in the target project (records ejected components + version)
.claude/skills/l-lessons-zfb-migration-parity/SKILL.mdLessons on why hydration smoke is mandatory

Revision History

CreatedUpdated

AI Assistant

Ask a question about the documentation.