@stackframe/init-stack 2.7.14 → 2.7.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/index.mjs +470 -187
- package/package.json +15 -6
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# @stackframe/init-stack
|
|
2
2
|
|
|
3
|
+
## 2.7.17
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Various changes
|
|
8
|
+
- Updated dependencies
|
|
9
|
+
- @stackframe/stack-shared@2.7.17
|
|
10
|
+
|
|
11
|
+
## 2.7.16
|
|
12
|
+
|
|
13
|
+
### Patch Changes
|
|
14
|
+
|
|
15
|
+
- Updated dependencies
|
|
16
|
+
- @stackframe/stack-shared@2.7.16
|
|
17
|
+
|
|
18
|
+
## 2.7.15
|
|
19
|
+
|
|
20
|
+
### Patch Changes
|
|
21
|
+
|
|
22
|
+
- Updated dependencies
|
|
23
|
+
- @stackframe/stack-shared@2.7.15
|
|
24
|
+
|
|
3
25
|
## 2.7.14
|
|
4
26
|
|
|
5
27
|
### Patch Changes
|
package/index.mjs
CHANGED
|
@@ -32,6 +32,10 @@ let savedProjectPath = process.argv[2] || undefined;
|
|
|
32
32
|
|
|
33
33
|
const isDryRun = process.argv.includes("--dry-run");
|
|
34
34
|
const isNeon = process.argv.includes("--neon");
|
|
35
|
+
const typeFromArgs = ["js", "next"].find(s => process.argv.includes(`--${s}`));
|
|
36
|
+
const packageManagerFromArgs = ["npm", "yarn", "pnpm", "bun"].find(s => process.argv.includes(`--${s}`));
|
|
37
|
+
const isClient = process.argv.includes("--client");
|
|
38
|
+
const isServer = process.argv.includes("--server");
|
|
35
39
|
|
|
36
40
|
const ansis = {
|
|
37
41
|
red: "\x1b[31m",
|
|
@@ -55,7 +59,14 @@ const filesCreated = [];
|
|
|
55
59
|
const filesModified = [];
|
|
56
60
|
const commandsExecuted = [];
|
|
57
61
|
|
|
62
|
+
const packagesToInstall = [];
|
|
63
|
+
const writeFileHandlers = [];
|
|
64
|
+
const nextSteps = [
|
|
65
|
+
`Create an account and Stack Auth API key for your project on https://app.stack-auth.com`,
|
|
66
|
+
];
|
|
67
|
+
|
|
58
68
|
async function main() {
|
|
69
|
+
// Welcome message
|
|
59
70
|
console.log();
|
|
60
71
|
console.log(`
|
|
61
72
|
██████
|
|
@@ -73,182 +84,75 @@ async function main() {
|
|
|
73
84
|
`);
|
|
74
85
|
console.log();
|
|
75
86
|
|
|
76
|
-
let projectPath = await getProjectPath();
|
|
77
|
-
if (!fs.existsSync(projectPath)) {
|
|
78
|
-
throw new UserError(`The project path ${projectPath} does not exist`);
|
|
79
|
-
}
|
|
80
87
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
throw new UserError(
|
|
84
|
-
`The package.json file does not exist in the project path ${projectPath}. You must initialize a new project first before installing Stack.`
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"));
|
|
88
|
-
const nextVersionInPackageJson = packageJson?.dependencies?.["next"] ?? packageJson?.devDependencies?.["next"];
|
|
89
|
-
if (!nextVersionInPackageJson) {
|
|
90
|
-
throw new UserError(
|
|
91
|
-
`The project at ${projectPath} does not appear to be a Next.js project, or does not have 'next' installed as a dependency. Only Next.js projects are currently supported.`
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
if (
|
|
95
|
-
!nextVersionInPackageJson.includes("14") &&
|
|
96
|
-
!nextVersionInPackageJson.includes("15") &&
|
|
97
|
-
nextVersionInPackageJson !== "latest"
|
|
98
|
-
) {
|
|
99
|
-
throw new UserError(
|
|
100
|
-
`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`
|
|
101
|
-
);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const nextConfigPathWithoutExtension = path.join(projectPath, "next.config");
|
|
105
|
-
const nextConfigFileExtension = await findJsExtension(
|
|
106
|
-
nextConfigPathWithoutExtension
|
|
107
|
-
);
|
|
108
|
-
const nextConfigPath =
|
|
109
|
-
nextConfigPathWithoutExtension + "." + (nextConfigFileExtension ?? "js");
|
|
110
|
-
if (!fs.existsSync(nextConfigPath)) {
|
|
111
|
-
throw new UserError(
|
|
112
|
-
`Expected file at ${nextConfigPath}. Only Next.js projects are currently supported.`
|
|
113
|
-
);
|
|
114
|
-
}
|
|
88
|
+
// Wait just briefly so we can use `Steps` in here (it's defined only after the call to `main()`)
|
|
89
|
+
await new Promise((resolve) => resolve());
|
|
115
90
|
|
|
116
91
|
|
|
117
|
-
|
|
92
|
+
// Prepare some stuff
|
|
93
|
+
await clearStdin();
|
|
94
|
+
const projectPath = await getProjectPath();
|
|
118
95
|
|
|
119
|
-
const potentialEnvLocations = [
|
|
120
|
-
path.join(projectPath, ".env"),
|
|
121
|
-
path.join(projectPath, ".env.development"),
|
|
122
|
-
path.join(projectPath, ".env.default"),
|
|
123
|
-
path.join(projectPath, ".env.defaults"),
|
|
124
|
-
path.join(projectPath, ".env.example"),
|
|
125
|
-
envLocalPath,
|
|
126
|
-
];
|
|
127
96
|
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
const stackAppFileExtension =
|
|
158
|
-
(await findJsExtension(stackAppPathWithoutExtension)) ?? defaultExtension;
|
|
159
|
-
const stackAppPath =
|
|
160
|
-
stackAppPathWithoutExtension + "." + stackAppFileExtension;
|
|
161
|
-
const stackAppContent = await readFile(stackAppPath);
|
|
162
|
-
if (stackAppContent) {
|
|
163
|
-
if (!stackAppContent.includes("@stackframe/stack")) {
|
|
164
|
-
throw new UserError(
|
|
165
|
-
`A file at the path ${stackAppPath} already exists. Stack uses the /src/stack.ts file to initialize the Stack SDK. Please remove the existing file and try again.`
|
|
166
|
-
);
|
|
97
|
+
// Steps
|
|
98
|
+
const { packageJson } = await Steps.getProject();
|
|
99
|
+
const type = await Steps.getProjectType({ packageJson });
|
|
100
|
+
|
|
101
|
+
await Steps.addStackPackage(type);
|
|
102
|
+
if (isNeon) packagesToInstall.push('@neondatabase/serverless');
|
|
103
|
+
|
|
104
|
+
await Steps.writeEnvVars(type);
|
|
105
|
+
|
|
106
|
+
if (type === "next") {
|
|
107
|
+
const projectInfo = await Steps.getNextProjectInfo({ packageJson });
|
|
108
|
+
await Steps.updateNextLayoutFile(projectInfo);
|
|
109
|
+
await Steps.writeStackAppFile(projectInfo, "server");
|
|
110
|
+
await Steps.writeNextHandlerFile(projectInfo);
|
|
111
|
+
await Steps.writeNextLoadingFile(projectInfo);
|
|
112
|
+
nextSteps.push(`Copy the environment variables from the new API key into your .env.local file`)
|
|
113
|
+
} else if (type === "js") {
|
|
114
|
+
const defaultExtension = await Steps.guessDefaultFileExtension();
|
|
115
|
+
const where = await Steps.getServerOrClientOrBoth();
|
|
116
|
+
const srcPath = await Steps.guessSrcPath();
|
|
117
|
+
const appFiles = [];
|
|
118
|
+
for (const w of where) {
|
|
119
|
+
const { fileName } = await Steps.writeStackAppFile({
|
|
120
|
+
type,
|
|
121
|
+
defaultExtension,
|
|
122
|
+
indentation: " ",
|
|
123
|
+
srcPath,
|
|
124
|
+
}, w);
|
|
125
|
+
appFiles.push(fileName);
|
|
167
126
|
}
|
|
168
|
-
|
|
169
|
-
`
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const handlerPathWithoutExtension = path.join(
|
|
174
|
-
appPath,
|
|
175
|
-
"handler/[...stack]/page"
|
|
176
|
-
);
|
|
177
|
-
const handlerFileExtension =
|
|
178
|
-
(await findJsExtension(handlerPathWithoutExtension)) ?? defaultExtension;
|
|
179
|
-
const handlerPath = handlerPathWithoutExtension + "." + handlerFileExtension;
|
|
180
|
-
const handlerContent = await readFile(handlerPath);
|
|
181
|
-
if (handlerContent && !handlerContent.includes("@stackframe/stack")) {
|
|
182
|
-
throw new UserError(
|
|
183
|
-
`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.`
|
|
127
|
+
nextSteps.push(
|
|
128
|
+
`Copy the environment variables from the new API key into your own environment and reference them in ${appFiles.join(" and ")}`,
|
|
129
|
+
`Follow the instructions on how to use Stack Auth's vanilla SDK at http://docs.stack-auth.com/others/js-client`,
|
|
184
130
|
);
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error("Unknown type: " + type);
|
|
185
133
|
}
|
|
186
134
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
(await findJsExtension(loadingPathWithoutExtension)) ?? defaultExtension;
|
|
190
|
-
const loadingPath = loadingPathWithoutExtension + "." + loadingFileExtension;
|
|
191
|
-
|
|
192
|
-
const packageManager = await getPackageManager();
|
|
193
|
-
const versionCommand = `${packageManager} --version`;
|
|
194
|
-
|
|
195
|
-
try {
|
|
196
|
-
await shellNicelyFormatted(versionCommand, { shell: true, quiet: true });
|
|
197
|
-
} catch (err) {
|
|
198
|
-
throw new UserError(
|
|
199
|
-
`Could not run the package manager command '${versionCommand}'. Please make sure ${packageManager} is installed on your system.`
|
|
200
|
-
);
|
|
201
|
-
}
|
|
135
|
+
const { packageManager } = await Steps.getPackageManager();
|
|
136
|
+
await Steps.ensureReady(type);
|
|
202
137
|
|
|
203
|
-
const isReady = await inquirer.prompt([
|
|
204
|
-
{
|
|
205
|
-
type: "confirm",
|
|
206
|
-
name: "ready",
|
|
207
|
-
message: `Found a Next.js project at ${projectPath} — ready to install Stack?`,
|
|
208
|
-
default: true,
|
|
209
|
-
},
|
|
210
|
-
]);
|
|
211
|
-
if (!isReady.ready) {
|
|
212
|
-
throw new UserError("Installation aborted.");
|
|
213
|
-
}
|
|
214
138
|
|
|
139
|
+
// Install dependencies
|
|
215
140
|
console.log();
|
|
216
141
|
console.log(colorize.bold`Installing dependencies...`);
|
|
217
|
-
const packagesToInstall = [process.env.STACK_PACKAGE_NAME_OVERRIDE || "@stackframe/stack"];
|
|
218
|
-
if (isNeon) {
|
|
219
|
-
packagesToInstall.push('@neondatabase/serverless');
|
|
220
|
-
}
|
|
221
|
-
|
|
222
142
|
const installCommand = packageManager === "yarn" ? "yarn add" : `${packageManager} install`;
|
|
223
143
|
await shellNicelyFormatted(`${installCommand} ${packagesToInstall.join(' ')}`, {
|
|
224
144
|
shell: true,
|
|
225
145
|
cwd: projectPath,
|
|
226
146
|
});
|
|
227
147
|
|
|
148
|
+
|
|
149
|
+
// Write files
|
|
228
150
|
console.log();
|
|
229
151
|
console.log(colorize.bold`Writing files...`);
|
|
230
152
|
console.log();
|
|
231
|
-
|
|
232
|
-
await
|
|
233
|
-
envLocalPath,
|
|
234
|
-
"# 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"
|
|
235
|
-
);
|
|
153
|
+
for (const writeFileHandler of writeFileHandlers) {
|
|
154
|
+
await writeFileHandler();
|
|
236
155
|
}
|
|
237
|
-
await writeFileIfNotExists(
|
|
238
|
-
loadingPath,
|
|
239
|
-
`export default function Loading() {\n${ind}// Stack uses React Suspense, which will render this page while user data is being fetched.\n${ind}// See: https://nextjs.org/docs/app/api-reference/file-conventions/loading\n${ind}return <></>;\n}\n`
|
|
240
|
-
);
|
|
241
|
-
await writeFileIfNotExists(
|
|
242
|
-
handlerPath,
|
|
243
|
-
`import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${
|
|
244
|
-
handlerFileExtension.includes("ts") ? ": unknown" : ""
|
|
245
|
-
}) {\n${ind}return <StackHandler fullPage app={stackServerApp} routeProps={props} />;\n}\n`
|
|
246
|
-
);
|
|
247
|
-
await writeFileIfNotExists(
|
|
248
|
-
stackAppPath,
|
|
249
|
-
`import "server-only";\n\nimport { StackServerApp } from "@stackframe/stack";\n\nexport const stackServerApp = new StackServerApp({\n${ind}tokenStore: "nextjs-cookie",\n});\n`
|
|
250
|
-
);
|
|
251
|
-
await writeFile(layoutPath, updatedLayoutContent);
|
|
252
156
|
console.log(`${colorize.green`√`} Done writing files`);
|
|
253
157
|
|
|
254
158
|
console.log('\n\n\n');
|
|
@@ -266,29 +170,32 @@ async function main() {
|
|
|
266
170
|
for (const file of filesCreated) {
|
|
267
171
|
console.log(` ${colorize.green`${file}`}`);
|
|
268
172
|
}
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
173
|
+
console.log();
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
// Success!
|
|
177
|
+
console.log(`
|
|
273
178
|
${colorize.green`===============================================`}
|
|
274
179
|
|
|
275
180
|
${colorize.green`Successfully installed Stack! 🚀🚀🚀`}
|
|
276
181
|
|
|
277
182
|
Next steps:
|
|
278
183
|
|
|
279
|
-
|
|
280
|
-
2. Copy the environment variables from the new API key into your .env.local file
|
|
184
|
+
${[...nextSteps.entries()].map(([index, step]) => `${index + 1}. ${step}`).join("\n")}
|
|
281
185
|
|
|
282
|
-
Then, you will be able to access your sign-in page on http://your-website.example.com/handler/sign-in. That's it
|
|
186
|
+
${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!`
|
|
187
|
+
: "That's it!"}
|
|
283
188
|
|
|
284
189
|
${colorize.green`===============================================`}
|
|
285
190
|
|
|
286
191
|
For more information, please visit https://docs.stack-auth.com/getting-started/setup
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
192
|
+
`.trim());
|
|
193
|
+
if (!process.env.STACK_DISABLE_INTERACTIVE) {
|
|
194
|
+
await open("https://app.stack-auth.com/wizard-congrats");
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
main()
|
|
292
199
|
.catch((err) => {
|
|
293
200
|
if (!(err instanceof UserError)) {
|
|
294
201
|
console.error(err);
|
|
@@ -315,6 +222,331 @@ For more information, please visit https://docs.stack-auth.com/getting-started/s
|
|
|
315
222
|
process.exit(1);
|
|
316
223
|
});
|
|
317
224
|
|
|
225
|
+
|
|
226
|
+
const Steps = {
|
|
227
|
+
async getProject() {
|
|
228
|
+
let projectPath = await getProjectPath();
|
|
229
|
+
if (!fs.existsSync(projectPath)) {
|
|
230
|
+
throw new UserError(`The project path ${projectPath} does not exist`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const packageJsonPath = path.join(projectPath, "package.json");
|
|
234
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
235
|
+
throw new UserError(
|
|
236
|
+
`The package.json file does not exist in the project path ${projectPath}. You must initialize a new project first before installing Stack.`
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const packageJsonText = fs.readFileSync(packageJsonPath, "utf-8");
|
|
241
|
+
let packageJson;
|
|
242
|
+
try {
|
|
243
|
+
packageJson = JSON.parse(packageJsonText);
|
|
244
|
+
} catch (e) {
|
|
245
|
+
throw new UserError(`package.json file is not valid JSON: ${e}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return { packageJson };
|
|
249
|
+
},
|
|
250
|
+
|
|
251
|
+
async getProjectType({ packageJson }) {
|
|
252
|
+
if (typeFromArgs) return typeFromArgs;
|
|
253
|
+
|
|
254
|
+
const maybeNextProject = await Steps.maybeGetNextProjectInfo({ packageJson });
|
|
255
|
+
if (!("error" in maybeNextProject)) return "next";
|
|
256
|
+
|
|
257
|
+
const { type } = assertInteractive() && await inquirer.prompt([
|
|
258
|
+
{
|
|
259
|
+
type: "list",
|
|
260
|
+
name: "type",
|
|
261
|
+
message: "Which integration would you like to install?",
|
|
262
|
+
choices: [
|
|
263
|
+
{ name: "None (vanilla JS, Node.js, etc)", value: "js" },
|
|
264
|
+
{ name: "Next.js", value: "next" },
|
|
265
|
+
]
|
|
266
|
+
}
|
|
267
|
+
]);
|
|
268
|
+
|
|
269
|
+
return type;
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
async getStackPackageName(type, install = false) {
|
|
273
|
+
return {
|
|
274
|
+
"js": (install && process.env.STACK_JS_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/js",
|
|
275
|
+
"next": (install && process.env.STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE) || "@stackframe/stack",
|
|
276
|
+
}[type] ?? throwErr("Unknown type in addStackPackage: " + type);
|
|
277
|
+
},
|
|
278
|
+
|
|
279
|
+
async addStackPackage(type) {
|
|
280
|
+
packagesToInstall.push(await Steps.getStackPackageName(type, true));
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
async getNextProjectInfo({ packageJson }) {
|
|
284
|
+
const maybe = await Steps.maybeGetNextProjectInfo({ packageJson });
|
|
285
|
+
if ("error" in maybe) throw new UserError(maybe.error);
|
|
286
|
+
return maybe;
|
|
287
|
+
},
|
|
288
|
+
|
|
289
|
+
async maybeGetNextProjectInfo({ packageJson }) {
|
|
290
|
+
const projectPath = await getProjectPath();
|
|
291
|
+
|
|
292
|
+
const nextVersionInPackageJson = packageJson?.dependencies?.["next"] ?? packageJson?.devDependencies?.["next"];
|
|
293
|
+
if (!nextVersionInPackageJson) {
|
|
294
|
+
return { error: `The project at ${projectPath} does not appear to be a Next.js project, or does not have 'next' installed as a dependency.` };
|
|
295
|
+
}
|
|
296
|
+
if (
|
|
297
|
+
!nextVersionInPackageJson.includes("14") &&
|
|
298
|
+
!nextVersionInPackageJson.includes("15") &&
|
|
299
|
+
nextVersionInPackageJson !== "latest"
|
|
300
|
+
) {
|
|
301
|
+
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` };
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
const nextConfigPathWithoutExtension = path.join(projectPath, "next.config");
|
|
305
|
+
const nextConfigFileExtension = await findJsExtension(
|
|
306
|
+
nextConfigPathWithoutExtension
|
|
307
|
+
);
|
|
308
|
+
const nextConfigPath =
|
|
309
|
+
nextConfigPathWithoutExtension + "." + (nextConfigFileExtension ?? "js");
|
|
310
|
+
if (!fs.existsSync(nextConfigPath)) {
|
|
311
|
+
return { error: `Expected file at ${nextConfigPath}. Only Next.js projects are currently supported.` };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const hasSrcAppFolder = fs.existsSync(path.join(projectPath, "src/app"));
|
|
315
|
+
const srcPath = path.join(projectPath, hasSrcAppFolder ? "src" : "");
|
|
316
|
+
const appPath = path.join(srcPath, "app");
|
|
317
|
+
if (!fs.existsSync(appPath)) {
|
|
318
|
+
return { error: `The app path ${appPath} does not exist. Only the Next.js app router is supported.` };
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const dryUpdateNextLayoutFileResult = await Steps.dryUpdateNextLayoutFile({ appPath, defaultExtension: "jsx" });
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
type: "next",
|
|
325
|
+
srcPath,
|
|
326
|
+
appPath,
|
|
327
|
+
defaultExtension: dryUpdateNextLayoutFileResult.fileExtension,
|
|
328
|
+
indentation: dryUpdateNextLayoutFileResult.indentation,
|
|
329
|
+
};
|
|
330
|
+
},
|
|
331
|
+
|
|
332
|
+
async writeEnvVars(type) {
|
|
333
|
+
const projectPath = await getProjectPath();
|
|
334
|
+
|
|
335
|
+
// TODO: in non-Next environments, ask the user what method they prefer for envvars
|
|
336
|
+
if (type !== "next") return false;
|
|
337
|
+
|
|
338
|
+
const envLocalPath = path.join(projectPath, ".env.local");
|
|
339
|
+
|
|
340
|
+
const potentialEnvLocations = [
|
|
341
|
+
path.join(projectPath, ".env"),
|
|
342
|
+
path.join(projectPath, ".env.development"),
|
|
343
|
+
path.join(projectPath, ".env.default"),
|
|
344
|
+
path.join(projectPath, ".env.defaults"),
|
|
345
|
+
path.join(projectPath, ".env.example"),
|
|
346
|
+
envLocalPath,
|
|
347
|
+
];
|
|
348
|
+
if (potentialEnvLocations.every((p) => !fs.existsSync(p))) {
|
|
349
|
+
laterWriteFile(
|
|
350
|
+
envLocalPath,
|
|
351
|
+
"# 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"
|
|
352
|
+
);
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return false;
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async dryUpdateNextLayoutFile({ appPath, defaultExtension }) {
|
|
360
|
+
const layoutPathWithoutExtension = path.join(appPath, "layout");
|
|
361
|
+
const layoutFileExtension =
|
|
362
|
+
(await findJsExtension(layoutPathWithoutExtension)) ?? defaultExtension;
|
|
363
|
+
const layoutPath = layoutPathWithoutExtension + "." + layoutFileExtension;
|
|
364
|
+
const layoutContent =
|
|
365
|
+
(await readFile(layoutPath)) ??
|
|
366
|
+
throwErr(
|
|
367
|
+
`The layout file at ${layoutPath} does not exist. Stack requires a layout file to be present in the /app folder.`
|
|
368
|
+
);
|
|
369
|
+
const updatedLayoutResult =
|
|
370
|
+
(await getUpdatedLayout(layoutContent)) ??
|
|
371
|
+
throwErr(
|
|
372
|
+
"Unable to parse root layout file. Make sure it contains a <body> 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"
|
|
373
|
+
);
|
|
374
|
+
const updatedLayoutContent = updatedLayoutResult.content;
|
|
375
|
+
return {
|
|
376
|
+
path: layoutPath,
|
|
377
|
+
updatedContent: updatedLayoutContent,
|
|
378
|
+
fileExtension: layoutFileExtension,
|
|
379
|
+
indentation: updatedLayoutResult.indentation
|
|
380
|
+
};
|
|
381
|
+
},
|
|
382
|
+
|
|
383
|
+
async updateNextLayoutFile(projectInfo) {
|
|
384
|
+
const res = await Steps.dryUpdateNextLayoutFile(projectInfo);
|
|
385
|
+
laterWriteFile(res.path, res.updatedContent);
|
|
386
|
+
return res;
|
|
387
|
+
},
|
|
388
|
+
|
|
389
|
+
async writeStackAppFile({ type, srcPath, defaultExtension, indentation }, clientOrServer) {
|
|
390
|
+
const packageName = await Steps.getStackPackageName(type);
|
|
391
|
+
|
|
392
|
+
const clientOrServerCap = {
|
|
393
|
+
client: "Client",
|
|
394
|
+
server: "Server",
|
|
395
|
+
}[clientOrServer] ?? throwErr("unknown clientOrServer " + clientOrServer);
|
|
396
|
+
|
|
397
|
+
const relativeStackAppPath = {
|
|
398
|
+
js: `stack/${clientOrServer}`,
|
|
399
|
+
next: "stack",
|
|
400
|
+
}[type] ?? throwErr("unknown type");
|
|
401
|
+
|
|
402
|
+
const stackAppPathWithoutExtension = path.join(srcPath, relativeStackAppPath);
|
|
403
|
+
const stackAppFileExtension =
|
|
404
|
+
(await findJsExtension(stackAppPathWithoutExtension)) ?? defaultExtension;
|
|
405
|
+
const stackAppPath =
|
|
406
|
+
stackAppPathWithoutExtension + "." + stackAppFileExtension;
|
|
407
|
+
const stackAppContent = await readFile(stackAppPath);
|
|
408
|
+
if (stackAppContent) {
|
|
409
|
+
if (!stackAppContent.includes("@stackframe/")) {
|
|
410
|
+
throw new UserError(
|
|
411
|
+
`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.`
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
throw new UserError(
|
|
415
|
+
`It seems that you already installed Stack in this project.`
|
|
416
|
+
);
|
|
417
|
+
}
|
|
418
|
+
laterWriteFileIfNotExists(
|
|
419
|
+
stackAppPath,
|
|
420
|
+
`
|
|
421
|
+
${type === "next" ? `import "server-only";` : ""}
|
|
422
|
+
|
|
423
|
+
import { Stack${clientOrServerCap}App } from ${JSON.stringify(packageName)};
|
|
424
|
+
|
|
425
|
+
export const stack${clientOrServerCap}App = new Stack${clientOrServerCap}App({
|
|
426
|
+
${indentation}tokenStore: ${type === "next" ? '"nextjs-cookie"' : (clientOrServer === "client" ? '"cookie"' : '"memory"')},${
|
|
427
|
+
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)` : ""}` : ""}${
|
|
428
|
+
type === "js" ? `\n${indentation}publishableClientKey: ${clientOrServer === "server" ? 'process.env.STACK_PUBLISHABLE_CLIENT_KEY' : 'INSERT_YOUR_PUBLISHABLE_CLIENT_KEY_HERE'}` : ""},${
|
|
429
|
+
type === "js" && clientOrServer === "server" ? `\n${indentation}secretServerKey: process.env.STACK_SECRET_SERVER_KEY,` : ""}
|
|
430
|
+
});
|
|
431
|
+
`.trim() + "\n"
|
|
432
|
+
);
|
|
433
|
+
return { fileName: stackAppPath };
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
async writeNextHandlerFile(projectInfo) {
|
|
437
|
+
const handlerPathWithoutExtension = path.join(
|
|
438
|
+
projectInfo.appPath,
|
|
439
|
+
"handler/[...stack]/page"
|
|
440
|
+
);
|
|
441
|
+
const handlerFileExtension =
|
|
442
|
+
(await findJsExtension(handlerPathWithoutExtension)) ?? projectInfo.defaultExtension;
|
|
443
|
+
const handlerPath = handlerPathWithoutExtension + "." + handlerFileExtension;
|
|
444
|
+
const handlerContent = await readFile(handlerPath);
|
|
445
|
+
if (handlerContent && !handlerContent.includes("@stackframe/")) {
|
|
446
|
+
throw new UserError(
|
|
447
|
+
`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.`
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
laterWriteFileIfNotExists(
|
|
451
|
+
handlerPath,
|
|
452
|
+
`\nimport { stackServerApp } from "../../../stack";\n\nexport default function Handler(props${
|
|
453
|
+
handlerFileExtension.includes("ts") ? ": unknown" : ""
|
|
454
|
+
}) {\n${projectInfo.indentation}return <StackHandler fullPage app={stackServerApp} routeProps={props} />;\n}\n`
|
|
455
|
+
);
|
|
456
|
+
},
|
|
457
|
+
|
|
458
|
+
async writeNextLoadingFile(projectInfo) {
|
|
459
|
+
let loadingPathWithoutExtension = path.join(projectInfo.appPath, "loading");
|
|
460
|
+
const loadingFileExtension =
|
|
461
|
+
(await findJsExtension(loadingPathWithoutExtension)) ?? projectInfo.defaultExtension;
|
|
462
|
+
const loadingPath = loadingPathWithoutExtension + "." + loadingFileExtension;
|
|
463
|
+
laterWriteFileIfNotExists(
|
|
464
|
+
loadingPath,
|
|
465
|
+
`export default function Loading() {\n${projectInfo.indentation}// Stack uses React Suspense, which will render this page while user data is being fetched.\n${projectInfo.indentation}// See: https://nextjs.org/docs/app/api-reference/file-conventions/loading\n${projectInfo.indentation}return <></>;\n}\n`
|
|
466
|
+
);
|
|
467
|
+
},
|
|
468
|
+
|
|
469
|
+
async getPackageManager() {
|
|
470
|
+
if (packageManagerFromArgs) return { packageManager: packageManagerFromArgs };
|
|
471
|
+
const packageManager = await promptPackageManager();
|
|
472
|
+
const versionCommand = `${packageManager} --version`;
|
|
473
|
+
|
|
474
|
+
try {
|
|
475
|
+
await shellNicelyFormatted(versionCommand, { shell: true, quiet: true });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
console.error(err);
|
|
478
|
+
throw new UserError(
|
|
479
|
+
`Could not run the package manager command '${versionCommand}'. Please make sure ${packageManager} is installed on your system.`
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
return { packageManager };
|
|
484
|
+
},
|
|
485
|
+
|
|
486
|
+
async ensureReady(type) {
|
|
487
|
+
const projectPath = await getProjectPath();
|
|
488
|
+
|
|
489
|
+
const typeString = {
|
|
490
|
+
js: "JavaScript",
|
|
491
|
+
next: "Next.js"
|
|
492
|
+
}[type] ?? throwErr("unknown type");
|
|
493
|
+
const isReady = !!process.env.STACK_DISABLE_INTERACTIVE || (await inquirer.prompt([
|
|
494
|
+
{
|
|
495
|
+
type: "confirm",
|
|
496
|
+
name: "ready",
|
|
497
|
+
message: `Found a ${typeString} project at ${projectPath} — ready to install Stack Auth?`,
|
|
498
|
+
default: true,
|
|
499
|
+
},
|
|
500
|
+
])).ready;
|
|
501
|
+
if (!isReady) {
|
|
502
|
+
throw new UserError("Installation aborted.");
|
|
503
|
+
}
|
|
504
|
+
},
|
|
505
|
+
|
|
506
|
+
async getServerOrClientOrBoth() {
|
|
507
|
+
if (isClient && isServer) return ["server", "client"];
|
|
508
|
+
if (isServer) return ["server"];
|
|
509
|
+
if (isClient) return ["client"];
|
|
510
|
+
|
|
511
|
+
return (await inquirer.prompt([{
|
|
512
|
+
type: "list",
|
|
513
|
+
name: "type",
|
|
514
|
+
message: "Do you want to use Stack Auth on the server, or on the client?",
|
|
515
|
+
choices: [
|
|
516
|
+
{ name: "Client (eg. Vite, HTML)", value: ["client"] },
|
|
517
|
+
{ name: "Server (eg. Node.js)", value: ["server"] },
|
|
518
|
+
{ name: "Both", value: ["server", "client"] }
|
|
519
|
+
]
|
|
520
|
+
}])).type;
|
|
521
|
+
},
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* note: this is a heuristic, specific frameworks may have better heuristics (eg. the Next.js code uses the extension of the global layout file)
|
|
525
|
+
*/
|
|
526
|
+
async guessDefaultFileExtension() {
|
|
527
|
+
const projectPath = await getProjectPath();
|
|
528
|
+
const hasTsConfig = fs.existsSync(
|
|
529
|
+
path.join(projectPath, "tsconfig.json")
|
|
530
|
+
);
|
|
531
|
+
return hasTsConfig ? "ts" : "js";
|
|
532
|
+
},
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* note: this is a heuristic, specific frameworks may have better heuristics (eg. the Next.js code uses the location of the app folder)
|
|
536
|
+
*/
|
|
537
|
+
async guessSrcPath() {
|
|
538
|
+
const projectPath = await getProjectPath();
|
|
539
|
+
const potentialSrcPath = path.join(projectPath, "src");
|
|
540
|
+
const hasSrcFolder = fs.existsSync(
|
|
541
|
+
path.join(projectPath, "src")
|
|
542
|
+
);
|
|
543
|
+
return hasSrcFolder ? potentialSrcPath : projectPath;
|
|
544
|
+
},
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
|
|
318
550
|
async function getUpdatedLayout(originalLayout) {
|
|
319
551
|
let layout = originalLayout;
|
|
320
552
|
const indentation = guessIndentation(originalLayout);
|
|
@@ -408,7 +640,7 @@ async function getProjectPath() {
|
|
|
408
640
|
);
|
|
409
641
|
if (askForPathModification) {
|
|
410
642
|
savedProjectPath = (
|
|
411
|
-
await inquirer.prompt([
|
|
643
|
+
assertInteractive() && await inquirer.prompt([
|
|
412
644
|
{
|
|
413
645
|
type: "input",
|
|
414
646
|
name: "newPath",
|
|
@@ -432,7 +664,7 @@ async function findJsExtension(fullPathWithoutExtension) {
|
|
|
432
664
|
return null;
|
|
433
665
|
}
|
|
434
666
|
|
|
435
|
-
async function
|
|
667
|
+
async function promptPackageManager() {
|
|
436
668
|
const projectPath = await getProjectPath();
|
|
437
669
|
const yarnLock = fs.existsSync(path.join(projectPath, "yarn.lock"));
|
|
438
670
|
const pnpmLock = fs.existsSync(path.join(projectPath, "pnpm-lock.yaml"));
|
|
@@ -449,7 +681,7 @@ async function getPackageManager() {
|
|
|
449
681
|
return "bun";
|
|
450
682
|
}
|
|
451
683
|
|
|
452
|
-
const answers = await inquirer.prompt([
|
|
684
|
+
const answers = assertInteractive() && await inquirer.prompt([
|
|
453
685
|
{
|
|
454
686
|
type: "list",
|
|
455
687
|
name: "packageManager",
|
|
@@ -461,19 +693,23 @@ async function getPackageManager() {
|
|
|
461
693
|
}
|
|
462
694
|
|
|
463
695
|
async function shellNicelyFormatted(command, { quiet, ...options }) {
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
696
|
+
let ui, interval;
|
|
697
|
+
if (!quiet) {
|
|
698
|
+
console.log();
|
|
699
|
+
ui = new inquirer.ui.BottomBar();
|
|
700
|
+
let dots = 4;
|
|
701
|
+
ui.updateBottomBar(
|
|
702
|
+
colorize.blue`Running command: ${command}...`
|
|
703
|
+
);
|
|
704
|
+
interval = setInterval(() => {
|
|
705
|
+
if (!isDryRun) {
|
|
706
|
+
ui.updateBottomBar(
|
|
707
|
+
colorize.blue`Running command: ${command}${".".repeat(dots++ % 5)}`
|
|
708
|
+
);
|
|
709
|
+
}
|
|
710
|
+
}, 700);
|
|
711
|
+
}
|
|
712
|
+
|
|
477
713
|
try {
|
|
478
714
|
if (!isDryRun) {
|
|
479
715
|
const child = child_process.spawn(command, options);
|
|
@@ -497,15 +733,22 @@ async function shellNicelyFormatted(command, { quiet, ...options }) {
|
|
|
497
733
|
|
|
498
734
|
if (!quiet) {
|
|
499
735
|
commandsExecuted.push(command);
|
|
736
|
+
ui.updateBottomBar(
|
|
737
|
+
`${colorize.green`√`} Command ${command} succeeded\n`
|
|
738
|
+
);
|
|
500
739
|
}
|
|
740
|
+
} catch (e) {
|
|
741
|
+
if (!quiet) {
|
|
742
|
+
ui.updateBottomBar(
|
|
743
|
+
`${colorize.red`X`} Command ${command} failed\n`
|
|
744
|
+
);
|
|
745
|
+
}
|
|
746
|
+
throw e;
|
|
501
747
|
} finally {
|
|
502
748
|
clearTimeout(interval);
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
: `${colorize.green`√`} Command ${command} succeeded\n`
|
|
507
|
-
);
|
|
508
|
-
ui.close();
|
|
749
|
+
if (!quiet) {
|
|
750
|
+
ui.close();
|
|
751
|
+
}
|
|
509
752
|
}
|
|
510
753
|
}
|
|
511
754
|
|
|
@@ -538,12 +781,31 @@ async function writeFile(fullPath, content) {
|
|
|
538
781
|
}
|
|
539
782
|
}
|
|
540
783
|
|
|
784
|
+
function laterWriteFile(...args) {
|
|
785
|
+
writeFileHandlers.push(async () => {
|
|
786
|
+
await writeFile(...args);
|
|
787
|
+
})
|
|
788
|
+
}
|
|
789
|
+
|
|
541
790
|
async function writeFileIfNotExists(fullPath, content) {
|
|
542
791
|
if (!fs.existsSync(fullPath)) {
|
|
543
792
|
await writeFile(fullPath, content);
|
|
544
793
|
}
|
|
545
794
|
}
|
|
546
795
|
|
|
796
|
+
function laterWriteFileIfNotExists(...args) {
|
|
797
|
+
writeFileHandlers.push(async () => {
|
|
798
|
+
await writeFileIfNotExists(...args);
|
|
799
|
+
})
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
function assertInteractive() {
|
|
803
|
+
if (process.env.STACK_DISABLE_INTERACTIVE) {
|
|
804
|
+
throw new UserError("STACK_DISABLE_INTERACTIVE is set, but wizard requires interactivity to complete. Make sure you supplied all required command line arguments!");
|
|
805
|
+
}
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
|
|
547
809
|
function throwErr(message) {
|
|
548
810
|
throw new Error(message);
|
|
549
811
|
}
|
|
@@ -555,3 +817,24 @@ export function templateIdentity(strings, ...values) {
|
|
|
555
817
|
|
|
556
818
|
return strings.slice(1).reduce((result, string, i) => `${result}${values[i] ?? "n/a"}${string}`, strings[0]);
|
|
557
819
|
}
|
|
820
|
+
|
|
821
|
+
async function clearStdin() {
|
|
822
|
+
await new Promise((resolve) => {
|
|
823
|
+
if (process.stdin.isTTY) {
|
|
824
|
+
process.stdin.setRawMode(true);
|
|
825
|
+
}
|
|
826
|
+
process.stdin.resume();
|
|
827
|
+
process.stdin.removeAllListeners('data');
|
|
828
|
+
|
|
829
|
+
const flush = () => {
|
|
830
|
+
while (process.stdin.read() !== null) {}
|
|
831
|
+
if (process.stdin.isTTY) {
|
|
832
|
+
process.stdin.setRawMode(false);
|
|
833
|
+
}
|
|
834
|
+
resolve();
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
// Add a small delay to allow any buffered input to clear
|
|
838
|
+
setTimeout(flush, 10);
|
|
839
|
+
});
|
|
840
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stackframe/init-stack",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.17",
|
|
4
4
|
"description": "The setup wizard for Stack. https://stack-auth.com",
|
|
5
5
|
"main": "index.mjs",
|
|
6
6
|
"type": "module",
|
|
@@ -18,13 +18,22 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"inquirer": "^9.2.19",
|
|
20
20
|
"open": "^10.1.0",
|
|
21
|
-
"@stackframe/stack-shared": "2.7.
|
|
21
|
+
"@stackframe/stack-shared": "2.7.17"
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"clean": "rimraf test-run-output && rimraf node_modules",
|
|
25
|
-
"init-stack": "node index.mjs",
|
|
26
|
-
"init-stack:local": "
|
|
27
|
-
"test-run": "
|
|
28
|
-
"test-run
|
|
25
|
+
"init-stack": "node init-stack/index.mjs",
|
|
26
|
+
"init-stack:local": "STACK_NEXT_INSTALL_PACKAGE_NAME_OVERRIDE=../../stack STACK_JS_INSTALL_PACKAGE_NAME_OVERRIDE=../../js node index.mjs",
|
|
27
|
+
"test-run": "pnpm run test-run-js && pnpm run test-run-node && pnpm run test-run-next && pnpm run test-run-neon",
|
|
28
|
+
"test-run:manual": "pnpm run test-run-js:manual && pnpm run test-run-node:manual && pnpm run test-run-next:manual && pnpm run test-run-neon:manual",
|
|
29
|
+
"ensure-neon": "grep -q '\"@neondatabase/serverless\"' ./test-run-output/package.json && echo 'Initialized Neon successfully!'",
|
|
30
|
+
"test-run-neon": "pnpm run test-run-node --neon && pnpm run ensure-neon",
|
|
31
|
+
"test-run-neon:manual": "pnpm run test-run-node:manual --neon && pnpm run ensure-neon",
|
|
32
|
+
"test-run-node:manual": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init && cd .. && pnpm run init-stack:local test-run-output",
|
|
33
|
+
"test-run-node": "rimraf test-run-output && mkdir test-run-output && cd test-run-output && npm init --init-author-name example-author --init-license UNLICENSED --init-author-url http://example.com --init-module test-run-output --init-version 1.0.0 -y && cd .. && STACK_DISABLE_INTERACTIVE=true pnpm run init-stack:local test-run-output --js --server --npm",
|
|
34
|
+
"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",
|
|
35
|
+
"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",
|
|
36
|
+
"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",
|
|
37
|
+
"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"
|
|
29
38
|
}
|
|
30
39
|
}
|