From 0c4958afb47e1954eb7d21840b67d2d8979b65f2 Mon Sep 17 00:00:00 2001 From: BilalG1 Date: Thu, 11 Sep 2025 10:10:36 -0700 Subject: [PATCH] init stack cli project-id and publishable-client-key args (#888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ---- > [!IMPORTANT] > Add CLI options for project ID and publishable client key, update initialization process, and modify documentation to reflect changes. > > - **CLI Options**: > - Added `--project-id` and `--publishable-client-key` options to `index.ts` for CLI setup. > - **Initialization**: > - Updated `writeEnvVars()` in `index.ts` to include project ID and publishable client key in `.env.local`. > - Modified `writeStackAppFile()` in `index.ts` to handle new CLI options. > - **Documentation**: > - Updated references from `stack.ts` to `stack/client.ts` and `stack/server.ts` in multiple `.mdx` files. > - Added examples for using project ID and publishable client key in `setup.mdx` and `example-pages.mdx`. > - **Testing**: > - Added `test-run-keys-next` and `test-run-keys-js` scripts in `package.json` for testing new CLI options. > > This description was created by [Ellipsis](https://www.ellipsis.dev?ref=stack-auth%2Fstack-auth&utm_source=github&utm_medium=referral) for b204910ebdde0989079ba5a278040f5faf11cd10. You can [customize](https://app.ellipsis.dev/stack-auth/settings/summaries) this summary. It will automatically update as commits are pushed. ---- ## Review by RecurseML _🔍 Review performed on [bd14f6b..92c332a](https://github.com/stack-auth/stack-auth/compare/bd14f6be6a97aa45506a06fb78745a2bc5c97d1c...92c332ad3f86de036a2b0661992af0d5bd1fa3f6)_ ✨ No bugs found, your code is sparkling clean
✅ Files analyzed, no issues (2) • `packages/init-stack/src/index.ts` • `apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx`
⏭️ Files skipped (trigger manually) (1) | Locations | Trigger Analysis | |-----------|------------------| `packages/init-stack/package.json` | [![Analyze](https://img.shields.io/badge/Analyze-238636?style=plastic)](https://squash-322339097191.europe-west3.run.app/interactive/4d33d423a3548f08a0c0374dd328cb7f7d3b57c709ba91951ccc3aa94c1a017c/?repo_owner=stack-auth&repo_name=stack-auth&pr_number=888)
[![Need help? Join our Discord](https://img.shields.io/badge/Need%20help%3F%20Join%20our%20Discord-5865F2?style=plastic&logo=discord&logoColor=white)](https://discord.gg/n3SsVDAW6U) ## Summary by CodeRabbit - New Features - Init tool accepts project ID and publishable client key; generated projects include those values in client/server outputs and env hints. - Next.js projects now generate both client and server app artifacts using a standardized client/server layout. - UI - Removed the vertical divider on the New Project page for a cleaner preview/form layout. - Documentation - Updated docs and examples to reference the new client/server file split. - Chores - Added key-based test-run scripts for Next.js and JS. --- .../new-project/page-client.tsx | 1 - .../[projectId]/(overview)/setup-page.tsx | 8 ++-- docs/templates/concepts/oauth.mdx | 2 +- docs/templates/customization/custom-pages.mdx | 8 ++-- .../page-examples/forgot-password.mdx | 2 +- .../page-examples/password-reset.mdx | 2 +- .../getting-started/example-pages.mdx | 12 +++--- docs/templates/getting-started/setup.mdx | 17 ++++---- docs/templates/getting-started/users.mdx | 2 +- packages/init-stack/package.json | 4 +- packages/init-stack/src/index.ts | 41 +++++++++++-------- 11 files changed, 54 insertions(+), 45 deletions(-) diff --git a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx index 175c4d0ba..be3a19ee3 100644 --- a/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/(outside-dashboard)/new-project/page-client.tsx @@ -146,7 +146,6 @@ export default function PageClient() { -
diff --git a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx index 19f2e03ab..133e0822d 100644 --- a/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx +++ b/apps/dashboard/src/app/(main)/(protected)/projects/[projectId]/(overview)/setup-page.tsx @@ -114,10 +114,10 @@ export default function SetupPage(props: { toMetrics: () => void }) { }, { step: 4, - title: "Create stack.ts file", + title: "Create stack/client.ts file", content: <> - Create a new file called stack.ts and add the following code. Here we use react-router-dom as an example. + Create a new file called stack/client.ts and add the following code. Here we use react-router-dom as an example. void }) { } }); `} - title="stack.ts" + title="stack/client.ts" icon="code" /> @@ -154,7 +154,7 @@ export default function SetupPage(props: { toMetrics: () => void }) { import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; import { Suspense } from "react"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; function HandlerRoutes() { const location = useLocation(); diff --git a/docs/templates/concepts/oauth.mdx b/docs/templates/concepts/oauth.mdx index 41d068fb4..a6c353664 100644 --- a/docs/templates/concepts/oauth.mdx +++ b/docs/templates/concepts/oauth.mdx @@ -91,7 +91,7 @@ To avoid showing the authorization page twice, you can already request scopes du To do this, edit the `oauthScopesOnSignIn` setting of your `stackServerApp`: -```jsx title='stack.ts' +```jsx title='stack/server.ts' export const stackServerApp = new StackServerApp({ // ...your other settings... oauthScopesOnSignIn: { diff --git a/docs/templates/customization/custom-pages.mdx b/docs/templates/customization/custom-pages.mdx index 839d0e6fd..c452aaadf 100644 --- a/docs/templates/customization/custom-pages.mdx +++ b/docs/templates/customization/custom-pages.mdx @@ -23,9 +23,9 @@ export default function CustomSignInPage() { } ``` -Then you can instruct the Stack app in `stack.ts` to use your custom sign in page: +Then you can instruct the Stack app in `stack/server.ts` to use your custom sign in page: -```tsx title="stack.ts" +```tsx title="stack/server.ts" export const stackServerApp = new StackServerApp({ // ... // add these three lines @@ -67,9 +67,9 @@ export default function CustomOAuthSignIn() { } ``` -Again, edit the Stack app in `stack.ts` to use your custom sign in page: +Again, edit the Stack app in `stack/server.ts` to use your custom sign in page: -```tsx title="stack.ts" +```tsx title="stack/server.ts" export const stackServerApp = new StackServerApp({ // ... // add these three lines diff --git a/docs/templates/customization/page-examples/forgot-password.mdx b/docs/templates/customization/page-examples/forgot-password.mdx index a6c942129..7da437e82 100644 --- a/docs/templates/customization/page-examples/forgot-password.mdx +++ b/docs/templates/customization/page-examples/forgot-password.mdx @@ -22,7 +22,7 @@ export default function DefaultForgotPassword() { To integrate the forgot password page with your application's routing: 1. Create a route for your forgot password page (e.g., `/forgot-password`) -2. Configure Stack Auth to use your custom route in your `stack.ts` file: +2. Configure Stack Auth to use your custom route in your `stack/server.ts` file: ```tsx export const stackServerApp = new StackServerApp({ diff --git a/docs/templates/customization/page-examples/password-reset.mdx b/docs/templates/customization/page-examples/password-reset.mdx index 51523cc24..e10681337 100644 --- a/docs/templates/customization/page-examples/password-reset.mdx +++ b/docs/templates/customization/page-examples/password-reset.mdx @@ -23,7 +23,7 @@ To integrate the password reset page with your application's routing: 1. Create a route handler that extracts the reset code from the URL (e.g., `/reset-password?code=xyz123`) 2. Pass the code to your password reset component -3. Configure Stack Auth to use your custom route in your `stack.ts` file: +3. Configure Stack Auth to use your custom route in your `stack/server.ts` file: ```tsx export const stackServerApp = new StackServerApp({ diff --git a/docs/templates/getting-started/example-pages.mdx b/docs/templates/getting-started/example-pages.mdx index e4d3240b5..913db8b00 100644 --- a/docs/templates/getting-started/example-pages.mdx +++ b/docs/templates/getting-started/example-pages.mdx @@ -7,7 +7,7 @@ This guide demonstrates how to integrate Stack Auth with Vite. The same principl ### Initialize the app -```typescript title="stack.ts" +```typescript title="stack/client.ts" import { StackClientApp } from "@stackframe/js"; // Add type declaration for Vite's import.meta.env @@ -76,7 +76,7 @@ export const stackClientApp = new StackClientApp({ ```typescript - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; const updateUIState = (user: any | null) => { const authOptions = document.getElementById("authOptions"); @@ -148,7 +148,7 @@ export const stackClientApp = new StackClientApp({ ```typescript - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; // Check if user is already signed in stackClientApp.getUser().then((user) => { @@ -253,7 +253,7 @@ export const stackClientApp = new StackClientApp({ ```typescript - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; // Check if user is already signed in stackClientApp.getUser().then((user) => { @@ -335,7 +335,7 @@ export const stackClientApp = new StackClientApp({ ```typescript - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; // Check if user is already signed in stackClientApp.getUser().then((user) => { @@ -413,7 +413,7 @@ export const stackClientApp = new StackClientApp({ ```typescript - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; // Check if user is already signed in stackClientApp.getUser().then((user) => { diff --git a/docs/templates/getting-started/setup.mdx b/docs/templates/getting-started/setup.mdx index 917fdfb09..a0b3275c7 100644 --- a/docs/templates/getting-started/setup.mdx +++ b/docs/templates/getting-started/setup.mdx @@ -50,7 +50,8 @@ We recommend using our **setup wizard** for a seamless installation experience. - `app/handler/[...stack]/page.tsx`: This file contains the default pages for sign-in, sign-out, account settings, and more. If you prefer, later you will learn how to [use custom pages](../customization/custom-pages.mdx) instead. - `app/layout.tsx`: The layout file was updated to wrap the entire body with `StackProvider` and `StackTheme`. - `app/loading.tsx`: If not yet found, Stack automatically adds a Suspense boundary to your app. This is shown to the user while Stack's async hooks, like `useUser`, are loading. - - `stack.ts`: This file contains the `stackServerApp` which you can use to access Stack from Server Components, Server Actions, API routes, and middleware. + - `stack/server.ts`: This file contains the `stackServerApp` which you can use to access Stack from Server Components, Server Actions, API routes, and middleware. + - `stack/client.ts`: This file contains the `stackClientApp` which you can use to access Stack from Client Components @@ -82,11 +83,11 @@ We recommend using our **setup wizard** for a seamless installation experience. ``` - ### Create `stack.ts` file + ### Create `stack/server.ts` file - Create a new file `stack.ts` in your root directory and fill it with the following: + Create a new file `stack/server.ts` in your root directory and fill it with the following: - ```tsx title="stack.ts" + ```tsx title="stack/server.ts" import "server-only"; import { StackServerApp } from "@stackframe/stack"; @@ -202,11 +203,11 @@ Before getting started, make sure you have a [React project](https://react.dev/l If you haven't already, [register a new account on Stack](https://app.stack-auth.com/projects), create a project in the dashboard, create a new API key from the left sidebar, and copy the project ID, publishable client key, and secret server key into a new file called `.env.local` in the root of your React project: - ### Create `stack.ts` file + ### Create `stack/client.ts` file - Create a new file `stack.ts` in your root directory and fill it with the following Stack app initialization code: + Create a new file `stack/client.ts` in your root directory and fill it with the following Stack app initialization code: - ```tsx title="stack.ts" + ```tsx title="stack/client.ts" import { StackClientApp } from "@stackframe/react"; import { useNavigate } from "react-router-dom"; @@ -231,7 +232,7 @@ Before getting started, make sure you have a [React project](https://react.dev/l import { StackHandler, StackProvider, StackTheme } from "@stackframe/react"; import { Suspense } from "react"; import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom"; - import { stackClientApp } from "./stack"; + import { stackClientApp } from "./stack/client"; function HandlerRoutes() { const location = useLocation(); diff --git a/docs/templates/getting-started/users.mdx b/docs/templates/getting-started/users.mdx index a60969f78..2e30184c2 100644 --- a/docs/templates/getting-started/users.mdx +++ b/docs/templates/getting-started/users.mdx @@ -30,7 +30,7 @@ Sometimes, you want to retrieve the user only if they're signed in, and redirect ## Server Component basics -Since `useUser()` is a stateful hook, you can't use it on server components. Instead, you can import `stackServerApp` from `stack.ts` and call `getUser()`: +Since `useUser()` is a stateful hook, you can't use it on server components. Instead, you can import `stackServerApp` from `stack/client.ts` and call `getUser()`: ```tsx title="my-server-component.tsx" import { stackServerApp } from "@/stack"; diff --git a/packages/init-stack/package.json b/packages/init-stack/package.json index f228e4bf0..6115834ed 100644 --- a/packages/init-stack/package.json +++ b/packages/init-stack/package.json @@ -24,7 +24,9 @@ "test-run-js:manual": "rimraf test-run-output && npx -y sv create test-run-output --no-install && pnpm run init-stack:local test-run-output", "test-run-js": "rimraf test-run-output && npx -y sv create test-run-output --template minimal --types ts --no-add-ons --no-install && STACK_DISABLE_INTERACTIVE=true pnpm run init-stack:local test-run-output --js --client --npm", "test-run-next:manual": "rimraf test-run-output && npx -y create-next-app@latest test-run-output && pnpm run init-stack:local test-run-output", - "test-run-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && STACK_DISABLE_INTERACTIVE=true pnpm run init-stack:local test-run-output" + "test-run-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && STACK_DISABLE_INTERACTIVE=true pnpm run init-stack:local test-run-output", + "test-run-keys-next": "rimraf test-run-output && npx -y create-next-app@latest test-run-output --app --ts --no-src-dir --tailwind --use-npm --eslint --import-alias '##@#/*' --turbopack && STACK_DISABLE_INTERACTIVE=true pnpm run init-stack:local test-run-output --project-id my-project-id --publishable-client-key my-publishable-client-key", + "test-run-keys-js": "rimraf test-run-output && npx -y sv create test-run-output --template minimal --types ts --no-add-ons --no-install && STACK_DISABLE_INTERACTIVE=true pnpm run init-stack:local test-run-output --js --client --npm --project-id my-project-id --publishable-client-key my-publishable-client-key" }, "files": [ "README.md", diff --git a/packages/init-stack/src/index.ts b/packages/init-stack/src/index.ts index 11059a178..1c82fd3a0 100644 --- a/packages/init-stack/src/index.ts +++ b/packages/init-stack/src/index.ts @@ -42,6 +42,8 @@ program .option("--bun", "Use bun as package manager") .option("--client", "Initialize client-side only") .option("--server", "Initialize server-side only") + .option("--project-id ", "Project ID to use in setup") + .option("--publishable-client-key ", "Publishable client key to use in setup") .option("--no-browser", "Don't open browser for environment variable setup") .addHelpText('after', ` For more information, please visit https://docs.stack-auth.com/getting-started/setup`); @@ -58,6 +60,8 @@ const typeFromArgs: string | undefined = options.js ? "js" : options.next ? "nex const packageManagerFromArgs: string | undefined = options.npm ? "npm" : options.yarn ? "yarn" : options.pnpm ? "pnpm" : options.bun ? "bun" : undefined; const isClient: boolean = options.client || false; const isServer: boolean = options.server || false; +const projectIdFromArgs: string | undefined = options.projectId; +const publishableClientKeyFromArgs: string | undefined = options.publishableClientKey; // Commander negates the boolean options with prefix `--no-` // so `--no-browser` becomes `browser: false` const noBrowser: boolean = !options.browser; @@ -195,6 +199,7 @@ async function main(): Promise { if (type === "next") { const projectInfo = await Steps.getNextProjectInfo({ packageJson: projectPackageJson }); await Steps.updateNextLayoutFile(projectInfo); + await Steps.writeStackAppFile(projectInfo, "client"); await Steps.writeStackAppFile(projectInfo, "server"); await Steps.writeNextHandlerFile(projectInfo); await Steps.writeNextLoadingFile(projectInfo); @@ -511,13 +516,13 @@ const Steps = { "# 1. Go to https://app.stack-auth.com\n" + "# 2. Create a new project\n" + "# 3. Copy the keys below\n" + - "NEXT_PUBLIC_STACK_PROJECT_ID=\n" + - "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=\n" + + `NEXT_PUBLIC_STACK_PROJECT_ID="${projectIdFromArgs ?? ""}"\n` + + `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY="${publishableClientKeyFromArgs ?? ""}"\n` + "STACK_SECRET_SERVER_KEY=\n" : "# Stack Auth keys\n" + "# Get these variables by creating a project on https://app.stack-auth.com.\n" + - "NEXT_PUBLIC_STACK_PROJECT_ID=\n" + - "NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=\n" + + `NEXT_PUBLIC_STACK_PROJECT_ID="${projectIdFromArgs ?? ""}"\n` + + `NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY="${publishableClientKeyFromArgs ?? ""}"\n` + "STACK_SECRET_SERVER_KEY=\n"; laterWriteFile(envLocalPath, envContent); @@ -567,18 +572,14 @@ const Steps = { return res; }, - async writeStackAppFile({ type, srcPath, defaultExtension, indentation }: StackAppFileOptions, clientOrServer: string): Promise { + async writeStackAppFile({ type, srcPath, defaultExtension, indentation }: StackAppFileOptions, clientOrServer: "server" | "client"): Promise { const packageName = await Steps.getStackPackageName(type); const clientOrServerCap = { client: "Client", server: "Server", - }[clientOrServer] ?? throwErr("unknown clientOrServer " + clientOrServer); - - const relativeStackAppPath = { - js: `stack/${clientOrServer}`, - next: "stack", - }[type] ?? throwErr("unknown type"); + }[clientOrServer as string] ?? throwErr("unknown clientOrServer " + clientOrServer); + const relativeStackAppPath = `stack/${clientOrServer}`; const stackAppPathWithoutExtension = path.join(srcPath, relativeStackAppPath); const stackAppFileExtension = @@ -589,24 +590,30 @@ const Steps = { if (stackAppContent) { if (!stackAppContent.includes("@stackframe/")) { throw new UserError( - `A file at the path ${stackAppPath} already exists. Stack uses the stack.ts file to initialize the Stack SDK. Please remove the existing file and try again.` + `A file at the path ${stackAppPath} already exists. Stack uses the stack/${clientOrServer}.ts file to initialize the Stack SDK. Please remove the existing file and try again.` ); } throw new UserError( `It seems that you already installed Stack in this project.` ); } + + const publishableClientKeyWrite = clientOrServer === "server" + ? `process.env.STACK_PUBLISHABLE_CLIENT_KEY ${publishableClientKeyFromArgs ? `|| '${publishableClientKeyFromArgs}'` : ""}` + : `'${publishableClientKeyFromArgs ?? 'INSERT_YOUR_PUBLISHABLE_CLIENT_KEY_HERE'}'`; + laterWriteFileIfNotExists( stackAppPath, ` -${type === "next" ? `import "server-only";` : ""} +${type === "next" && clientOrServer === "server" ? `import "server-only";` : ""} import { Stack${clientOrServerCap}App } from ${JSON.stringify(packageName)}; export const stack${clientOrServerCap}App = new Stack${clientOrServerCap}App({ ${indentation}tokenStore: ${type === "next" ? '"nextjs-cookie"' : (clientOrServer === "client" ? '"cookie"' : '"memory"')},${ type === "js" ? `\n\n${indentation}// get your Stack Auth API keys from https://app.stack-auth.com${clientOrServer === "client" ? ` and store them in a safe place (eg. environment variables)` : ""}` : ""}${ -type === "js" ? `\n${indentation}publishableClientKey: ${clientOrServer === "server" ? 'process.env.STACK_PUBLISHABLE_CLIENT_KEY' : 'INSERT_YOUR_PUBLISHABLE_CLIENT_KEY_HERE'},` : ""}${ +type === "js" && projectIdFromArgs ? `\n${indentation}projectId: '${projectIdFromArgs}',` : ""}${ +type === "js" ? `\n${indentation}publishableClientKey: ${publishableClientKeyWrite},` : ""}${ type === "js" && clientOrServer === "server" ? `\n${indentation}secretServerKey: process.env.STACK_SECRET_SERVER_KEY,` : ""} }); `.trim() + "\n" @@ -630,7 +637,7 @@ type === "js" && clientOrServer === "server" ? `\n${indentation}secretServerKey: } laterWriteFileIfNotExists( handlerPath, - `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${ + `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack/server";\n\nexport default function Handler(props${ handlerFileExtension.includes("ts") ? ": unknown" : "" }) {\n${projectInfo.indentation}return ;\n}\n` ); @@ -684,7 +691,7 @@ type === "js" && clientOrServer === "server" ? `\n${indentation}secretServerKey: } }, - async getServerOrClientOrBoth(): Promise { + async getServerOrClientOrBoth(): Promise> { if (isClient && isServer) return ["server", "client"]; if (isServer) return ["server"]; if (isClient) return ["client"]; @@ -742,7 +749,7 @@ async function getUpdatedLayout(originalLayout: string): Promise