Skip to content

Commit 3e1edf8

Browse files
LekoArtswobsoriano
andauthored
feat: Add react-router SDK (clerk#4621)
Co-authored-by: Robert Soriano <sorianorobertc@gmail.com>
1 parent 1b86a1d commit 3e1edf8

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

76 files changed

+2565
-52
lines changed

.changeset/clean-pigs-hope.md

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@clerk/react-router': patch
3+
---
4+
5+
Initial beta release of `@clerk/react-router`.
6+
7+
[React Router v7](https://remix.run/blog/react-router-v7) was released and Clerk's existing `@clerk/remix` SDK isn't compatible anymore. Thus the need for a brand new SDK came up. `@clerk/react-router` allows you to use React Router v7 + Clerk both in framework/library mode.
8+
9+
Read the [React Router quickstart](https://clerk.com/docs/quickstarts/react-router) and [reference documenation](https://clerk.com/docs/references/react-router/overview) to learn more.

.github/workflows/validate-renovate-config.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,4 @@ jobs:
2828
- name: Validate Renovate Config
2929
env:
3030
RENOVATE_VERSION: 37.440.7 # Last version compatible with Node 18
31-
run: pnpm dlx renovate@${{ env.RENOVATE_VERSION }} renovate-config-validator
31+
run: npx --yes --package renovate@${{ env.RENOVATE_VERSION }} renovate-config-validator

integration/.keys.json.sample

+32
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,37 @@
1414
"core-2-all-enabled": {
1515
"pk": "",
1616
"sk": ""
17+
},
18+
"sessions-prod-1": {
19+
"pk": "",
20+
"sk": ""
21+
},
22+
"sessions-dev-1": {
23+
"pk": "",
24+
"sk": ""
25+
},
26+
"sessions-prod-2": {
27+
"pk": "",
28+
"sk": ""
29+
},
30+
"sessions-dev-2": {
31+
"pk": "",
32+
"sk": ""
33+
},
34+
"with-restricted-mode": {
35+
"pk": "",
36+
"sk": ""
37+
},
38+
"with-reverification": {
39+
"pk": "",
40+
"sk": ""
41+
},
42+
"with-legal-consent": {
43+
"pk": "",
44+
"sk": ""
45+
},
46+
"with-waitlist-mode": {
47+
"pk": "",
48+
"sk": ""
1749
}
1850
}

integration/presets/index.ts

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { createLongRunningApps } from './longRunningApps';
77
import { next } from './next';
88
import { nuxt } from './nuxt';
99
import { react } from './react';
10+
import { reactRouter } from './react-router';
1011
import { remix } from './remix';
1112
import { tanstack } from './tanstack';
1213

@@ -22,6 +23,7 @@ export const appConfigs = {
2223
astro,
2324
tanstack,
2425
nuxt,
26+
reactRouter,
2527
secrets: {
2628
instanceKeys,
2729
},

integration/presets/longRunningApps.ts

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { express } from './express';
88
import { next } from './next';
99
import { nuxt } from './nuxt';
1010
import { react } from './react';
11+
import { reactRouter } from './react-router';
1112
import { remix } from './remix';
1213
import { tanstack } from './tanstack';
1314
import { vue } from './vue';
@@ -42,6 +43,7 @@ export const createLongRunningApps = () => {
4243
{ id: 'tanstack.router', config: tanstack.router, env: envs.withEmailCodes },
4344
{ id: 'vue.vite', config: vue.vite, env: envs.withCustomRoles },
4445
{ id: 'nuxt.node', config: nuxt.node, env: envs.withCustomRoles },
46+
{ id: 'react-router.node', config: reactRouter.reactRouterNode, env: envs.withEmailCodes },
4547
] as const;
4648

4749
const apps = configs.map(longRunningApplication);

integration/presets/react-router.ts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { constants } from '../constants';
2+
import { applicationConfig } from '../models/applicationConfig.js';
3+
import { templates } from '../templates/index.js';
4+
5+
const reactRouterNode = applicationConfig()
6+
.setName('react-router-node')
7+
.useTemplate(templates['react-router-node'])
8+
.setEnvFormatter('public', key => `VITE_${key}`)
9+
.addScript('setup', 'pnpm install')
10+
.addScript('dev', 'pnpm dev')
11+
.addScript('build', 'pnpm build')
12+
.addScript('serve', 'pnpm start')
13+
.addDependency('@clerk/react-router', constants.E2E_CLERK_VERSION || '*');
14+
15+
export const reactRouter = {
16+
reactRouterNode,
17+
} as const;

integration/templates/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export const templates = {
1818
'tanstack-router': resolve(__dirname, './tanstack-router'),
1919
'vue-vite': resolve(__dirname, './vue-vite'),
2020
'nuxt-node': resolve(__dirname, './nuxt-node'),
21+
'react-router-node': resolve(__dirname, './react-router-node'),
2122
} as const;
2223

2324
if (new Set([...Object.values(templates)]).size !== Object.values(templates).length) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.env
2+
!.env.example
3+
.DS_Store
4+
.react-router
5+
build
6+
node_modules
7+
*.tsbuildinfo
8+
.seccorc
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# react-router
2+
3+
Example playground for React Router (Framework mode) and Clerk.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router';
2+
import { rootAuthLoader } from '@clerk/react-router/ssr.server';
3+
import { ClerkProvider } from '@clerk/react-router';
4+
5+
import type { Route } from './+types/root';
6+
7+
export async function loader(args: Route.LoaderArgs) {
8+
return rootAuthLoader(args);
9+
}
10+
11+
export function Layout({ children }: { children: React.ReactNode }) {
12+
return (
13+
<html lang='en'>
14+
<head>
15+
<meta charSet='utf-8' />
16+
<meta
17+
name='viewport'
18+
content='width=device-width, initial-scale=1'
19+
/>
20+
<Meta />
21+
<Links />
22+
</head>
23+
<body>
24+
{children}
25+
<ScrollRestoration />
26+
<Scripts />
27+
</body>
28+
</html>
29+
);
30+
}
31+
32+
export default function App({ loaderData }: Route.ComponentProps) {
33+
return (
34+
<ClerkProvider loaderData={loaderData}>
35+
<main>
36+
<Outlet />
37+
</main>
38+
</ClerkProvider>
39+
);
40+
}
41+
42+
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
43+
let message = 'Oops!';
44+
let details = 'An unexpected error occurred.';
45+
let stack: string | undefined;
46+
47+
if (isRouteErrorResponse(error)) {
48+
message = error.status === 404 ? '404' : 'Error';
49+
details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details;
50+
} else if (import.meta.env.DEV && error && error instanceof Error) {
51+
details = error.message;
52+
stack = error.stack;
53+
}
54+
55+
return (
56+
<main>
57+
<h1>{message}</h1>
58+
<p>{details}</p>
59+
{stack && (
60+
<pre>
61+
<code>{stack}</code>
62+
</pre>
63+
)}
64+
</main>
65+
);
66+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type RouteConfig, index, route } from '@react-router/dev/routes';
2+
3+
export default [
4+
index('routes/home.tsx'),
5+
route('sign-in/*', 'routes/sign-in.tsx'),
6+
route('sign-up/*', 'routes/sign-up.tsx'),
7+
route('protected', 'routes/protected.tsx'),
8+
] satisfies RouteConfig;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { SignedIn, SignedOut, UserButton } from '@clerk/react-router';
2+
import type { Route } from './+types/home';
3+
4+
export function meta({}: Route.MetaArgs) {
5+
return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }];
6+
}
7+
8+
export default function Home() {
9+
return (
10+
<div>
11+
<UserButton />
12+
<SignedIn>SignedIn</SignedIn>
13+
<SignedOut>SignedOut</SignedOut>
14+
</div>
15+
);
16+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { redirect } from 'react-router';
2+
import { getAuth } from '@clerk/react-router/ssr.server';
3+
import type { Route } from './+types/profile';
4+
5+
export async function loader(args: Route.LoaderArgs) {
6+
const { userId } = await getAuth(args);
7+
8+
if (!userId) {
9+
return redirect('/sign-in?redirect_url=' + args.request.url);
10+
}
11+
12+
return {};
13+
}
14+
15+
export default function Profile(args: Route.ComponentProps) {
16+
return (
17+
<div>
18+
<h1>Protected</h1>
19+
</div>
20+
);
21+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SignIn } from '@clerk/react-router';
2+
3+
export default function SignInPage() {
4+
return (
5+
<div>
6+
<h1>Sign In route</h1>
7+
<SignIn
8+
routing={'path'}
9+
path={'/sign-in'}
10+
signUpUrl={'/sign-up'}
11+
/>
12+
</div>
13+
);
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import { SignUp } from '@clerk/react-router';
2+
3+
export default function SignUpPage() {
4+
return (
5+
<div>
6+
<h1>Sign Up route</h1>
7+
<SignUp
8+
routing={'path'}
9+
path={'/sign-up'}
10+
signInUrl={'/sign-in'}
11+
/>
12+
</div>
13+
);
14+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"name": "clerk-react-router-quickstart",
3+
"private": true,
4+
"type": "module",
5+
"scripts": {
6+
"build": "react-router build",
7+
"dev": "react-router dev --port $PORT",
8+
"start": "react-router-serve ./build/server/index.js",
9+
"typecheck": "react-router typegen && tsc --build --noEmit"
10+
},
11+
"dependencies": {
12+
"@clerk/react-router": "latest",
13+
"@react-router/node": "^7.0.2",
14+
"@react-router/serve": "^7.0.2",
15+
"isbot": "^5.1.17",
16+
"react": "^18.3.1",
17+
"react-dom": "^18.3.1",
18+
"react-router": "^7.0.2"
19+
},
20+
"devDependencies": {
21+
"@react-router/dev": "^7.0.2",
22+
"@types/node": "^20",
23+
"@types/react": "^18.3.12",
24+
"@types/react-dom": "^18.3.1",
25+
"typescript": "^5.6.3",
26+
"vite": "^5.4.11",
27+
"vite-tsconfig-paths": "^5.1.2"
28+
}
29+
}
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import type { Config } from '@react-router/dev/config';
2+
3+
export default {
4+
// Config options...
5+
// Server-side render by default, to enable SPA mode set this to `false`
6+
ssr: true,
7+
} satisfies Config;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"include": ["**/*", "**/.server/**/*", "**/.client/**/*", ".react-router/types/**/*"],
3+
"compilerOptions": {
4+
"lib": ["DOM", "DOM.Iterable", "ES2022"],
5+
"types": ["node", "vite/client"],
6+
"target": "ES2022",
7+
"module": "ES2022",
8+
"moduleResolution": "bundler",
9+
"jsx": "react-jsx",
10+
"rootDirs": [".", "./.react-router/types"],
11+
"baseUrl": ".",
12+
"paths": {
13+
"~/*": ["./app/*"]
14+
},
15+
"esModuleInterop": true,
16+
"forceConsistentCasingInFileNames": true,
17+
"isolatedModules": true,
18+
"noEmit": true,
19+
"resolveJsonModule": true,
20+
"skipLibCheck": true,
21+
"strict": true
22+
}
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { reactRouter } from '@react-router/dev/vite';
2+
import { defineConfig } from 'vite';
3+
import tsconfigPaths from 'vite-tsconfig-paths';
4+
5+
export default defineConfig({
6+
plugins: [
7+
reactRouter(),
8+
tsconfigPaths({
9+
projects: ['./tsconfig.json'],
10+
}),
11+
],
12+
server: {
13+
port: Number(process.env.PORT),
14+
},
15+
});

0 commit comments

Comments
 (0)