@stackframe/init-stack 1.0.1

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.
Files changed (4) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +0 -0
  3. package/index.mjs +289 -0
  4. package/package.json +20 -0
package/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2024 Stackframe
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
File without changes
package/index.mjs ADDED
@@ -0,0 +1,289 @@
1
+ #!/usr/bin/env node
2
+
3
+ import inquirer from 'inquirer';
4
+ import * as fs from 'fs';
5
+ import * as child_process from 'child_process';
6
+ import * as path from 'path';
7
+
8
+ const jsLikeFileExtensions = ['mtsx', 'ctsx', 'tsx', 'mts', 'cts', 'ts', 'mjsx', 'cjsx', 'jsx', 'mjs', 'cjs', 'js'];
9
+
10
+ async function main() {
11
+ console.log("Welcome to the Stack installation wizard! 🧙‍♂️");
12
+
13
+ const projectPath = getProjectPath();
14
+ if (!fs.existsSync(projectPath)) {
15
+ throw new Error(`The project path ${projectPath} does not exist`);
16
+ }
17
+
18
+ const packageJsonPath = path.join(projectPath, 'package.json');
19
+ if (!fs.existsSync(packageJsonPath)) {
20
+ throw new Error(`The package.json file does not exist in the project path ${projectPath}. You must initialize a new project first before installing Stack.`);
21
+ }
22
+
23
+ const nextConfigPathWithoutExtension = path.join(projectPath, 'next.config');
24
+ const nextConfigFileExtension = await findJsExtension(nextConfigPathWithoutExtension);
25
+ const nextConfigPath = nextConfigPathWithoutExtension + '.' + nextConfigFileExtension;
26
+ if (!fs.existsSync(nextConfigPath)) {
27
+ throw new Error(`Expected file at ${nextConfigPath}. Only Next.js projects are currently supported supported.`);
28
+ }
29
+
30
+ const envLocalPath = path.join(projectPath, '.env.local');
31
+
32
+ const hasSrcFolder = fs.existsSync(path.join(projectPath, 'src'));
33
+ const srcPath = path.join(projectPath, hasSrcFolder ? 'src' : '');
34
+ const appPath = path.join(srcPath, 'app');
35
+ if (!fs.existsSync(appPath)) {
36
+ throw new Error(`The app path ${appPath} does not exist. Only the Next.js app router is supported.`);
37
+ }
38
+
39
+ const layoutPathWithoutExtension = path.join(appPath, 'layout');
40
+ const layoutFileExtension = await findJsExtension(layoutPathWithoutExtension) ?? "jsx";
41
+ const layoutPath = layoutPathWithoutExtension + '.' + layoutFileExtension;
42
+ 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.`);
43
+ const updatedLayoutResult = await getUpdatedLayout(layoutContent) ?? throwErr("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");
44
+ const updatedLayoutContent = updatedLayoutResult.content;
45
+
46
+ const defaultExtension = layoutFileExtension;
47
+ const ind = updatedLayoutResult.indentation;
48
+
49
+
50
+ const stackAppPathWithoutExtension = path.join(srcPath, 'stack');
51
+ const stackAppFileExtension = await findJsExtension(stackAppPathWithoutExtension) ?? defaultExtension;
52
+ const stackAppPath = stackAppPathWithoutExtension + '.' + stackAppFileExtension;
53
+ const stackAppContent = await readFile(stackAppPath);
54
+ if (stackAppContent) {
55
+ if (!stackAppContent.includes("@stackframe/stack")) {
56
+ throw new Error(`A file at the path ${stackAppPath} already exists. Stack uses the /src/stack-app file to initialize the Stack SDK. Please remove the existing file and try again.`);
57
+ }
58
+ throw new Error(`It seems that you've already installed Stack in this project.`);
59
+ }
60
+
61
+
62
+ const handlerPathWithoutExtension = path.join(appPath, 'handler/[...stack]/page');
63
+ const handlerFileExtension = await findJsExtension(handlerPathWithoutExtension) ?? defaultExtension;
64
+ const handlerPath = handlerPathWithoutExtension + '.' + handlerFileExtension;
65
+ const handlerContent = await readFile(handlerPath);
66
+ if (handlerContent && !handlerContent.includes("@stackframe/stack")) {
67
+ throw new Error(`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.`);
68
+ }
69
+
70
+
71
+ let loadingPathWithoutExtension = path.join(appPath, 'loading');
72
+ const loadingFileExtension = await findJsExtension(loadingPathWithoutExtension) ?? defaultExtension;
73
+ const loadingPath = loadingPathWithoutExtension + '.' + loadingFileExtension;
74
+
75
+ console.log();
76
+ console.log("Found supported project at:", projectPath);
77
+ console.log("Installing now! 💃");
78
+
79
+ const packageManager = await getPackageManager();
80
+ const versionCommand = `${packageManager} --version`;
81
+ const installCommand = packageManager === 'yarn' ? 'yarn add' : `${packageManager} install`;
82
+
83
+ process.stdout.write("\nChecking package manager version... ");
84
+ try {
85
+ await shellNicelyFormatted(versionCommand, { shell: true });
86
+ } catch (err) {
87
+ throw new Error(`Could not run the package manager command ${versionCommand}. Please make sure ${packageManager} is installed on your system.`);
88
+ }
89
+
90
+ console.log();
91
+ console.log("Writing files...");
92
+ await writeFileIfNotExists(envLocalPath, "NEXT_PUBLIC_STACK_PROJECT_ID=\nNEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=\nSTACK_SECRET_SERVER_KEY=\n");
93
+ await writeFileIfNotExists(loadingPath, `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`);
94
+ await writeFileIfNotExists(handlerPath, `import { StackHandler } from "@stackframe/stack";\nimport { stackServerApp } from "../../../stack";\nexport default function Handler(props) {\n${ind}return <StackHandler app={stackServerApp} {...props} />;\n}\n`);
95
+ await writeFileIfNotExists(stackAppPath, `import "server-only";\n\nimport { StackServerApp } from "@stackframe/stack";\n\nexport const stackServerApp = new StackServerApp({\n${ind}tokenStore: "nextjs-cookie",\n});\n`);
96
+ await writeFile(layoutPath, updatedLayoutContent);
97
+ console.log("Files written successfully!");
98
+
99
+ console.log();
100
+ console.log("Installing dependencies...");
101
+ await shellNicelyFormatted(`${installCommand} @stackframe/stack`, { shell: true, cwd: projectPath });
102
+ }
103
+ main().catch((err) => {
104
+ console.error(err);
105
+ console.error();
106
+ console.error();
107
+ console.error();
108
+ console.error();
109
+ console.error("===============================================");
110
+ console.error();
111
+ console.error("[ERR] An error occured during the initialization process. Please try manually installing Stack as described in https://docs.stack-auth.com/docs/getting-started/setup");
112
+ console.error("[ERR]");
113
+ console.error("[ERR] If you need assistance, please join our Discord where we're happy to help: https://discord.stack-auth.com");
114
+ console.error("[ERR]");
115
+ console.error(`[ERR] Error message: ${err.message}`);
116
+ console.error();
117
+ console.error("===============================================");
118
+ console.error();
119
+ process.exit(1);
120
+ }).then(() => {
121
+ console.log();
122
+ console.log();
123
+ console.log();
124
+ console.log();
125
+ console.log("===============================================");
126
+ console.log();
127
+ console.log("Successfully installed Stack! 🚀🚀🚀");
128
+ console.log();
129
+ console.log("Next up, please create an account on https://app.stack-auth.com to create a new project, and copy the Next.js environment variables from a new API key into your .env.local file.");
130
+ console.log();
131
+ console.log("Then, you will be able to access your sign-in page on http://your-website.example.com/handler/signin. Congratulations!");
132
+ console.log();
133
+ console.log("For more information, please visit https://docs.stack-auth.com/docs/getting-started/setup");
134
+ console.log();
135
+ console.log("===============================================");
136
+ console.log();
137
+ });
138
+
139
+ async function getUpdatedLayout(originalLayout) {
140
+ let layout = originalLayout;
141
+ const indentation = guessIndentation(originalLayout);
142
+
143
+ const firstImportLocationM1 = (/\simport\s/).exec(layout)?.index;
144
+ const hasStringAsFirstLine = layout.startsWith('"') || layout.startsWith("'");
145
+ const importInsertLocationM1 = firstImportLocationM1 ?? (hasStringAsFirstLine ? layout.indexOf('\n') : -1);
146
+ const importInsertLocation = importInsertLocationM1 + 1;
147
+ const importStatement = `import { StackProvider, StackTheme } from "@stackframe/stack";\nimport { stackServerApp } from "../stack";\n`;
148
+ layout = layout.slice(0, importInsertLocation) + importStatement + layout.slice(importInsertLocation);
149
+
150
+
151
+ const bodyOpenTag = /<\s*body[^>]*>/.exec(layout);
152
+ const bodyCloseTag = /<\s*\/\s*body[^>]*>/.exec(layout);
153
+ if (!bodyOpenTag || !bodyCloseTag) {
154
+ return undefined;
155
+ }
156
+ const bodyOpenEndIndex = bodyOpenTag.index + bodyOpenTag[0].length;
157
+ const bodyCloseStartIndex = bodyCloseTag.index;
158
+ if (bodyCloseStartIndex <= bodyOpenEndIndex) {
159
+ return undefined;
160
+ }
161
+
162
+ const lines = layout.split('\n');
163
+ const [bodyOpenEndLine, bodyOpenEndIndexInLine] = getLineIndex(lines, bodyOpenEndIndex);
164
+ const [bodyCloseStartLine, bodyCloseStartIndexInLine] = getLineIndex(lines, bodyCloseStartIndex);
165
+
166
+ const insertOpen = "<StackProvider app={stackServerApp}><StackTheme>";
167
+ const insertClose = "</StackTheme></StackProvider>";
168
+
169
+ layout = layout.slice(0, bodyCloseStartIndex) + insertClose + layout.slice(bodyCloseStartIndex);
170
+ layout = layout.slice(0, bodyOpenEndIndex) + insertOpen + layout.slice(bodyOpenEndIndex);
171
+
172
+ return {
173
+ content: `// Below is the old layout file.\n\n${layout}`,
174
+ indentation,
175
+ };
176
+ }
177
+
178
+ function guessIndentation(str) {
179
+ const lines = str.split('\n');
180
+ const linesLeadingWhitespaces = lines.map((line) => line.match(/^\s*/)[0]).filter((ws) => ws.length > 0);
181
+ const isMostlyTabs = linesLeadingWhitespaces.filter((ws) => ws.includes('\t')).length >= linesLeadingWhitespaces.length * 2 / 3;
182
+ if (isMostlyTabs) return "\t";
183
+ const linesLeadingWhitespacesCount = linesLeadingWhitespaces.map((ws) => ws.length);
184
+ const min = Math.min(Infinity, ...linesLeadingWhitespacesCount);
185
+ return Number.isFinite(min) ? ' '.repeat(Math.max(2, min)) : ' ';
186
+ }
187
+
188
+ function getLineIndex(lines, stringIndex) {
189
+ let lineIndex = 0;
190
+ for (let l = 0; l < lines.length; l++) {
191
+ const line = lines[l];
192
+ if (stringIndex < lineIndex + line.length) {
193
+ return [l, stringIndex - lineIndex];
194
+ }
195
+ lineIndex += line.length + 1;
196
+ }
197
+ throw new Error(`Index ${stringIndex} is out of bounds for lines ${JSON.stringify(lines)}`);
198
+ }
199
+
200
+ function getProjectPath() {
201
+ const path = process.argv[2] || process.cwd();
202
+ return path;
203
+ }
204
+
205
+ async function findJsExtension(fullPathWithoutExtension) {
206
+ for (const ext of jsLikeFileExtensions) {
207
+ const fullPath = fullPathWithoutExtension + '.' + ext;
208
+ if (fs.existsSync(fullPath)) {
209
+ return ext;
210
+ }
211
+ }
212
+ return null;
213
+ }
214
+
215
+ async function getPackageManager() {
216
+ const yarnLock = fs.existsSync(path.join(getProjectPath(), 'yarn.lock'));
217
+ const pnpmLock = fs.existsSync(path.join(getProjectPath(), 'pnpm-lock.yaml'));
218
+ const npmLock = fs.existsSync(path.join(getProjectPath(), 'package-lock.json'));
219
+
220
+ if (yarnLock && !pnpmLock && !npmLock) {
221
+ return 'yarn';
222
+ } else if (!yarnLock && pnpmLock && !npmLock) {
223
+ return 'pnpm';
224
+ } else if (!yarnLock && !pnpmLock && npmLock) {
225
+ return 'npm';
226
+ }
227
+
228
+ const answers = await inquirer.prompt([
229
+ {
230
+ type: 'list',
231
+ name: 'packageManager',
232
+ message: 'Which package manager are you using for this project?',
233
+ choices: ['npm', 'yarn', 'pnpm'],
234
+ },
235
+ ]);
236
+ return answers.packageManager;
237
+ }
238
+
239
+ async function shellNicelyFormatted(command, options) {
240
+ console.log();
241
+ const ui = new inquirer.ui.BottomBar();
242
+ ui.updateBottomBar(`Running command: ${command}...`);
243
+ const child = child_process.spawn(command, options);
244
+ child.stdout.pipe(ui.log);
245
+ child.stderr.pipe(ui.log);
246
+
247
+ await new Promise((resolve, reject) => {
248
+ child.on('exit', (code) => {
249
+ if (code === 0) {
250
+ resolve();
251
+ } else {
252
+ reject(new Error(`Command ${command} failed with code ${code}`));
253
+ }
254
+ });
255
+ });
256
+
257
+ ui.updateBottomBar(`Command ${command} finished successfully!\n`);
258
+ ui.close();
259
+ }
260
+
261
+ async function readFile(fullPath) {
262
+ try {
263
+ return fs.readFileSync(fullPath, 'utf-8');
264
+ } catch (err) {
265
+ if (err.code === 'ENOENT') {
266
+ return null;
267
+ }
268
+ throw err;
269
+ }
270
+ }
271
+
272
+ async function writeFile(fullPath, content) {
273
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
274
+ fs.writeFileSync(fullPath, content);
275
+ }
276
+
277
+ async function writeFileIfNotExists(fullPath, content) {
278
+ if (!fs.existsSync(fullPath)) {
279
+ await writeFile(fullPath, content);
280
+ }
281
+ }
282
+
283
+ async function wait(ms) {
284
+ return new Promise((resolve) => setTimeout(resolve, ms));
285
+ }
286
+
287
+ function throwErr(message) {
288
+ throw new Error(message);
289
+ }
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@stackframe/init-stack",
3
+ "version": "1.0.1",
4
+ "description": "The setup wizard for Stack. https://stack-auth.com",
5
+ "main": "index.mjs",
6
+ "bin": "./index.mjs",
7
+ "files": [
8
+ "index.mjs"
9
+ ],
10
+ "homepage": "https://stack-auth.com",
11
+ "keywords": [],
12
+ "author": "",
13
+ "license": "MIT",
14
+ "dependencies": {
15
+ "inquirer": "^9.2.19"
16
+ },
17
+ "scripts": {
18
+ "init-stack": "node index.mjs"
19
+ }
20
+ }