Skip to content

Fix duplicate waku instances in docs build - #204

Merged
tuler merged 4 commits into
prerelease/v2from
claude/docs-blinking-disappearing-v51h6l
Aug 13, 2026
Merged

Fix duplicate waku instances in docs build#204
tuler merged 4 commits into
prerelease/v2from
claude/docs-blinking-disappearing-v51h6l

Conversation

@tuler

@tuler tuler commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

Adds a build-time check to prevent duplicate waku package instances from being bundled in the docs app, which was causing hydration failures in the browser.

Problem

The docs app uses vocs, which declares waku as a peer dependency. Under bun's isolated linker, divergences in dependency graphs between the app and vocs (such as different versions of transitive dependencies like tsx) can result in two separate physical copies of waku being resolved. Both copies end up in the client bundle, causing vocs' components to read from one waku router context while the Router provider comes from another, resulting in "Missing Router" hydration errors.

Changes

  • New script (apps/docs/scripts/check-single-waku.mjs): Validates that waku resolves to a single physical copy by comparing the resolved paths from both the app and vocs. Fails the build with a helpful error message if duplicates are detected.
  • Updated build scripts (apps/docs/package.json): Integrated the check into build and dev commands to catch the issue early.
  • Dependency pinning (package.json): Added tsx override to ensure consistent resolution across dependency graphs and prevent future divergence.

Implementation Details

The check uses Node's createRequire() to resolve waku/package.json from both the app's and vocs' own node_modules contexts. If the resolved paths differ, it exits with a detailed error message guiding developers to identify and pin the offending dependency.

https://claude.ai/code/session_01FkuwpoMdwT9heR9MUTAiFV

deroll.dev rendered every page and then went blank a moment later. The
server-rendered HTML is fine — the client bundle throws during hydration:

    Error: Missing Router
        at O (assets/client-CLCqVXAf.js)      <- waku/router/client
        at C (assets/Head-C7z0VYp5.js)        <- vocs' <Head>

React unmounts the root when hydration throws, so the page paints and
disappears. Nothing fails at build time and SSR still returns 200, which
is why the previous fix looked like it had worked.

The client bundle contained waku's router runtime twice. vocs declares
waku as a peer dependency and apps/docs satisfies it, so the two resolve
waku independently, and under bun's isolated linker a package gets a
separate copy per distinct dependency graph. The graphs diverged on
`tsx`: the root package.json asks for ^4.23.12 while the lockfile pinned
`vocs/tsx` at 4.23.1. `tsx` is an optional peer of vite, so that split
vite@8.2.1 into two copies, which split waku into two copies, which put
two waku router modules in the bundle. vocs' <Head> then read its router
context from one copy while the <Router> provider came from the other.

Pin tsx to a single resolution with a root `overrides` entry: one tsx,
one vite@8.2.1, one waku, one router context.

Add apps/docs/scripts/check-single-waku.mjs, run from the docs `build`
and `dev` scripts, so a future split fails the build instead of shipping
a site that blanks out in the browser. Both the unrelated-optional-peer
mechanism and the fix are documented in the script.

Verified with headless Chromium against a local `vocs build` served by
dist/serve-node.js: before, /, /app/quick-start, /app/wallet, /explorer
and the rest all went from rendered text to 0 characters with "Missing
Router"; after, every route keeps its content, hydrates (the search hint
switches from ⌘K to Ctrl K) and client-side navigation works. The guard
exits 1 on the pre-fix lockfile and 0 after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuwpoMdwT9heR9MUTAiFV
@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 61aed39

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel

vercel Bot commented Aug 13, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
deroll Ready Ready Preview Aug 13, 2026 6:13am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
deroll-explorer Skipped Skipped Aug 13, 2026 6:13am

PR #204's docs preview failed the new guard, with the two waku copies
carrying the same store hashes as before the dedupe:

    from this app: .../waku@1.0.0-beta.8+ad2d21bfe8d1fed2/...
    from vocs:     .../waku@1.0.0-beta.8+e5046fa2a97318d0/...

The `tsx` override is correct — a clean install resolves one waku — but
`bun install` does not re-link packages it already considers satisfied.
Vercel restores node_modules from its build cache, so the tree built
before the dedupe survived the install untouched and the docs app kept
resolving its own stale waku copy.

Reproduced exactly: clean-install the pre-fix package.json/lockfile (two
copies), restore the fixed pair, then `bun install --frozen-lockfile`
without deleting node_modules — both copies persist, the same way they
did on Vercel. Adding --force re-links and the guard passes.
--frozen-lockfile is still enforced alongside --force: an out-of-date
lockfile still fails with "lockfile had changes, but lockfile is frozen".

So set the docs project's install command in apps/docs/vercel.json.
CI is unaffected — it installs into an empty tree, which is why it went
green while the deploy did not.

Also spell out the stale-tree case in the guard's error message, since
it is the failure mode someone is most likely to hit again.

Verified from scratch with the committed lockfile: one waku copy, one
waku router chunk in the client bundle, and headless Chromium finds /,
/app/quick-start, /app/wallet, /genext2fs and /explorer all keeping
their content through hydration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuwpoMdwT9heR9MUTAiFV
…d it

The root package.json listed `tsx` in `dependencies`, but nothing at the
root runs it — no script, no turbo task, no workflow. The packages that
do run tsx already declare it themselves (apps/examples for its `dev:*`
scripts, @deroll/create-app for `start`).

That stray declaration is what split waku in two. `tsx` is an optional
peer of vite, so a root-level tsx that differed from vocs' copy gave
vite@8.2.1 two variants, which gave waku two variants, which put two
router contexts in the docs client bundle and blanked every page on
hydration.

Deleting it fixes the split at the source, so the `overrides` entry that
pinned tsx is no longer needed either — with both gone a clean install
resolves one tsx, one vite@8.2.1 and one waku, and the lockfile change
is a pure deletion with no other churn.

Also drop apps/docs/scripts/check-single-waku.mjs and restore the plain
`vocs build` / `vocs dev` scripts. It was a guard against a divergence
that no longer has a cause here, and it isn't worth carrying.

apps/docs/vercel.json stays, and now carries the whole load: `bun
install` does not re-link a tree it already considers satisfied, so
without --force Vercel's restored build cache would keep serving the
pre-dedupe node_modules no matter how correct the lockfile is. That is
exactly how the split reached production, and nothing else in the repo
prevents it.

Verified from a clean install: one waku copy, one waku router chunk in
the client bundle, `bun run build` and `bun run lint` green, and
headless Chromium finds /, /app/quick-start, /app/application,
/app/wallet, /app/vouchers, /app/migrating, /genext2fs and /explorer all
keeping their content through hydration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuwpoMdwT9heR9MUTAiFV
Removing the unused root `tsx` dependency fixes the waku split at its
source, so the only thing --force still bought was protection against
Vercel's build cache restoring a node_modules that predates the fix.
That is a one-time concern, handled by a redeploy without build cache,
and not worth a config file.

Note for whoever merges this: the `deroll` project's production cache
still holds the pre-fix tree from c552f33. `bun install` does not
re-link a tree it already considers satisfied — a lockfile change alone
did not invalidate it on PR #204 — so the first prerelease/v2 build
after this merge must be a "Redeploy without Build Cache", or deroll.dev
will come back blank.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuwpoMdwT9heR9MUTAiFV
@vercel
vercel Bot temporarily deployed to Preview – deroll-explorer August 13, 2026 06:12 Inactive
@tuler
tuler merged commit 27fae58 into prerelease/v2 Aug 13, 2026
4 checks passed
tuler pushed a commit that referenced this pull request Aug 13, 2026
PR #204's docs preview failed the new guard, with the two waku copies
carrying the same store hashes as before the dedupe:

    from this app: .../waku@1.0.0-beta.8+ad2d21bfe8d1fed2/...
    from vocs:     .../waku@1.0.0-beta.8+e5046fa2a97318d0/...

The `tsx` override is correct — a clean install resolves one waku — but
`bun install` does not re-link packages it already considers satisfied.
Vercel restores node_modules from its build cache, so the tree built
before the dedupe survived the install untouched and the docs app kept
resolving its own stale waku copy.

Reproduced exactly: clean-install the pre-fix package.json/lockfile (two
copies), restore the fixed pair, then `bun install --frozen-lockfile`
without deleting node_modules — both copies persist, the same way they
did on Vercel. Adding --force re-links and the guard passes.
--frozen-lockfile is still enforced alongside --force: an out-of-date
lockfile still fails with "lockfile had changes, but lockfile is frozen".

So set the docs project's install command in apps/docs/vercel.json.
CI is unaffected — it installs into an empty tree, which is why it went
green while the deploy did not.

Also spell out the stale-tree case in the guard's error message, since
it is the failure mode someone is most likely to hit again.

Verified from scratch with the committed lockfile: one waku copy, one
waku router chunk in the client bundle, and headless Chromium finds /,
/app/quick-start, /app/wallet, /genext2fs and /explorer all keeping
their content through hydration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuwpoMdwT9heR9MUTAiFV
@tuler
tuler deleted the claude/docs-blinking-disappearing-v51h6l branch August 13, 2026 06:14
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