From e0bc30755d3328fa4d8673b222944aac0ebdcea1 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 21 Jul 2026 13:53:00 -0600 Subject: [PATCH 1/2] fix: modernize partner integration add-ons --- .changeset/fresh-partner-paths.md | 9 + packages/create/src/add-to-app.ts | 86 ++++++- packages/create/src/config-file.ts | 11 +- .../frameworks/react/add-ons/clerk/README.md | 52 ++-- .../clerk/assets/_dot_env.local.append | 3 +- .../src/integrations/clerk/header-user.tsx | 15 +- .../src/integrations/clerk/provider.tsx | 13 +- .../clerk/assets/src/routes/demo/clerk.tsx | 10 +- .../frameworks/react/add-ons/clerk/info.json | 7 + .../react/add-ons/clerk/package.json | 2 +- .../sentry/assets/instrument.server.mjs | 14 +- .../frameworks/react/add-ons/sentry/info.json | 2 +- .../react/add-ons/sentry/package.json | 7 +- .../frameworks/react/add-ons/workos/README.md | 31 ++- .../workos/assets/_dot_env.local.append | 12 +- .../assets/src/components/workos-user.tsx | 22 +- .../workos/assets/src/hooks/useUser.tsx | 23 -- .../src/integrations/workos/provider.tsx | 29 +-- .../assets/src/routes/api/auth/callback.tsx | 10 + .../assets/src/routes/api/auth/sign-in.tsx | 22 ++ .../workos/assets/src/routes/demo/workos.tsx | 19 +- .../frameworks/react/add-ons/workos/info.json | 39 +++ .../react/add-ons/workos/package.json | 7 +- .../react/hosts/cloudflare/package.json.ejs | 3 + .../frameworks/react/hosts/nitro/package.json | 2 +- .../frameworks/react/hosts/railway/README.md | 13 +- .../react/hosts/railway/assets/nixpacks.toml | 8 - .../frameworks/react/hosts/railway/info.json | 1 + .../react/hosts/railway/package.json | 8 - .../react/hosts/railway/package.json.ejs | 8 + .../project/base/pnpm-workspace.yaml.ejs | 14 ++ .../react/project/base/src/start.ts.ejs | 20 ++ .../solid/add-ons/sentry/package.json | 2 +- .../solid/hosts/cloudflare/package.json.ejs | 3 + .../frameworks/solid/hosts/nitro/package.json | 2 +- .../frameworks/solid/hosts/railway/README.md | 13 +- .../solid/hosts/railway/assets/nixpacks.toml | 8 - .../frameworks/solid/hosts/railway/info.json | 1 + .../solid/hosts/railway/package.json | 2 +- .../project/base/pnpm-workspace.yaml.ejs | 8 + packages/create/src/types.ts | 6 + packages/create/tests/add-to-app.test.ts | 49 ++++ packages/create/tests/config-file.test.ts | 6 + packages/create/tests/edge-manifest.test.ts | 4 +- packages/create/tests/partner-addons.test.ts | 237 ++++++++++++++++++ 45 files changed, 677 insertions(+), 186 deletions(-) create mode 100644 .changeset/fresh-partner-paths.md delete mode 100644 packages/create/src/frameworks/react/add-ons/workos/assets/src/hooks/useUser.tsx create mode 100644 packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/callback.tsx create mode 100644 packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/sign-in.tsx delete mode 100644 packages/create/src/frameworks/react/hosts/railway/assets/nixpacks.toml delete mode 100644 packages/create/src/frameworks/react/hosts/railway/package.json create mode 100644 packages/create/src/frameworks/react/hosts/railway/package.json.ejs create mode 100644 packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs create mode 100644 packages/create/src/frameworks/react/project/base/src/start.ts.ejs delete mode 100644 packages/create/src/frameworks/solid/hosts/railway/assets/nixpacks.toml create mode 100644 packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs create mode 100644 packages/create/tests/partner-addons.test.ts diff --git a/.changeset/fresh-partner-paths.md b/.changeset/fresh-partner-paths.md new file mode 100644 index 00000000..96fe14c8 --- /dev/null +++ b/.changeset/fresh-partner-paths.md @@ -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. diff --git a/packages/create/src/add-to-app.ts b/packages/create/src/add-to-app.ts index 8349160e..54436c1d 100644 --- a/packages/create/src/add-to-app.ts +++ b/packages/create/src/add-to-app.ts @@ -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() + for (const line of existing.split(/\r?\n/)) { + const match = line.match(ENV_VARIABLE_PATTERN) + if (match) { + declaredVariables.add(match[1]) + } + } + + const additions: Array = [] + 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 = [] + 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, @@ -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 = [] const changedFiles: Array = [] for (const relativeFile of Object.keys(relativeOutputFiles)) { @@ -146,10 +215,14 @@ 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) { @@ -157,11 +230,8 @@ export async function writeFiles( } } - 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({ diff --git a/packages/create/src/config-file.ts b/packages/create/src/config-file.ts index adffa280..ab61df25 100644 --- a/packages/create/src/config-file.ts +++ b/packages/create/src/config-file.ts @@ -7,7 +7,12 @@ import type { Environment, Options } from './types.js' export type PersistedOptions = Omit< Partial, - 'addOns' | 'chosenAddOns' | 'framework' | 'starter' | 'targetDir' + | 'addOns' + | 'chosenAddOns' + | 'envVarValues' + | 'framework' + | 'starter' + | 'targetDir' > & { framework: string version: number @@ -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, @@ -67,6 +72,8 @@ export async function readConfigFileFromEnvironment( originalJSON.framework = 'react' } + delete originalJSON.envVarValues + return originalJSON } catch { return null diff --git a/packages/create/src/frameworks/react/add-ons/clerk/README.md b/packages/create/src/frameworks/react/add-ons/clerk/README.md index 0a9b45ec..f834d664 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/README.md +++ b/packages/create/src/frameworks/react/add-ons/clerk/README.md @@ -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 -- **``** at the app root (`src/integrations/clerk/provider.tsx`) handles auth context for the whole tree -- **`` / ``** 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`. +- `` supplies auth state throughout the app. +- `` and `` 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 `` / ``: +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 ( - <> - - - - - - - - ) -} +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: '/' }) + }, +}) ``` -For server-side checks (route loaders, server functions), see the Clerk docs on [`auth()`](https://clerk.com/docs/references/backend/auth). +`` 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. diff --git a/packages/create/src/frameworks/react/add-ons/clerk/assets/_dot_env.local.append b/packages/create/src/frameworks/react/add-ons/clerk/assets/_dot_env.local.append index 831a8a2e..fe336067 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/assets/_dot_env.local.append +++ b/packages/create/src/frameworks/react/add-ons/clerk/assets/_dot_env.local.append @@ -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= diff --git a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/header-user.tsx b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/header-user.tsx index 69b4e834..0011cce3 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/header-user.tsx +++ b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/header-user.tsx @@ -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 ( <> - + - - + + - + ) } diff --git a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/provider.tsx b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/provider.tsx index 3dbbbf1c..8e909e6d 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/provider.tsx +++ b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/integrations/clerk/provider.tsx @@ -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 ( - - {children} - - ) + return {children} } diff --git a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx index d680f5b6..18769d06 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx +++ b/packages/create/src/frameworks/react/add-ons/clerk/assets/src/routes/demo/clerk.tsx @@ -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, @@ -9,7 +9,7 @@ function ClerkDemo() { return (
- +

Clerk

Sign in to continue

@@ -33,11 +33,11 @@ function ClerkDemo() { .

- + - + - +
) diff --git a/packages/create/src/frameworks/react/add-ons/clerk/info.json b/packages/create/src/frameworks/react/add-ons/clerk/info.json index 08087eeb..276d0238 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/info.json +++ b/packages/create/src/frameworks/react/add-ons/clerk/info.json @@ -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" } ] } diff --git a/packages/create/src/frameworks/react/add-ons/clerk/package.json b/packages/create/src/frameworks/react/add-ons/clerk/package.json index a3be4657..a19f714e 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/package.json +++ b/packages/create/src/frameworks/react/add-ons/clerk/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@clerk/clerk-react": "^5.61.3" + "@clerk/tanstack-react-start": "^1.4.21" } } diff --git a/packages/create/src/frameworks/react/add-ons/sentry/assets/instrument.server.mjs b/packages/create/src/frameworks/react/add-ons/sentry/assets/instrument.server.mjs index 4f2cbcb0..5612c5f9 100644 --- a/packages/create/src/frameworks/react/add-ons/sentry/assets/instrument.server.mjs +++ b/packages/create/src/frameworks/react/add-ons/sentry/assets/instrument.server.mjs @@ -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, }) } diff --git a/packages/create/src/frameworks/react/add-ons/sentry/info.json b/packages/create/src/frameworks/react/add-ons/sentry/info.json index 19c213df..fcd80767 100644 --- a/packages/create/src/frameworks/react/add-ons/sentry/info.json +++ b/packages/create/src/frameworks/react/add-ons/sentry/info.json @@ -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", diff --git a/packages/create/src/frameworks/react/add-ons/sentry/package.json b/packages/create/src/frameworks/react/add-ons/sentry/package.json index 18b747e4..7e8ac49f 100644 --- a/packages/create/src/frameworks/react/add-ons/sentry/package.json +++ b/packages/create/src/frameworks/react/add-ons/sentry/package.json @@ -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" + ] } } diff --git a/packages/create/src/frameworks/react/add-ons/workos/README.md b/packages/create/src/frameworks/react/add-ons/workos/README.md index 87b2725d..8b954ffa 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/README.md +++ b/packages/create/src/frameworks/react/add-ons/workos/README.md @@ -1,3 +1,32 @@ ## Setting up WorkOS -- Set the `VITE_WORKOS_CLIENT_ID` in your `.env.local`. +This integration uses WorkOS AuthKit's full-stack TanStack Start SDK and requires +Node.js 22.11 or newer. + +1. Copy your client ID and API key from the [WorkOS dashboard](https://dashboard.workos.com/api-keys). +2. Fill in `.env.local`: + + ```bash + WORKOS_CLIENT_ID=client_... + WORKOS_API_KEY=sk_test_... + WORKOS_REDIRECT_URI=http://localhost:3000/api/auth/callback + WORKOS_COOKIE_PASSWORD=a-random-value-at-least-32-characters-long + WORKOS_API_HOSTNAME=api.workos.com + ``` + +3. On the dashboard's [Redirects page](https://dashboard.workos.com/redirects), add + `http://localhost:3000/api/auth/callback` as a redirect URI and + `http://localhost:3000/api/auth/sign-in` as the sign-in endpoint. +4. Start the app and visit `/demo/workos`. + +### What's wired up + +- `authkitMiddleware()` manages the encrypted server-side session in `src/start.ts`. +- `/api/auth/callback` completes the OAuth callback. +- `/api/auth/sign-in` initiates sign-in and supports a `returnPathname` query parameter. +- `` supplies reactive auth state to the header and demo route. + +For authorization in loaders and server functions, use `getAuth()` from +`@workos/authkit-tanstack-react-start`. Keep `WORKOS_API_KEY` and +`WORKOS_COOKIE_PASSWORD` server-only, and replace the local callback and sign-in URLs +with their production equivalents when deploying. diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/_dot_env.local.append b/packages/create/src/frameworks/react/add-ons/workos/assets/_dot_env.local.append index 9c3851c8..16e8df72 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/assets/_dot_env.local.append +++ b/packages/create/src/frameworks/react/add-ons/workos/assets/_dot_env.local.append @@ -1,4 +1,10 @@ -VITE_WORKOS_CLIENT_ID= +# WorkOS configuration: https://dashboard.workos.com/api-keys +WORKOS_CLIENT_ID= +WORKOS_API_KEY= +WORKOS_REDIRECT_URI=http://localhost:3000/api/auth/callback -# This should be your Custom Authentication Domain in production -VITE_WORKOS_API_HOSTNAME=api.workos.com +# Generate a random value at least 32 characters long. +WORKOS_COOKIE_PASSWORD= + +# Use your custom authentication domain in production, if configured. +WORKOS_API_HOSTNAME=api.workos.com diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/src/components/workos-user.tsx b/packages/create/src/frameworks/react/add-ons/workos/assets/src/components/workos-user.tsx index 1db20387..03b444d3 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/assets/src/components/workos-user.tsx +++ b/packages/create/src/frameworks/react/add-ons/workos/assets/src/components/workos-user.tsx @@ -1,11 +1,13 @@ -import { useAuth } from '@workos-inc/authkit-react' +import { useAuth } from '@workos/authkit-tanstack-react-start/client' -export default function SignInButton({ large }: { large?: boolean }) { - const { user, isLoading, signIn, signOut } = useAuth() +export default function WorkOSUser({ large }: { large?: boolean }) { + const { user, loading, signOut } = useAuth() const buttonClasses = `${ large ? 'px-6 py-3 text-base' : 'px-4 py-2 text-sm' - } demo-button disabled:opacity-50 disabled:cursor-not-allowed` + } demo-button` + + if (loading) return null if (user) { return ( @@ -20,7 +22,7 @@ export default function SignInButton({ large }: { large?: boolean }) { )} {user.firstName} {user.lastName} - @@ -28,14 +30,8 @@ export default function SignInButton({ large }: { large?: boolean }) { } return ( - + ) } diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/src/hooks/useUser.tsx b/packages/create/src/frameworks/react/add-ons/workos/assets/src/hooks/useUser.tsx deleted file mode 100644 index 53254e24..00000000 --- a/packages/create/src/frameworks/react/add-ons/workos/assets/src/hooks/useUser.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useEffect } from "react"; -import { useAuth } from "@workos-inc/authkit-react"; -import { useLocation } from "@tanstack/react-router"; - -type UserOrNull = ReturnType["user"]; - -// redirects to the sign-in page if the user is not signed in -export const useUser = (): UserOrNull => { - const { user, isLoading, signIn } = useAuth(); - const location = useLocation(); - - useEffect(() => { - if (!isLoading && !user) { - signIn({ - state: { returnTo: location.pathname }, - }); - } else { - console.log(user); - } - }, [isLoading, user]); - - return user; -}; diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/src/integrations/workos/provider.tsx b/packages/create/src/frameworks/react/add-ons/workos/assets/src/integrations/workos/provider.tsx index b0c6dd78..3304b43e 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/assets/src/integrations/workos/provider.tsx +++ b/packages/create/src/frameworks/react/add-ons/workos/assets/src/integrations/workos/provider.tsx @@ -1,34 +1,9 @@ -import { AuthKitProvider } from '@workos-inc/authkit-react' -import { useNavigate } from '@tanstack/react-router' - -const VITE_WORKOS_CLIENT_ID = import.meta.env.VITE_WORKOS_CLIENT_ID -if (!VITE_WORKOS_CLIENT_ID) { - throw new Error('Add your WorkOS Client ID to the .env.local file') -} - -const VITE_WORKOS_API_HOSTNAME = import.meta.env.VITE_WORKOS_API_HOSTNAME -if (!VITE_WORKOS_API_HOSTNAME) { - throw new Error('Add your WorkOS API Hostname to the .env.local file') -} +import { AuthKitProvider } from '@workos/authkit-tanstack-react-start/client' export default function AppWorkOSProvider({ children, }: { children: React.ReactNode }) { - const navigate = useNavigate() - - return ( - { - if (state?.returnTo) { - navigate(state.returnTo) - } - }} - > - {children} - - ) + return {children} } diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/callback.tsx b/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/callback.tsx new file mode 100644 index 00000000..4eb90708 --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/callback.tsx @@ -0,0 +1,10 @@ +import { createFileRoute } from '@tanstack/react-router' +import { handleCallbackRoute } from '@workos/authkit-tanstack-react-start' + +export const Route = createFileRoute('/api/auth/callback')({ + server: { + handlers: { + GET: handleCallbackRoute(), + }, + }, +}) diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/sign-in.tsx b/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/sign-in.tsx new file mode 100644 index 00000000..20fbe56c --- /dev/null +++ b/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/api/auth/sign-in.tsx @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' +import { getSignInUrl } from '@workos/authkit-tanstack-react-start' + +export const Route = createFileRoute('/api/auth/sign-in')({ + server: { + handlers: { + GET: async ({ request }) => { + const returnPathname = new URL(request.url).searchParams.get( + 'returnPathname', + ) + const url = await getSignInUrl( + returnPathname ? { data: { returnPathname } } : undefined, + ) + + return new Response(null, { + status: 307, + headers: { Location: url }, + }) + }, + }, + }, +}) diff --git a/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/demo/workos.tsx b/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/demo/workos.tsx index ceb0b6b7..55e45ce2 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/demo/workos.tsx +++ b/packages/create/src/frameworks/react/add-ons/workos/assets/src/routes/demo/workos.tsx @@ -1,15 +1,14 @@ import { createFileRoute } from '@tanstack/react-router' -import { useAuth } from '@workos-inc/authkit-react' +import { useAuth } from '@workos/authkit-tanstack-react-start/client' export const Route = createFileRoute('/demo/workos')({ - ssr: false, component: App, }) function App() { - const { user, isLoading, signIn, signOut } = useAuth() + const { user, loading, signOut } = useAuth() - if (isLoading) { + if (loading) { return (
@@ -69,7 +68,10 @@ function App() { - @@ -86,13 +88,12 @@ function App() {

Sign in to view your profile information

- +
) diff --git a/packages/create/src/frameworks/react/add-ons/workos/info.json b/packages/create/src/frameworks/react/add-ons/workos/info.json index 94fa0538..c1359059 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/info.json +++ b/packages/create/src/frameworks/react/add-ons/workos/info.json @@ -30,5 +30,44 @@ "jsName": "WorkOSProvider", "path": "src/integrations/workos/provider.tsx" } + ], + "envVars": [ + { + "name": "WORKOS_CLIENT_ID", + "description": "WorkOS client ID", + "required": true, + "secret": false, + "file": ".env.local" + }, + { + "name": "WORKOS_API_KEY", + "description": "WorkOS API key", + "required": true, + "secret": true, + "file": ".env.local" + }, + { + "name": "WORKOS_REDIRECT_URI", + "description": "AuthKit callback URL", + "required": true, + "default": "http://localhost:3000/api/auth/callback", + "secret": false, + "file": ".env.local" + }, + { + "name": "WORKOS_COOKIE_PASSWORD", + "description": "Session cookie encryption password (at least 32 characters)", + "required": true, + "secret": true, + "file": ".env.local" + }, + { + "name": "WORKOS_API_HOSTNAME", + "description": "WorkOS API hostname", + "required": false, + "default": "api.workos.com", + "secret": false, + "file": ".env.local" + } ] } diff --git a/packages/create/src/frameworks/react/add-ons/workos/package.json b/packages/create/src/frameworks/react/add-ons/workos/package.json index 18a1e84f..d4b53599 100644 --- a/packages/create/src/frameworks/react/add-ons/workos/package.json +++ b/packages/create/src/frameworks/react/add-ons/workos/package.json @@ -1,6 +1,9 @@ { + "engines": { + "node": ">=22.11.0" + }, "dependencies": { - "@workos-inc/authkit-react": "^0.16.0", - "zod": "^4.3.6" + "@workos-inc/node": "^10.8.0", + "@workos/authkit-tanstack-react-start": "^0.11.0" } } diff --git a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs index 5a15c454..32826530 100644 --- a/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/cloudflare/package.json.ejs @@ -5,5 +5,8 @@ "devDependencies": { "@cloudflare/vite-plugin": "^1.26.0", "wrangler": "^4.70.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["sharp", "workerd"] } } diff --git a/packages/create/src/frameworks/react/hosts/nitro/package.json b/packages/create/src/frameworks/react/hosts/nitro/package.json index 6c790935..41e3adc9 100644 --- a/packages/create/src/frameworks/react/hosts/nitro/package.json +++ b/packages/create/src/frameworks/react/hosts/nitro/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "nitro": "npm:nitro-nightly@latest" + "nitro": "latest" } } diff --git a/packages/create/src/frameworks/react/hosts/railway/README.md b/packages/create/src/frameworks/react/hosts/railway/README.md index d60c1f46..67ce7ff1 100644 --- a/packages/create/src/frameworks/react/hosts/railway/README.md +++ b/packages/create/src/frameworks/react/hosts/railway/README.md @@ -1,10 +1,17 @@ ## Deploy to Railway -This project ships with `nixpacks.toml` so Railway detects the build automatically: +Railway's Railpack builder detects the project's package manager and package +scripts automatically. 1. Push this repo to GitHub 2. Visit https://railway.com/new and create a project from your repo 3. In the **Variables** tab, add the entries from `.env.example` with their production values -4. Railway runs `vite build` and serves from `dist/client` +4. Deploy, then open **Networking** and select **Generate Domain** -Need a database? Click **+ New** in your project to provision Postgres, MySQL, or Redis directly into the same environment — the connection string is auto-injected as `DATABASE_URL`. +Railpack runs the project's build script and starts the generated Nitro server +with `node .output/server/index.mjs`. The server handles SSR, server functions, +API routes, and static assets. + +Need a database? Add one from the Railway project canvas, then connect it to the +app with a Railway reference variable. The variable name and value depend on the +database service you choose. diff --git a/packages/create/src/frameworks/react/hosts/railway/assets/nixpacks.toml b/packages/create/src/frameworks/react/hosts/railway/assets/nixpacks.toml deleted file mode 100644 index 70ae68d2..00000000 --- a/packages/create/src/frameworks/react/hosts/railway/assets/nixpacks.toml +++ /dev/null @@ -1,8 +0,0 @@ -[phases.setup] -nixPkgs = ["nodejs_22"] - -[phases.build] -cmds = ["npm install", "npm run build"] - -[start] -cmd = "npm run start" diff --git a/packages/create/src/frameworks/react/hosts/railway/info.json b/packages/create/src/frameworks/react/hosts/railway/info.json index 700c7508..4ddf6b96 100644 --- a/packages/create/src/frameworks/react/hosts/railway/info.json +++ b/packages/create/src/frameworks/react/hosts/railway/info.json @@ -10,6 +10,7 @@ "color": "#9B4DCA", "priority": 160, "partner": { "id": "railway", "tier": "gold" }, + "deletedFiles": ["nixpacks.toml"], "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/react/hosts/railway/package.json b/packages/create/src/frameworks/react/hosts/railway/package.json deleted file mode 100644 index 5006a8bb..00000000 --- a/packages/create/src/frameworks/react/hosts/railway/package.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "scripts": { - "start": "node .output/server/index.mjs" - }, - "dependencies": { - "nitro": "npm:nitro-nightly@latest" - } -} diff --git a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs new file mode 100644 index 00000000..0be07b03 --- /dev/null +++ b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs @@ -0,0 +1,8 @@ +{ + "scripts": { + "start": "<%= addOnEnabled.sentry ? 'node --import ./.output/server/instrument.server.mjs .output/server/index.mjs' : 'node .output/server/index.mjs' %>" + }, + "dependencies": { + "nitro": "latest" + } +} diff --git a/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs b/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs new file mode 100644 index 00000000..7c8dcf63 --- /dev/null +++ b/packages/create/src/frameworks/react/project/base/pnpm-workspace.yaml.ejs @@ -0,0 +1,14 @@ +<% if (packageManager !== 'pnpm') { ignoreFile(); return; } -%> +allowBuilds: + esbuild: true + lightningcss: true +<% if (addOnEnabled.sentry) { -%> + '@sentry/cli': true +<% } -%> +<% if (addOnEnabled.cloudflare) { -%> + sharp: true + workerd: true +<% } -%> +<% if (addOnEnabled.drizzle || addOnEnabled.prisma) { -%> + better-sqlite3: true +<% } -%> diff --git a/packages/create/src/frameworks/react/project/base/src/start.ts.ejs b/packages/create/src/frameworks/react/project/base/src/start.ts.ejs new file mode 100644 index 00000000..d82100f3 --- /dev/null +++ b/packages/create/src/frameworks/react/project/base/src/start.ts.ejs @@ -0,0 +1,20 @@ +<% if (routerOnly || (!addOnEnabled.clerk && !addOnEnabled.workos)) { ignoreFile(); return; } %> +import { createCsrfMiddleware, createStart } from '@tanstack/react-start' +<% if (addOnEnabled.clerk) { %> +import { clerkMiddleware } from '@clerk/tanstack-react-start/server' +<% } %> +<% if (addOnEnabled.workos) { %> +import { authkitMiddleware } from '@workos/authkit-tanstack-react-start' +<% } %> + +const csrfMiddleware = createCsrfMiddleware({ + filter: (context) => context.handlerType === 'serverFn', +}) + +export const startInstance = createStart(() => ({ + requestMiddleware: [ + csrfMiddleware, + <% if (addOnEnabled.clerk) { %>clerkMiddleware(),<% } %> + <% if (addOnEnabled.workos) { %>authkitMiddleware(),<% } %> + ], +})) diff --git a/packages/create/src/frameworks/solid/add-ons/sentry/package.json b/packages/create/src/frameworks/solid/add-ons/sentry/package.json index 7bec0d14..a438d20d 100644 --- a/packages/create/src/frameworks/solid/add-ons/sentry/package.json +++ b/packages/create/src/frameworks/solid/add-ons/sentry/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "@sentry/solid": "^10.42.0" + "@sentry/solid": "^10.67.0" } } diff --git a/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs b/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs index 83540cb5..129dd299 100644 --- a/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs +++ b/packages/create/src/frameworks/solid/hosts/cloudflare/package.json.ejs @@ -7,5 +7,8 @@ }, "devDependencies": { "wrangler": "^4.70.0" + }, + "pnpm": { + "onlyBuiltDependencies": ["sharp", "workerd"] } } diff --git a/packages/create/src/frameworks/solid/hosts/nitro/package.json b/packages/create/src/frameworks/solid/hosts/nitro/package.json index 6c790935..41e3adc9 100644 --- a/packages/create/src/frameworks/solid/hosts/nitro/package.json +++ b/packages/create/src/frameworks/solid/hosts/nitro/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "nitro": "npm:nitro-nightly@latest" + "nitro": "latest" } } diff --git a/packages/create/src/frameworks/solid/hosts/railway/README.md b/packages/create/src/frameworks/solid/hosts/railway/README.md index d60c1f46..67ce7ff1 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/README.md +++ b/packages/create/src/frameworks/solid/hosts/railway/README.md @@ -1,10 +1,17 @@ ## Deploy to Railway -This project ships with `nixpacks.toml` so Railway detects the build automatically: +Railway's Railpack builder detects the project's package manager and package +scripts automatically. 1. Push this repo to GitHub 2. Visit https://railway.com/new and create a project from your repo 3. In the **Variables** tab, add the entries from `.env.example` with their production values -4. Railway runs `vite build` and serves from `dist/client` +4. Deploy, then open **Networking** and select **Generate Domain** -Need a database? Click **+ New** in your project to provision Postgres, MySQL, or Redis directly into the same environment — the connection string is auto-injected as `DATABASE_URL`. +Railpack runs the project's build script and starts the generated Nitro server +with `node .output/server/index.mjs`. The server handles SSR, server functions, +API routes, and static assets. + +Need a database? Add one from the Railway project canvas, then connect it to the +app with a Railway reference variable. The variable name and value depend on the +database service you choose. diff --git a/packages/create/src/frameworks/solid/hosts/railway/assets/nixpacks.toml b/packages/create/src/frameworks/solid/hosts/railway/assets/nixpacks.toml deleted file mode 100644 index 70ae68d2..00000000 --- a/packages/create/src/frameworks/solid/hosts/railway/assets/nixpacks.toml +++ /dev/null @@ -1,8 +0,0 @@ -[phases.setup] -nixPkgs = ["nodejs_22"] - -[phases.build] -cmds = ["npm install", "npm run build"] - -[start] -cmd = "npm run start" diff --git a/packages/create/src/frameworks/solid/hosts/railway/info.json b/packages/create/src/frameworks/solid/hosts/railway/info.json index ea150ba1..713a0195 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/info.json +++ b/packages/create/src/frameworks/solid/hosts/railway/info.json @@ -10,6 +10,7 @@ "color": "#9B4DCA", "priority": 160, "partner": { "id": "railway", "tier": "gold" }, + "deletedFiles": ["nixpacks.toml"], "integrations": [ { "type": "vite-plugin", diff --git a/packages/create/src/frameworks/solid/hosts/railway/package.json b/packages/create/src/frameworks/solid/hosts/railway/package.json index 5006a8bb..1bfcaa6f 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/package.json +++ b/packages/create/src/frameworks/solid/hosts/railway/package.json @@ -3,6 +3,6 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "nitro": "npm:nitro-nightly@latest" + "nitro": "latest" } } diff --git a/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs b/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs new file mode 100644 index 00000000..c31a713e --- /dev/null +++ b/packages/create/src/frameworks/solid/project/base/pnpm-workspace.yaml.ejs @@ -0,0 +1,8 @@ +<% if (packageManager !== 'pnpm') { ignoreFile(); return; } -%> +allowBuilds: + esbuild: true + lightningcss: true +<% if (addOnEnabled.cloudflare) { -%> + sharp: true + workerd: true +<% } -%> diff --git a/packages/create/src/types.ts b/packages/create/src/types.ts index 20dcc2c1..e6458165 100644 --- a/packages/create/src/types.ts +++ b/packages/create/src/types.ts @@ -89,6 +89,12 @@ export const AddOnBaseSchema = z.object({ .object({ dependencies: z.record(z.string(), z.string()).optional(), devDependencies: z.record(z.string(), z.string()).optional(), + engines: z.record(z.string(), z.string()).optional(), + pnpm: z + .object({ + onlyBuiltDependencies: z.array(z.string()).optional(), + }) + .optional(), scripts: z.record(z.string(), z.string()).optional(), }) .optional(), diff --git a/packages/create/tests/add-to-app.test.ts b/packages/create/tests/add-to-app.test.ts index e3d9e423..da7c5476 100644 --- a/packages/create/tests/add-to-app.test.ts +++ b/packages/create/tests/add-to-app.test.ts @@ -285,6 +285,36 @@ describe('writeFiles', () => { }) }) + it.each(['.env', '.env.local', '.env.example'])( + 'should preserve existing values when updating %s', + async (fileName) => { + const { environment, output } = createMemoryEnvironment('/foo') + environment.startRun() + await environment.writeFile( + `/foo/${fileName}`, + '# Existing configuration\nAPI_SECRET=keep-me\n', + ) + + await writeFiles( + environment, + '/foo', + { + files: { + [fileName]: + '# Generated configuration\nAPI_SECRET=\nNEW_VALUE=generated\n\n# Another integration\nANOTHER_VALUE=\n', + }, + deletedFiles: [], + }, + true, + ) + + environment.finishRun() + expect(output.files[fileName]).toBe( + '# Existing configuration\nAPI_SECRET=keep-me\n\n# Generated configuration\nNEW_VALUE=generated\n\n# Another integration\nANOTHER_VALUE=\n', + ) + }, + ) + it('should delete files', async () => { const { environment, output } = createMemoryEnvironment('/foo') environment.startRun() @@ -299,6 +329,25 @@ describe('writeFiles', () => { expect(output.deletedFiles).toEqual(['bloop.txt']) }) + it('should prompt before deleting existing files', async () => { + const { environment } = createMemoryEnvironment('/foo') + await environment.writeFile('/foo/nixpacks.toml', '[start]') + const confirm = vi.fn(() => Promise.resolve(false)) + environment.confirm = confirm + + await expect( + writeFiles( + environment, + '/foo', + { files: {}, deletedFiles: ['nixpacks.toml'] }, + false, + ), + ).rejects.toThrow('User cancelled') + + expect(confirm).toHaveBeenCalledOnce() + expect(environment.exists('/foo/nixpacks.toml')).toBe(true) + }) + it('should normalize windows-style output paths before writing', async () => { const writePaths: Array = [] const environment = { diff --git a/packages/create/tests/config-file.test.ts b/packages/create/tests/config-file.test.ts index 6a7af829..6067c295 100644 --- a/packages/create/tests/config-file.test.ts +++ b/packages/create/tests/config-file.test.ts @@ -33,6 +33,9 @@ describe('writeConfigFile', () => { } as AddOn, ], targetDir, + envVarValues: { + API_SECRET: 'do-not-persist', + }, } as unknown as Options const persistedOptions = { version: 1, @@ -78,6 +81,9 @@ describe('readConfigFileFromEnvironment', () => { mode: 'code-router', git: true, variableValues: {}, + envVarValues: { + API_SECRET: 'legacy-secret', + }, version: 1, existingAddOns: [], } diff --git a/packages/create/tests/edge-manifest.test.ts b/packages/create/tests/edge-manifest.test.ts index e60fcfa3..d3bc9aa5 100644 --- a/packages/create/tests/edge-manifest.test.ts +++ b/packages/create/tests/edge-manifest.test.ts @@ -158,7 +158,9 @@ describe('@tanstack/create/edge manifest', () => { ) expect(packageJSON.dependencies).toHaveProperty('@tanstack/react-start') expect(packageJSON.dependencies).toHaveProperty('@tanstack/react-query') - expect(packageJSON.dependencies).toHaveProperty('@clerk/clerk-react') + expect(packageJSON.dependencies).toHaveProperty( + '@clerk/tanstack-react-start', + ) expect(packageJSON.devDependencies).toHaveProperty('wrangler') expect(output.files['wrangler.jsonc']).toContain('tanstack-start-app') expect(output.files['.env.example']).toContain( diff --git a/packages/create/tests/partner-addons.test.ts b/packages/create/tests/partner-addons.test.ts new file mode 100644 index 00000000..6d031351 --- /dev/null +++ b/packages/create/tests/partner-addons.test.ts @@ -0,0 +1,237 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' + +import { finalizeAddOns, populateAddOnOptionsDefaults } from '../src/add-ons.js' +import { createApp } from '../src/create-app.js' +import { createMemoryEnvironment } from '../src/environment.js' +import { createFrameworkDefinition as createReactFrameworkDefinition } from '../src/frameworks/react/index.js' +import { createFrameworkDefinition as createSolidFrameworkDefinition } from '../src/frameworks/solid/index.js' + +import type { Framework, FrameworkDefinition, Options } from '../src/types.js' + +function frameworkFromDefinition(definition: FrameworkDefinition): Framework { + const { addOns, base, ...framework } = definition + + return { + ...framework, + getFiles: () => Promise.resolve(Object.keys(base)), + getFileContents: (path: string) => Promise.resolve(base[path]), + getDeletedFiles: () => Promise.resolve([]), + getAddOns: () => addOns, + } +} + +async function generateApp( + definition: FrameworkDefinition, + addOnIds: Array, +) { + const framework = frameworkFromDefinition(definition) + const chosenAddOns = await finalizeAddOns(framework, 'file-router', addOnIds) + const targetDir = '/partner-app' + const { environment, output } = createMemoryEnvironment(targetDir) + + await createApp(environment, { + projectName: 'partner-app', + targetDir, + framework, + mode: 'file-router', + typescript: true, + tailwind: true, + packageManager: 'pnpm', + git: false, + install: false, + intent: false, + chosenAddOns, + addOnOptions: populateAddOnOptionsDefaults(chosenAddOns), + includeExamples: true, + } satisfies Options) + + return output +} + +beforeEach(() => { + vi.stubGlobal( + 'fetch', + vi.fn( + async () => + new Response(JSON.stringify({ version: '1.0.0' }), { status: 200 }), + ), + ) +}) + +afterEach(() => { + vi.unstubAllGlobals() +}) + +describe('partner add-on scaffolds', () => { + it('generates Clerk with its full-stack SDK and ordered middleware', async () => { + const output = await generateApp(createReactFrameworkDefinition(), [ + 'clerk', + ]) + const packageJSON = JSON.parse(output.files['package.json']) + const envLocal = output.files['.env.local'] + const envExample = output.files['.env.example'] + const start = output.files['src/start.ts'] + const header = output.files['src/integrations/clerk/header-user.tsx'] + const demo = output.files['src/routes/demo/clerk.tsx'] + + expect(packageJSON.dependencies).toHaveProperty( + '@clerk/tanstack-react-start', + ) + expect(packageJSON.dependencies).not.toHaveProperty('@clerk/clerk-react') + expect(envLocal).toContain('VITE_CLERK_PUBLISHABLE_KEY=') + expect(envLocal).toContain('CLERK_SECRET_KEY=') + expect(envExample).toContain('VITE_CLERK_PUBLISHABLE_KEY=') + expect(envExample).toContain('CLERK_SECRET_KEY=') + expect(start).toContain( + "import { clerkMiddleware } from '@clerk/tanstack-react-start/server'", + ) + expect(start).toContain('clerkMiddleware()') + expect(start.indexOf('csrfMiddleware,')).toBeLessThan( + start.lastIndexOf('clerkMiddleware'), + ) + expect(output.files['src/integrations/clerk/provider.tsx']).toContain( + "from '@clerk/tanstack-react-start'", + ) + expect(header).toContain('') + expect(header).toContain('') + expect(header).not.toContain('') + expect(header).not.toContain('') + expect(demo).toContain("from '@clerk/tanstack-react-start'") + expect(demo).toContain('') + expect(demo).toContain('') + expect(demo).not.toContain('') + expect(demo).not.toContain('') + }) + + it('generates WorkOS with server auth, callback routes, and ordered middleware', async () => { + const output = await generateApp(createReactFrameworkDefinition(), [ + 'workos', + ]) + const packageJSON = JSON.parse(output.files['package.json']) + const envLocal = output.files['.env.local'] + const envExample = output.files['.env.example'] + const start = output.files['src/start.ts'] + + expect(packageJSON.engines).toEqual({ node: '>=22.11.0' }) + expect(packageJSON.dependencies).toHaveProperty( + '@workos/authkit-tanstack-react-start', + ) + expect(packageJSON.dependencies).toHaveProperty('@workos-inc/node') + expect(packageJSON.dependencies).not.toHaveProperty( + '@workos-inc/authkit-react', + ) + expect(envLocal).toContain('WORKOS_CLIENT_ID=') + expect(envLocal).toContain('WORKOS_API_KEY=') + expect(envLocal).toContain( + 'WORKOS_REDIRECT_URI=http://localhost:3000/api/auth/callback', + ) + expect(envLocal).toContain('WORKOS_COOKIE_PASSWORD=') + expect(envExample).toContain('WORKOS_CLIENT_ID=') + expect(envExample).toContain('WORKOS_API_KEY=') + expect(envExample).toContain('WORKOS_REDIRECT_URI=') + expect(envExample).toContain('WORKOS_COOKIE_PASSWORD=') + expect(start).toContain( + "import { authkitMiddleware } from '@workos/authkit-tanstack-react-start'", + ) + expect(start).toContain('authkitMiddleware()') + expect(start.indexOf('csrfMiddleware,')).toBeLessThan( + start.lastIndexOf('authkitMiddleware'), + ) + expect(output.files['src/routes/api/auth/callback.tsx']).toContain( + 'handleCallbackRoute()', + ) + expect(output.files['src/routes/api/auth/sign-in.tsx']).toContain( + 'getSignInUrl(', + ) + expect(output.files['src/integrations/workos/provider.tsx']).toContain( + "from '@workos/authkit-tanstack-react-start/client'", + ) + }) + + it.each([ + ['React', createReactFrameworkDefinition], + ['Solid', createSolidFrameworkDefinition], + ])( + 'generates a Railpack-compatible %s deployment', + async (_name, createFrameworkDefinition) => { + const output = await generateApp(createFrameworkDefinition(), ['railway']) + const packageJSON = JSON.parse(output.files['package.json']) + const readme = output.files['README.md'] + + expect(packageJSON.dependencies).toHaveProperty('nitro') + expect(packageJSON.dependencies).not.toHaveProperty('nitro-nightly') + expect(Object.values(packageJSON.dependencies)).not.toContain( + 'npm:nitro-nightly@latest', + ) + expect(packageJSON.scripts.start).toBe('node .output/server/index.mjs') + expect(output.files['nixpacks.toml']).toBeUndefined() + expect(output.deletedFiles).toContain('nixpacks.toml') + expect(readme).toContain("Railway's Railpack builder") + expect(readme).toContain('node .output/server/index.mjs') + expect(readme).not.toContain('Nixpacks') + expect(readme).not.toContain('dist/client') + }, + ) + + it.each([ + ['React', createReactFrameworkDefinition], + ['Solid', createSolidFrameworkDefinition], + ])( + 'approves Cloudflare build dependencies for pnpm in %s projects', + async (_name, createFrameworkDefinition) => { + const output = await generateApp(createFrameworkDefinition(), [ + 'cloudflare', + ]) + const packageJSON = JSON.parse(output.files['package.json']) + const pnpmWorkspace = output.files['pnpm-workspace.yaml'] + + expect(packageJSON.pnpm.onlyBuiltDependencies).toEqual( + expect.arrayContaining(['sharp', 'workerd']), + ) + expect(pnpmWorkspace).toContain('sharp: true') + expect(pnpmWorkspace).toContain('workerd: true') + }, + ) + + it('generates Sentry with privacy-conscious sampling defaults', async () => { + const output = await generateApp(createReactFrameworkDefinition(), [ + 'sentry', + ]) + const packageJSON = JSON.parse(output.files['package.json']) + const instrumentation = output.files['instrument.server.mjs'] + + expect(packageJSON.dependencies).toHaveProperty( + '@sentry/tanstackstart-react', + '^10.67.0', + ) + expect(packageJSON.pnpm.onlyBuiltDependencies).toContain('@sentry/cli') + expect(output.files['pnpm-workspace.yaml']).toContain("'@sentry/cli': true") + expect(instrumentation).toContain('userInfo: false') + expect(instrumentation).toContain('httpBodies: []') + expect(instrumentation).toContain('tracesSampleRate: 0.1') + expect(instrumentation).not.toContain('sendDefaultPii') + expect(instrumentation).not.toContain('replaysSessionSampleRate') + expect(instrumentation).not.toContain('replaysOnErrorSampleRate') + }) + + it.each([[['clerk', 'sentry', 'railway']], [['railway', 'sentry', 'clerk']]])( + 'composes Clerk, Sentry, and Railway independent of selection order: %j', + async (addOnIds) => { + const output = await generateApp( + createReactFrameworkDefinition(), + addOnIds, + ) + const packageJSON = JSON.parse(output.files['package.json']) + + expect(output.files['src/start.ts']).toContain('clerkMiddleware()') + expect(packageJSON.dependencies).toHaveProperty('nitro') + expect(packageJSON.dependencies).not.toHaveProperty('nitro-nightly') + expect(packageJSON.scripts.start).toContain( + '--import ./.output/server/instrument.server.mjs', + ) + expect(packageJSON.scripts.start).toContain('.output/server/index.mjs') + expect(output.files['instrument.server.mjs']).toBeDefined() + expect(output.files['nixpacks.toml']).toBeUndefined() + }, + ) +}) From 2a7e07fb6ab87f6b8d55b19372717cb8de5235d9 Mon Sep 17 00:00:00 2001 From: Tanner Linsley Date: Tue, 21 Jul 2026 14:03:29 -0600 Subject: [PATCH 2/2] fix: address partner add-on review --- .../create/src/frameworks/react/add-ons/clerk/README.md | 8 +++++++- .../create/src/frameworks/react/hosts/nitro/package.json | 2 +- .../src/frameworks/react/hosts/railway/package.json.ejs | 2 +- .../create/src/frameworks/solid/hosts/nitro/package.json | 2 +- .../src/frameworks/solid/hosts/railway/package.json | 2 +- packages/create/tests/partner-addons.test.ts | 5 ++++- 6 files changed, 15 insertions(+), 6 deletions(-) diff --git a/packages/create/src/frameworks/react/add-ons/clerk/README.md b/packages/create/src/frameworks/react/add-ons/clerk/README.md index f834d664..e61d4bfe 100644 --- a/packages/create/src/frameworks/react/add-ons/clerk/README.md +++ b/packages/create/src/frameworks/react/add-ons/clerk/README.md @@ -24,11 +24,17 @@ server: ```tsx 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 auth() + const { userId } = await getAuth() if (!userId) throw redirect({ to: '/' }) }, }) diff --git a/packages/create/src/frameworks/react/hosts/nitro/package.json b/packages/create/src/frameworks/react/hosts/nitro/package.json index 41e3adc9..56e871bc 100644 --- a/packages/create/src/frameworks/react/hosts/nitro/package.json +++ b/packages/create/src/frameworks/react/hosts/nitro/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "nitro": "latest" + "nitro": "3.0.260610-beta" } } diff --git a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs index 0be07b03..909ab021 100644 --- a/packages/create/src/frameworks/react/hosts/railway/package.json.ejs +++ b/packages/create/src/frameworks/react/hosts/railway/package.json.ejs @@ -3,6 +3,6 @@ "start": "<%= addOnEnabled.sentry ? 'node --import ./.output/server/instrument.server.mjs .output/server/index.mjs' : 'node .output/server/index.mjs' %>" }, "dependencies": { - "nitro": "latest" + "nitro": "3.0.260610-beta" } } diff --git a/packages/create/src/frameworks/solid/hosts/nitro/package.json b/packages/create/src/frameworks/solid/hosts/nitro/package.json index 41e3adc9..56e871bc 100644 --- a/packages/create/src/frameworks/solid/hosts/nitro/package.json +++ b/packages/create/src/frameworks/solid/hosts/nitro/package.json @@ -1,5 +1,5 @@ { "dependencies": { - "nitro": "latest" + "nitro": "3.0.260610-beta" } } diff --git a/packages/create/src/frameworks/solid/hosts/railway/package.json b/packages/create/src/frameworks/solid/hosts/railway/package.json index 1bfcaa6f..a3b22116 100644 --- a/packages/create/src/frameworks/solid/hosts/railway/package.json +++ b/packages/create/src/frameworks/solid/hosts/railway/package.json @@ -3,6 +3,6 @@ "start": "node .output/server/index.mjs" }, "dependencies": { - "nitro": "latest" + "nitro": "3.0.260610-beta" } } diff --git a/packages/create/tests/partner-addons.test.ts b/packages/create/tests/partner-addons.test.ts index 6d031351..5eb11ffc 100644 --- a/packages/create/tests/partner-addons.test.ts +++ b/packages/create/tests/partner-addons.test.ts @@ -158,7 +158,10 @@ describe('partner add-on scaffolds', () => { const packageJSON = JSON.parse(output.files['package.json']) const readme = output.files['README.md'] - expect(packageJSON.dependencies).toHaveProperty('nitro') + expect(packageJSON.dependencies).toHaveProperty( + 'nitro', + '3.0.260610-beta', + ) expect(packageJSON.dependencies).not.toHaveProperty('nitro-nightly') expect(Object.values(packageJSON.dependencies)).not.toContain( 'npm:nitro-nightly@latest',