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.
The target project was generated by
create-zudo-doc(apackage.jsonand asrc/exist at the project root).config/ settings. ts The working tree is clean (
git status --porcelainreturns empty). If not, commit or stash first — the migration writes files and a dirty tree makes audit ambiguous.Identify the pinned
@takazudo/zudo-docversion frompackage.jsonand confirm the correspondingcreate-zudo-docrelease 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.--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/ (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.shCapture 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:
| Category | Criterion | Action |
|---|---|---|
| A — byte-identical | diff -q returns 0 against the template counterpart | Safe to auto-delete (no local edits) |
| B — edited | diff -q returns 1 | Pause → human confirmation → default: eject |
| C — missing in prod | Template file has no counterpart | Usually 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
doneReport 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 toNEEDS_CONFIRMATIONregardless 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] ejectWait 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:
Copies the component's TS source from the published
eject/bundle intosrc/inside the target project.components/ zudo- doc/ <component>/ Rewrites parent-relative cross-component imports to
@takazudo/zudo-doc/<dir>subpath specifiers so they keep resolving against the installed package.Records the ejected component in
.zudo-doc.json:{ "ejected":.{ "<component>": "src/ components/ zudo- doc/ <component>" } } 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 name | Import subpath |
|---|---|
header | @takazudo/ |
footer | @takazudo/ |
breadcrumb | @takazudo/ |
toc | @takazudo/ |
sidebar | @takazudo/ |
theme-toggle | @takazudo/ |
page-loading | @takazudo/ |
tab-item | @takazudo/ |
doc-pager | @takazudo/ |
content-admonition | @takazudo/ |
code-group | @takazudo/ |
details | @takazudo/ |
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-checkwas explicitly passed.
After completing Steps 3 and 4, run the hydration smoke check:
pnpm buildThen 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 -20For 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 / for a computed-style
smoke:
pnpm dev &
# Then check the component interacts as expected.If any island fails the hydration check:
Stop immediately — do not proceed to Step 6.
Report which component is dead (grep
dist/for the island marker pattern).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 checkFix 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
| Flag | Effect |
|---|---|
--dry-run | Run Steps 1–3 classification only; print the plan without mutating any files. |
--skip-hydration-check | Skip Step 5 (only for non-island migrations or known-safe deletions). Must be explicit — not implied by any other flag. |
Key files and references
| Path | Role |
|---|---|
scripts/ | Template-baseline diff mechanism (the source of truth) |
.template-drift-allowlist | Files excluded from the automated drift check (still need manual review) |
packages/ | Pristine base template |
packages/ | Pristine feature-specific templates |
packages/ | Full eject CLI contract (C0 #2359) |
packages/ | EJECTABLE map (authoritative list of valid component names) |
.zudo-doc.json | Provenance marker in the target project (records ejected components + version) |
. | Lessons on why hydration smoke is mandatory |