Files
dotfiles/dot_agents/skills/blume-migrate/references/monorepo.md
T

226 lines
14 KiB
Markdown

# Migrating into a real repo (monorepos, pnpm, Vercel)
The framework references (`mintlify.md`, etc.) cover translating _content and config_. This file covers the **host repo** the docs live in — the concerns that surface when the target isn't a bare docs folder but a pnpm/Turbo workspace deployed on Vercel. All six were real failures in a production migration; none are Blume bugs, they're integration steps.
**Golden rule for this whole file: detect, then edit surgically, then say what you changed and why.** Every edit here touches a file the repo owns (`pnpm-workspace.yaml`, `vercel.json`, `package.json`, the lockfile). Never guess a path depth or a workspace name — read it out of the repo first. Where a step can't be fully automated (the Astro patch, the Vercel Root Directory setting), leave a copy-pasteable manual instruction rather than a half-applied change.
---
## 1. Non-standard content layout → scope `content.root` + `include`
`content.root` defaults to `"docs"`, and `blume` runs from the directory that holds `blume.config.ts` (in a monorepo, that's the docs _package_, e.g. `apps/docs/`). Many repos **don't** have a `docs/` subfolder — the content sits directly under the app dir in topic folders (`apps/docs/api/`, `apps/docs/getting-started/`). With the default root, Blume looks for `apps/docs/docs/` and finds nothing.
**Detect by finding where `.md`/`.mdx` actually live** — never assume `docs/`. From the docs package dir:
```bash
# top two path segments of every content file, deduped — your content folders
find . -path ./node_modules -prune -o \( -name '*.md' -o -name '*.mdx' \) -print \
| sed 's|^\./||' | cut -d/ -f1 | sort -u
```
If that lists a real `docs` folder, keep the default. If it lists content folders directly (`api`, `getting-started`, `guides`), the content root is the package dir itself.
**Set `content.root` to the package dir and scope with explicit `include` globs — do _not_ use `content.root: "."` alone.** A bare `.` root scans the whole package: `src/`, `public/`, `README.md`, config files, and (in a monorepo) anything a loose glob reaches. That's slow and pulls non-content Markdown into the site. Instead pin `root` and list the real content folders in `content.include`:
```ts blume.config.ts
import { defineConfig } from "blume";
export default defineConfig({
content: {
root: ".", // content sits directly under this package (apps/docs/), no docs/ subfolder
// include/exclude globs are relative to `content.root`. List the actual
// content folders so the scan never wanders into src/, public/, etc.
include: [
"api/**/*.{md,mdx}",
"getting-started/**/*.{md,mdx}",
"guides/**/*.{md,mdx}",
],
exclude: ["**/node_modules/**", "**/_*", "**/.*"],
},
});
```
Schema field names, exactly: **`content.root`** (string, relative to the project/cwd), **`content.include`** (array of globs, relative to `content.root`, default `["**/*.{md,mdx}"]`), **`content.exclude`** (array of globs, relative to `content.root`, default `["**/_*", "**/.*"]`). Singular `include`/`exclude`, both arrays.
Report to the user: the detected content root, the folders you scoped `include` to, and anything Markdown you deliberately left out (a top-level `README.md`, a `CHANGELOG.md`) so they can confirm it isn't content.
---
## 2. pnpm `minimumReleaseAge` blocks installing fresh Blume
pnpm's supply-chain guard (`minimumReleaseAge`, often set to `1440` = 24h) refuses to install any package version published more recently than the window. Right after Blume publishes, that's **every** Blume release — so `pnpm add blume` / the workspace install fails or silently pins an older version.
**Do not disable the policy globally** — that removes the guard for the whole dependency tree. Add **only `blume`** to the exclude list.
**Detect** in `pnpm-workspace.yaml` (pnpm 10+, the primary location) and, for older setups, `.npmrc`:
```bash
grep -rn "minimumReleaseAge\|minimum-release-age" pnpm-workspace.yaml .npmrc 2>/dev/null
```
**Fix — `pnpm-workspace.yaml`** (surgical add; create the key if the policy is set but no exclude exists):
```yaml
minimumReleaseAge: 1440
minimumReleaseAgeExclude:
- blume
```
**Fix — `.npmrc`** form (if the policy lives there instead):
```ini
minimum-release-age=1440
minimum-release-age-exclude[]=blume
```
Report: "Added `blume` to `minimumReleaseAgeExclude` so the just-published version installs; the release-age guard still applies to everything else."
---
## 3. Frozen lockfile — regenerate and commit `pnpm-lock.yaml` in the same change
CI and Vercel install with `--frozen-lockfile` (pnpm's default in CI): if `package.json` and `pnpm-lock.yaml` disagree by even one dependency, the install **fails immediately** — before any build step runs. A migration always edits dependencies (add `blume`, drop the old framework), so the lockfile is guaranteed stale.
**After every dependency edit, regenerate the lockfile and stage it with the manifest:**
```bash
pnpm install # NOT --frozen-lockfile — this rewrites pnpm-lock.yaml
git add package.json apps/docs/package.json pnpm-lock.yaml
```
Rules:
- Run `pnpm install` (plain) from the **workspace root**, not the docs package, so the single root lockfile updates.
- The lockfile change belongs in the **same commit** as the `package.json` change. A commit that edits deps without the matching lockfile is a broken build for anyone who pulls it.
- Verify locally the way CI will: `pnpm install --frozen-lockfile` should succeed with no diff. If it errors "lockfile not up to date," you forgot the plain install.
---
## 4. Vercel monorepo recipe (root-aware install + build)
A plain `blume build` works locally, but Vercel in a workspace needs three things aligned: the install must run at the **workspace root** (so pnpm resolves the whole graph under a frozen lockfile), the build must run in the **docs package**, and Vercel must be pointed at the built `dist/`. `blume build` emits a static site to `dist/` **relative to where it runs** — so building in `apps/docs/` produces `apps/docs/dist/`.
This is a **copyable template**, not prose. Three pieces:
**a) Docs package `apps/docs/package.json`** — the standard Blume scripts:
```json
{
"scripts": {
"dev": "blume dev",
"build": "blume build",
"preview": "blume preview"
},
"dependencies": {
"blume": "^1"
}
}
```
**b) `apps/docs/vercel.json`** — root-aware install, local build, `dist` output:
```json
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": null,
"installCommand": "cd ../.. && pnpm install --frozen-lockfile",
"buildCommand": "pnpm run build",
"outputDirectory": "dist"
}
```
**c) The one setting `vercel.json` can't hold — set it in the Vercel project (dashboard → Settings → General, or `vercel` CLI):**
- **Root Directory** = `apps/docs`. This makes install/build/output paths resolve from the docs package, which is what the `vercel.json` above assumes.
- **Node version** = 22 or newer (Blume requires it). Or pin it in the docs package: `"engines": { "node": ">=22" }`.
How the pieces fit, with Root Directory = `apps/docs`:
- `installCommand` does `cd ../..` to reach the **workspace root** and runs a frozen install of the whole graph. **Adjust `../..` to the app's actual depth** — an app at `packages/web/docs/` needs `cd ../../..`. Read the path; don't assume two levels.
- `buildCommand` runs in `apps/docs/` and invokes `blume build` (via the package's `build` script) → emits `apps/docs/dist/`.
- `outputDirectory: "dist"` is relative to the Root Directory (`apps/docs`), so it points at `apps/docs/dist/`.
- `framework: null` stops Vercel from auto-detecting Astro and overriding your build/output with its Astro preset (which expects a different output path). Blume's static output is plain files — Vercel just serves the folder.
If the repo uses **Turborepo**, you can instead build through the root pipeline — `"buildCommand": "cd ../.. && pnpm turbo build --filter=docs"` — for remote-cache reuse. Keep `outputDirectory: "dist"` and Root Directory `apps/docs`.
Report every piece you wrote and the two dashboard settings the user must set by hand (Root Directory, Node version) — those can't be committed.
**Redirect caveat on this deploy path:** a static build emits `vercel.json` redirect rules into `dist/`, but Vercel's **git-integration** builds read `vercel.json` only from the project's Root Directory — the copy inside `dist/` is honored only when the dist folder is deployed directly via the Vercel CLI. On the §4 setup, `blume.config.ts` redirects still work (Blume also emits per-route meta-refresh pages), but they're soft redirects, not real 3xx. If real 3xx responses matter (SEO for a large moved site), copy the generated redirect rules from `dist/vercel.json` after a build into the committed `apps/docs/vercel.json`'s `redirects` array, and note they must be re-synced when `redirects` change. (Most migrations are fine with the soft fallback — say which you chose.)
---
## 5. Astro/Vite version mismatch → pnpm patch (manual)
Blume pins **`astro ^7`**. If the workspace forces a specific **Vite** version — a root `pnpm.overrides` / `resolutions` entry, or a `vite` in the workspace catalog — that pinned Vite can diverge from the version Astro 7 expects, and Astro's internal build behavior mismatches. The symptom is a **build-time crash inside Astro/Vite** (a Rollup/Vite internal error, a missing-export or hook-signature error deep in `astro`/`vite` during `blume build`) that doesn't reproduce in a clean, override-free project.
**Detect the risk factor** before it bites:
```bash
grep -rn "\"vite\"\|vite@\| vite:" pnpm-workspace.yaml package.json 2>/dev/null | grep -i "override\|resolution\|catalog\|vite"
```
Blume is adding early version validation separately, so a future release will flag this at startup. Until then, **recognize the symptom and apply a pinned patch** — pnpm's `patch` workflow writes a committed diff that pnpm reapplies on every install (survives CI and frozen installs):
```bash
# 1. Open the offending version in an editable temp dir (use the exact version
# pnpm resolved — check `pnpm why astro`):
pnpm patch astro@7.0.6
# 2. Edit files in the printed temp path to reconcile the Astro/Vite interface
# (the exact edit depends on the mismatch; the crash's stack trace names the
# file and call site). Then commit the patch:
pnpm patch-commit '<the temp path pnpm printed>'
```
`patch-commit` writes `patches/astro@7.0.6.patch` and registers it under `patchedDependencies` in `pnpm-workspace.yaml` (pnpm 10+) or root `package.json` (`pnpm.patchedDependencies` on pnpm 9):
```yaml
# pnpm-workspace.yaml
patchedDependencies:
astro@7.0.6: patches/astro@7.0.6.patch
```
Commit **both** `patches/astro@7.0.6.patch` and the `patchedDependencies` entry, then re-run `pnpm install` and `blume build`.
This is a **manual step — do not fabricate the patch contents.** Leave the user this exact recipe plus the crash's stack trace, and tell them: the cleaner long-term fix is to relax the workspace's Vite pin so Astro 7 resolves its own compatible Vite, and drop the patch once Blume ships version validation. Don't fail silently — if a build crashes inside Astro/Vite and a Vite override exists, name this as the likely cause.
---
## 6. Ultracite / oxfmt formatting → oxfmt patch (ship the bundled patch)
If the repo uses (or the user wants to adopt) **[Ultracite](https://www.ultracite.ai)** for formatting — an `ultracite check` / `ultracite fix` script, an `ultracite` dev dep, or oxlint/oxfmt in the toolchain — it will format the migrated `.md`/`.mdx` **and break Blume's `:::` directives.** Ultracite's formatter is **oxfmt**, and under `proseWrap` oxfmt joins the opening/closing `:::` fence line into the surrounding prose, which invalidates the directive (`:::note` … `:::` callouts, tabs, steps — the exact syntax you convert callouts _into_ in step 5). Blume's own repo hits this and pins a patched oxfmt; a migrated repo needs the same patch or every directive silently degrades to literal text on the next `ultracite fix`.
The fix is a committed **pnpm patch** (`patches/oxfmt@0.55.0.patch`), shipped with this skill at `assets/oxfmt@0.55.0.patch`. Unlike the Astro/Vite patch, this one is a **known, deterministic diff** — copy it in, don't regenerate it:
```bash
# 1. Copy the shipped patch into the target repo's patches/ dir (keep the exact filename;
# <skill> = this skill's directory, the one containing SKILL.md):
mkdir -p patches
cp "<skill>/assets/oxfmt@0.55.0.patch" patches/oxfmt@0.55.0.patch
```
Then register it under `patchedDependencies` — `pnpm-workspace.yaml` on pnpm 10+, or root `package.json` (`pnpm.patchedDependencies`) on pnpm 9:
```yaml
# pnpm-workspace.yaml
patchedDependencies:
oxfmt@0.55.0: patches/oxfmt@0.55.0.patch
```
Commit **both** the patch file and the `patchedDependencies` entry, then re-run `pnpm install`.
- **The patch is pinned to `oxfmt@0.55.0`** (the version Ultracite 7.8.x resolves). pnpm requires an exact version match — if `pnpm why oxfmt` reports a different version, the patch won't apply. Bump the key to the resolved version (the hunk is a one-line prose-wrap guard and usually still applies cleanly); if it doesn't, tell the user and fall back to keeping directive-heavy files out of the formatter's globs.
- **Not on pnpm?** The patch mechanism is pnpm-specific. For npm/yarn, either pin oxfmt and apply the diff with `patch-package`, or exclude `.md`/`.mdx` from Ultracite formatting so it never touches the directives — report whichever you chose.
---
## Checklist for a workspace migration
- [ ] Located real content folders (not assumed `docs/`); set `content.root` + scoped `content.include`.
- [ ] `minimumReleaseAge` present? Added `blume` to `minimumReleaseAgeExclude` only.
- [ ] Ran plain `pnpm install`; committed `pnpm-lock.yaml` with the `package.json` change; verified `pnpm install --frozen-lockfile` is clean.
- [ ] Wrote `apps/docs/vercel.json` + package scripts; told the user to set Root Directory + Node 22 in the Vercel project.
- [ ] Checked for a workspace Vite override; if the build crashes inside Astro/Vite, gave the pnpm-patch recipe.
- [ ] Uses Ultracite/oxfmt? Shipped `patches/oxfmt@0.55.0.patch` + registered it under `patchedDependencies` so formatting doesn't mangle `:::` directives.
- [ ] Reported every repo-specific edit and every manual step left to the user.