create-stackrjs 1.4.0
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/README.md +154 -0
- package/dist/index.js +1135 -0
- package/package.json +62 -0
- package/template/architectures/microservices/_gitignore +26 -0
- package/template/architectures/microservices/apps/web/Dockerfile +11 -0
- package/template/architectures/microservices/apps/web/next-env.d.ts +5 -0
- package/template/architectures/microservices/apps/web/next.config.mjs +4 -0
- package/template/architectures/microservices/apps/web/package.json +25 -0
- package/template/architectures/microservices/apps/web/src/app/account/page.tsx +56 -0
- package/template/architectures/microservices/apps/web/src/app/globals.css +3 -0
- package/template/architectures/microservices/apps/web/src/app/layout.tsx +16 -0
- package/template/architectures/microservices/apps/web/src/app/login/page.tsx +70 -0
- package/template/architectures/microservices/apps/web/src/app/page.tsx +33 -0
- package/template/architectures/microservices/apps/web/src/app/register/page.tsx +71 -0
- package/template/architectures/microservices/apps/web/src/lib/api.ts +15 -0
- package/template/architectures/microservices/apps/web/tsconfig.json +23 -0
- package/template/architectures/microservices/docker-compose.yml +45 -0
- package/template/architectures/microservices/gateway/nginx.conf +20 -0
- package/template/architectures/microservices/package.json +21 -0
- package/template/architectures/microservices/services/api/Dockerfile +11 -0
- package/template/architectures/microservices/services/api/package.json +34 -0
- package/template/architectures/microservices/services/api/prisma/schema.prisma +17 -0
- package/template/architectures/microservices/services/api/prisma.config.ts +17 -0
- package/template/architectures/microservices/services/api/src/auth.ts +22 -0
- package/template/architectures/microservices/services/api/src/db.ts +12 -0
- package/template/architectures/microservices/services/api/src/index.ts +22 -0
- package/template/architectures/microservices/services/api/src/routes/auth.ts +73 -0
- package/template/architectures/microservices/services/api/tsconfig.json +16 -0
- package/template/base/_eslintrc.json +3 -0
- package/template/base/_gitignore +24 -0
- package/template/base/_prettierrc.json +6 -0
- package/template/base/next-env.d.ts +5 -0
- package/template/base/next.config.mjs +4 -0
- package/template/base/package.json +27 -0
- package/template/base/src/app/globals.css +14 -0
- package/template/base/src/app/layout.tsx +16 -0
- package/template/base/src/app/page.tsx +10 -0
- package/template/base/tsconfig.json +23 -0
- package/template/extras/account-better-auth/src/server/account.ts +52 -0
- package/template/extras/account-clerk/src/server/account.ts +64 -0
- package/template/extras/account-nextauth/src/server/account.ts +59 -0
- package/template/extras/account-supabase/src/lib/supabase/admin.ts +14 -0
- package/template/extras/account-supabase/src/server/account.ts +65 -0
- package/template/extras/admin/src/app/admin/page.tsx +55 -0
- package/template/extras/auth-pages/src/app/account/page.tsx +37 -0
- package/template/extras/auth-pages/src/app/forgot-password/page.tsx +33 -0
- package/template/extras/auth-pages/src/app/login/page.tsx +32 -0
- package/template/extras/auth-pages/src/app/page.tsx +46 -0
- package/template/extras/auth-pages/src/app/register/page.tsx +36 -0
- package/template/extras/auth-pages/src/app/reset-password/page.tsx +49 -0
- package/template/extras/auth-pages/src/components/auth/ui.tsx +57 -0
- package/template/extras/auth-pages-drizzle/src/server/actions.ts +122 -0
- package/template/extras/auth-pages-mongoose/src/server/actions.ts +121 -0
- package/template/extras/auth-pages-prisma/src/server/actions.ts +117 -0
- package/template/extras/better-auth/prisma/schema.prisma +70 -0
- package/template/extras/better-auth/src/app/account/page.tsx +42 -0
- package/template/extras/better-auth/src/app/api/auth/[...all]/route.ts +5 -0
- package/template/extras/better-auth/src/app/login/page.tsx +42 -0
- package/template/extras/better-auth/src/app/register/page.tsx +44 -0
- package/template/extras/better-auth/src/components/auth/ui.tsx +58 -0
- package/template/extras/better-auth/src/lib/auth-client.ts +7 -0
- package/template/extras/better-auth/src/server/auth.ts +13 -0
- package/template/extras/clerk/middleware.ts +14 -0
- package/template/extras/clerk/src/app/account/page.tsx +30 -0
- package/template/extras/clerk/src/app/layout.tsx +19 -0
- package/template/extras/clerk/src/app/page.tsx +40 -0
- package/template/extras/clerk/src/app/sign-in/[[...sign-in]]/page.tsx +9 -0
- package/template/extras/clerk/src/app/sign-up/[[...sign-up]]/page.tsx +9 -0
- package/template/extras/dashboard/src/app/dashboard/page.tsx +48 -0
- package/template/extras/databases/mongodb/docker-compose.yml +13 -0
- package/template/extras/databases/mysql/docker-compose.yml +14 -0
- package/template/extras/databases/postgres/docker-compose.yml +15 -0
- package/template/extras/drizzle/mysql/drizzle.config.ts +9 -0
- package/template/extras/drizzle/mysql/src/server/db.ts +11 -0
- package/template/extras/drizzle/mysql/src/server/schema.ts +17 -0
- package/template/extras/drizzle/postgres/drizzle.config.ts +9 -0
- package/template/extras/drizzle/postgres/src/server/db.ts +11 -0
- package/template/extras/drizzle/postgres/src/server/schema.ts +20 -0
- package/template/extras/landing/src/app/page.tsx +53 -0
- package/template/extras/mongoose/src/server/db.ts +12 -0
- package/template/extras/mongoose/src/server/models.ts +25 -0
- package/template/extras/nextauth/middleware.ts +11 -0
- package/template/extras/nextauth/src/app/api/auth/[...nextauth]/route.ts +3 -0
- package/template/extras/nextauth/src/server/auth.config.ts +39 -0
- package/template/extras/nextauth/src/server/roles.ts +5 -0
- package/template/extras/nextauth/src/types/next-auth.d.ts +22 -0
- package/template/extras/nextauth-drizzle/src/server/auth.ts +44 -0
- package/template/extras/nextauth-mongoose/src/server/auth.ts +42 -0
- package/template/extras/nextauth-prisma/src/server/auth.ts +43 -0
- package/template/extras/prisma/base/prisma/schema.prisma +67 -0
- package/template/extras/prisma/base/prisma.config.ts +10 -0
- package/template/extras/prisma/mysql/src/server/db.ts +25 -0
- package/template/extras/prisma/postgres/src/server/db.ts +18 -0
- package/template/extras/shadcn/components.json +20 -0
- package/template/extras/shadcn/postcss.config.mjs +7 -0
- package/template/extras/shadcn/src/app/globals.css +59 -0
- package/template/extras/shadcn/src/components/ui/button.tsx +42 -0
- package/template/extras/shadcn/src/components/ui/card.tsx +54 -0
- package/template/extras/shadcn/src/components/ui/input.tsx +19 -0
- package/template/extras/shadcn/src/lib/utils.ts +7 -0
- package/template/extras/shadcn/tailwind.config.ts +71 -0
- package/template/extras/supabase/middleware.ts +13 -0
- package/template/extras/supabase/src/app/account/page.tsx +32 -0
- package/template/extras/supabase/src/app/login/page.tsx +47 -0
- package/template/extras/supabase/src/app/register/page.tsx +52 -0
- package/template/extras/supabase/src/components/auth/sign-out-button.tsx +25 -0
- package/template/extras/supabase/src/components/auth/ui.tsx +58 -0
- package/template/extras/supabase/src/lib/supabase/client.ts +9 -0
- package/template/extras/supabase/src/lib/supabase/middleware.ts +34 -0
- package/template/extras/supabase/src/lib/supabase/server.ts +29 -0
- package/template/extras/tailwind/postcss.config.mjs +7 -0
- package/template/extras/tailwind/src/app/globals.css +3 -0
- package/template/extras/tailwind/tailwind.config.ts +9 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
|
|
6
|
+
// src/shared/registry.ts
|
|
7
|
+
var REGISTRY = {
|
|
8
|
+
architecture: [
|
|
9
|
+
{ value: "monolith", label: "Monolith", status: "stable" },
|
|
10
|
+
// The microservices bundle ships its own Postgres data layer.
|
|
11
|
+
{ value: "microservices", label: "Microservices", status: "stable", requires: { database: ["postgres"] } }
|
|
12
|
+
],
|
|
13
|
+
database: [
|
|
14
|
+
{ value: "postgres", label: "PostgreSQL", status: "stable" },
|
|
15
|
+
{ value: "mysql", label: "MySQL", status: "stable" },
|
|
16
|
+
// MongoDB pairs with the Mongoose ODM (Prisma/Drizzle here are SQL-only).
|
|
17
|
+
{ value: "mongodb", label: "MongoDB", status: "stable", requires: { orm: ["mongoose"] } }
|
|
18
|
+
],
|
|
19
|
+
orm: [
|
|
20
|
+
{ value: "prisma", label: "Prisma", status: "stable" },
|
|
21
|
+
// Drizzle is SQL-only: no MongoDB.
|
|
22
|
+
{ value: "drizzle", label: "Drizzle", status: "stable", requires: { database: ["postgres", "mysql"] } },
|
|
23
|
+
// Mongoose is MongoDB-only.
|
|
24
|
+
{ value: "mongoose", label: "Mongoose", status: "stable", requires: { database: ["mongodb"] } }
|
|
25
|
+
],
|
|
26
|
+
auth: [
|
|
27
|
+
{ value: "nextauth", label: "NextAuth (Auth.js)", status: "stable" },
|
|
28
|
+
// Clerk is hosted auth, independent of the chosen ORM/database.
|
|
29
|
+
{ value: "clerk", label: "Clerk", status: "stable" },
|
|
30
|
+
// Supabase Auth is backed by a Supabase Postgres project.
|
|
31
|
+
{ value: "supabase", label: "Supabase Auth", status: "stable", requires: { database: ["postgres"] } },
|
|
32
|
+
// Better Auth ships with its own Prisma schema + adapter.
|
|
33
|
+
{ value: "better-auth", label: "Better Auth", status: "stable", requires: { orm: ["prisma"] } }
|
|
34
|
+
],
|
|
35
|
+
styling: [
|
|
36
|
+
{ value: "tailwind", label: "Tailwind CSS", status: "stable" },
|
|
37
|
+
// shadcn ships the full Tailwind stack plus the shadcn/ui components.
|
|
38
|
+
{ value: "shadcn", label: "shadcn/ui (+ Tailwind)", status: "stable" }
|
|
39
|
+
],
|
|
40
|
+
interfaces: [
|
|
41
|
+
// The auth portal works with every provider: NextAuth gets the credentials
|
|
42
|
+
// pages (ORM-specific server actions as an overlay); Clerk/Supabase/Better Auth
|
|
43
|
+
// already ship their own login/register/account pages.
|
|
44
|
+
{
|
|
45
|
+
value: "auth-pages",
|
|
46
|
+
label: "Auth portal (login / register / reset)",
|
|
47
|
+
status: "stable"
|
|
48
|
+
},
|
|
49
|
+
// Dashboard + admin read user data through a per-auth `@/server/account`
|
|
50
|
+
// adapter. The DB-backed adapters (NextAuth / Better Auth) use Prisma, so both
|
|
51
|
+
// interfaces require it; Clerk/Supabase read from their own APIs but still
|
|
52
|
+
// generate against a Prisma app database.
|
|
53
|
+
{
|
|
54
|
+
value: "dashboard",
|
|
55
|
+
label: "User dashboard",
|
|
56
|
+
status: "stable",
|
|
57
|
+
requires: { orm: ["prisma"] }
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
value: "admin",
|
|
61
|
+
label: "Admin / back-office",
|
|
62
|
+
status: "stable",
|
|
63
|
+
requires: { orm: ["prisma"] }
|
|
64
|
+
},
|
|
65
|
+
{ value: "landing", label: "Landing page", status: "stable" }
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
var DIMENSIONS = Object.keys(REGISTRY);
|
|
69
|
+
var OPTION_LABELS = Object.fromEntries(
|
|
70
|
+
DIMENSIONS.flatMap((d) => REGISTRY[d].map((o) => [o.value, o.label]))
|
|
71
|
+
);
|
|
72
|
+
var optionLabel = (value) => OPTION_LABELS[value] ?? value;
|
|
73
|
+
var ALL_VALUES = Object.fromEntries(
|
|
74
|
+
DIMENSIONS.map((d) => [d, REGISTRY[d].map((o) => o.value)])
|
|
75
|
+
);
|
|
76
|
+
var STABLE_VALUES = Object.fromEntries(
|
|
77
|
+
DIMENSIONS.map((d) => [d, REGISTRY[d].filter((o) => o.status === "stable").map((o) => o.value)])
|
|
78
|
+
);
|
|
79
|
+
var DEFAULT_VALUES = Object.fromEntries(
|
|
80
|
+
DIMENSIONS.map((d) => [d, (REGISTRY[d].find((o) => o.status === "stable") ?? REGISTRY[d][0]).value])
|
|
81
|
+
);
|
|
82
|
+
function findOption(dimension, value) {
|
|
83
|
+
return REGISTRY[dimension].find((o) => o.value === value);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/cli/flags.ts
|
|
87
|
+
var SUPPORTED = STABLE_VALUES;
|
|
88
|
+
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
89
|
+
"architecture",
|
|
90
|
+
"database",
|
|
91
|
+
"orm",
|
|
92
|
+
"auth",
|
|
93
|
+
"styling",
|
|
94
|
+
"interfaces"
|
|
95
|
+
]);
|
|
96
|
+
function parseArgs(argv) {
|
|
97
|
+
const result = { positionals: [], yes: false, help: false, version: false };
|
|
98
|
+
const positionals = [];
|
|
99
|
+
for (let i = 0; i < argv.length; i++) {
|
|
100
|
+
const token = argv[i];
|
|
101
|
+
if (!token.startsWith("-")) {
|
|
102
|
+
positionals.push(token);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
let key = token.replace(/^--?/, "");
|
|
106
|
+
let value;
|
|
107
|
+
const eq = key.indexOf("=");
|
|
108
|
+
if (eq !== -1) {
|
|
109
|
+
value = key.slice(eq + 1);
|
|
110
|
+
key = key.slice(0, eq);
|
|
111
|
+
}
|
|
112
|
+
switch (key) {
|
|
113
|
+
case "help":
|
|
114
|
+
case "h":
|
|
115
|
+
result.help = true;
|
|
116
|
+
break;
|
|
117
|
+
case "version":
|
|
118
|
+
case "v":
|
|
119
|
+
result.version = true;
|
|
120
|
+
break;
|
|
121
|
+
case "yes":
|
|
122
|
+
case "y":
|
|
123
|
+
result.yes = true;
|
|
124
|
+
break;
|
|
125
|
+
case "ci":
|
|
126
|
+
result.yes = true;
|
|
127
|
+
result.install = false;
|
|
128
|
+
result.git = false;
|
|
129
|
+
break;
|
|
130
|
+
case "install":
|
|
131
|
+
result.install = true;
|
|
132
|
+
break;
|
|
133
|
+
case "no-install":
|
|
134
|
+
result.install = false;
|
|
135
|
+
break;
|
|
136
|
+
case "git":
|
|
137
|
+
result.git = true;
|
|
138
|
+
break;
|
|
139
|
+
case "no-git":
|
|
140
|
+
result.git = false;
|
|
141
|
+
break;
|
|
142
|
+
default: {
|
|
143
|
+
if (VALUE_FLAGS.has(key)) {
|
|
144
|
+
if (value === void 0) value = argv[++i];
|
|
145
|
+
if (key === "interfaces") {
|
|
146
|
+
result.interfaces = (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
147
|
+
} else {
|
|
148
|
+
result[key] = value;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
result.positionals = positionals;
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// src/cli/create.ts
|
|
159
|
+
import { promises as fs7 } from "fs";
|
|
160
|
+
import path22 from "path";
|
|
161
|
+
import * as p2 from "@clack/prompts";
|
|
162
|
+
|
|
163
|
+
// src/utils/paths.ts
|
|
164
|
+
import { fileURLToPath } from "url";
|
|
165
|
+
import path from "path";
|
|
166
|
+
function getTemplateDir() {
|
|
167
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
168
|
+
return path.resolve(here, "..", "template");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// src/utils/logger.ts
|
|
172
|
+
import pc from "picocolors";
|
|
173
|
+
var logger = {
|
|
174
|
+
info: (msg) => console.log(msg),
|
|
175
|
+
success: (msg) => console.log(pc.green(msg)),
|
|
176
|
+
warn: (msg) => console.log(pc.yellow(msg)),
|
|
177
|
+
error: (msg) => console.error(pc.red(msg)),
|
|
178
|
+
dim: (msg) => console.log(pc.dim(msg))
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/helpers/scaffoldProject.ts
|
|
182
|
+
import { promises as fs6 } from "fs";
|
|
183
|
+
import path20 from "path";
|
|
184
|
+
|
|
185
|
+
// src/utils/copyTemplate.ts
|
|
186
|
+
import { promises as fs } from "fs";
|
|
187
|
+
import path2 from "path";
|
|
188
|
+
function restoreFilename(name) {
|
|
189
|
+
return name.startsWith("_") ? "." + name.slice(1) : name;
|
|
190
|
+
}
|
|
191
|
+
async function copyTemplate(srcDir, destDir) {
|
|
192
|
+
const entries = await fs.readdir(srcDir, { withFileTypes: true });
|
|
193
|
+
await fs.mkdir(destDir, { recursive: true });
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const srcPath = path2.join(srcDir, entry.name);
|
|
196
|
+
const destPath = path2.join(destDir, restoreFilename(entry.name));
|
|
197
|
+
if (entry.isDirectory()) {
|
|
198
|
+
await copyTemplate(srcPath, destPath);
|
|
199
|
+
} else {
|
|
200
|
+
await fs.mkdir(path2.dirname(destPath), { recursive: true });
|
|
201
|
+
await fs.copyFile(srcPath, destPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/utils/packageJson.ts
|
|
207
|
+
import { promises as fs2 } from "fs";
|
|
208
|
+
function addDependencies(pkg, deps) {
|
|
209
|
+
pkg.dependencies = { ...pkg.dependencies, ...deps };
|
|
210
|
+
}
|
|
211
|
+
function addDevDependencies(pkg, deps) {
|
|
212
|
+
pkg.devDependencies = { ...pkg.devDependencies, ...deps };
|
|
213
|
+
}
|
|
214
|
+
function addScripts(pkg, scripts) {
|
|
215
|
+
pkg.scripts = { ...pkg.scripts, ...scripts };
|
|
216
|
+
}
|
|
217
|
+
function sortRecord(record) {
|
|
218
|
+
return Object.fromEntries(Object.entries(record).sort(([a], [b]) => a.localeCompare(b)));
|
|
219
|
+
}
|
|
220
|
+
function finalizePackageJson(pkg) {
|
|
221
|
+
return {
|
|
222
|
+
...pkg,
|
|
223
|
+
dependencies: sortRecord(pkg.dependencies),
|
|
224
|
+
devDependencies: sortRecord(pkg.devDependencies)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
async function mergePackageDependencies(pkgPath, deps, devDeps) {
|
|
228
|
+
const pkg = JSON.parse(await fs2.readFile(pkgPath, "utf8"));
|
|
229
|
+
pkg.dependencies = sortRecord({ ...pkg.dependencies, ...deps });
|
|
230
|
+
pkg.devDependencies = sortRecord({ ...pkg.devDependencies, ...devDeps });
|
|
231
|
+
await fs2.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// src/utils/env.ts
|
|
235
|
+
import { promises as fs3 } from "fs";
|
|
236
|
+
import path3 from "path";
|
|
237
|
+
function addEnv(env, entry) {
|
|
238
|
+
const existing = env.findIndex((e) => e.key === entry.key);
|
|
239
|
+
if (existing === -1) {
|
|
240
|
+
env.push(entry);
|
|
241
|
+
} else {
|
|
242
|
+
env[existing] = entry;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
function renderEnvFile(env, useExampleValues) {
|
|
246
|
+
const lines = [
|
|
247
|
+
"# Generated by Stackr. Adjust values as needed.",
|
|
248
|
+
""
|
|
249
|
+
];
|
|
250
|
+
for (const entry of env) {
|
|
251
|
+
if (entry.comment) lines.push(`# ${entry.comment}`);
|
|
252
|
+
const value = useExampleValues ? entry.exampleValue ?? entry.value : entry.value;
|
|
253
|
+
lines.push(`${entry.key}=${value}`);
|
|
254
|
+
lines.push("");
|
|
255
|
+
}
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
async function writeEnvFiles(projectDir, env) {
|
|
259
|
+
await fs3.writeFile(path3.join(projectDir, ".env"), renderEnvFile(env, false), "utf8");
|
|
260
|
+
await fs3.writeFile(
|
|
261
|
+
path3.join(projectDir, ".env.example"),
|
|
262
|
+
renderEnvFile(env, true),
|
|
263
|
+
"utf8"
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/installers/database.ts
|
|
268
|
+
import path4 from "path";
|
|
269
|
+
var CONFIG = {
|
|
270
|
+
postgres: {
|
|
271
|
+
dir: "postgres",
|
|
272
|
+
url: "postgresql://postgres:postgres@localhost:5432/app?schema=public",
|
|
273
|
+
comment: "Postgres connection string (matches the bundled docker-compose.yml)"
|
|
274
|
+
},
|
|
275
|
+
mysql: {
|
|
276
|
+
dir: "mysql",
|
|
277
|
+
url: "mysql://root:root@localhost:3306/app",
|
|
278
|
+
comment: "MySQL connection string (matches the bundled docker-compose.yml)"
|
|
279
|
+
},
|
|
280
|
+
mongodb: {
|
|
281
|
+
dir: "mongodb",
|
|
282
|
+
url: "mongodb://localhost:27017/app",
|
|
283
|
+
comment: "MongoDB connection string (matches the bundled docker-compose.yml)"
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
var databaseInstaller = async (ctx) => {
|
|
287
|
+
const config = CONFIG[ctx.selections.database];
|
|
288
|
+
if (!config) return;
|
|
289
|
+
await copyTemplate(path4.join(ctx.templateDir, "extras", "databases", config.dir), ctx.projectDir);
|
|
290
|
+
addEnv(ctx.env, { key: "DATABASE_URL", value: config.url, comment: config.comment });
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
// src/installers/prisma.ts
|
|
294
|
+
import path6 from "path";
|
|
295
|
+
|
|
296
|
+
// src/utils/prismaSchema.ts
|
|
297
|
+
import path5 from "path";
|
|
298
|
+
import { promises as fs4 } from "fs";
|
|
299
|
+
var PROVIDER = {
|
|
300
|
+
postgres: "postgresql",
|
|
301
|
+
mysql: "mysql"
|
|
302
|
+
};
|
|
303
|
+
async function setPrismaProvider(projectDir, database) {
|
|
304
|
+
const provider = PROVIDER[database];
|
|
305
|
+
if (!provider || provider === "postgresql") return;
|
|
306
|
+
const schemaPath = path5.join(projectDir, "prisma", "schema.prisma");
|
|
307
|
+
const schema = await fs4.readFile(schemaPath, "utf8");
|
|
308
|
+
await fs4.writeFile(
|
|
309
|
+
schemaPath,
|
|
310
|
+
schema.replace('provider = "postgresql"', `provider = "${provider}"`),
|
|
311
|
+
"utf8"
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/installers/prisma.ts
|
|
316
|
+
var ADAPTER = {
|
|
317
|
+
postgres: {
|
|
318
|
+
deps: { "@prisma/adapter-pg": "^7.8.0", pg: "^8.13.1" },
|
|
319
|
+
devDeps: { "@types/pg": "^8.11.10" }
|
|
320
|
+
},
|
|
321
|
+
mysql: {
|
|
322
|
+
deps: { "@prisma/adapter-mariadb": "^7.8.0" },
|
|
323
|
+
devDeps: {}
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
var prismaInstaller = async (ctx) => {
|
|
327
|
+
await copyTemplate(path6.join(ctx.templateDir, "extras", "prisma", "base"), ctx.projectDir);
|
|
328
|
+
await setPrismaProvider(ctx.projectDir, ctx.selections.database);
|
|
329
|
+
const engine = ctx.selections.database === "mysql" ? "mysql" : "postgres";
|
|
330
|
+
await copyTemplate(path6.join(ctx.templateDir, "extras", "prisma", engine), ctx.projectDir);
|
|
331
|
+
const adapter = ADAPTER[engine];
|
|
332
|
+
addDependencies(ctx.pkg, { "@prisma/client": "^7.8.0", ...adapter.deps });
|
|
333
|
+
addDevDependencies(ctx.pkg, { prisma: "^7.8.0", dotenv: "^16.4.7", ...adapter.devDeps });
|
|
334
|
+
addScripts(ctx.pkg, {
|
|
335
|
+
"db:push": "prisma db push",
|
|
336
|
+
"db:migrate": "prisma migrate dev",
|
|
337
|
+
"db:studio": "prisma studio",
|
|
338
|
+
// Ensure the client is generated after install and before `next build`.
|
|
339
|
+
postinstall: "prisma generate"
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// src/installers/drizzle.ts
|
|
344
|
+
import path7 from "path";
|
|
345
|
+
var DRIVER = {
|
|
346
|
+
postgres: {
|
|
347
|
+
deps: { pg: "^8.13.1" },
|
|
348
|
+
devDeps: { "@types/pg": "^8.11.10" }
|
|
349
|
+
},
|
|
350
|
+
mysql: {
|
|
351
|
+
deps: { mysql2: "^3.12.0" },
|
|
352
|
+
devDeps: {}
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
var drizzleInstaller = async (ctx) => {
|
|
356
|
+
const dialect = ctx.selections.database === "mysql" ? "mysql" : "postgres";
|
|
357
|
+
await copyTemplate(path7.join(ctx.templateDir, "extras", "drizzle", dialect), ctx.projectDir);
|
|
358
|
+
const driver = DRIVER[dialect];
|
|
359
|
+
addDependencies(ctx.pkg, { "drizzle-orm": "^0.38.3", ...driver.deps });
|
|
360
|
+
addDevDependencies(ctx.pkg, { "drizzle-kit": "^0.30.1", dotenv: "^16.4.7", ...driver.devDeps });
|
|
361
|
+
addScripts(ctx.pkg, {
|
|
362
|
+
"db:push": "drizzle-kit push",
|
|
363
|
+
"db:generate": "drizzle-kit generate",
|
|
364
|
+
"db:studio": "drizzle-kit studio"
|
|
365
|
+
});
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
// src/installers/mongoose.ts
|
|
369
|
+
import path8 from "path";
|
|
370
|
+
var mongooseInstaller = async (ctx) => {
|
|
371
|
+
await copyTemplate(path8.join(ctx.templateDir, "extras", "mongoose"), ctx.projectDir);
|
|
372
|
+
addDependencies(ctx.pkg, { mongoose: "^8.9.3" });
|
|
373
|
+
};
|
|
374
|
+
|
|
375
|
+
// src/installers/nextauth.ts
|
|
376
|
+
import path9 from "path";
|
|
377
|
+
import { randomBytes } from "crypto";
|
|
378
|
+
var nextauthInstaller = async (ctx) => {
|
|
379
|
+
await copyTemplate(path9.join(ctx.templateDir, "extras", "nextauth"), ctx.projectDir);
|
|
380
|
+
const overlay = `nextauth-${ctx.selections.orm}`;
|
|
381
|
+
await copyTemplate(path9.join(ctx.templateDir, "extras", overlay), ctx.projectDir);
|
|
382
|
+
addDependencies(ctx.pkg, {
|
|
383
|
+
"next-auth": "^5.0.0-beta.25",
|
|
384
|
+
bcryptjs: "^2.4.3",
|
|
385
|
+
zod: "^3.24.1"
|
|
386
|
+
});
|
|
387
|
+
if (ctx.selections.orm === "prisma") {
|
|
388
|
+
addDependencies(ctx.pkg, { "@auth/prisma-adapter": "^2.7.4" });
|
|
389
|
+
}
|
|
390
|
+
addDevDependencies(ctx.pkg, { "@types/bcryptjs": "^2.4.6" });
|
|
391
|
+
addEnv(ctx.env, {
|
|
392
|
+
key: "AUTH_SECRET",
|
|
393
|
+
value: randomBytes(32).toString("base64"),
|
|
394
|
+
exampleValue: "generate-with-openssl-rand-base64-32",
|
|
395
|
+
comment: "Secret used by Auth.js to encrypt JWTs. Keep this private."
|
|
396
|
+
});
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/installers/better-auth.ts
|
|
400
|
+
import path10 from "path";
|
|
401
|
+
import { promises as fs5 } from "fs";
|
|
402
|
+
import { randomBytes as randomBytes2 } from "crypto";
|
|
403
|
+
var ADAPTER_PROVIDER = {
|
|
404
|
+
postgres: "postgresql",
|
|
405
|
+
mysql: "mysql"
|
|
406
|
+
};
|
|
407
|
+
var betterAuthInstaller = async (ctx) => {
|
|
408
|
+
await copyTemplate(path10.join(ctx.templateDir, "extras", "better-auth"), ctx.projectDir);
|
|
409
|
+
await setPrismaProvider(ctx.projectDir, ctx.selections.database);
|
|
410
|
+
const provider = ADAPTER_PROVIDER[ctx.selections.database] ?? "postgresql";
|
|
411
|
+
if (provider !== "postgresql") {
|
|
412
|
+
const authPath = path10.join(ctx.projectDir, "src", "server", "auth.ts");
|
|
413
|
+
const auth = await fs5.readFile(authPath, "utf8");
|
|
414
|
+
await fs5.writeFile(authPath, auth.replace('provider: "postgresql"', `provider: "${provider}"`), "utf8");
|
|
415
|
+
}
|
|
416
|
+
addDependencies(ctx.pkg, { "better-auth": "^1.2.7" });
|
|
417
|
+
addEnv(ctx.env, {
|
|
418
|
+
key: "BETTER_AUTH_SECRET",
|
|
419
|
+
value: randomBytes2(32).toString("base64"),
|
|
420
|
+
exampleValue: "generate-with-openssl-rand-base64-32",
|
|
421
|
+
comment: "Secret Better Auth uses to sign sessions. Keep this private."
|
|
422
|
+
});
|
|
423
|
+
addEnv(ctx.env, {
|
|
424
|
+
key: "BETTER_AUTH_URL",
|
|
425
|
+
value: "http://localhost:3000",
|
|
426
|
+
comment: "Base URL of the app (used by Better Auth for callbacks)."
|
|
427
|
+
});
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/installers/clerk.ts
|
|
431
|
+
import path11 from "path";
|
|
432
|
+
var clerkInstaller = async (ctx) => {
|
|
433
|
+
await copyTemplate(path11.join(ctx.templateDir, "extras", "clerk"), ctx.projectDir);
|
|
434
|
+
addDependencies(ctx.pkg, { "@clerk/nextjs": "^6.9.6" });
|
|
435
|
+
addEnv(ctx.env, {
|
|
436
|
+
key: "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY",
|
|
437
|
+
// Valid-format placeholder (decodes to clerk.example.com$) so the app builds.
|
|
438
|
+
value: "pk_test_Y2xlcmsuZXhhbXBsZS5jb20k",
|
|
439
|
+
exampleValue: "pk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
440
|
+
comment: "Clerk publishable key (from the Clerk dashboard)."
|
|
441
|
+
});
|
|
442
|
+
addEnv(ctx.env, {
|
|
443
|
+
key: "CLERK_SECRET_KEY",
|
|
444
|
+
value: "sk_test_clerk-placeholder-secret-replace-me",
|
|
445
|
+
exampleValue: "sk_test_xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
|
446
|
+
comment: "Clerk secret key (from the Clerk dashboard). Keep this private."
|
|
447
|
+
});
|
|
448
|
+
addEnv(ctx.env, {
|
|
449
|
+
key: "NEXT_PUBLIC_CLERK_SIGN_IN_URL",
|
|
450
|
+
value: "/sign-in",
|
|
451
|
+
comment: "Route to the custom sign-in page."
|
|
452
|
+
});
|
|
453
|
+
addEnv(ctx.env, {
|
|
454
|
+
key: "NEXT_PUBLIC_CLERK_SIGN_UP_URL",
|
|
455
|
+
value: "/sign-up",
|
|
456
|
+
comment: "Route to the custom sign-up page."
|
|
457
|
+
});
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
// src/installers/supabase.ts
|
|
461
|
+
import path12 from "path";
|
|
462
|
+
var supabaseInstaller = async (ctx) => {
|
|
463
|
+
await copyTemplate(path12.join(ctx.templateDir, "extras", "supabase"), ctx.projectDir);
|
|
464
|
+
addDependencies(ctx.pkg, {
|
|
465
|
+
"@supabase/supabase-js": "^2.47.10",
|
|
466
|
+
"@supabase/ssr": "^0.5.2"
|
|
467
|
+
});
|
|
468
|
+
addEnv(ctx.env, {
|
|
469
|
+
key: "NEXT_PUBLIC_SUPABASE_URL",
|
|
470
|
+
value: "https://your-project.supabase.co",
|
|
471
|
+
exampleValue: "https://your-project.supabase.co",
|
|
472
|
+
comment: "Supabase project URL (Project Settings \u2192 API)."
|
|
473
|
+
});
|
|
474
|
+
addEnv(ctx.env, {
|
|
475
|
+
key: "NEXT_PUBLIC_SUPABASE_ANON_KEY",
|
|
476
|
+
value: "your-supabase-anon-key",
|
|
477
|
+
exampleValue: "your-supabase-anon-key",
|
|
478
|
+
comment: "Supabase anon/public key (Project Settings \u2192 API)."
|
|
479
|
+
});
|
|
480
|
+
};
|
|
481
|
+
|
|
482
|
+
// src/installers/tailwind.ts
|
|
483
|
+
import path13 from "path";
|
|
484
|
+
var TAILWIND_DEV_DEPS = {
|
|
485
|
+
tailwindcss: "^3.4.17",
|
|
486
|
+
postcss: "^8.4.49",
|
|
487
|
+
autoprefixer: "^10.4.20"
|
|
488
|
+
};
|
|
489
|
+
var tailwindInstaller = async (ctx) => {
|
|
490
|
+
const webDir = path13.join(ctx.projectDir, ctx.layout.webRoot);
|
|
491
|
+
await copyTemplate(path13.join(ctx.templateDir, "extras", "tailwind"), webDir);
|
|
492
|
+
if (ctx.layout.multiService) {
|
|
493
|
+
await mergePackageDependencies(path13.join(webDir, "package.json"), {}, TAILWIND_DEV_DEPS);
|
|
494
|
+
} else {
|
|
495
|
+
addDevDependencies(ctx.pkg, TAILWIND_DEV_DEPS);
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
// src/installers/shadcn.ts
|
|
500
|
+
import path14 from "path";
|
|
501
|
+
var SHADCN_DEPS = {
|
|
502
|
+
"class-variance-authority": "^0.7.1",
|
|
503
|
+
clsx: "^2.1.1",
|
|
504
|
+
"tailwind-merge": "^2.6.0",
|
|
505
|
+
"lucide-react": "^0.469.0"
|
|
506
|
+
};
|
|
507
|
+
var SHADCN_DEV_DEPS = {
|
|
508
|
+
tailwindcss: "^3.4.17",
|
|
509
|
+
postcss: "^8.4.49",
|
|
510
|
+
autoprefixer: "^10.4.20",
|
|
511
|
+
"tailwindcss-animate": "^1.0.7"
|
|
512
|
+
};
|
|
513
|
+
var shadcnInstaller = async (ctx) => {
|
|
514
|
+
const webDir = path14.join(ctx.projectDir, ctx.layout.webRoot);
|
|
515
|
+
await copyTemplate(path14.join(ctx.templateDir, "extras", "shadcn"), webDir);
|
|
516
|
+
if (ctx.layout.multiService) {
|
|
517
|
+
await mergePackageDependencies(path14.join(webDir, "package.json"), SHADCN_DEPS, SHADCN_DEV_DEPS);
|
|
518
|
+
} else {
|
|
519
|
+
addDependencies(ctx.pkg, SHADCN_DEPS);
|
|
520
|
+
addDevDependencies(ctx.pkg, SHADCN_DEV_DEPS);
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// src/installers/auth-pages.ts
|
|
525
|
+
import path15 from "path";
|
|
526
|
+
var authPagesInstaller = async (ctx) => {
|
|
527
|
+
if (ctx.selections.auth !== "nextauth") return;
|
|
528
|
+
await copyTemplate(path15.join(ctx.templateDir, "extras", "auth-pages"), ctx.projectDir);
|
|
529
|
+
const overlay = `auth-pages-${ctx.selections.orm}`;
|
|
530
|
+
await copyTemplate(path15.join(ctx.templateDir, "extras", overlay), ctx.projectDir);
|
|
531
|
+
};
|
|
532
|
+
|
|
533
|
+
// src/installers/account.ts
|
|
534
|
+
import path16 from "path";
|
|
535
|
+
var accountAdapterInstaller = async (ctx) => {
|
|
536
|
+
await copyTemplate(
|
|
537
|
+
path16.join(ctx.templateDir, "extras", `account-${ctx.selections.auth}`),
|
|
538
|
+
ctx.projectDir
|
|
539
|
+
);
|
|
540
|
+
if (ctx.selections.interfaces.includes("admin")) {
|
|
541
|
+
addEnv(ctx.env, {
|
|
542
|
+
key: "ADMIN_EMAILS",
|
|
543
|
+
value: "",
|
|
544
|
+
exampleValue: "you@example.com,teammate@example.com",
|
|
545
|
+
comment: "Comma-separated emails granted ADMIN access on the /admin page."
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (ctx.selections.auth === "supabase") {
|
|
549
|
+
addEnv(ctx.env, {
|
|
550
|
+
key: "SUPABASE_SERVICE_ROLE_KEY",
|
|
551
|
+
value: "your-supabase-service-role-key",
|
|
552
|
+
exampleValue: "your-supabase-service-role-key",
|
|
553
|
+
comment: "Supabase service-role key (Project Settings \u2192 API). Server-only; bypasses RLS."
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
};
|
|
557
|
+
|
|
558
|
+
// src/installers/dashboard.ts
|
|
559
|
+
import path17 from "path";
|
|
560
|
+
var dashboardInstaller = async (ctx) => {
|
|
561
|
+
await copyTemplate(path17.join(ctx.templateDir, "extras", "dashboard"), ctx.projectDir);
|
|
562
|
+
};
|
|
563
|
+
|
|
564
|
+
// src/installers/admin.ts
|
|
565
|
+
import path18 from "path";
|
|
566
|
+
var adminInstaller = async (ctx) => {
|
|
567
|
+
await copyTemplate(path18.join(ctx.templateDir, "extras", "admin"), ctx.projectDir);
|
|
568
|
+
};
|
|
569
|
+
|
|
570
|
+
// src/installers/landing.ts
|
|
571
|
+
import path19 from "path";
|
|
572
|
+
var landingInstaller = async (ctx) => {
|
|
573
|
+
await copyTemplate(path19.join(ctx.templateDir, "extras", "landing"), ctx.projectDir);
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
// src/installers/microservices.ts
|
|
577
|
+
import { randomBytes as randomBytes3 } from "crypto";
|
|
578
|
+
var microservicesInstaller = (ctx) => {
|
|
579
|
+
addEnv(ctx.env, {
|
|
580
|
+
key: "DATABASE_URL",
|
|
581
|
+
value: "postgresql://postgres:postgres@localhost:5432/app?schema=public",
|
|
582
|
+
comment: "Postgres connection string (compose overrides the host to `db`)."
|
|
583
|
+
});
|
|
584
|
+
addEnv(ctx.env, {
|
|
585
|
+
key: "AUTH_SECRET",
|
|
586
|
+
value: randomBytes3(32).toString("base64"),
|
|
587
|
+
exampleValue: "generate-with-openssl-rand-base64-32",
|
|
588
|
+
comment: "Secret the API service uses to sign session JWTs. Keep this private."
|
|
589
|
+
});
|
|
590
|
+
};
|
|
591
|
+
|
|
592
|
+
// src/installers/index.ts
|
|
593
|
+
function resolveInstallers(selections) {
|
|
594
|
+
const installers = [];
|
|
595
|
+
if (selections.architecture === "microservices") {
|
|
596
|
+
installers.push(microservicesInstaller);
|
|
597
|
+
if (selections.styling === "tailwind") installers.push(tailwindInstaller);
|
|
598
|
+
if (selections.styling === "shadcn") installers.push(shadcnInstaller);
|
|
599
|
+
return installers;
|
|
600
|
+
}
|
|
601
|
+
installers.push(databaseInstaller);
|
|
602
|
+
if (selections.orm === "prisma") installers.push(prismaInstaller);
|
|
603
|
+
if (selections.orm === "drizzle") installers.push(drizzleInstaller);
|
|
604
|
+
if (selections.orm === "mongoose") installers.push(mongooseInstaller);
|
|
605
|
+
if (selections.auth === "nextauth") installers.push(nextauthInstaller);
|
|
606
|
+
if (selections.auth === "better-auth") installers.push(betterAuthInstaller);
|
|
607
|
+
if (selections.auth === "clerk") installers.push(clerkInstaller);
|
|
608
|
+
if (selections.auth === "supabase") installers.push(supabaseInstaller);
|
|
609
|
+
if (selections.styling === "tailwind") installers.push(tailwindInstaller);
|
|
610
|
+
if (selections.styling === "shadcn") installers.push(shadcnInstaller);
|
|
611
|
+
if (selections.interfaces.includes("auth-pages")) installers.push(authPagesInstaller);
|
|
612
|
+
if (selections.interfaces.includes("dashboard") || selections.interfaces.includes("admin")) {
|
|
613
|
+
installers.push(accountAdapterInstaller);
|
|
614
|
+
}
|
|
615
|
+
if (selections.interfaces.includes("dashboard")) installers.push(dashboardInstaller);
|
|
616
|
+
if (selections.interfaces.includes("admin")) installers.push(adminInstaller);
|
|
617
|
+
if (selections.interfaces.includes("landing")) installers.push(landingInstaller);
|
|
618
|
+
return installers;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// src/shared/layout.ts
|
|
622
|
+
var LAYOUTS = {
|
|
623
|
+
monolith: { webRoot: ".", serverRoot: ".", multiService: false },
|
|
624
|
+
microservices: { webRoot: "apps/web", serverRoot: "services/api", multiService: true }
|
|
625
|
+
};
|
|
626
|
+
function getLayout(architecture) {
|
|
627
|
+
return LAYOUTS[architecture];
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/helpers/generateReadme.ts
|
|
631
|
+
function generateReadme(selections) {
|
|
632
|
+
const interfaces = selections.interfaces.length ? selections.interfaces.map((i) => `- ${optionLabel(i)}`).join("\n") : "- _(none)_";
|
|
633
|
+
const dbEngine = { postgres: "Postgres", mysql: "MySQL", mongodb: "MongoDB" }[selections.database] ?? "Postgres";
|
|
634
|
+
const gettingStarted = selections.architecture === "microservices" ? microservicesGettingStarted() : monolithGettingStarted(dbEngine, selections.orm);
|
|
635
|
+
return `# ${selections.projectName}
|
|
636
|
+
|
|
637
|
+
Generated with [Stackr](https://github.com/) \u2014 a runnable full-stack Next.js starter.
|
|
638
|
+
|
|
639
|
+
## Stack
|
|
640
|
+
|
|
641
|
+
- **Architecture:** ${optionLabel(selections.architecture)}
|
|
642
|
+
- **Database:** ${optionLabel(selections.database)}
|
|
643
|
+
- **ORM:** ${optionLabel(selections.orm)}
|
|
644
|
+
- **Auth:** ${optionLabel(selections.auth)}
|
|
645
|
+
- **Styling:** ${optionLabel(selections.styling)}
|
|
646
|
+
|
|
647
|
+
### Interfaces
|
|
648
|
+
|
|
649
|
+
${interfaces}
|
|
650
|
+
|
|
651
|
+
${gettingStarted}`;
|
|
652
|
+
}
|
|
653
|
+
function monolithGettingStarted(dbEngine, orm) {
|
|
654
|
+
const schemaStep = orm === "prisma" ? "\n# 3. Create the database schema\nnpx prisma migrate dev --name init\n" : orm === "drizzle" ? "\n# 3. Create the database schema\nnpm run db:push\n" : "";
|
|
655
|
+
const dbScriptRows = orm === "prisma" ? "| `npm run db:migrate` | Run Prisma migrations |\n| `npm run db:studio` | Open Prisma Studio |\n" : orm === "drizzle" ? "| `npm run db:push` | Push the schema to the database |\n| `npm run db:studio` | Open Drizzle Studio |\n" : "";
|
|
656
|
+
return `## Getting started
|
|
657
|
+
|
|
658
|
+
\`\`\`bash
|
|
659
|
+
# 1. Start a local ${dbEngine} (requires Docker)
|
|
660
|
+
docker compose up -d
|
|
661
|
+
|
|
662
|
+
# 2. Install dependencies
|
|
663
|
+
npm install
|
|
664
|
+
${schemaStep}
|
|
665
|
+
# Run the app
|
|
666
|
+
npm run dev
|
|
667
|
+
\`\`\`
|
|
668
|
+
|
|
669
|
+
Then open [http://localhost:3000](http://localhost:3000).
|
|
670
|
+
|
|
671
|
+
- Register a new account at \`/register\`, then sign in at \`/login\`.
|
|
672
|
+
- \`/account\` is a protected route.
|
|
673
|
+
|
|
674
|
+
## Environment
|
|
675
|
+
|
|
676
|
+
Copy \`.env.example\` to \`.env\` and adjust as needed. A working \`.env\` with a
|
|
677
|
+
generated \`AUTH_SECRET\` was already created for you.
|
|
678
|
+
|
|
679
|
+
## Scripts
|
|
680
|
+
|
|
681
|
+
| Script | Description |
|
|
682
|
+
| ------ | ----------- |
|
|
683
|
+
| \`npm run dev\` | Start the dev server |
|
|
684
|
+
| \`npm run build\` | Production build |
|
|
685
|
+
${dbScriptRows}| \`npm run typecheck\` | Type-check without emitting |
|
|
686
|
+
`;
|
|
687
|
+
}
|
|
688
|
+
function microservicesGettingStarted() {
|
|
689
|
+
return `## Architecture
|
|
690
|
+
|
|
691
|
+
This is an npm-workspaces monorepo split into independent services:
|
|
692
|
+
|
|
693
|
+
| Path | Service | Description |
|
|
694
|
+
| ---- | ------- | ----------- |
|
|
695
|
+
| \`apps/web\` | web | Next.js front-end (App Router) |
|
|
696
|
+
| \`services/api\` | api | Standalone [Hono](https://hono.dev) REST API: Prisma + Postgres, session auth |
|
|
697
|
+
| \`gateway\` | gateway | nginx reverse proxy \u2014 one public origin, \`/api/*\` \u2192 api, everything else \u2192 web |
|
|
698
|
+
|
|
699
|
+
The browser talks only to the gateway; the gateway forwards \`/api/*\` to the API
|
|
700
|
+
service (prefix stripped) and all other routes to the web app.
|
|
701
|
+
|
|
702
|
+
## Getting started (local dev)
|
|
703
|
+
|
|
704
|
+
\`\`\`bash
|
|
705
|
+
# 1. Start a local Postgres (requires Docker)
|
|
706
|
+
docker compose up -d db
|
|
707
|
+
|
|
708
|
+
# 2. Install dependencies for every workspace
|
|
709
|
+
npm install
|
|
710
|
+
|
|
711
|
+
# 3. Create the database schema (runs in services/api)
|
|
712
|
+
npm run db:migrate
|
|
713
|
+
|
|
714
|
+
# 4. Run the web app and the API together
|
|
715
|
+
npm run dev
|
|
716
|
+
\`\`\`
|
|
717
|
+
|
|
718
|
+
- web: [http://localhost:3000](http://localhost:3000) \xB7 api: [http://localhost:3001](http://localhost:3001)
|
|
719
|
+
- Register at \`/register\`, then sign in at \`/login\`; \`/account\` is protected.
|
|
720
|
+
|
|
721
|
+
## Running the full stack with the gateway
|
|
722
|
+
|
|
723
|
+
\`\`\`bash
|
|
724
|
+
docker compose up -d --build
|
|
725
|
+
\`\`\`
|
|
726
|
+
|
|
727
|
+
Then open [http://localhost:8080](http://localhost:8080) \u2014 the gateway in front of
|
|
728
|
+
web + api + Postgres.
|
|
729
|
+
|
|
730
|
+
## Environment
|
|
731
|
+
|
|
732
|
+
Copy \`.env.example\` to \`.env\` and adjust as needed. A working \`.env\` with a
|
|
733
|
+
generated \`AUTH_SECRET\` was already created for you.
|
|
734
|
+
|
|
735
|
+
## Scripts (root)
|
|
736
|
+
|
|
737
|
+
| Script | Description |
|
|
738
|
+
| ------ | ----------- |
|
|
739
|
+
| \`npm run dev\` | Start every workspace (web + api) |
|
|
740
|
+
| \`npm run build\` | Build every workspace |
|
|
741
|
+
| \`npm run db:migrate\` | Run Prisma migrations in the API service |
|
|
742
|
+
| \`npm run db:studio\` | Open Prisma Studio for the API database |
|
|
743
|
+
| \`npm run compose:up\` | Start the full stack via docker-compose |
|
|
744
|
+
`;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// src/helpers/scaffoldProject.ts
|
|
748
|
+
function architectureBase(templateDir, selections) {
|
|
749
|
+
if (selections.architecture === "microservices") {
|
|
750
|
+
return path20.join(templateDir, "architectures", "microservices");
|
|
751
|
+
}
|
|
752
|
+
return path20.join(templateDir, "base");
|
|
753
|
+
}
|
|
754
|
+
function withImpliedInterfaces(selections) {
|
|
755
|
+
const wantsAccount = selections.interfaces.includes("dashboard") || selections.interfaces.includes("admin");
|
|
756
|
+
const needsPortal = selections.auth === "nextauth" && wantsAccount && !selections.interfaces.includes("auth-pages");
|
|
757
|
+
if (!needsPortal) return selections;
|
|
758
|
+
return { ...selections, interfaces: ["auth-pages", ...selections.interfaces] };
|
|
759
|
+
}
|
|
760
|
+
async function scaffoldProject(templateDir, projectDir, inputSelections) {
|
|
761
|
+
const selections = withImpliedInterfaces(inputSelections);
|
|
762
|
+
await fs6.mkdir(projectDir, { recursive: true });
|
|
763
|
+
await copyTemplate(architectureBase(templateDir, selections), projectDir);
|
|
764
|
+
const pkgPath = path20.join(projectDir, "package.json");
|
|
765
|
+
const pkg = JSON.parse(await fs6.readFile(pkgPath, "utf8"));
|
|
766
|
+
pkg.name = selections.projectName;
|
|
767
|
+
const env = [];
|
|
768
|
+
const layout = getLayout(selections.architecture);
|
|
769
|
+
const ctx = { projectDir, templateDir, selections, layout, pkg, env };
|
|
770
|
+
for (const installer of resolveInstallers(selections)) {
|
|
771
|
+
await installer(ctx);
|
|
772
|
+
}
|
|
773
|
+
await fs6.writeFile(pkgPath, JSON.stringify(finalizePackageJson(pkg), null, 2) + "\n", "utf8");
|
|
774
|
+
await writeEnvFiles(projectDir, env);
|
|
775
|
+
await fs6.writeFile(
|
|
776
|
+
path20.join(projectDir, "README.md"),
|
|
777
|
+
generateReadme(selections),
|
|
778
|
+
"utf8"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/helpers/installDependencies.ts
|
|
783
|
+
import { spawn } from "child_process";
|
|
784
|
+
function installDependencies(projectDir) {
|
|
785
|
+
return new Promise((resolve, reject) => {
|
|
786
|
+
const child = spawn("npm", ["install"], {
|
|
787
|
+
cwd: projectDir,
|
|
788
|
+
stdio: "inherit",
|
|
789
|
+
// npm is a .cmd shim on Windows; shell:true resolves it correctly.
|
|
790
|
+
shell: process.platform === "win32"
|
|
791
|
+
});
|
|
792
|
+
child.on("error", reject);
|
|
793
|
+
child.on("close", (code) => {
|
|
794
|
+
if (code === 0) resolve();
|
|
795
|
+
else reject(new Error(`npm install exited with code ${code}`));
|
|
796
|
+
});
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
// src/helpers/initGit.ts
|
|
801
|
+
import { spawn as spawn2 } from "child_process";
|
|
802
|
+
function run(cmd, args, cwd) {
|
|
803
|
+
return new Promise((resolve, reject) => {
|
|
804
|
+
const child = spawn2(cmd, args, {
|
|
805
|
+
cwd,
|
|
806
|
+
stdio: "ignore",
|
|
807
|
+
shell: process.platform === "win32"
|
|
808
|
+
});
|
|
809
|
+
child.on("error", reject);
|
|
810
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
async function initGit(projectDir) {
|
|
814
|
+
try {
|
|
815
|
+
if (await run("git", ["init"], projectDir) !== 0) return false;
|
|
816
|
+
await run("git", ["add", "-A"], projectDir);
|
|
817
|
+
await run("git", ["commit", "-m", "Initial commit from Stackr"], projectDir);
|
|
818
|
+
return true;
|
|
819
|
+
} catch {
|
|
820
|
+
return false;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// src/helpers/logNextSteps.ts
|
|
825
|
+
import path21 from "path";
|
|
826
|
+
function logNextSteps({ projectDir, selections, depsInstalled }) {
|
|
827
|
+
const rel = path21.relative(process.cwd(), projectDir) || ".";
|
|
828
|
+
const steps = [`cd ${rel}`];
|
|
829
|
+
if (selections.architecture === "microservices") {
|
|
830
|
+
steps.push("docker compose up -d db # start local Postgres");
|
|
831
|
+
if (!depsInstalled) steps.push("npm install # installs every workspace");
|
|
832
|
+
steps.push("npm run db:migrate # migrate the API database");
|
|
833
|
+
steps.push("npm run dev # web + api together");
|
|
834
|
+
} else {
|
|
835
|
+
const engineLabel = {
|
|
836
|
+
postgres: "Postgres",
|
|
837
|
+
mysql: "MySQL",
|
|
838
|
+
mongodb: "MongoDB"
|
|
839
|
+
};
|
|
840
|
+
if (engineLabel[selections.database]) {
|
|
841
|
+
steps.push(`docker compose up -d # start local ${engineLabel[selections.database]}`);
|
|
842
|
+
}
|
|
843
|
+
if (!depsInstalled) {
|
|
844
|
+
steps.push("npm install");
|
|
845
|
+
}
|
|
846
|
+
if (selections.orm === "prisma") {
|
|
847
|
+
steps.push("npx prisma migrate dev --name init");
|
|
848
|
+
} else if (selections.orm === "drizzle") {
|
|
849
|
+
steps.push("npm run db:push # create the schema with Drizzle");
|
|
850
|
+
}
|
|
851
|
+
steps.push("npm run dev");
|
|
852
|
+
}
|
|
853
|
+
console.log("");
|
|
854
|
+
console.log(pc.bold("Next steps:"));
|
|
855
|
+
for (const step of steps) {
|
|
856
|
+
console.log(" " + pc.cyan(step));
|
|
857
|
+
}
|
|
858
|
+
console.log("");
|
|
859
|
+
return steps;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// src/cli/wizard.ts
|
|
863
|
+
import * as p from "@clack/prompts";
|
|
864
|
+
|
|
865
|
+
// src/shared/compat.ts
|
|
866
|
+
function selectedValues(selection, dimension) {
|
|
867
|
+
const raw = selection[dimension];
|
|
868
|
+
if (raw === void 0) return [];
|
|
869
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
870
|
+
}
|
|
871
|
+
function conflict(selection, dimension, value) {
|
|
872
|
+
const own = findOption(dimension, value);
|
|
873
|
+
for (const other of DIMENSIONS) {
|
|
874
|
+
if (other === dimension) continue;
|
|
875
|
+
for (const otherValue of selectedValues(selection, other)) {
|
|
876
|
+
const forward = own?.requires?.[other];
|
|
877
|
+
if (forward && !forward.includes(otherValue)) return [other, otherValue];
|
|
878
|
+
const backward = findOption(other, otherValue)?.requires?.[dimension];
|
|
879
|
+
if (backward && !backward.includes(value)) return [other, otherValue];
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
return void 0;
|
|
883
|
+
}
|
|
884
|
+
function resolveAvailability(selection) {
|
|
885
|
+
const out = {};
|
|
886
|
+
for (const dimension of DIMENSIONS) {
|
|
887
|
+
out[dimension] = REGISTRY[dimension].map((option) => {
|
|
888
|
+
const clash = conflict(selection, dimension, option.value);
|
|
889
|
+
const generatable = option.status === "stable";
|
|
890
|
+
let reason;
|
|
891
|
+
if (clash) {
|
|
892
|
+
reason = `incompatible with ${clash[0]} "${optionLabel(clash[1])}"`;
|
|
893
|
+
} else if (!generatable) {
|
|
894
|
+
reason = "coming soon";
|
|
895
|
+
}
|
|
896
|
+
return { value: option.value, enabled: !clash, generatable, reason };
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
return out;
|
|
900
|
+
}
|
|
901
|
+
function validateCompat(selection) {
|
|
902
|
+
const errors = [];
|
|
903
|
+
const seen = /* @__PURE__ */ new Set();
|
|
904
|
+
for (const dimension of DIMENSIONS) {
|
|
905
|
+
for (const value of selectedValues(selection, dimension)) {
|
|
906
|
+
const clash = conflict(selection, dimension, value);
|
|
907
|
+
if (!clash) continue;
|
|
908
|
+
const key = [dimension, value, clash[0], clash[1]].sort().join("|");
|
|
909
|
+
if (seen.has(key)) continue;
|
|
910
|
+
seen.add(key);
|
|
911
|
+
errors.push(
|
|
912
|
+
`${dimension} "${optionLabel(value)}" is incompatible with ${clash[0]} "${optionLabel(clash[1])}".`
|
|
913
|
+
);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
return errors;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// src/cli/wizard.ts
|
|
920
|
+
function bail() {
|
|
921
|
+
p.cancel("Operation cancelled.");
|
|
922
|
+
process.exit(0);
|
|
923
|
+
}
|
|
924
|
+
function unwrap(value) {
|
|
925
|
+
if (p.isCancel(value)) bail();
|
|
926
|
+
return value;
|
|
927
|
+
}
|
|
928
|
+
function buildOptions(dimension, selection) {
|
|
929
|
+
const availability = resolveAvailability(selection)[dimension];
|
|
930
|
+
return REGISTRY[dimension].map((option) => {
|
|
931
|
+
const a = availability.find((x) => x.value === option.value);
|
|
932
|
+
return { value: option.value, label: option.label, hint: a.reason };
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
function isSelectable(dimension, value, selection) {
|
|
936
|
+
const a = resolveAvailability(selection)[dimension].find((x) => x.value === value);
|
|
937
|
+
return a.enabled && a.generatable;
|
|
938
|
+
}
|
|
939
|
+
async function selectDimension(message, dimension, selection) {
|
|
940
|
+
for (; ; ) {
|
|
941
|
+
const value = unwrap(
|
|
942
|
+
await p.select({
|
|
943
|
+
message,
|
|
944
|
+
options: buildOptions(dimension, selection),
|
|
945
|
+
initialValue: DEFAULT_VALUES[dimension]
|
|
946
|
+
})
|
|
947
|
+
);
|
|
948
|
+
if (isSelectable(dimension, value, selection)) return value;
|
|
949
|
+
const reason = resolveAvailability(selection)[dimension].find((x) => x.value === value).reason;
|
|
950
|
+
p.log.warn(`"${optionLabel(value)}" is ${reason}. Please pick another.`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async function runWizard(defaults) {
|
|
954
|
+
p.intro("Stackr \u2014 scaffold a runnable full-stack Next.js project");
|
|
955
|
+
const projectName = unwrap(
|
|
956
|
+
await p.text({
|
|
957
|
+
message: "Project name?",
|
|
958
|
+
placeholder: "my-app",
|
|
959
|
+
initialValue: defaults.projectName,
|
|
960
|
+
validate: (v) => {
|
|
961
|
+
if (!v) return "Please enter a project name.";
|
|
962
|
+
if (!/^[a-z0-9][a-z0-9-_.]*$/i.test(v)) return "Use letters, numbers, '-', '_' or '.'.";
|
|
963
|
+
return void 0;
|
|
964
|
+
}
|
|
965
|
+
})
|
|
966
|
+
);
|
|
967
|
+
const selection = {};
|
|
968
|
+
selection.architecture = await selectDimension("Architecture?", "architecture", selection);
|
|
969
|
+
selection.database = await selectDimension("Database?", "database", selection);
|
|
970
|
+
selection.orm = await selectDimension("ORM / data layer?", "orm", selection);
|
|
971
|
+
selection.auth = await selectDimension("Authentication?", "auth", selection);
|
|
972
|
+
selection.styling = await selectDimension("Styling?", "styling", selection);
|
|
973
|
+
const picked = unwrap(
|
|
974
|
+
await p.multiselect({
|
|
975
|
+
message: "Pre-built interfaces? (space to toggle)",
|
|
976
|
+
options: buildOptions("interfaces", selection),
|
|
977
|
+
initialValues: ["auth-pages"],
|
|
978
|
+
required: false
|
|
979
|
+
})
|
|
980
|
+
);
|
|
981
|
+
const interfaces = [];
|
|
982
|
+
for (const value of picked) {
|
|
983
|
+
if (isSelectable("interfaces", value, selection)) interfaces.push(value);
|
|
984
|
+
else {
|
|
985
|
+
const reason = resolveAvailability(selection).interfaces.find((x) => x.value === value).reason;
|
|
986
|
+
p.log.warn(`Skipping "${optionLabel(value)}" \u2014 ${reason}.`);
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
selection.interfaces = interfaces;
|
|
990
|
+
return {
|
|
991
|
+
projectName,
|
|
992
|
+
architecture: selection.architecture,
|
|
993
|
+
database: selection.database,
|
|
994
|
+
orm: selection.orm,
|
|
995
|
+
auth: selection.auth,
|
|
996
|
+
styling: selection.styling,
|
|
997
|
+
interfaces
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// src/cli/selections.ts
|
|
1002
|
+
function projectNameFromArgs(args) {
|
|
1003
|
+
const positionals = [...args.positionals];
|
|
1004
|
+
if (positionals[0] === "create") positionals.shift();
|
|
1005
|
+
return positionals[0];
|
|
1006
|
+
}
|
|
1007
|
+
function selectionsFromFlags(args) {
|
|
1008
|
+
return {
|
|
1009
|
+
projectName: projectNameFromArgs(args) ?? "my-app",
|
|
1010
|
+
architecture: args.architecture ?? "monolith",
|
|
1011
|
+
database: args.database ?? "postgres",
|
|
1012
|
+
orm: args.orm ?? "prisma",
|
|
1013
|
+
auth: args.auth ?? "nextauth",
|
|
1014
|
+
styling: args.styling ?? "tailwind",
|
|
1015
|
+
interfaces: args.interfaces ?? ["auth-pages"]
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
function validateSelections(s) {
|
|
1019
|
+
const errors = [];
|
|
1020
|
+
const check = (dim, value) => {
|
|
1021
|
+
if (!SUPPORTED[dim].includes(value)) {
|
|
1022
|
+
errors.push(
|
|
1023
|
+
`${dim} "${value}" is not supported yet (currently generatable: ${SUPPORTED[dim].join(", ")}).`
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
};
|
|
1027
|
+
check("architecture", s.architecture);
|
|
1028
|
+
check("database", s.database);
|
|
1029
|
+
check("orm", s.orm);
|
|
1030
|
+
check("auth", s.auth);
|
|
1031
|
+
check("styling", s.styling);
|
|
1032
|
+
for (const i of s.interfaces) check("interfaces", i);
|
|
1033
|
+
errors.push(...validateCompat(s));
|
|
1034
|
+
return errors;
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// src/cli/create.ts
|
|
1038
|
+
async function isEmptyDir(dir) {
|
|
1039
|
+
try {
|
|
1040
|
+
const entries = await fs7.readdir(dir);
|
|
1041
|
+
return entries.length === 0;
|
|
1042
|
+
} catch (err) {
|
|
1043
|
+
if (err.code === "ENOENT") return true;
|
|
1044
|
+
throw err;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
async function runCreate(args) {
|
|
1048
|
+
const selections = args.yes ? selectionsFromFlags(args) : await runWizard({ projectName: projectNameFromArgs(args) });
|
|
1049
|
+
const errors = validateSelections(selections);
|
|
1050
|
+
if (errors.length > 0) {
|
|
1051
|
+
logger.error("\nStackr can't generate that stack yet:\n");
|
|
1052
|
+
for (const e of errors) logger.error(" \u2022 " + e);
|
|
1053
|
+
logger.dim("\nTry the v1 path: --architecture monolith --database postgres --orm prisma --auth nextauth --styling tailwind\n");
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
const projectDir = path22.resolve(process.cwd(), selections.projectName);
|
|
1057
|
+
if (!await isEmptyDir(projectDir)) {
|
|
1058
|
+
logger.error(`
|
|
1059
|
+
Target directory "${selections.projectName}" exists and is not empty. Aborting.`);
|
|
1060
|
+
process.exit(1);
|
|
1061
|
+
}
|
|
1062
|
+
const spinner2 = p2.spinner();
|
|
1063
|
+
spinner2.start("Scaffolding project");
|
|
1064
|
+
try {
|
|
1065
|
+
await scaffoldProject(getTemplateDir(), projectDir, selections);
|
|
1066
|
+
spinner2.stop("Project scaffolded");
|
|
1067
|
+
} catch (err) {
|
|
1068
|
+
spinner2.stop("Scaffolding failed");
|
|
1069
|
+
throw err;
|
|
1070
|
+
}
|
|
1071
|
+
let depsInstalled = false;
|
|
1072
|
+
if (args.install !== false) {
|
|
1073
|
+
const installSpinner = p2.spinner();
|
|
1074
|
+
installSpinner.start("Installing dependencies (npm install)");
|
|
1075
|
+
try {
|
|
1076
|
+
await installDependencies(projectDir);
|
|
1077
|
+
installSpinner.stop("Dependencies installed");
|
|
1078
|
+
depsInstalled = true;
|
|
1079
|
+
} catch {
|
|
1080
|
+
installSpinner.stop("Dependency install skipped (run npm install manually)");
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
if (args.git !== false) {
|
|
1084
|
+
if (await initGit(projectDir)) logger.dim("Initialized git repository.");
|
|
1085
|
+
}
|
|
1086
|
+
logger.success(`
|
|
1087
|
+
\u2714 Created ${pc.bold(selections.projectName)} (${optionLabel(selections.architecture)})`);
|
|
1088
|
+
logNextSteps({ projectDir, selections, depsInstalled });
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// src/index.ts
|
|
1092
|
+
var require2 = createRequire(import.meta.url);
|
|
1093
|
+
var { version } = require2("../package.json");
|
|
1094
|
+
var HELP = `
|
|
1095
|
+
create-stackrjs \u2014 scaffold a runnable full-stack Next.js project.
|
|
1096
|
+
|
|
1097
|
+
Usage:
|
|
1098
|
+
npm create stackrjs@latest [name] -- [options]
|
|
1099
|
+
npx create-stackrjs [name] [options]
|
|
1100
|
+
|
|
1101
|
+
Options:
|
|
1102
|
+
--architecture <a> monolith (monorepo/microservices/bff: coming soon)
|
|
1103
|
+
--database <db> postgres (mysql/sqlite/mongodb: coming soon)
|
|
1104
|
+
--orm <orm> prisma (drizzle/mongoose: coming soon)
|
|
1105
|
+
--auth <auth> nextauth (clerk/supabase/jwt: coming soon)
|
|
1106
|
+
--styling <s> tailwind (css-modules/styled-components/shadcn: coming soon)
|
|
1107
|
+
--interfaces <list> comma-separated; v1: auth-pages
|
|
1108
|
+
-y, --yes skip the wizard, use flags + defaults
|
|
1109
|
+
--ci non-interactive: implies --yes --no-install --no-git
|
|
1110
|
+
--no-install do not run npm install
|
|
1111
|
+
--no-git do not initialize a git repository
|
|
1112
|
+
-h, --help show this help
|
|
1113
|
+
-v, --version show version
|
|
1114
|
+
|
|
1115
|
+
Examples:
|
|
1116
|
+
npm create stackrjs@latest my-app
|
|
1117
|
+
npx create-stackrjs my-app --yes
|
|
1118
|
+
npx create-stackrjs demo --ci
|
|
1119
|
+
`;
|
|
1120
|
+
async function main() {
|
|
1121
|
+
const args = parseArgs(process.argv.slice(2));
|
|
1122
|
+
if (args.version) {
|
|
1123
|
+
console.log(version);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
if (args.help) {
|
|
1127
|
+
console.log(HELP);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
await runCreate(args);
|
|
1131
|
+
}
|
|
1132
|
+
main().catch((err) => {
|
|
1133
|
+
logger.error("\n" + (err instanceof Error ? err.stack ?? err.message : String(err)));
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
});
|