#!/usr/bin/env node import * as child_process from "child_process"; import * as fs from "fs"; import inquirer from "inquirer"; import open from "open"; import * as path from "path"; const jsLikeFileExtensions = [ "mtsx", "ctsx", "tsx", "mts", "cts", "ts", "mjsx", "cjsx", "jsx", "mjs", "cjs", "js", ]; class UserError extends Error { constructor(message) { super(message); this.name = "UserError"; } } let savedProjectPath = process.argv[2] || undefined; const isDryRun = process.argv.includes("--dry-run"); const isNeon = process.argv.includes("--neon"); const typeFromArgs = ["js", "next"].find(s => process.argv.includes(`--${s}`)); const packageManagerFromArgs = ["npm", "yarn", "pnpm", "bun"].find(s => process.argv.includes(`--${s}`)); const isClient = process.argv.includes("--client"); const isServer = process.argv.includes("--server"); const ansis = { red: "\x1b[31m", blue: "\x1b[34m", green: "\x1b[32m", yellow: "\x1b[33m", clear: "\x1b[0m", bold: "\x1b[1m", }; const colorize = { red: (strings, ...values) => ansis.red + templateIdentity(strings, ...values) + ansis.clear, blue: (strings, ...values) => ansis.blue + templateIdentity(strings, ...values) + ansis.clear, green: (strings, ...values) => ansis.green + templateIdentity(strings, ...values) + ansis.clear, yellow: (strings, ...values) => ansis.yellow + templateIdentity(strings, ...values) + ansis.clear, bold: (strings, ...values) => ansis.bold + templateIdentity(strings, ...values) + ansis.clear, } const filesCreated = []; const filesModified = []; const commandsExecuted = []; const packagesToInstall = []; const writeFileHandlers = []; const nextSteps = [ `Create an account and Stack Auth API key for your project on https://app.stack-auth.com`, ]; async function main() { // Welcome message console.log(); console.log(` ██████ ██████████████ ████████████████████ ████████████████████ WELCOME TO █████████████████ ╔═╗╔╦╗╔═╗╔═╗╦╔═ ┌─┐┬ ┬┌┬┐┬ ┬ █████████████ ╚═╗ ║ ╠═╣║ ╠╩╗ ├─┤│ │ │ ├─┤ █████████████ ████ ╚═╝ ╩ ╩ ╩╚═╝╩ ╩ ┴ ┴└─┘ ┴ ┴ ┴ █████████████████ ██████ ██ ████ ████ █████ █████ ██████ `); console.log(); // Wait just briefly so we can use `Steps` in here (it's defined only after the call to `main()`) await new Promise((resolve) => resolve()); // Prepare some stuff await clearStdin(); const projectPath = await getProjectPath(); // Steps const { packageJson } = await Steps.getProject(); const type = await Steps.getProjectType({ packageJson }); await Steps.addStackPackage(type); if (isNeon) packagesToInstall.push('@neondatabase/serverless'); await Steps.writeEnvVars(type); if (type === "next") { const projectInfo = await Steps.getNextProjectInfo({ packageJson }); await Steps.updateNextLayoutFile(projectInfo); await Steps.writeStackAppFile(projectInfo, "server"); await Steps.writeNextHandlerFile(projectInfo); await Steps.writeNextLoadingFile(projectInfo); nextSteps.push(`Copy the environment variables from the new API key into your .env.local file`) } else if (type === "js") { const defaultExtension = await Steps.guessDefaultFileExtension(); const where = await Steps.getServerOrClientOrBoth(); const srcPath = await Steps.guessSrcPath(); const appFiles = []; for (const w of where) { const { fileName } = await Steps.writeStackAppFile({ type, defaultExtension, indentation: " ", srcPath, }, w); appFiles.push(fileName); } nextSteps.push( `Copy the environment variables from the new API key into your own environment and reference them in ${appFiles.join(" and ")}`, `Follow the instructions on how to use Stack Auth's vanilla SDK at http://docs.stack-auth.com/others/js-client`, ); } else { throw new Error("Unknown type: " + type); } const { packageManager } = await Steps.getPackageManager(); await Steps.ensureReady(type); // Install dependencies console.log(); console.log(colorize.bold`Installing dependencies...`); const installCommand = packageManager === "yarn" ? "yarn add" : `${packageManager} install`; await shellNicelyFormatted(`${installCommand} ${packagesToInstall.join(' ')}`, { shell: true, cwd: projectPath, }); // Write files console.log(); console.log(colorize.bold`Writing files...`); console.log(); for (const writeFileHandler of writeFileHandlers) { await writeFileHandler(); } console.log(`${colorize.green`√`} Done writing files`); console.log('\n\n\n'); console.log(colorize.bold`${colorize.green`Installation succeeded!`}`); console.log(); console.log("Commands executed:"); for (const command of commandsExecuted) { console.log(` ${colorize.blue`${command}`}`); } console.log(); console.log("Files written:"); for (const file of filesModified) { console.log(` ${colorize.yellow`${file}`}`); } for (const file of filesCreated) { console.log(` ${colorize.green`${file}`}`); } console.log(); // Success! console.log(` ${colorize.green`===============================================`} ${colorize.green`Successfully installed Stack! 🚀🚀🚀`} Next steps: ${[...nextSteps.entries()].map(([index, step]) => `${index + 1}. ${step}`).join("\n")} ${type === "next" ? `Then, you will be able to access your sign-in page on http://your-website.example.com/handler/sign-in. That's it!` : "That's it!"} ${colorize.green`===============================================`} For more information, please visit https://docs.stack-auth.com/getting-started/setup `.trim()); if (!process.env.STACK_DISABLE_INTERACTIVE) { await open("https://app.stack-auth.com/wizard-congrats"); } } main() .catch((err) => { if (!(err instanceof UserError)) { console.error(err); } console.error('\n\n\n\n'); console.log(colorize.red`===============================================`); console.error(); if (err instanceof UserError) { console.error(`${colorize.red`ERROR!`} ${err.message}`); } else { console.error("An error occurred during the initialization process."); } console.error(); console.log(colorize.red`===============================================`); console.error(); console.error( "If you need assistance, please try installing Stack manually as described in https://docs.stack-auth.com/getting-started/setup or join our Discord where we're happy to help: https://discord.stack-auth.com" ); if (!(err instanceof UserError)) { console.error(""); console.error(`Error message: ${err.message}`); } console.error(); process.exit(1); }); const Steps = { async getProject() { let projectPath = await getProjectPath(); if (!fs.existsSync(projectPath)) { throw new UserError(`The project path ${projectPath} does not exist`); } const packageJsonPath = path.join(projectPath, "package.json"); if (!fs.existsSync(packageJsonPath)) { throw new UserError( `The package.json file does not exist in the project path ${projectPath}. You must initialize a new project first before installing Stack.` ); } const packageJsonText = fs.readFileSync(packageJsonPath, "utf-8"); let packageJson; try { packageJson = JSON.parse(packageJsonText); } catch (e) { throw new UserError(`package.json file is not valid JSON: ${e}`); } return { packageJson }; }, async getProjectType({ packageJson }) { if (typeFromArgs) return typeFromArgs; const maybeNextProject = await Steps.maybeGetNextProjectInfo({ packageJson }); if (!("error" in maybeNextProject)) return "next"; const { type } = assertInteractive() && await inquirer.prompt([ { type: "list", name: "type", message: "Which integration would you like to install?", choices: [ { name: "None (vanilla JS, Node.js, etc)", value: "js" }, { name: "Next.js", value: "next" }, ] } ]); return type; }, async getStackPackageName(type, install = false) { return { "js": (install && process.env.STACK_JS_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/js", "next": (install && process.env.STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/stack", }[type] ?? throwErr("Unknown type in addStackPackage: " + type); }, async addStackPackage(type) { packagesToInstall.push(await Steps.getStackPackageName(type, true)); }, async getNextProjectInfo({ packageJson }) { const maybe = await Steps.maybeGetNextProjectInfo({ packageJson }); if ("error" in maybe) throw new UserError(maybe.error); return maybe; }, async maybeGetNextProjectInfo({ packageJson }) { const projectPath = await getProjectPath(); const nextVersionInPackageJson = packageJson?.dependencies?.["next"] ?? packageJson?.devDependencies?.["next"]; if (!nextVersionInPackageJson) { return { error: `The project at ${projectPath} does not appear to be a Next.js project, or does not have 'next' installed as a dependency.` }; } if ( !nextVersionInPackageJson.includes("14") && !nextVersionInPackageJson.includes("15") && nextVersionInPackageJson !== "latest" ) { return { error: `The project at ${projectPath} is using an unsupported version of Next.js (found ${nextVersionInPackageJson}).\n\nOnly Next.js 14 & 15 projects are currently supported. See Next's upgrade guide: https://nextjs.org/docs/app/building-your-application/upgrading/version-14` }; } const nextConfigPathWithoutExtension = path.join(projectPath, "next.config"); const nextConfigFileExtension = await findJsExtension( nextConfigPathWithoutExtension ); const nextConfigPath = nextConfigPathWithoutExtension + "." + (nextConfigFileExtension ?? "js"); if (!fs.existsSync(nextConfigPath)) { return { error: `Expected file at ${nextConfigPath}. Only Next.js projects are currently supported.` }; } const hasSrcAppFolder = fs.existsSync(path.join(projectPath, "src/app")); const srcPath = path.join(projectPath, hasSrcAppFolder ? "src" : ""); const appPath = path.join(srcPath, "app"); if (!fs.existsSync(appPath)) { return { error: `The app path ${appPath} does not exist. Only the Next.js app router is supported.` }; } const dryUpdateNextLayoutFileResult = await Steps.dryUpdateNextLayoutFile({ appPath, defaultExtension: "jsx" }); return { type: "next", srcPath, appPath, defaultExtension: dryUpdateNextLayoutFileResult.fileExtension, indentation: dryUpdateNextLayoutFileResult.indentation, }; }, async writeEnvVars(type) { const projectPath = await getProjectPath(); // TODO: in non-Next environments, ask the user what method they prefer for envvars if (type !== "next") return false; const envLocalPath = path.join(projectPath, ".env.local"); const potentialEnvLocations = [ path.join(projectPath, ".env"), path.join(projectPath, ".env.development"), path.join(projectPath, ".env.default"), path.join(projectPath, ".env.defaults"), path.join(projectPath, ".env.example"), envLocalPath, ]; if (potentialEnvLocations.every((p) => !fs.existsSync(p))) { laterWriteFile( envLocalPath, "# Stack Auth keys\n# Get these variables by creating a project on https://app.stack-auth.com.\nNEXT_PUBLIC_STACK_PROJECT_ID=\nNEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=\nSTACK_SECRET_SERVER_KEY=\n" ); return true; } return false; }, async dryUpdateNextLayoutFile({ appPath, defaultExtension }) { const layoutPathWithoutExtension = path.join(appPath, "layout"); const layoutFileExtension = (await findJsExtension(layoutPathWithoutExtension)) ?? defaultExtension; const layoutPath = layoutPathWithoutExtension + "." + layoutFileExtension; const layoutContent = (await readFile(layoutPath)) ?? throwErr( `The layout file at ${layoutPath} does not exist. Stack requires a layout file to be present in the /app folder.` ); const updatedLayoutResult = (await getUpdatedLayout(layoutContent)) ?? throwErr( "Unable to parse root layout file. Make sure it contains a
tag. If it still doesn't work, you may need to manually install Stack. See: https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required" ); const updatedLayoutContent = updatedLayoutResult.content; return { path: layoutPath, updatedContent: updatedLayoutContent, fileExtension: layoutFileExtension, indentation: updatedLayoutResult.indentation }; }, async updateNextLayoutFile(projectInfo) { const res = await Steps.dryUpdateNextLayoutFile(projectInfo); laterWriteFile(res.path, res.updatedContent); return res; }, async writeStackAppFile({ type, srcPath, defaultExtension, indentation }, clientOrServer) { 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"); const stackAppPathWithoutExtension = path.join(srcPath, relativeStackAppPath); const stackAppFileExtension = (await findJsExtension(stackAppPathWithoutExtension)) ?? defaultExtension; const stackAppPath = stackAppPathWithoutExtension + "." + stackAppFileExtension; const stackAppContent = await readFile(stackAppPath); 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.` ); } throw new UserError( `It seems that you already installed Stack in this project.` ); } laterWriteFileIfNotExists( stackAppPath, ` ${type === "next" ? `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" && clientOrServer === "server" ? `\n${indentation}secretServerKey: process.env.STACK_SECRET_SERVER_KEY,` : ""} }); `.trim() + "\n" ); return { fileName: stackAppPath }; }, async writeNextHandlerFile(projectInfo) { const handlerPathWithoutExtension = path.join( projectInfo.appPath, "handler/[...stack]/page" ); const handlerFileExtension = (await findJsExtension(handlerPathWithoutExtension)) ?? projectInfo.defaultExtension; const handlerPath = handlerPathWithoutExtension + "." + handlerFileExtension; const handlerContent = await readFile(handlerPath); if (handlerContent && !handlerContent.includes("@stackframe/")) { throw new UserError( `A file at the path ${handlerPath} already exists. Stack uses the /handler path to handle incoming requests. Please remove the existing file and try again.` ); } laterWriteFileIfNotExists( handlerPath, `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${ handlerFileExtension.includes("ts") ? ": unknown" : "" }) {\n${projectInfo.indentation}return