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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(init): scaffold ssr.noExternal for React Router v8 projects by manovotny · Pull Request #374 · clerk/cli · GitHub
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-botBot 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
NameType
clerkPatch

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
ContributorAuthor

Companion PRs, now all open:

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

coderabbitaiBot 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 nameStatusExplanation
Title check✅ PassedThe title is concise and accurately summarizes the main React Router v8 Vite SSR scaffolding change.
Description check✅ PassedThe description is clearly related to the change and explains the React Router v8 noExternal scaffolding update.
Docstring Coverage✅ PassedNo functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck 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.

@coderabbitaicoderabbitaiBot 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 threadpackages/cli-core/src/commands/init/frameworks/react-router.ts Outdated
…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 mainJul 7, 2026
10 checks passed
@manovotny
manovotny deleted the manovotny/react-router-v8-noexternal branch July 7, 2026 22:32
@github-actionsgithub-actionsBot mentioned this pull request Jul 7, 2026
Sign up for freeto 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

@manovotny@rafa-thayto