Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
52 changes: 26 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,44 @@
## 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 { auth } from '@clerk/tanstack-react-start/server'

export const Route = createFileRoute('/dashboard')({
beforeLoad: async () => {
const { userId } = await auth()
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