Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/fresh-partner-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/create': patch
---

Modernize the Clerk and WorkOS add-ons with their full-stack TanStack Start SDKs,
update Railway projects for Railpack, and use safer Sentry defaults. Secret
environment values are no longer stored in `.cta.json` or overwritten by
`tanstack add`, and pnpm 11 projects receive the build approvals their selected
integrations require.
86 changes: 78 additions & 8 deletions packages/create/src/add-to-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,61 @@ import { setupIntent } from './integrations/intent.js'
import type { Environment, Options } from './types.js'
import type { PersistedOptions } from './config-file.js'

const ENV_FILE_NAMES = new Set(['.env', '.env.local', '.env.example'])
const ENV_VARIABLE_PATTERN = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/

function mergeEnvFileContents(existing: string, generated: string) {
const declaredVariables = new Set<string>()
for (const line of existing.split(/\r?\n/)) {
const match = line.match(ENV_VARIABLE_PATTERN)
if (match) {
declaredVariables.add(match[1])
}
}

const additions: Array<string> = []
for (const block of generated.split(/\r?\n(?:[ \t]*\r?\n)+/)) {
const blockLines = block.split(/\r?\n/)
while (blockLines.at(-1) === '') {
blockLines.pop()
}
const lines: Array<string> = []
let addedVariableCount = 0
for (const line of blockLines) {
const match = line.match(ENV_VARIABLE_PATTERN)
if (!match) {
lines.push(line)
continue
}
if (declaredVariables.has(match[1])) {
continue
}

declaredVariables.add(match[1])
addedVariableCount++
lines.push(line)
}

if (addedVariableCount > 0) {
additions.push(lines.join('\n'))
}
}

if (additions.length === 0) {
return existing
}

const separator = existing.length
? existing.endsWith('\n\n')
? ''
: existing.endsWith('\n')
? '\n'
: '\n\n'
: ''
const trailingNewline = generated.endsWith('\n') ? '\n' : ''
return `${existing}${separator}${additions.join('\n\n')}${trailingNewline}`
}

export async function hasPendingGitChanges(
environment: Environment,
cwd: string,
Expand Down Expand Up @@ -134,6 +189,20 @@ export async function writeFiles(
false,
)

for (const [relativeFile, generatedContents] of Object.entries(
relativeOutputFiles,
)) {
if (
ENV_FILE_NAMES.has(basename(relativeFile)) &&
relativeFile in currentFiles
) {
relativeOutputFiles[relativeFile] = mergeEnvFileContents(
currentFiles[relativeFile],
generatedContents,
)
}
}

const overwrittenFiles: Array<string> = []
const changedFiles: Array<string> = []
for (const relativeFile of Object.keys(relativeOutputFiles)) {
Expand All @@ -146,22 +215,23 @@ export async function writeFiles(
}
}

if (!forced && overwrittenFiles.length) {
const deletedFiles = output.deletedFiles
.map(toRelativePath)
.filter((file) => environment.exists(resolve(cwd, file)))

if (!forced && (overwrittenFiles.length || deletedFiles.length)) {
environment.warn(
'The following will be overwritten',
[...overwrittenFiles, ...output.deletedFiles].join('\n'),
'The following files will be changed or deleted',
[...overwrittenFiles, ...deletedFiles].join('\n'),
)
const shouldContinue = await environment.confirm('Do you want to continue?')
if (!shouldContinue) {
throw new Error('User cancelled')
}
}

for (const filePath of output.deletedFiles) {
const relativeFilePath = toRelativePath(filePath)
if (environment.exists(resolve(cwd, relativeFilePath))) {
await environment.deleteFile(resolve(cwd, relativeFilePath))
}
for (const relativeFilePath of deletedFiles) {
await environment.deleteFile(resolve(cwd, relativeFilePath))
}

environment.startStep({
Expand Down
11 changes: 9 additions & 2 deletions packages/create/src/config-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,12 @@ import type { Environment, Options } from './types.js'

export type PersistedOptions = Omit<
Partial<Options>,
'addOns' | 'chosenAddOns' | 'framework' | 'starter' | 'targetDir'
| 'addOns'
| 'chosenAddOns'
| 'envVarValues'
| 'framework'
| 'starter'
| 'targetDir'
> & {
framework: string
version: number
Expand All @@ -17,7 +22,7 @@ export type PersistedOptions = Omit<

function createPersistedOptions(options: Options): PersistedOptions {
/* eslint-disable unused-imports/no-unused-vars */
const { chosenAddOns, framework, targetDir, ...rest } = options
const { chosenAddOns, envVarValues, framework, targetDir, ...rest } = options
/* eslint-enable unused-imports/no-unused-vars */
return {
...rest,
Expand Down Expand Up @@ -67,6 +72,8 @@ export async function readConfigFileFromEnvironment(
originalJSON.framework = 'react'
}

delete originalJSON.envVarValues

return originalJSON
} catch {
return null
Expand Down
58 changes: 32 additions & 26 deletions packages/create/src/frameworks/react/add-ons/clerk/README.md
Original file line number Diff line number Diff line change
@@ -1,44 +1,50 @@
## Setting up Clerk

1. Sign up at [clerk.com](https://clerk.com) and create an application
2. Copy the **Publishable Key** from the Clerk dashboard
3. Set it in your `.env.local`:
1. Create an application in the [Clerk dashboard](https://dashboard.clerk.com).
2. Copy its publishable and secret keys into `.env.local`:

```bash
VITE_CLERK_PUBLISHABLE_KEY=pk_test_...
CLERK_SECRET_KEY=sk_test_...
```
4. Visit the demo route at `/demo/clerk` once `npm run dev` is running

3. Start the app and visit `/demo/clerk`.

### What's wired up

- **`<ClerkProvider>`** at the app root (`src/integrations/clerk/provider.tsx`) handles auth context for the whole tree
- **`<SignInButton>` / `<UserButton>`** in the header swap based on auth state
- **`/demo/clerk`** shows Clerk's prebuilt sign-in UI and a signed-in greeting
- `clerkMiddleware()` authenticates each server request from `src/start.ts`.
- `<ClerkProvider>` supplies auth state throughout the app.
- `<SignInButton>` and `<UserButton>` in the header respond to the session.
- `/demo/clerk` shows Clerk's prebuilt sign-in UI and signed-in user data.

### Protecting a route

Wrap any component in `<SignedIn>` / `<SignedOut>`:
Use `auth()` in a loader or server function when authorization must happen on the
server:

```tsx
import { SignedIn, SignedOut, RedirectToSignIn } from '@clerk/clerk-react'

function ProtectedPage() {
return (
<>
<SignedIn>
<YourPageContent />
</SignedIn>
<SignedOut>
<RedirectToSignIn />
</SignedOut>
</>
)
}
import { createFileRoute, redirect } from '@tanstack/react-router'
import { createServerFn } from '@tanstack/react-start'
import { auth } from '@clerk/tanstack-react-start/server'

const getAuth = createServerFn({ method: 'GET' }).handler(async () => {
const { userId } = await auth()
return { userId }
})

export const Route = createFileRoute('/dashboard')({
beforeLoad: async () => {
const { userId } = await getAuth()
if (!userId) throw redirect({ to: '/' })
},
})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
```

For server-side checks (route loaders, server functions), see the Clerk docs on [`auth()`](https://clerk.com/docs/references/backend/auth).
`<Show when="signed-in">` remains useful for presentation, but server-side checks
are the security boundary. See Clerk's [TanStack Start docs](https://clerk.com/docs/tanstack-react-start/getting-started/quickstart).

### Production checklist

- Replace the test keys with **production keys** from a dedicated production Clerk instance
- Configure your production domain under **Domains** in the Clerk dashboard
- Set up social providers (Google, GitHub, etc.) under **User & Authentication → Social Connections**
- Set both keys in the production environment; never expose `CLERK_SECRET_KEY`.
- Use production keys from a dedicated production Clerk instance.
- Configure the production domain and any social connections in the Clerk dashboard.
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
# Clerk configuration, get this key from your [Dashboard](dashboard.clerk.com)
# Clerk configuration: https://dashboard.clerk.com/last-active?path=api-keys
VITE_CLERK_PUBLISHABLE_KEY=
CLERK_SECRET_KEY=
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
import {
SignedIn,
SignInButton,
SignedOut,
UserButton,
} from '@clerk/clerk-react'
import { Show, SignInButton, UserButton } from '@clerk/tanstack-react-start'

export default function HeaderUser() {
return (
<>
<SignedIn>
<Show when="signed-in">
<UserButton />
</SignedIn>
<SignedOut>
</Show>
<Show when="signed-out">
<SignInButton />
</SignedOut>
</Show>
</>
)
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
import { ClerkProvider } from '@clerk/clerk-react'

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY
if (!PUBLISHABLE_KEY) {
throw new Error('Add your Clerk Publishable Key to the .env.local file')
}
import { ClerkProvider } from '@clerk/tanstack-react-start'

export default function AppClerkProvider({
children,
}: {
children: React.ReactNode
}) {
return (
<ClerkProvider publishableKey={PUBLISHABLE_KEY} afterSignOutUrl="/">
{children}
</ClerkProvider>
)
return <ClerkProvider>{children}</ClerkProvider>
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createFileRoute } from '@tanstack/react-router'
import { SignIn, SignedIn, SignedOut, useUser } from '@clerk/clerk-react'
import { Show, SignIn, useUser } from '@clerk/tanstack-react-start'

export const Route = createFileRoute('/demo/clerk')({
component: ClerkDemo,
Expand All @@ -9,7 +9,7 @@ function ClerkDemo() {
return (
<main className="demo-page demo-center">
<section className="demo-panel w-full max-w-md space-y-6">
<SignedOut>
<Show when="signed-out">
<div className="space-y-1.5">
<p className="island-kicker mb-2">Clerk</p>
<h1 className="demo-title">Sign in to continue</h1>
Expand All @@ -33,11 +33,11 @@ function ClerkDemo() {
</a>
.
</p>
</SignedOut>
</Show>

<SignedIn>
<Show when="signed-in">
<SignedInGreeting />
</SignedIn>
</Show>
</section>
</main>
)
Expand Down
7 changes: 7 additions & 0 deletions packages/create/src/frameworks/react/add-ons/clerk/info.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,13 @@
"required": true,
"secret": false,
"file": ".env.local"
},
{
"name": "CLERK_SECRET_KEY",
"description": "Clerk secret key",
"required": true,
"secret": true,
"file": ".env.local"
}
]
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"dependencies": {
"@clerk/clerk-react": "^5.61.3"
"@clerk/tanstack-react-start": "^1.4.21"
}
}
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import * as Sentry from '@sentry/tanstackstart-react'

const sentryDsn = import.meta.env?.VITE_SENTRY_DSN ?? process.env.VITE_SENTRY_DSN
const sentryDsn =
import.meta.env?.VITE_SENTRY_DSN ?? process.env.VITE_SENTRY_DSN

if (!sentryDsn) {
console.warn('VITE_SENTRY_DSN is not defined. Sentry is not running.')
} else {
Sentry.init({
dsn: sentryDsn,
// Adds request headers and IP for users, for more info visit:
// https://docs.sentry.io/platforms/javascript/guides/tanstackstart-react/configuration/options/#sendDefaultPii
sendDefaultPii: true,
tracesSampleRate: 1.0,
replaysSessionSampleRate: 1.0,
replaysOnErrorSampleRate: 1.0,
dataCollection: {
userInfo: false,
httpBodies: [],
},
tracesSampleRate: 0.1,
})
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Sentry",
"phase": "setup",
"description": "Add Sentry for error monitoring, tracing, and session replays (requires Start).",
"description": "Add Sentry's TanStack Start SDK for server error monitoring and tracing.",
"link": "https://sentry.com/",
"modes": ["file-router"],
"type": "add-on",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
"start": "node --import ./.output/server/instrument.server.mjs .output/server/index.mjs"
},
"dependencies": {
"@sentry/tanstackstart-react": "^10.42.0",
"@sentry/tanstackstart-react": "^10.67.0",
"dotenv-cli": "^11.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@sentry/cli"
]
}
}
Loading
Loading