Releasing

Step-by-step guide for publishing a new version of vite-plugin-react-server and updating downstream demo projects. Written for both human maintainers and AI agents.

Prerequisites

  • Push access to the plugin repo and demo repos
  • npm publish access (requires 2FA — human step)
  • Node.js 20+, npm 10+

1. Verify everything passes

The unit/integration suite alone is not sufficient — the plugin can pass its own tests while breaking downstream consumers via cross-condition module leaks (PR #30 is one historical example). You must also verify against the linked demo repos.

cd ~/code/vite-plugin-react-server

# 1a. Full plugin suite — both halves must be green.
npm test                 # test:both — the suite in the client env, then again under --conditions react-server

# 1b. Build the package.
npm run build

# 1c. Link the local build into each demo and exercise it end-to-end.
#     A passing 'npm test' does not exclude regressions that only surface
#     when the package is consumed via file:.. from a real downstream app.
for demo in ~/code/bidoof-template ~/code/mmc ~/code/vprs-starter; do
  pushd "$demo"
  cp package.json /tmp/$(basename "$demo").pkg.bak
  cp -f package-lock.json /tmp/$(basename "$demo").lock.bak 2>/dev/null || true
  node -e 'const p=require("./package.json");p.dependencies["vite-plugin-react-server"]="file:../vite-plugin-react-server";require("fs").writeFileSync("./package.json",JSON.stringify(p,null,2)+"\n");'
  npm install
  npm run build           # bidoof-template: build:preview; mmc: build; vprs-starter: npm test (build:vercel + hydration smoke test)
  # Smoke-test the dev server too:
  npx vite --port 4173 --strictPort &
  VITE_PID=$!
  sleep 5
  curl -fS http://localhost:4173/ > /dev/null
  curl -fS -H "Accept: text/x-component" http://localhost:4173/index.rsc > /dev/null
  kill $VITE_PID
  # Restore the demo so you don't accidentally commit the file:.. link.
  cp /tmp/$(basename "$demo").pkg.bak ./package.json
  cp /tmp/$(basename "$demo").lock.bak ./package-lock.json 2>/dev/null || true
  npm install
  popd
done

# 1d. Full Playwright browser gate (runs against bidoof-template per
#     playwright.config.ts). Do NOT substitute the hmr.spec.ts subset — the
#     release gate covers both dev conditions (global webServer = dev:rsc plus
#     per-file spawned dev:ssr/dev:rsc servers), hydration/client navigation,
#     client-imported server functions, state-preserving CSS/React HMR,
#     server-action persistence, and dynamic route props. Specs that need
#     Counter / ServerFnProbe /e2e-actions install those under test/e2e/*Fixtures
#     and tear them down only after a successful capture.
npx playwright test

# Every test must pass. A test marked test.fail() pins a filed bug — an
# UNEXPECTED pass there means the bug got fixed; remove the annotation rather
# than shipping around it. Silent skips on missing demo routes are not allowed.

If any of 1a–1d fails, do not publish. File an issue and bisect to the introducing commit. A regression that is already on main but not yet released is still a release-blocker — it ships to consumers the moment you npm publish.

⚠️ file: link masks React version mismatches. Linked demos resolve react/react-dom against the plugin's own node_modules, which means whatever experimental React is installed inside vite-plugin-react-server/ gets hoisted into the consumer for free. A demo that pins stable React 19 in its own package.json will still run against the plugin's experimental React under file: link, and never exercise the actual stable-React path. To verify a release works for npm consumers — not just file:-link consumers — also npm pack the tarball and install it into a clean fixture before publishing. Skipping this is what let v1.4.6 ship broken on stable React (fixed in PR #32).

2. Bump the version

Releases go through a PR like any other change, so the bump happens on a branch and the tag comes later. npm version is not used: it would tag the pre-merge commit on the branch rather than what lands on main.

npm run version:patch   # 3.1.9 → 3.1.10
# or: npm run version:minor  # 3.1.9 → 3.2.0
# or: npm run version:major  # 3.1.9 → 4.0.0

That writes package.json + package-lock.json only. Commit them, open the PR, merge it.

3. Tag the merged commit, then publish

Tag main after the merge, so the tag points at the commit that is actually published:

git checkout main && git pull
git tag -a v<version> -m "v<version>"
git push origin v<version>
npm publish   # requires 2FA (human step)

This tag step is manual and has been missed before (v3.1.7 shipped untagged and needed backfilling; v3.1.9 nearly did). prepublishOnly runs verify:tag, then build, then verify:package (publint + entrypoint checks). verify:tag refuses to publish unless v<version> exists and points at HEAD — a forgotten tag now fails the publish instead of shipping. For a deliberate untagged publish, set VPRS_SKIP_TAG_CHECK=1. Last, test:stable-names builds the stable-names fixtures under both conditions and fails the publish if any hashed browser file (JS, CSS, images) kept its name while its bytes changed, or if a <link>, flight payload or server build names a file the browser build did not write: the guarantee that lets a host cache those URLs as immutable. After prepublishOnly, npm's prepack step rebuilds dist with plain tsc and then verifies every exported entrypoint, so the tarball's dist is the tsc output. Validate the packed output as well as the Vite build: entry files must be included in tsconfig.json to survive prepack.

For pre-releases, hand-edit the version (e.g. 3.2.0-alpha.0), tag it the same way, then npm publish --tag alpha.

4. Update demo projects

For each demo repo (bidoof-template, mmc, vprs-starter):

cd ~/code/<demo-repo>
git checkout main && git pull
git checkout -b fix/v<version>

Update the version in package.json:

sed -i 's/"vite-plugin-react-server": "[^"]*"/"vite-plugin-react-server": "^<version>"/' package.json
npm install

Verify the correct version resolved:

grep -A1 '"vite-plugin-react-server"' package-lock.json | head -4

Commit both package.json and package-lock.json — CI uses npm ci which requires them in sync:

git add package.json package-lock.json
git commit -m "chore: bump vite-plugin-react-server to ^<version>"
git push -u origin fix/v<version>
gh pr create --title "chore: bump vite-plugin-react-server to v<version>" \
  --body "Bumps vite-plugin-react-server to v<version>."

5. Verify CI

Wait for GitHub Actions to pass on both demo PRs. The workflows will:

  • Install dependencies (npm ci)
  • Build the project
  • Deploy to GitHub Pages (on merge)

If CI fails, check that package-lock.json was committed and the version actually published to npm.

6. Merge demo PRs

Once CI passes, merge the PRs. This triggers the GitHub Pages deploy.

End-to-end verification (optional)

After merging, verify the deployed demos work:

# bidoof-template
curl -s https://nicobrinkkemper.github.io/vite-plugin-react-server-demo-official/ | head -5

# mmc
curl -s https://nicobrinkkemper.github.io/mmc/ | head -5

Local dev testing (linked package)

When developing locally, npm install in demo projects will remove the npm link symlink. After any npm install, re-link:

cd ~/code/<demo-repo>
npm link vite-plugin-react-server

The plugin's configResolved hook auto-creates the react-server-dom-esm symlink in node_modules/ on every Vite startup — no manual step needed.

⚠️ Never run killall -9 node — this kills Cursor/VS Code's WSL server and crashes the editor. Use targeted kills: lsof -ti:<port> | xargs kill or kill %1.

Demo repos

Repo GitHub Deploy
bidoof-template nicobrinkkemper/vite-plugin-react-server-demo-official GitHub Pages
mmc nicobrinkkemper/mmc GitHub Pages
vprs-starter nicobrinkkemper/vprs-starter Vercel

Checklist

  • npm test — both test:client AND test:server halves green (no skipped tests added in this release without a tracked issue)
  • npm run build succeeds
  • bidoof-template linked via file:../vite-plugin-react-server — npm run build:preview AND dev server (vite) both succeed; home page returns 200 with content
  • mmc linked via file:../vite-plugin-react-server — npm run build succeeds; dev server starts cleanly
  • vprs-starter linked via file:../vite-plugin-react-server — npm test succeeds: build:vercel plus the Playwright hydration smoke test over the per-request /, a prerendered docs page and a client navigation (the only consumer of runner: "edge" + transport: "webpack" + inlineFlight: "stream")
  • All three demo repos restored to their committed package.json/package-lock.json afterwards
  • npx playwright test — full browser gate green (both dev conditions; do not substitute the hmr.spec.ts subset)
  • npm run version:<type> — commit package.json + package-lock.json, PR, merge
  • git tag -a v<version> -m "v<version>" && git push origin v<version> — on merged main
  • npm publish (2FA — human step; verify:tag blocks an untagged publish, test:stable-names blocks a hashed URL whose bytes changed)
  • For each demo repo:
    • Create branch from main
    • Update package.json version
    • npm install — commit both package.json and package-lock.json
    • Push branch, create PR
    • CI passes
    • Merge PR