@stackframe/init-stack
Advanced tools
+14
-0
| # @stackframe/init-stack | ||
| ## 2.8.43 | ||
| ### Patch Changes | ||
| - Various changes | ||
| - @stackframe/stack-shared@2.8.43 | ||
| ## 2.8.42 | ||
| ### Patch Changes | ||
| - Updated dependencies | ||
| - @stackframe/stack-shared@2.8.42 | ||
| ## 2.8.41 | ||
@@ -4,0 +18,0 @@ |
+385
-8
@@ -17,3 +17,3 @@ #!/usr/bin/env node | ||
| name: "@stackframe/init-stack", | ||
| version: "2.8.41", | ||
| version: "2.8.43", | ||
| description: "The setup wizard for Stack. https://stack-auth.com", | ||
@@ -223,2 +223,6 @@ main: "dist/index.js", | ||
| await Steps.writeEnvVars(type); | ||
| const convexIntegration = await Steps.maybeInstallConvexIntegration({ packageJson: projectPackageJson, type }); | ||
| if (convexIntegration) { | ||
| nextSteps.push(...convexIntegration.instructions); | ||
| } | ||
| if (type === "next") { | ||
@@ -453,2 +457,8 @@ const projectInfo = await Steps.getNextProjectInfo({ packageJson: projectPackageJson }); | ||
| } | ||
| 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 \u2014 are you maybe on the Pages router instead?` }; | ||
| } | ||
| const nextConfigPathWithoutExtension = path.join(projectPath, "next.config"); | ||
@@ -460,10 +470,4 @@ const nextConfigFileExtension = await findJsExtension( | ||
| if (!fs.existsSync(nextConfigPath)) { | ||
| return { error: `Expected file at ${nextConfigPath}. Only Next.js projects are currently supported.` }; | ||
| return { error: `Expected file at ${nextConfigPath} for Next.js projects.` }; | ||
| } | ||
| 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" }); | ||
@@ -510,2 +514,50 @@ return { | ||
| }, | ||
| async maybeInstallConvexIntegration({ packageJson, type }) { | ||
| const hasConvexDependency = Boolean(packageJson.dependencies?.["convex"] || packageJson.devDependencies?.["convex"]); | ||
| if (!hasConvexDependency) { | ||
| return null; | ||
| } | ||
| const projectPath = await getProjectPath(); | ||
| const convexDir = path.join(projectPath, "convex"); | ||
| if (!fs.existsSync(convexDir)) { | ||
| return null; | ||
| } | ||
| const stackPackageName = await Steps.getStackPackageName(type); | ||
| const instructions = []; | ||
| const authConfigPath = path.join(convexDir, "auth.config.ts"); | ||
| const desiredAuthConfig = createConvexAuthConfigContent({ stackPackageName, type }); | ||
| const existingAuthConfig = await readFile(authConfigPath); | ||
| if (!existingAuthConfig || !existingAuthConfig.includes("getConvexProvidersConfig") && !existingAuthConfig.includes("@stackframe/")) { | ||
| laterWriteFile(authConfigPath, desiredAuthConfig); | ||
| } | ||
| const convexConfigPath = path.join(convexDir, "convex.config.ts"); | ||
| const existingConvexConfig = await readFile(convexConfigPath); | ||
| const desiredConvexConfig = createConvexIntegrationConvexConfigContent(stackPackageName); | ||
| let needsManualConvexConfig = false; | ||
| if (!existingConvexConfig) { | ||
| laterWriteFile(convexConfigPath, desiredConvexConfig); | ||
| } else if (existingConvexConfig.includes("app.use(stackAuthComponent") && existingConvexConfig.includes("/convex.config") && existingConvexConfig.includes("stackframe")) { | ||
| } else { | ||
| const integratedContent = integrateConvexConfig(existingConvexConfig, stackPackageName); | ||
| if (integratedContent) { | ||
| laterWriteFile(convexConfigPath, integratedContent); | ||
| } else if (isSimpleConvexConfig(existingConvexConfig)) { | ||
| laterWriteFile(convexConfigPath, desiredConvexConfig); | ||
| } else { | ||
| needsManualConvexConfig = true; | ||
| } | ||
| } | ||
| if (needsManualConvexConfig) { | ||
| instructions.push(`Update convex/convex.config.ts to import ${stackPackageName}/convex.config and call app.use(stackAuthComponent).`); | ||
| } | ||
| const convexClientUpdateResult = await updateConvexClients({ projectPath, type }); | ||
| if (convexClientUpdateResult.skippedFiles.length > 0) { | ||
| instructions.push("Review your Convex client setup and call stackClientApp.getConvexClientAuth({}) or stackServerApp.getConvexClientAuth({}) manually where needed."); | ||
| } | ||
| instructions.push( | ||
| "Set the Stack Auth environment variables in Convex (Deployment \u2192 Settings \u2192 Environment Variables).", | ||
| "Verify your Convex clients call stackClientApp.getConvexClientAuth({}) or stackServerApp.getConvexClientAuth({}) so they share authentication with Stack Auth." | ||
| ); | ||
| return { instructions }; | ||
| }, | ||
| async dryUpdateNextLayoutFile({ appPath, defaultExtension }) { | ||
@@ -964,2 +1016,327 @@ const layoutPathWithoutExtension = path.join(appPath, "layout"); | ||
| } | ||
| function createConvexAuthConfigContent(options2) { | ||
| const envVarName = getPublicProjectEnvVarName(options2.type); | ||
| return `import { getConvexProvidersConfig } from ${JSON.stringify(options2.stackPackageName)}; | ||
| export default { | ||
| providers: getConvexProvidersConfig({ | ||
| projectId: process.env.${envVarName}, | ||
| }), | ||
| }; | ||
| `; | ||
| } | ||
| function createConvexIntegrationConvexConfigContent(stackPackageName) { | ||
| const importPath = `${stackPackageName}/convex.config`; | ||
| return `import stackAuthComponent from ${JSON.stringify(importPath)}; | ||
| import { defineApp } from "convex/server"; | ||
| const app = defineApp(); | ||
| app.use(stackAuthComponent); | ||
| export default app; | ||
| `; | ||
| } | ||
| function integrateConvexConfig(existingContent, stackPackageName) { | ||
| if (!existingContent.includes("defineApp")) { | ||
| return null; | ||
| } | ||
| const newline = existingContent.includes("\r\n") ? "\r\n" : "\n"; | ||
| const normalizedLines = existingContent.replace(/\r\n/g, "\n").split("\n"); | ||
| const importPath = `${stackPackageName}/convex.config`; | ||
| const hasImport = normalizedLines.some((line) => line.includes(importPath)); | ||
| if (!hasImport) { | ||
| let insertIndex = 0; | ||
| while (insertIndex < normalizedLines.length && normalizedLines[insertIndex].trim() === "") { | ||
| insertIndex++; | ||
| } | ||
| while (insertIndex < normalizedLines.length && normalizedLines[insertIndex].trim().startsWith("import")) { | ||
| insertIndex++; | ||
| } | ||
| normalizedLines.splice(insertIndex, 0, `import stackAuthComponent from "${importPath}";`); | ||
| } | ||
| let lastImportIndex = -1; | ||
| for (let i = 0; i < normalizedLines.length; i++) { | ||
| if (normalizedLines[i].trim().startsWith("import")) { | ||
| lastImportIndex = i; | ||
| continue; | ||
| } | ||
| if (normalizedLines[i].trim() === "") { | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
| if (lastImportIndex >= 0) { | ||
| const nextIndex = lastImportIndex + 1; | ||
| if (!normalizedLines[nextIndex] || normalizedLines[nextIndex].trim() !== "") { | ||
| normalizedLines.splice(nextIndex, 0, ""); | ||
| } | ||
| } | ||
| const hasStackUse = normalizedLines.some((line) => line.includes("app.use(stackAuthComponent")); | ||
| if (!hasStackUse) { | ||
| const appLineIndex = normalizedLines.findIndex((line) => /const\s+app\s*=\s*defineApp/.test(line)); | ||
| if (appLineIndex === -1) { | ||
| return null; | ||
| } | ||
| const indent = normalizedLines[appLineIndex].match(/^\s*/)?.[0] ?? ""; | ||
| const insertIndexForUse = appLineIndex + 1; | ||
| normalizedLines.splice(insertIndexForUse, 0, `${indent}app.use(stackAuthComponent);`); | ||
| const nextLineIndex = insertIndexForUse + 1; | ||
| if (!normalizedLines[nextLineIndex] || normalizedLines[nextLineIndex].trim() !== "") { | ||
| normalizedLines.splice(nextLineIndex, 0, ""); | ||
| } | ||
| } | ||
| let updated = normalizedLines.join(newline); | ||
| if (!updated.endsWith(newline)) { | ||
| updated += newline; | ||
| } | ||
| return updated; | ||
| } | ||
| function isSimpleConvexConfig(content) { | ||
| const normalized = content.replace(/\r\n/g, "\n").split("\n").map((line) => line.trim()).filter((line) => line.length > 0); | ||
| if (normalized.length !== 3) { | ||
| return false; | ||
| } | ||
| const [line1, line2, line3] = normalized; | ||
| const importRegex = /^import\s+\{\s*defineApp\s*\}\s+from\s+['"]convex\/server['"];?$/; | ||
| const appRegex = /^const\s+app\s*=\s*defineApp\(\s*\);?$/; | ||
| const exportRegex = /^export\s+default\s+app;?$/; | ||
| return importRegex.test(line1) && appRegex.test(line2) && exportRegex.test(line3); | ||
| } | ||
| function getPublicProjectEnvVarName(type) { | ||
| if (type === "react") { | ||
| return "VITE_PUBLIC_STACK_PROJECT_ID"; | ||
| } | ||
| if (type === "next") { | ||
| return "NEXT_PUBLIC_STACK_PROJECT_ID"; | ||
| } | ||
| return "STACK_PROJECT_ID"; | ||
| } | ||
| async function updateConvexClients({ projectPath, type }) { | ||
| const files = collectConvexClientCandidateFiles(projectPath); | ||
| const updatedFiles = []; | ||
| const skippedFiles = []; | ||
| for (const filePath of files) { | ||
| const fileContent = await readFile(filePath); | ||
| if (!fileContent) continue; | ||
| if (!/new\s+Convex(?:React|Http)?Client\b/.test(fileContent)) continue; | ||
| const addResult = addSetAuthToConvexClients(fileContent, type); | ||
| if (!addResult.changed) { | ||
| if (addResult.instantiationCount > 0 && addResult.skippedHttpCount > 0) { | ||
| skippedFiles.push(filePath); | ||
| } | ||
| continue; | ||
| } | ||
| let finalContent = addResult.updatedContent; | ||
| if (addResult.usedClientApp) { | ||
| finalContent = await ensureStackAppImport(finalContent, filePath, "client"); | ||
| } | ||
| if (addResult.usedServerApp) { | ||
| finalContent = await ensureStackAppImport(finalContent, filePath, "server"); | ||
| } | ||
| if (finalContent !== fileContent) { | ||
| laterWriteFile(filePath, finalContent); | ||
| updatedFiles.push(filePath); | ||
| } | ||
| } | ||
| return { | ||
| updatedFiles, | ||
| skippedFiles | ||
| }; | ||
| } | ||
| async function ensureStackAppImport(content, filePath, kind) { | ||
| const identifier = kind === "client" ? "stackClientApp" : "stackServerApp"; | ||
| if (new RegExp(`import\\s+[^;]*\\b${identifier}\\b`).test(content)) { | ||
| return content; | ||
| } | ||
| const stackBasePath = await getStackAppBasePath(kind); | ||
| const relativeImportPath = convertToModuleSpecifier(path.relative(path.dirname(filePath), stackBasePath)); | ||
| const newline = content.includes("\r\n") ? "\r\n" : "\n"; | ||
| const lines = content.split(/\r?\n/); | ||
| const importLine = `import { ${identifier} } from "${relativeImportPath}";`; | ||
| let insertIndex = 0; | ||
| while (insertIndex < lines.length) { | ||
| const line = lines[insertIndex]; | ||
| if (/^\s*['"]use (client|server)['"];?\s*$/.test(line)) { | ||
| insertIndex += 1; | ||
| continue; | ||
| } | ||
| if (/^\s*import\b/.test(line)) { | ||
| insertIndex += 1; | ||
| continue; | ||
| } | ||
| if (line.trim() === "") { | ||
| insertIndex += 1; | ||
| continue; | ||
| } | ||
| break; | ||
| } | ||
| lines.splice(insertIndex, 0, importLine); | ||
| const nextLine = lines[insertIndex + 1]; | ||
| if (nextLine && nextLine.trim() !== "" && !/^\s*import\b/.test(nextLine)) { | ||
| lines.splice(insertIndex + 1, 0, ""); | ||
| } | ||
| return lines.join(newline); | ||
| } | ||
| function convertToModuleSpecifier(relativePath) { | ||
| let specifier = relativePath.replace(/\\/g, "/"); | ||
| if (!specifier.startsWith(".")) { | ||
| specifier = "./" + specifier; | ||
| } | ||
| return specifier; | ||
| } | ||
| async function getStackAppBasePath(kind) { | ||
| const srcPath = await Steps.guessSrcPath(); | ||
| return path.join(srcPath, "stack", kind); | ||
| } | ||
| function addSetAuthToConvexClients(content, type) { | ||
| const newline = content.includes("\r\n") ? "\r\n" : "\n"; | ||
| const instantiationRegex = /^[ \t]*(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*new\s+(Convex(?:React|Http)?Client)\b([\s\S]*?);/gm; | ||
| const replacements = []; | ||
| let instantiationCount = 0; | ||
| let skippedHttpCount = 0; | ||
| let usedClientApp = false; | ||
| let usedServerApp = false; | ||
| let match; | ||
| while ((match = instantiationRegex.exec(content)) !== null) { | ||
| instantiationCount += 1; | ||
| const fullMatch = match[0]; | ||
| const variableName = match[1]; | ||
| const className = match[2]; | ||
| if (className === "ConvexHttpClient") { | ||
| skippedHttpCount += 1; | ||
| continue; | ||
| } | ||
| const remainder = content.slice(match.index + fullMatch.length); | ||
| const setAuthRegex = new RegExp(`\\b${escapeRegExp(variableName)}\\s*\\.setAuth\\s*\\(`); | ||
| if (setAuthRegex.test(remainder)) { | ||
| continue; | ||
| } | ||
| const indentation = fullMatch.match(/^[\t ]*/)?.[0] ?? ""; | ||
| const authCall = determineAuthCallExpression({ type, className, content }); | ||
| if (authCall.identifier === "stackClientApp") { | ||
| usedClientApp = true; | ||
| } else { | ||
| usedServerApp = true; | ||
| } | ||
| const replacementText = `${fullMatch}${newline}${indentation}${variableName}.setAuth(${authCall.expression});`; | ||
| replacements.push({ | ||
| start: match.index, | ||
| end: match.index + fullMatch.length, | ||
| text: replacementText | ||
| }); | ||
| } | ||
| if (replacements.length === 0) { | ||
| return { | ||
| updatedContent: content, | ||
| changed: false, | ||
| usedClientApp, | ||
| usedServerApp, | ||
| instantiationCount, | ||
| skippedHttpCount | ||
| }; | ||
| } | ||
| let updatedContent = content; | ||
| for (let i = replacements.length - 1; i >= 0; i--) { | ||
| const replacement = replacements[i]; | ||
| updatedContent = `${updatedContent.slice(0, replacement.start)}${replacement.text}${updatedContent.slice(replacement.end)}`; | ||
| } | ||
| return { | ||
| updatedContent, | ||
| changed: true, | ||
| usedClientApp, | ||
| usedServerApp, | ||
| instantiationCount, | ||
| skippedHttpCount | ||
| }; | ||
| } | ||
| function determineAuthCallExpression({ type, className, content }) { | ||
| const hasClientAppReference = /\bstackClientApp\b/.test(content); | ||
| const hasServerAppReference = /\bstackServerApp\b/.test(content); | ||
| if (type === "js") { | ||
| return { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" }; | ||
| } | ||
| if (hasClientAppReference) { | ||
| return { expression: getClientAuthCall(type), identifier: "stackClientApp" }; | ||
| } | ||
| if (hasServerAppReference && className !== "ConvexReactClient") { | ||
| return { expression: "stackServerApp.getConvexClientAuth({})", identifier: "stackServerApp" }; | ||
| } | ||
| return { expression: getClientAuthCall(type), identifier: "stackClientApp" }; | ||
| } | ||
| function getClientAuthCall(type) { | ||
| return "stackClientApp.getConvexClientAuth({})"; | ||
| } | ||
| function collectConvexClientCandidateFiles(projectPath) { | ||
| const roots = getConvexSearchRoots(projectPath); | ||
| const files = /* @__PURE__ */ new Set(); | ||
| const visited = /* @__PURE__ */ new Set(); | ||
| for (const root of roots) { | ||
| walkDirectory(root, files, visited); | ||
| } | ||
| return Array.from(files); | ||
| } | ||
| function getConvexSearchRoots(projectPath) { | ||
| const candidateDirs = ["convex", "src", "app", "components"]; | ||
| const existing = candidateDirs.map((dir) => path.join(projectPath, dir)).filter((dirPath) => { | ||
| try { | ||
| return fs.existsSync(dirPath) && fs.statSync(dirPath).isDirectory(); | ||
| } catch { | ||
| return false; | ||
| } | ||
| }); | ||
| if (existing.length > 0) { | ||
| return existing; | ||
| } | ||
| return [projectPath]; | ||
| } | ||
| var directorySkipList = /* @__PURE__ */ new Set([ | ||
| "node_modules", | ||
| ".git", | ||
| ".next", | ||
| ".turbo", | ||
| ".output", | ||
| ".vercel", | ||
| "dist", | ||
| "build", | ||
| "coverage", | ||
| ".cache", | ||
| ".storybook", | ||
| "storybook-static" | ||
| ]); | ||
| function walkDirectory(currentDir, files, visited) { | ||
| const realPath = (() => { | ||
| try { | ||
| return fs.realpathSync(currentDir); | ||
| } catch { | ||
| return currentDir; | ||
| } | ||
| })(); | ||
| if (visited.has(realPath)) return; | ||
| visited.add(realPath); | ||
| let dirEntries; | ||
| try { | ||
| dirEntries = fs.readdirSync(realPath, { withFileTypes: true }); | ||
| } catch { | ||
| return; | ||
| } | ||
| for (const entry of dirEntries) { | ||
| const entryName = entry.name; | ||
| if (entry.isDirectory()) { | ||
| if (directorySkipList.has(entryName)) continue; | ||
| if (entryName.startsWith(".") || entryName.startsWith("_")) continue; | ||
| walkDirectory(path.join(realPath, entryName), files, visited); | ||
| continue; | ||
| } | ||
| if (!entry.isFile()) continue; | ||
| if (entryName.endsWith(".d.ts")) continue; | ||
| if (!hasJsLikeExtension(entryName)) continue; | ||
| files.add(path.join(realPath, entryName)); | ||
| } | ||
| } | ||
| function hasJsLikeExtension(fileName) { | ||
| return jsLikeFileExtensions.some((ext) => fileName.endsWith(`.${ext}`)); | ||
| } | ||
| function escapeRegExp(str) { | ||
| return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); | ||
| } | ||
| function throwErr(message) { | ||
@@ -966,0 +1343,0 @@ throw new Error(message); |
+2
-2
| { | ||
| "name": "@stackframe/init-stack", | ||
| "version": "2.8.41", | ||
| "version": "2.8.43", | ||
| "description": "The setup wizard for Stack. https://stack-auth.com", | ||
@@ -23,3 +23,3 @@ "main": "dist/index.js", | ||
| "posthog-node": "^4.1.0", | ||
| "@stackframe/stack-shared": "2.8.41" | ||
| "@stackframe/stack-shared": "2.8.43" | ||
| }, | ||
@@ -26,0 +26,0 @@ "devDependencies": { |
Sorry, the diff of this file is too big to display
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 8 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 8 instances in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
166102
30.5%1356
38.09%22
10%+ Added
- Removed