Skip to content

fix(init): scaffold ssr.noExternal for React Router v8 projects#374

Merged
manovotny merged 2 commits into
mainfrom
manovotny/react-router-v8-noexternal
Jul 7, 2026
Merged

fix(init): scaffold ssr.noExternal for React Router v8 projects#374
manovotny merged 2 commits into
mainfrom
manovotny/react-router-v8-noexternal

Conversation

@manovotny

Copy link
Copy Markdown
Contributor

Summary

clerk init on a React Router 8 project scaffolds an app that is broken in dev out of the box: every request fails during SSR with useNavigate() may be used only in the context of a <Router> component. This came out of root-causing a public field report where an agent couldn't one-shot a Clerk + React Router app.

RR8 ships development/production conditional exports. react-router dev externalizes @clerk/react-router for SSR, so Node resolves react-router's production build for Clerk while the app code gets the development build through Vite — two module instances, two Router contexts, and Clerk's useNavigate() call inside ClerkProvider throws. Upstream: remix-run/react-router#15232. The scaffolder already handles the v7/v8 split for the v8_middleware flag but never touched the vite config.

Changes

  • Scaffold ssr: { noExternal: ["@clerk/react-router"] } into vite.config.{ts,js,mts,mjs} when react-router is v8+ (or unparseable, matching how the v8_middleware gate already treats unknown versions). v7 has no conditional exports and is left alone.
  • Uses magicast with a string-replace fallback, mirroring the astro scaffolder; appends to an existing ssr.noExternal array instead of clobbering it; skips when the config already references @clerk/react-router.
  • Emits a manual post-instruction (with the verbatim error and upstream link) when the vite config is missing or uses a function form that can't be safely modified.
  • 6 new unit tests covering: fresh add, append-to-existing-array, already-present skip, v7 no-op, missing-config instruction, and function-form instruction.

Verification

  • Reproduced in a scratch app (create-react-router@latest → RR 8.0.0 + @clerk/react-router@3.5.5): dev SSR 500s without the entry, HTTP 200 with it; react-router build unaffected.
  • bun run format, lint, typecheck clean; react-router.test.ts 21/21 pass. (Full bun run test has 363 pre-existing failures in credential-store/host-execution on clean main in my sandbox — identical count with and without this change.)

Not planned (follow-up candidates)

  • The e2e fixture (test/e2e/fixtures/react-router) pins react-router@7.15.0, so CI never exercises v8 — worth adding an RR8 fixture so the next regression of this class is caught.

References

🤖 Generated with Claude Code

React Router 8 ships development/production conditional exports.
`react-router dev` externalizes @clerk/react-router for SSR, so it
resolves react-router's production build while app code gets the
development build — two module instances, two Router contexts — and
every SSR render throws "useNavigate() may be used only in the context
of a <Router>" (remix-run/react-router#15232). A clerk init'd RR8 app
was broken in dev out of the box.

Scaffold ssr: { noExternal: ["@clerk/react-router"] } into the vite
config for react-router >= 8 (v7 has no conditional exports and needs
nothing). Emits a manual post-instruction when the config is missing or
uses a function form magicast can't safely modify.

Verified against a live repro: create-react-router@latest (RR 8.0.0) +
@clerk/react-router@3.5.5 fails in dev SSR without the entry and
renders cleanly with it; production build unaffected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4d247bc

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
clerk Patch

Not sure what this means? Click here to learn what changesets are.

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

@manovotny

manovotny commented Jul 6, 2026

Copy link
Copy Markdown
Contributor Author

Companion PRs, now all open:

@manovotny manovotny marked this pull request as ready for review July 6, 2026 22:23
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 37f0373e-3c80-44b6-931c-c48b05f1b9e0

📥 Commits

Reviewing files that changed from the base of the PR and between e96261a and 4d247bc.

📒 Files selected for processing (2)
  • packages/cli-core/src/commands/init/frameworks/react-router.test.ts
  • packages/cli-core/src/commands/init/frameworks/react-router.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/cli-core/src/commands/init/frameworks/react-router.test.ts
  • packages/cli-core/src/commands/init/frameworks/react-router.ts

📝 Walkthrough

Walkthrough

This PR adds React Router v8 scaffold logic that updates vite.config.ts to include @clerk/react-router in ssr.noExternal. It uses AST-based modification when a Vite config file is found, skips changes if the package is already present, and emits a manual wiring instruction when automated editing is not possible. The scaffold flow now runs this step alongside existing initialization work, and the change is covered by tests and a changeset.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant reactRouter.scaffold
  participant scaffoldViteConfig
  participant vite.config.* file
  participant postInstructions

  reactRouter.scaffold->>scaffoldViteConfig: scaffoldViteConfig(ctx)
  scaffoldViteConfig->>vite.config.* file: locate and read config
  alt config contains `@clerk/react-router`
    scaffoldViteConfig-->>reactRouter.scaffold: no change
  else config is writable
    scaffoldViteConfig->>vite.config.* file: add `@clerk/react-router` to ssr.noExternal
    scaffoldViteConfig-->>reactRouter.scaffold: FileAction modify
  else manual wiring needed
    scaffoldViteConfig-->>reactRouter.scaffold: needsManualViteWire
    reactRouter.scaffold->>postInstructions: append v8 SSR note
  end
Loading

Related PRs None

Suggested labels None

Suggested reviewers None

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check ✅ Passed The description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Review ran into problems

🔥 Problems

Linked repositories: Your configuration references 7 linked repositories, but your current plan allows 1. Analyzed clerk/clerk_go, skipped clerk/dashboard, clerk/accounts, clerk/backoffice, clerk/clerk, clerk/clerk-docs, clerk/cloudflare-workers.


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
packages/cli-core/src/commands/init/frameworks/react-router.test.ts (1)

461-483: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Strengthen assertions to catch duplicate ssr keys, not just substring presence.

This test only checks that the output contains "some-other-pkg" and "@clerk/react-router" as substrings. It wouldn't catch a regression where the fix ends up in a duplicate ssr: block that gets shadowed by the original one at runtime (see the related comment on addClerkNoExternal in react-router.ts). Consider asserting the actual parsed array (e.g., via a second parseModule on viteAction.content and checking ssr.noExternal contains exactly one @clerk/react-router alongside some-other-pkg), or at least asserting there's only a single ssr: occurrence in the output.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli-core/src/commands/init/frameworks/react-router.test.ts` around
lines 461 - 483, Strengthen the React Router 8 scaffold test so it verifies the
final Vite config structure, not just raw substrings. In react-router.test.ts,
update the assertion around reactRouter.scaffold and viteAction.content to parse
the generated config (or otherwise inspect structure) and confirm ssr.noExternal
contains both some-other-pkg and exactly one `@clerk/react-router` entry, with
only a single ssr block present. Use the existing addClerkNoExternal /
reactRouter.scaffold / viteAction checks to locate the test and make the
assertion resistant to duplicate ssr keys.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli-core/src/commands/init/frameworks/react-router.ts`:
- Around line 434-444: The early skip in react-router init is using a naive
content.includes("`@clerk/react-router`") check that can falsely treat unrelated
mentions as already configured. Update the logic in the config inspection path
around the existing skip branch so it only skips after confirming
`@clerk/react-router` is actually present in the ssr.noExternal configuration
(preferably via the AST parse/inspection already used in this flow, or a much
narrower pattern tied to noExternal). If the match is only incidental, continue
with the normal wiring path and emit the manual Vite instruction instead of
returning skip from the current check.
- Around line 399-420: The addClerkNoExternal helper is mutating
mod.exports.default too early and the defineConfig fallback can create a
duplicate ssr block. Unwrap the defineConfig(...) argument first, then update
the existing ssr.noExternal array on that config object before generating code;
if ssr already exists, merge into it instead of injecting a second ssr key. Use
addClerkNoExternal, parseModule, and mod.exports.default to locate the fix.

---

Nitpick comments:
In `@packages/cli-core/src/commands/init/frameworks/react-router.test.ts`:
- Around line 461-483: Strengthen the React Router 8 scaffold test so it
verifies the final Vite config structure, not just raw substrings. In
react-router.test.ts, update the assertion around reactRouter.scaffold and
viteAction.content to parse the generated config (or otherwise inspect
structure) and confirm ssr.noExternal contains both some-other-pkg and exactly
one `@clerk/react-router` entry, with only a single ssr block present. Use the
existing addClerkNoExternal / reactRouter.scaffold / viteAction checks to locate
the test and make the assertion resistant to duplicate ssr keys.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5107ccbd-34ba-4b06-99d2-2a373394222c

📥 Commits

Reviewing files that changed from the base of the PR and between 1494d63 and e96261a.

📒 Files selected for processing (3)
  • .changeset/react-router-v8-noexternal.md
  • packages/cli-core/src/commands/init/frameworks/react-router.test.ts
  • packages/cli-core/src/commands/init/frameworks/react-router.ts
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • clerk/clerk_go (manual)

Comment thread packages/cli-core/src/commands/init/frameworks/react-router.ts
Comment thread packages/cli-core/src/commands/init/frameworks/react-router.ts
Comment thread packages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
Comment thread packages/cli-core/src/commands/init/frameworks/react-router.test.ts
…icating it

magicast proxies `export default defineConfig({...})` as a function-call
expression, and property writes on that proxy throw — so the AST branch
never ran and every defineConfig config went through the regex fallback.
With a pre-existing ssr block the fallback injected a second ssr key,
and since the later key wins, the @clerk/react-router entry was
silently dropped.

Operate on the call's first argument instead and drop the string
fallback entirely: unparseable or non-array shapes now surface the
manual post-instruction rather than a maybe-broken patch. Tests now
assert a single ssr block so a duplicate-key regression fails.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@manovotny manovotny merged commit 1409a22 into main Jul 7, 2026
10 checks passed
@manovotny manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
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