create-stackrjs 1.5.0 → 2.0.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 +23 -8
- package/dist/index.js +341 -278
- package/package.json +3 -2
- package/template/extras/auth-pages/src/components/auth/ui.tsx +0 -57
- package/template/extras/supabase/src/components/auth/ui.tsx +0 -58
- /package/template/extras/{better-auth → auth-ui}/src/components/auth/ui.tsx +0 -0
package/README.md
CHANGED
|
@@ -40,24 +40,27 @@ npm create stackrjs@latest [name] -- [options]
|
|
|
40
40
|
# or: npx create-stackrjs [name] [options]
|
|
41
41
|
|
|
42
42
|
--architecture <a> monolith | microservices
|
|
43
|
-
--database <db> postgres | mysql | mongodb
|
|
44
|
-
--orm <orm> prisma | drizzle | mongoose
|
|
43
|
+
--database <db> postgres | mysql | mongodb | none
|
|
44
|
+
--orm <orm> prisma | drizzle | mongoose | none
|
|
45
45
|
--auth <auth> nextauth | better-auth | clerk | supabase
|
|
46
46
|
--styling <s> tailwind | shadcn (shadcn ships the full Tailwind stack)
|
|
47
|
-
--interfaces <list> auth-pages,dashboard,admin,landing
|
|
47
|
+
--interfaces <list> auth-pages,dashboard,admin,landing (`--interfaces=` for none)
|
|
48
48
|
-y, --yes skip the wizard, use flags + defaults
|
|
49
49
|
--ci non-interactive: implies --yes --no-install --no-git
|
|
50
50
|
--no-install skip npm install
|
|
51
51
|
--no-git skip git init
|
|
52
52
|
```
|
|
53
53
|
|
|
54
|
-
Run with no options for an interactive wizard (`@clack/prompts`).
|
|
54
|
+
Run with no options for an interactive wizard (`@clack/prompts`). The flag list
|
|
55
|
+
above is printed by `--help`, generated from the registry so it can't drift. An
|
|
56
|
+
unknown flag is an error, not a silently-ignored token.
|
|
55
57
|
|
|
56
58
|
### Compatible by construction
|
|
57
59
|
|
|
58
60
|
The dimensions are linked, so you can't assemble an incompatible stack. Picking a
|
|
59
61
|
technology constrains the rest (MongoDB rules out Drizzle; Supabase Auth needs
|
|
60
|
-
Postgres; shadcn implies Tailwind
|
|
62
|
+
Postgres; shadcn implies Tailwind; the microservices bundle ships its own data
|
|
63
|
+
layer, auth and pages, so it pins every other dimension but styling). Incompatible flag combinations are rejected
|
|
61
64
|
with a clear message, and both the wizard and the [web configurator](#web-configurator)
|
|
62
65
|
grey out options that clash with your current choices or aren't generatable yet.
|
|
63
66
|
The catalogue + rules live in one place — `src/shared/registry.ts` — consumed by the
|
|
@@ -89,9 +92,16 @@ npm run dev # http://localhost:3000
|
|
|
89
92
|
```
|
|
90
93
|
|
|
91
94
|
The configurator reads the same option catalogue + compatibility rules as the CLI.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
Since the deployable Next app compiles nothing from outside `web/`, the shared
|
|
96
|
+
modules are generated into it:
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
npm run gen:web # src/shared/{registry,compat}.ts -> web/src/lib/shared/
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
`web/src/lib/stack.ts` holds only the UI on top (grouping, `reconcile`, the command
|
|
103
|
+
builder). CI re-runs the generator and fails on any diff, so `src/shared/registry.ts`
|
|
104
|
+
stays the single source of truth.
|
|
95
105
|
|
|
96
106
|
## Develop Stackr
|
|
97
107
|
|
|
@@ -99,6 +109,7 @@ fails if the two ever drift, so the registry stays the single source of truth.
|
|
|
99
109
|
npm install
|
|
100
110
|
npm run build # bundle the CLI with tsup -> dist/
|
|
101
111
|
npm run typecheck # type-check the engine
|
|
112
|
+
npm run gen:web # copy src/shared into the web configurator
|
|
102
113
|
npm run smoke # generate every stack and verify each installs + builds
|
|
103
114
|
node dist/index.js demo --ci # try it locally
|
|
104
115
|
```
|
|
@@ -112,6 +123,7 @@ npm test # engine: flags, selections, package.json/env merge, install
|
|
|
112
123
|
# plus an integration test that scaffolds a project to a temp dir
|
|
113
124
|
npm run smoke # heavier: generate each stack (monolith, shadcn, microservices),
|
|
114
125
|
# then npm install, tsc and build the output
|
|
126
|
+
npm run smoke -- clerk # a single case; `-- --list` prints the case names
|
|
115
127
|
cd web && npm test # the command builder
|
|
116
128
|
```
|
|
117
129
|
|
|
@@ -121,6 +133,9 @@ Two GitHub Actions workflows:
|
|
|
121
133
|
|
|
122
134
|
- **`.github/workflows/ci.yml`** — on every push/PR: typecheck, test and build the CLI
|
|
123
135
|
(Node 18/20/22) and the web app.
|
|
136
|
+
- **`.github/workflows/smoke.yml`** — nightly, on demand, and on any PR touching
|
|
137
|
+
`template/**` or the installers: generates every stack in a parallel job matrix and
|
|
138
|
+
checks each one installs, type-checks and builds.
|
|
124
139
|
- **`.github/workflows/release.yml`** — on push to `main`: runs the checks, then
|
|
125
140
|
[`semantic-release`](https://semantic-release.gitbook.io) versions, publishes to npm
|
|
126
141
|
(with provenance), tags, writes `CHANGELOG.md` and creates a GitHub Release —
|
package/dist/index.js
CHANGED
|
@@ -3,12 +3,28 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire } from "module";
|
|
5
5
|
|
|
6
|
+
// src/cli/flags.ts
|
|
7
|
+
import { parseArgs as nodeParseArgs } from "util";
|
|
8
|
+
|
|
6
9
|
// src/shared/registry.ts
|
|
7
10
|
var REGISTRY = {
|
|
8
11
|
architecture: [
|
|
9
12
|
{ value: "monolith", label: "Monolith", status: "stable" },
|
|
10
|
-
// The microservices bundle ships its own Postgres
|
|
11
|
-
|
|
13
|
+
// The microservices bundle is a fixed stack: it ships its own Prisma/Postgres
|
|
14
|
+
// data layer, its own session auth in the API service, and its own web pages.
|
|
15
|
+
// The other dimensions can't be layered onto it, so it pins them (and an empty
|
|
16
|
+
// `interfaces` list means "no interface option fits").
|
|
17
|
+
{
|
|
18
|
+
value: "microservices",
|
|
19
|
+
label: "Microservices",
|
|
20
|
+
status: "stable",
|
|
21
|
+
requires: {
|
|
22
|
+
database: ["postgres"],
|
|
23
|
+
orm: ["prisma"],
|
|
24
|
+
auth: ["nextauth"],
|
|
25
|
+
interfaces: []
|
|
26
|
+
}
|
|
27
|
+
}
|
|
12
28
|
],
|
|
13
29
|
database: [
|
|
14
30
|
{ value: "postgres", label: "PostgreSQL", status: "stable" },
|
|
@@ -79,9 +95,6 @@ var OPTION_LABELS = Object.fromEntries(
|
|
|
79
95
|
DIMENSIONS.flatMap((d) => REGISTRY[d].map((o) => [o.value, o.label]))
|
|
80
96
|
);
|
|
81
97
|
var optionLabel = (value) => OPTION_LABELS[value] ?? value;
|
|
82
|
-
var ALL_VALUES = Object.fromEntries(
|
|
83
|
-
DIMENSIONS.map((d) => [d, REGISTRY[d].map((o) => o.value)])
|
|
84
|
-
);
|
|
85
98
|
var STABLE_VALUES = Object.fromEntries(
|
|
86
99
|
DIMENSIONS.map((d) => [d, REGISTRY[d].filter((o) => o.status === "stable").map((o) => o.value)])
|
|
87
100
|
);
|
|
@@ -93,75 +106,48 @@ function findOption(dimension, value) {
|
|
|
93
106
|
}
|
|
94
107
|
|
|
95
108
|
// src/cli/flags.ts
|
|
96
|
-
var
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
"
|
|
103
|
-
"
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const eq = key.indexOf("=");
|
|
117
|
-
if (eq !== -1) {
|
|
118
|
-
value = key.slice(eq + 1);
|
|
119
|
-
key = key.slice(0, eq);
|
|
120
|
-
}
|
|
121
|
-
switch (key) {
|
|
122
|
-
case "help":
|
|
123
|
-
case "h":
|
|
124
|
-
result.help = true;
|
|
125
|
-
break;
|
|
126
|
-
case "version":
|
|
127
|
-
case "v":
|
|
128
|
-
result.version = true;
|
|
129
|
-
break;
|
|
130
|
-
case "yes":
|
|
131
|
-
case "y":
|
|
132
|
-
result.yes = true;
|
|
133
|
-
break;
|
|
134
|
-
case "ci":
|
|
135
|
-
result.yes = true;
|
|
136
|
-
result.install = false;
|
|
137
|
-
result.git = false;
|
|
138
|
-
break;
|
|
139
|
-
case "install":
|
|
140
|
-
result.install = true;
|
|
141
|
-
break;
|
|
142
|
-
case "no-install":
|
|
143
|
-
result.install = false;
|
|
144
|
-
break;
|
|
145
|
-
case "git":
|
|
146
|
-
result.git = true;
|
|
147
|
-
break;
|
|
148
|
-
case "no-git":
|
|
149
|
-
result.git = false;
|
|
150
|
-
break;
|
|
151
|
-
default: {
|
|
152
|
-
if (VALUE_FLAGS.has(key)) {
|
|
153
|
-
if (value === void 0) value = argv[++i];
|
|
154
|
-
if (key === "interfaces") {
|
|
155
|
-
result.interfaces = (value ?? "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
156
|
-
} else {
|
|
157
|
-
result[key] = value;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
109
|
+
var UsageError = class extends Error {
|
|
110
|
+
};
|
|
111
|
+
var OPTIONS = {
|
|
112
|
+
...Object.fromEntries(
|
|
113
|
+
DIMENSIONS.map((d) => [d, { type: "string" }])
|
|
114
|
+
),
|
|
115
|
+
yes: { type: "boolean", short: "y" },
|
|
116
|
+
ci: { type: "boolean" },
|
|
117
|
+
install: { type: "boolean" },
|
|
118
|
+
"no-install": { type: "boolean" },
|
|
119
|
+
git: { type: "boolean" },
|
|
120
|
+
"no-git": { type: "boolean" },
|
|
121
|
+
help: { type: "boolean", short: "h" },
|
|
122
|
+
version: { type: "boolean", short: "v" }
|
|
123
|
+
};
|
|
124
|
+
function strictParse(argv) {
|
|
125
|
+
try {
|
|
126
|
+
return nodeParseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new UsageError(err instanceof Error ? err.message : String(err));
|
|
162
129
|
}
|
|
163
|
-
|
|
164
|
-
|
|
130
|
+
}
|
|
131
|
+
function parseArgs(argv) {
|
|
132
|
+
const { values, positionals } = strictParse(argv);
|
|
133
|
+
const ci = values.ci === true;
|
|
134
|
+
const flag = (on, off) => on === true ? true : off === true || ci ? false : void 0;
|
|
135
|
+
const install = flag(values.install, values["no-install"]);
|
|
136
|
+
const git = flag(values.git, values["no-git"]);
|
|
137
|
+
return {
|
|
138
|
+
positionals,
|
|
139
|
+
architecture: values.architecture,
|
|
140
|
+
database: values.database,
|
|
141
|
+
orm: values.orm,
|
|
142
|
+
auth: values.auth,
|
|
143
|
+
styling: values.styling,
|
|
144
|
+
interfaces: values.interfaces === void 0 ? void 0 : values.interfaces.split(",").map((i) => i.trim()).filter(Boolean),
|
|
145
|
+
yes: values.yes === true || ci,
|
|
146
|
+
install,
|
|
147
|
+
git,
|
|
148
|
+
help: values.help === true,
|
|
149
|
+
version: values.version === true
|
|
150
|
+
};
|
|
165
151
|
}
|
|
166
152
|
|
|
167
153
|
// src/cli/create.ts
|
|
@@ -180,13 +166,32 @@ function getTemplateDir() {
|
|
|
180
166
|
// src/utils/logger.ts
|
|
181
167
|
import pc from "picocolors";
|
|
182
168
|
var logger = {
|
|
183
|
-
info: (msg) => console.log(msg),
|
|
184
169
|
success: (msg) => console.log(pc.green(msg)),
|
|
185
|
-
warn: (msg) => console.log(pc.yellow(msg)),
|
|
186
170
|
error: (msg) => console.error(pc.red(msg)),
|
|
187
171
|
dim: (msg) => console.log(pc.dim(msg))
|
|
188
172
|
};
|
|
189
173
|
|
|
174
|
+
// src/utils/packageManager.ts
|
|
175
|
+
function detectPackageManager() {
|
|
176
|
+
const agent = process.env.npm_config_user_agent ?? "";
|
|
177
|
+
if (agent.startsWith("pnpm")) return "pnpm";
|
|
178
|
+
if (agent.startsWith("yarn")) return "yarn";
|
|
179
|
+
if (agent.startsWith("bun")) return "bun";
|
|
180
|
+
return "npm";
|
|
181
|
+
}
|
|
182
|
+
function execCommand(pm) {
|
|
183
|
+
switch (pm) {
|
|
184
|
+
case "pnpm":
|
|
185
|
+
return "pnpm exec";
|
|
186
|
+
case "yarn":
|
|
187
|
+
return "yarn";
|
|
188
|
+
case "bun":
|
|
189
|
+
return "bunx";
|
|
190
|
+
case "npm":
|
|
191
|
+
return "npx";
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
190
195
|
// src/helpers/scaffoldProject.ts
|
|
191
196
|
import { promises as fs6 } from "fs";
|
|
192
197
|
import path20 from "path";
|
|
@@ -488,57 +493,46 @@ var supabaseInstaller = async (ctx) => {
|
|
|
488
493
|
});
|
|
489
494
|
};
|
|
490
495
|
|
|
491
|
-
// src/installers/
|
|
496
|
+
// src/installers/styling.ts
|
|
492
497
|
import path13 from "path";
|
|
493
498
|
var TAILWIND_DEV_DEPS = {
|
|
494
499
|
tailwindcss: "^3.4.17",
|
|
495
500
|
postcss: "^8.4.49",
|
|
496
501
|
autoprefixer: "^10.4.20"
|
|
497
502
|
};
|
|
498
|
-
var tailwindInstaller = async (ctx) => {
|
|
499
|
-
const webDir = path13.join(ctx.projectDir, ctx.layout.webRoot);
|
|
500
|
-
await copyTemplate(path13.join(ctx.templateDir, "extras", "tailwind"), webDir);
|
|
501
|
-
if (ctx.layout.multiService) {
|
|
502
|
-
await mergePackageDependencies(path13.join(webDir, "package.json"), {}, TAILWIND_DEV_DEPS);
|
|
503
|
-
} else {
|
|
504
|
-
addDevDependencies(ctx.pkg, TAILWIND_DEV_DEPS);
|
|
505
|
-
}
|
|
506
|
-
};
|
|
507
|
-
|
|
508
|
-
// src/installers/shadcn.ts
|
|
509
|
-
import path14 from "path";
|
|
510
503
|
var SHADCN_DEPS = {
|
|
511
504
|
"class-variance-authority": "^0.7.1",
|
|
512
505
|
clsx: "^2.1.1",
|
|
513
506
|
"tailwind-merge": "^2.6.0",
|
|
514
507
|
"lucide-react": "^0.469.0"
|
|
515
508
|
};
|
|
516
|
-
var SHADCN_DEV_DEPS = {
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
"tailwindcss-animate": "^1.0.7"
|
|
521
|
-
};
|
|
522
|
-
var shadcnInstaller = async (ctx) => {
|
|
523
|
-
const webDir = path14.join(ctx.projectDir, ctx.layout.webRoot);
|
|
524
|
-
await copyTemplate(path14.join(ctx.templateDir, "extras", "shadcn"), webDir);
|
|
509
|
+
var SHADCN_DEV_DEPS = { ...TAILWIND_DEV_DEPS, "tailwindcss-animate": "^1.0.7" };
|
|
510
|
+
async function installStyling(ctx, templateName, deps, devDeps) {
|
|
511
|
+
const webDir = path13.join(ctx.projectDir, ctx.layout.webRoot);
|
|
512
|
+
await copyTemplate(path13.join(ctx.templateDir, "extras", templateName), webDir);
|
|
525
513
|
if (ctx.layout.multiService) {
|
|
526
|
-
await mergePackageDependencies(
|
|
527
|
-
|
|
528
|
-
addDependencies(ctx.pkg, SHADCN_DEPS);
|
|
529
|
-
addDevDependencies(ctx.pkg, SHADCN_DEV_DEPS);
|
|
514
|
+
await mergePackageDependencies(path13.join(webDir, "package.json"), deps, devDeps);
|
|
515
|
+
return;
|
|
530
516
|
}
|
|
531
|
-
|
|
517
|
+
addDependencies(ctx.pkg, deps);
|
|
518
|
+
addDevDependencies(ctx.pkg, devDeps);
|
|
519
|
+
}
|
|
520
|
+
var tailwindInstaller = (ctx) => installStyling(ctx, "tailwind", {}, TAILWIND_DEV_DEPS);
|
|
521
|
+
var shadcnInstaller = (ctx) => installStyling(ctx, "shadcn", SHADCN_DEPS, SHADCN_DEV_DEPS);
|
|
532
522
|
|
|
533
523
|
// src/installers/auth-pages.ts
|
|
534
|
-
import
|
|
524
|
+
import path14 from "path";
|
|
535
525
|
var authPagesInstaller = async (ctx) => {
|
|
536
526
|
if (ctx.selections.auth !== "nextauth") return;
|
|
537
|
-
await copyTemplate(
|
|
527
|
+
await copyTemplate(path14.join(ctx.templateDir, "extras", "auth-pages"), ctx.projectDir);
|
|
538
528
|
const overlay = `auth-pages-${ctx.selections.orm}`;
|
|
539
|
-
await copyTemplate(
|
|
529
|
+
await copyTemplate(path14.join(ctx.templateDir, "extras", overlay), ctx.projectDir);
|
|
540
530
|
};
|
|
541
531
|
|
|
532
|
+
// src/installers/auth-ui.ts
|
|
533
|
+
import path15 from "path";
|
|
534
|
+
var authUiInstaller = (ctx) => copyTemplate(path15.join(ctx.templateDir, "extras", "auth-ui"), ctx.projectDir);
|
|
535
|
+
|
|
542
536
|
// src/installers/account.ts
|
|
543
537
|
import path16 from "path";
|
|
544
538
|
var accountAdapterInstaller = async (ctx) => {
|
|
@@ -615,6 +609,8 @@ function resolveInstallers(selections) {
|
|
|
615
609
|
if (selections.auth === "better-auth") installers.push(betterAuthInstaller);
|
|
616
610
|
if (selections.auth === "clerk") installers.push(clerkInstaller);
|
|
617
611
|
if (selections.auth === "supabase") installers.push(supabaseInstaller);
|
|
612
|
+
const rendersAuthPages = selections.auth === "better-auth" || selections.auth === "supabase" || selections.auth === "nextauth" && selections.interfaces.includes("auth-pages");
|
|
613
|
+
if (rendersAuthPages) installers.push(authUiInstaller);
|
|
618
614
|
if (selections.styling === "tailwind") installers.push(tailwindInstaller);
|
|
619
615
|
if (selections.styling === "shadcn") installers.push(shadcnInstaller);
|
|
620
616
|
if (selections.interfaces.includes("auth-pages")) installers.push(authPagesInstaller);
|
|
@@ -636,16 +632,122 @@ function getLayout(architecture) {
|
|
|
636
632
|
return LAYOUTS[architecture];
|
|
637
633
|
}
|
|
638
634
|
|
|
635
|
+
// src/shared/compat.ts
|
|
636
|
+
function selectedValues(selection, dimension) {
|
|
637
|
+
const raw = selection[dimension];
|
|
638
|
+
if (raw === void 0) return [];
|
|
639
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
640
|
+
}
|
|
641
|
+
function conflict(selection, dimension, value) {
|
|
642
|
+
const own = findOption(dimension, value);
|
|
643
|
+
for (const other of DIMENSIONS) {
|
|
644
|
+
if (other === dimension) continue;
|
|
645
|
+
for (const otherValue of selectedValues(selection, other)) {
|
|
646
|
+
const forward = own?.requires?.[other];
|
|
647
|
+
if (forward && !forward.includes(otherValue)) return [other, otherValue];
|
|
648
|
+
const backward = findOption(other, otherValue)?.requires?.[dimension];
|
|
649
|
+
if (backward && !backward.includes(value)) return [other, otherValue];
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
return void 0;
|
|
653
|
+
}
|
|
654
|
+
function resolveAvailability(selection) {
|
|
655
|
+
const out = {};
|
|
656
|
+
for (const dimension of DIMENSIONS) {
|
|
657
|
+
out[dimension] = REGISTRY[dimension].map((option) => {
|
|
658
|
+
const clash = conflict(selection, dimension, option.value);
|
|
659
|
+
const generatable = option.status === "stable";
|
|
660
|
+
let reason;
|
|
661
|
+
if (clash) {
|
|
662
|
+
reason = `incompatible with ${clash[0]} "${optionLabel(clash[1])}"`;
|
|
663
|
+
} else if (!generatable) {
|
|
664
|
+
reason = "coming soon";
|
|
665
|
+
}
|
|
666
|
+
return { value: option.value, enabled: !clash, generatable, reason };
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
function validateCompat(selection) {
|
|
672
|
+
const errors = [];
|
|
673
|
+
const seen = /* @__PURE__ */ new Set();
|
|
674
|
+
for (const dimension of DIMENSIONS) {
|
|
675
|
+
for (const value of selectedValues(selection, dimension)) {
|
|
676
|
+
const clash = conflict(selection, dimension, value);
|
|
677
|
+
if (!clash) continue;
|
|
678
|
+
const key = [dimension, value, clash[0], clash[1]].sort().join("|");
|
|
679
|
+
if (seen.has(key)) continue;
|
|
680
|
+
seen.add(key);
|
|
681
|
+
errors.push(
|
|
682
|
+
`${dimension} "${optionLabel(value)}" is incompatible with ${clash[0]} "${optionLabel(clash[1])}".`
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return errors;
|
|
687
|
+
}
|
|
688
|
+
function impliedInterfaces(auth, interfaces) {
|
|
689
|
+
const wantsAccount = interfaces.includes("dashboard") || interfaces.includes("admin");
|
|
690
|
+
if (auth !== "nextauth" || !wantsAccount || interfaces.includes("auth-pages")) return interfaces;
|
|
691
|
+
return ["auth-pages", ...interfaces];
|
|
692
|
+
}
|
|
693
|
+
function defaultSelection(pinned) {
|
|
694
|
+
const singles = DIMENSIONS.filter((d) => d !== "interfaces");
|
|
695
|
+
const out = { ...pinned };
|
|
696
|
+
for (let pass = 0; pass < singles.length; pass++) {
|
|
697
|
+
const availability = resolveAvailability(out);
|
|
698
|
+
let changed = false;
|
|
699
|
+
for (const dimension of singles) {
|
|
700
|
+
if (pinned[dimension] !== void 0) continue;
|
|
701
|
+
const pick = availability[dimension].find((a) => a.enabled && a.generatable)?.value;
|
|
702
|
+
if (pick !== void 0 && pick !== out[dimension]) {
|
|
703
|
+
out[dimension] = pick;
|
|
704
|
+
changed = true;
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (!changed) break;
|
|
708
|
+
}
|
|
709
|
+
if (pinned.interfaces === void 0) {
|
|
710
|
+
const availability = resolveAvailability(out);
|
|
711
|
+
out.interfaces = [DEFAULT_VALUES.interfaces].filter(
|
|
712
|
+
(value) => availability.interfaces.some((a) => a.value === value && a.enabled && a.generatable)
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
return out;
|
|
716
|
+
}
|
|
717
|
+
|
|
639
718
|
// src/helpers/generateReadme.ts
|
|
640
|
-
function generateReadme(selections) {
|
|
641
|
-
const
|
|
642
|
-
const dbEngine = { postgres: "Postgres", mysql: "MySQL", mongodb: "MongoDB", none: null }[selections.database];
|
|
643
|
-
const gettingStarted = selections.architecture === "microservices" ? microservicesGettingStarted() : monolithGettingStarted(dbEngine, selections.orm);
|
|
719
|
+
function generateReadme(selections, pm) {
|
|
720
|
+
const body = selections.architecture === "microservices" ? microservicesBody(selections, pm) : monolithBody(selections, pm);
|
|
644
721
|
return `# ${selections.projectName}
|
|
645
722
|
|
|
646
|
-
Generated with [Stackr](https://github.com/) \u2014 a runnable full-stack Next.js starter.
|
|
723
|
+
Generated with [Stackr](https://github.com/Tristan-stack/stackr) \u2014 a runnable full-stack Next.js starter.
|
|
724
|
+
|
|
725
|
+
${body}`;
|
|
726
|
+
}
|
|
727
|
+
function monolithBody(selections, pm) {
|
|
728
|
+
const interfaces = selections.interfaces.length ? selections.interfaces.map((i) => `- ${optionLabel(i)}`).join("\n") : "- _(none)_";
|
|
729
|
+
const dbEngine = selections.database === "none" ? null : optionLabel(selections.database);
|
|
730
|
+
const schemaStep = selections.orm === "prisma" ? `
|
|
731
|
+
# 3. Create the database schema
|
|
732
|
+
${execCommand(pm)} prisma migrate dev --name init
|
|
733
|
+
` : selections.orm === "drizzle" ? `
|
|
734
|
+
# 3. Create the database schema
|
|
735
|
+
${pm} run db:push
|
|
736
|
+
` : "";
|
|
737
|
+
const dbScriptRows = selections.orm === "prisma" ? `| \`${pm} run db:migrate\` | Run Prisma migrations |
|
|
738
|
+
| \`${pm} run db:studio\` | Open Prisma Studio |
|
|
739
|
+
` : selections.orm === "drizzle" ? `| \`${pm} run db:push\` | Push the schema to the database |
|
|
740
|
+
| \`${pm} run db:studio\` | Open Drizzle Studio |
|
|
741
|
+
` : "";
|
|
742
|
+
const dbStep = dbEngine ? `# 1. Start a local ${dbEngine} (requires Docker)
|
|
743
|
+
docker compose up -d
|
|
647
744
|
|
|
648
|
-
|
|
745
|
+
# 2. Install dependencies
|
|
746
|
+
${pm} install
|
|
747
|
+
` : `# 1. Install dependencies
|
|
748
|
+
${pm} install
|
|
749
|
+
`;
|
|
750
|
+
return `## Stack
|
|
649
751
|
|
|
650
752
|
- **Architecture:** ${optionLabel(selections.architecture)}
|
|
651
753
|
- **Database:** ${optionLabel(selections.database)}
|
|
@@ -657,23 +759,12 @@ Generated with [Stackr](https://github.com/) \u2014 a runnable full-stack Next.j
|
|
|
657
759
|
|
|
658
760
|
${interfaces}
|
|
659
761
|
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
function monolithGettingStarted(dbEngine, orm) {
|
|
663
|
-
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" : "";
|
|
664
|
-
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" : "";
|
|
665
|
-
const dbStep = dbEngine ? `# 1. Start a local ${dbEngine} (requires Docker)
|
|
666
|
-
docker compose up -d
|
|
667
|
-
|
|
668
|
-
# 2. Install dependencies
|
|
669
|
-
npm install
|
|
670
|
-
` : "# 1. Install dependencies\nnpm install\n";
|
|
671
|
-
return `## Getting started
|
|
762
|
+
## Getting started
|
|
672
763
|
|
|
673
764
|
\`\`\`bash
|
|
674
765
|
${dbStep}${schemaStep}
|
|
675
766
|
# Run the app
|
|
676
|
-
|
|
767
|
+
${pm} run dev
|
|
677
768
|
\`\`\`
|
|
678
769
|
|
|
679
770
|
Then open [http://localhost:3000](http://localhost:3000).
|
|
@@ -690,15 +781,19 @@ generated \`AUTH_SECRET\` was already created for you.
|
|
|
690
781
|
|
|
691
782
|
| Script | Description |
|
|
692
783
|
| ------ | ----------- |
|
|
693
|
-
|
|
|
694
|
-
|
|
|
695
|
-
${dbScriptRows}|
|
|
784
|
+
| \`${pm} run dev\` | Start the dev server |
|
|
785
|
+
| \`${pm} run build\` | Production build |
|
|
786
|
+
${dbScriptRows}| \`${pm} run typecheck\` | Type-check without emitting |
|
|
696
787
|
`;
|
|
697
788
|
}
|
|
698
|
-
function
|
|
699
|
-
return `##
|
|
789
|
+
function microservicesBody(selections, pm) {
|
|
790
|
+
return `## Stack
|
|
700
791
|
|
|
701
|
-
|
|
792
|
+
- **Architecture:** ${optionLabel(selections.architecture)} (npm workspaces: web + API + gateway)
|
|
793
|
+
- **Database:** PostgreSQL (docker-compose)
|
|
794
|
+
- **ORM:** Prisma, owned by \`services/api\`
|
|
795
|
+
- **Auth:** email + password, sessions signed as JWTs by the API service
|
|
796
|
+
- **Styling:** ${optionLabel(selections.styling)}
|
|
702
797
|
|
|
703
798
|
| Path | Service | Description |
|
|
704
799
|
| ---- | ------- | ----------- |
|
|
@@ -716,13 +811,13 @@ service (prefix stripped) and all other routes to the web app.
|
|
|
716
811
|
docker compose up -d db
|
|
717
812
|
|
|
718
813
|
# 2. Install dependencies for every workspace
|
|
719
|
-
|
|
814
|
+
${pm} install
|
|
720
815
|
|
|
721
816
|
# 3. Create the database schema (runs in services/api)
|
|
722
|
-
|
|
817
|
+
${pm} run db:migrate
|
|
723
818
|
|
|
724
819
|
# 4. Run the web app and the API together
|
|
725
|
-
|
|
820
|
+
${pm} run dev
|
|
726
821
|
\`\`\`
|
|
727
822
|
|
|
728
823
|
- web: [http://localhost:3000](http://localhost:3000) \xB7 api: [http://localhost:3001](http://localhost:3001)
|
|
@@ -746,11 +841,11 @@ generated \`AUTH_SECRET\` was already created for you.
|
|
|
746
841
|
|
|
747
842
|
| Script | Description |
|
|
748
843
|
| ------ | ----------- |
|
|
749
|
-
|
|
|
750
|
-
|
|
|
751
|
-
|
|
|
752
|
-
|
|
|
753
|
-
|
|
|
844
|
+
| \`${pm} run dev\` | Start every workspace (web + api) |
|
|
845
|
+
| \`${pm} run build\` | Build every workspace |
|
|
846
|
+
| \`${pm} run db:migrate\` | Run Prisma migrations in the API service |
|
|
847
|
+
| \`${pm} run db:studio\` | Open Prisma Studio for the API database |
|
|
848
|
+
| \`${pm} run compose:up\` | Start the full stack via docker-compose |
|
|
754
849
|
`;
|
|
755
850
|
}
|
|
756
851
|
|
|
@@ -761,14 +856,11 @@ function architectureBase(templateDir, selections) {
|
|
|
761
856
|
}
|
|
762
857
|
return path20.join(templateDir, "base");
|
|
763
858
|
}
|
|
764
|
-
function
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
}
|
|
770
|
-
async function scaffoldProject(templateDir, projectDir, inputSelections) {
|
|
771
|
-
const selections = withImpliedInterfaces(inputSelections);
|
|
859
|
+
async function scaffoldProject(templateDir, projectDir, inputSelections, pm = "npm") {
|
|
860
|
+
const selections = {
|
|
861
|
+
...inputSelections,
|
|
862
|
+
interfaces: impliedInterfaces(inputSelections.auth, inputSelections.interfaces)
|
|
863
|
+
};
|
|
772
864
|
await fs6.mkdir(projectDir, { recursive: true });
|
|
773
865
|
await copyTemplate(architectureBase(templateDir, selections), projectDir);
|
|
774
866
|
const pkgPath = path20.join(projectDir, "package.json");
|
|
@@ -784,25 +876,25 @@ async function scaffoldProject(templateDir, projectDir, inputSelections) {
|
|
|
784
876
|
await writeEnvFiles(projectDir, env);
|
|
785
877
|
await fs6.writeFile(
|
|
786
878
|
path20.join(projectDir, "README.md"),
|
|
787
|
-
generateReadme(selections),
|
|
879
|
+
generateReadme(selections, pm),
|
|
788
880
|
"utf8"
|
|
789
881
|
);
|
|
790
882
|
}
|
|
791
883
|
|
|
792
884
|
// src/helpers/installDependencies.ts
|
|
793
885
|
import { spawn } from "child_process";
|
|
794
|
-
function installDependencies(projectDir) {
|
|
886
|
+
function installDependencies(projectDir, pm) {
|
|
795
887
|
return new Promise((resolve, reject) => {
|
|
796
|
-
const child = spawn(
|
|
888
|
+
const child = spawn(pm, ["install"], {
|
|
797
889
|
cwd: projectDir,
|
|
798
890
|
stdio: "inherit",
|
|
799
|
-
//
|
|
891
|
+
// Package managers are .cmd shims on Windows; shell:true resolves them.
|
|
800
892
|
shell: process.platform === "win32"
|
|
801
893
|
});
|
|
802
894
|
child.on("error", reject);
|
|
803
895
|
child.on("close", (code) => {
|
|
804
896
|
if (code === 0) resolve();
|
|
805
|
-
else reject(new Error(
|
|
897
|
+
else reject(new Error(`${pm} install exited with code ${code}`));
|
|
806
898
|
});
|
|
807
899
|
});
|
|
808
900
|
}
|
|
@@ -833,98 +925,43 @@ async function initGit(projectDir) {
|
|
|
833
925
|
|
|
834
926
|
// src/helpers/logNextSteps.ts
|
|
835
927
|
import path21 from "path";
|
|
836
|
-
function logNextSteps({ projectDir, selections, depsInstalled }) {
|
|
837
|
-
const
|
|
838
|
-
const
|
|
928
|
+
function logNextSteps({ projectDir, selections, depsInstalled, pm }) {
|
|
929
|
+
const steps = [];
|
|
930
|
+
const step = (command, comment) => steps.push(comment ? `${command.padEnd(26)}# ${comment}` : command);
|
|
931
|
+
step(`cd ${path21.relative(process.cwd(), projectDir) || "."}`);
|
|
839
932
|
if (selections.architecture === "microservices") {
|
|
840
|
-
|
|
841
|
-
if (!depsInstalled)
|
|
842
|
-
|
|
843
|
-
|
|
933
|
+
step("docker compose up -d db", "start local Postgres");
|
|
934
|
+
if (!depsInstalled) step(`${pm} install`, "installs every workspace");
|
|
935
|
+
step(`${pm} run db:migrate`, "migrate the API database");
|
|
936
|
+
step(`${pm} run dev`, "web + api together");
|
|
844
937
|
} else {
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
mysql: "MySQL",
|
|
848
|
-
mongodb: "MongoDB"
|
|
849
|
-
};
|
|
850
|
-
if (engineLabel[selections.database]) {
|
|
851
|
-
steps.push(`docker compose up -d # start local ${engineLabel[selections.database]}`);
|
|
852
|
-
}
|
|
853
|
-
if (!depsInstalled) {
|
|
854
|
-
steps.push("npm install");
|
|
938
|
+
if (selections.database !== "none") {
|
|
939
|
+
step("docker compose up -d", `start local ${optionLabel(selections.database)}`);
|
|
855
940
|
}
|
|
941
|
+
if (!depsInstalled) step(`${pm} install`);
|
|
856
942
|
if (selections.orm === "prisma") {
|
|
857
|
-
|
|
943
|
+
step(`${execCommand(pm)} prisma migrate dev --name init`);
|
|
858
944
|
} else if (selections.orm === "drizzle") {
|
|
859
|
-
|
|
945
|
+
step(`${pm} run db:push`, "create the schema with Drizzle");
|
|
860
946
|
}
|
|
861
|
-
|
|
947
|
+
step(`${pm} run dev`);
|
|
862
948
|
}
|
|
863
949
|
console.log("");
|
|
864
950
|
console.log(pc.bold("Next steps:"));
|
|
865
|
-
for (const
|
|
866
|
-
console.log(" " + pc.cyan(step));
|
|
867
|
-
}
|
|
951
|
+
for (const s of steps) console.log(" " + pc.cyan(s));
|
|
868
952
|
console.log("");
|
|
869
|
-
return steps;
|
|
870
953
|
}
|
|
871
954
|
|
|
872
955
|
// src/cli/wizard.ts
|
|
873
956
|
import * as p from "@clack/prompts";
|
|
874
957
|
|
|
875
|
-
// src/shared/
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if (
|
|
879
|
-
|
|
880
|
-
}
|
|
881
|
-
function conflict(selection, dimension, value) {
|
|
882
|
-
const own = findOption(dimension, value);
|
|
883
|
-
for (const other of DIMENSIONS) {
|
|
884
|
-
if (other === dimension) continue;
|
|
885
|
-
for (const otherValue of selectedValues(selection, other)) {
|
|
886
|
-
const forward = own?.requires?.[other];
|
|
887
|
-
if (forward && !forward.includes(otherValue)) return [other, otherValue];
|
|
888
|
-
const backward = findOption(other, otherValue)?.requires?.[dimension];
|
|
889
|
-
if (backward && !backward.includes(value)) return [other, otherValue];
|
|
890
|
-
}
|
|
891
|
-
}
|
|
958
|
+
// src/shared/projectName.ts
|
|
959
|
+
var VALID = /^[a-z0-9][a-z0-9-_.]*$/i;
|
|
960
|
+
function validateProjectName(name) {
|
|
961
|
+
if (!name) return "Please enter a project name.";
|
|
962
|
+
if (!VALID.test(name)) return "Use letters, numbers, '-', '_' or '.'.";
|
|
892
963
|
return void 0;
|
|
893
964
|
}
|
|
894
|
-
function resolveAvailability(selection) {
|
|
895
|
-
const out = {};
|
|
896
|
-
for (const dimension of DIMENSIONS) {
|
|
897
|
-
out[dimension] = REGISTRY[dimension].map((option) => {
|
|
898
|
-
const clash = conflict(selection, dimension, option.value);
|
|
899
|
-
const generatable = option.status === "stable";
|
|
900
|
-
let reason;
|
|
901
|
-
if (clash) {
|
|
902
|
-
reason = `incompatible with ${clash[0]} "${optionLabel(clash[1])}"`;
|
|
903
|
-
} else if (!generatable) {
|
|
904
|
-
reason = "coming soon";
|
|
905
|
-
}
|
|
906
|
-
return { value: option.value, enabled: !clash, generatable, reason };
|
|
907
|
-
});
|
|
908
|
-
}
|
|
909
|
-
return out;
|
|
910
|
-
}
|
|
911
|
-
function validateCompat(selection) {
|
|
912
|
-
const errors = [];
|
|
913
|
-
const seen = /* @__PURE__ */ new Set();
|
|
914
|
-
for (const dimension of DIMENSIONS) {
|
|
915
|
-
for (const value of selectedValues(selection, dimension)) {
|
|
916
|
-
const clash = conflict(selection, dimension, value);
|
|
917
|
-
if (!clash) continue;
|
|
918
|
-
const key = [dimension, value, clash[0], clash[1]].sort().join("|");
|
|
919
|
-
if (seen.has(key)) continue;
|
|
920
|
-
seen.add(key);
|
|
921
|
-
errors.push(
|
|
922
|
-
`${dimension} "${optionLabel(value)}" is incompatible with ${clash[0]} "${optionLabel(clash[1])}".`
|
|
923
|
-
);
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
return errors;
|
|
927
|
-
}
|
|
928
965
|
|
|
929
966
|
// src/cli/wizard.ts
|
|
930
967
|
function bail() {
|
|
@@ -946,7 +983,16 @@ function isSelectable(dimension, value, selection) {
|
|
|
946
983
|
const a = resolveAvailability(selection)[dimension].find((x) => x.value === value);
|
|
947
984
|
return a.enabled && a.generatable;
|
|
948
985
|
}
|
|
986
|
+
function selectable(dimension, selection) {
|
|
987
|
+
return resolveAvailability(selection)[dimension].filter((a) => a.enabled && a.generatable).map((a) => a.value);
|
|
988
|
+
}
|
|
949
989
|
async function selectDimension(message, dimension, selection) {
|
|
990
|
+
const options = selectable(dimension, selection);
|
|
991
|
+
if (options.length === 1) {
|
|
992
|
+
const only = options[0];
|
|
993
|
+
p.log.info(`${message.replace(/\?$/, "")}: only ${optionLabel(only)} fits your stack.`);
|
|
994
|
+
return only;
|
|
995
|
+
}
|
|
950
996
|
for (; ; ) {
|
|
951
997
|
const value = unwrap(
|
|
952
998
|
await p.select({
|
|
@@ -960,45 +1006,40 @@ async function selectDimension(message, dimension, selection) {
|
|
|
960
1006
|
p.log.warn(`"${optionLabel(value)}" is ${reason}. Please pick another.`);
|
|
961
1007
|
}
|
|
962
1008
|
}
|
|
963
|
-
async function runWizard(
|
|
1009
|
+
async function runWizard(defaults2) {
|
|
964
1010
|
p.intro("Stackr \u2014 scaffold a runnable full-stack Next.js project");
|
|
965
1011
|
const projectName = unwrap(
|
|
966
1012
|
await p.text({
|
|
967
1013
|
message: "Project name?",
|
|
968
1014
|
placeholder: "my-app",
|
|
969
|
-
initialValue:
|
|
970
|
-
validate:
|
|
971
|
-
if (!v) return "Please enter a project name.";
|
|
972
|
-
if (!/^[a-z0-9][a-z0-9-_.]*$/i.test(v)) return "Use letters, numbers, '-', '_' or '.'.";
|
|
973
|
-
return void 0;
|
|
974
|
-
}
|
|
1015
|
+
initialValue: defaults2.projectName,
|
|
1016
|
+
validate: validateProjectName
|
|
975
1017
|
})
|
|
976
1018
|
);
|
|
977
1019
|
const selection = {};
|
|
978
1020
|
selection.architecture = await selectDimension("Architecture?", "architecture", selection);
|
|
979
1021
|
selection.database = await selectDimension("Database?", "database", selection);
|
|
980
|
-
|
|
981
|
-
selection.orm = "none";
|
|
982
|
-
p.log.info("No database selected \u2014 skipping the ORM step.");
|
|
983
|
-
} else {
|
|
984
|
-
selection.orm = await selectDimension("ORM / data layer?", "orm", selection);
|
|
985
|
-
}
|
|
1022
|
+
selection.orm = await selectDimension("ORM / data layer?", "orm", selection);
|
|
986
1023
|
selection.auth = await selectDimension("Authentication?", "auth", selection);
|
|
987
1024
|
selection.styling = await selectDimension("Styling?", "styling", selection);
|
|
988
|
-
const picked = unwrap(
|
|
989
|
-
await p.multiselect({
|
|
990
|
-
message: "Pre-built interfaces? (space to toggle)",
|
|
991
|
-
options: buildOptions("interfaces", selection),
|
|
992
|
-
initialValues: ["auth-pages"],
|
|
993
|
-
required: false
|
|
994
|
-
})
|
|
995
|
-
);
|
|
996
1025
|
const interfaces = [];
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
p.
|
|
1026
|
+
if (selectable("interfaces", selection).length === 0) {
|
|
1027
|
+
p.log.info("This stack ships its own pages \u2014 no extra interfaces to add.");
|
|
1028
|
+
} else {
|
|
1029
|
+
const picked = unwrap(
|
|
1030
|
+
await p.multiselect({
|
|
1031
|
+
message: "Pre-built interfaces? (space to toggle)",
|
|
1032
|
+
options: buildOptions("interfaces", selection),
|
|
1033
|
+
initialValues: ["auth-pages"],
|
|
1034
|
+
required: false
|
|
1035
|
+
})
|
|
1036
|
+
);
|
|
1037
|
+
for (const value of picked) {
|
|
1038
|
+
if (isSelectable("interfaces", value, selection)) interfaces.push(value);
|
|
1039
|
+
else {
|
|
1040
|
+
const reason = resolveAvailability(selection).interfaces.find((x) => x.value === value).reason;
|
|
1041
|
+
p.log.warn(`Skipping "${optionLabel(value)}" \u2014 ${reason}.`);
|
|
1042
|
+
}
|
|
1002
1043
|
}
|
|
1003
1044
|
}
|
|
1004
1045
|
selection.interfaces = interfaces;
|
|
@@ -1020,25 +1061,31 @@ function projectNameFromArgs(args) {
|
|
|
1020
1061
|
return positionals[0];
|
|
1021
1062
|
}
|
|
1022
1063
|
function selectionsFromFlags(args) {
|
|
1023
|
-
const
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1064
|
+
const pinned = {};
|
|
1065
|
+
for (const dimension of DIMENSIONS) {
|
|
1066
|
+
const value = args[dimension];
|
|
1067
|
+
if (value !== void 0) pinned[dimension] = value;
|
|
1068
|
+
}
|
|
1069
|
+
const settled = defaultSelection(pinned);
|
|
1070
|
+
const single = (dimension) => settled[dimension];
|
|
1026
1071
|
return {
|
|
1027
1072
|
projectName: projectNameFromArgs(args) ?? "my-app",
|
|
1028
|
-
architecture:
|
|
1029
|
-
database,
|
|
1030
|
-
orm:
|
|
1031
|
-
auth:
|
|
1032
|
-
styling:
|
|
1033
|
-
interfaces:
|
|
1073
|
+
architecture: single("architecture"),
|
|
1074
|
+
database: single("database"),
|
|
1075
|
+
orm: single("orm"),
|
|
1076
|
+
auth: single("auth"),
|
|
1077
|
+
styling: single("styling"),
|
|
1078
|
+
interfaces: settled.interfaces
|
|
1034
1079
|
};
|
|
1035
1080
|
}
|
|
1036
1081
|
function validateSelections(s) {
|
|
1037
1082
|
const errors = [];
|
|
1083
|
+
const nameError = validateProjectName(s.projectName);
|
|
1084
|
+
if (nameError) errors.push(`project name "${s.projectName}" is invalid. ${nameError}`);
|
|
1038
1085
|
const check = (dim, value) => {
|
|
1039
|
-
if (!
|
|
1086
|
+
if (!STABLE_VALUES[dim].includes(value)) {
|
|
1040
1087
|
errors.push(
|
|
1041
|
-
`${dim} "${value}" is not supported yet (currently generatable: ${
|
|
1088
|
+
`${dim} "${value}" is not supported yet (currently generatable: ${STABLE_VALUES[dim].join(", ")}).`
|
|
1042
1089
|
);
|
|
1043
1090
|
}
|
|
1044
1091
|
};
|
|
@@ -1058,7 +1105,9 @@ async function isEmptyDir(dir) {
|
|
|
1058
1105
|
const entries = await fs7.readdir(dir);
|
|
1059
1106
|
return entries.length === 0;
|
|
1060
1107
|
} catch (err) {
|
|
1061
|
-
|
|
1108
|
+
const code = err.code;
|
|
1109
|
+
if (code === "ENOENT") return true;
|
|
1110
|
+
if (code === "ENOTDIR") return false;
|
|
1062
1111
|
throw err;
|
|
1063
1112
|
}
|
|
1064
1113
|
}
|
|
@@ -1077,25 +1126,27 @@ async function runCreate(args) {
|
|
|
1077
1126
|
Target directory "${selections.projectName}" exists and is not empty. Aborting.`);
|
|
1078
1127
|
process.exit(1);
|
|
1079
1128
|
}
|
|
1129
|
+
const pm = detectPackageManager();
|
|
1080
1130
|
const spinner2 = p2.spinner();
|
|
1081
1131
|
spinner2.start("Scaffolding project");
|
|
1082
1132
|
try {
|
|
1083
|
-
await scaffoldProject(getTemplateDir(), projectDir, selections);
|
|
1133
|
+
await scaffoldProject(getTemplateDir(), projectDir, selections, pm);
|
|
1084
1134
|
spinner2.stop("Project scaffolded");
|
|
1085
1135
|
} catch (err) {
|
|
1086
1136
|
spinner2.stop("Scaffolding failed");
|
|
1137
|
+
await fs7.rm(projectDir, { recursive: true, force: true });
|
|
1087
1138
|
throw err;
|
|
1088
1139
|
}
|
|
1089
1140
|
let depsInstalled = false;
|
|
1090
1141
|
if (args.install !== false) {
|
|
1091
1142
|
const installSpinner = p2.spinner();
|
|
1092
|
-
installSpinner.start(
|
|
1143
|
+
installSpinner.start(`Installing dependencies (${pm} install)`);
|
|
1093
1144
|
try {
|
|
1094
|
-
await installDependencies(projectDir);
|
|
1145
|
+
await installDependencies(projectDir, pm);
|
|
1095
1146
|
installSpinner.stop("Dependencies installed");
|
|
1096
1147
|
depsInstalled = true;
|
|
1097
1148
|
} catch {
|
|
1098
|
-
installSpinner.stop(
|
|
1149
|
+
installSpinner.stop(`Dependency install skipped (run ${pm} install manually)`);
|
|
1099
1150
|
}
|
|
1100
1151
|
}
|
|
1101
1152
|
if (args.git !== false) {
|
|
@@ -1103,12 +1154,19 @@ Target directory "${selections.projectName}" exists and is not empty. Aborting.`
|
|
|
1103
1154
|
}
|
|
1104
1155
|
logger.success(`
|
|
1105
1156
|
\u2714 Created ${pc.bold(selections.projectName)} (${optionLabel(selections.architecture)})`);
|
|
1106
|
-
logNextSteps({ projectDir, selections, depsInstalled });
|
|
1157
|
+
logNextSteps({ projectDir, selections, depsInstalled, pm });
|
|
1107
1158
|
}
|
|
1108
1159
|
|
|
1109
1160
|
// src/index.ts
|
|
1110
1161
|
var require2 = createRequire(import.meta.url);
|
|
1111
1162
|
var { version } = require2("../package.json");
|
|
1163
|
+
var stackOptions = DIMENSIONS.map((d) => {
|
|
1164
|
+
const isList = d === "interfaces";
|
|
1165
|
+
const values = STABLE_VALUES[d].join(isList ? "," : " | ");
|
|
1166
|
+
const suffix = isList ? " (comma-separated; `--interfaces=` for none)" : "";
|
|
1167
|
+
return ` ${`--${d} <value>`.padEnd(24)} ${values}${suffix}`;
|
|
1168
|
+
}).join("\n");
|
|
1169
|
+
var defaults = DIMENSIONS.map((d) => DEFAULT_VALUES[d]).join(", ");
|
|
1112
1170
|
var HELP = `
|
|
1113
1171
|
create-stackrjs \u2014 scaffold a runnable full-stack Next.js project.
|
|
1114
1172
|
|
|
@@ -1116,16 +1174,16 @@ Usage:
|
|
|
1116
1174
|
npm create stackrjs@latest [name] -- [options]
|
|
1117
1175
|
npx create-stackrjs [name] [options]
|
|
1118
1176
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1177
|
+
Stack options:
|
|
1178
|
+
${stackOptions}
|
|
1179
|
+
|
|
1180
|
+
Defaults: ${defaults}.
|
|
1181
|
+
Incompatible combinations are rejected with an explanation.
|
|
1182
|
+
|
|
1183
|
+
Other options:
|
|
1126
1184
|
-y, --yes skip the wizard, use flags + defaults
|
|
1127
1185
|
--ci non-interactive: implies --yes --no-install --no-git
|
|
1128
|
-
--no-install do not run
|
|
1186
|
+
--no-install do not run the dependency install
|
|
1129
1187
|
--no-git do not initialize a git repository
|
|
1130
1188
|
-h, --help show this help
|
|
1131
1189
|
-v, --version show version
|
|
@@ -1148,6 +1206,11 @@ async function main() {
|
|
|
1148
1206
|
await runCreate(args);
|
|
1149
1207
|
}
|
|
1150
1208
|
main().catch((err) => {
|
|
1151
|
-
|
|
1209
|
+
if (err instanceof UsageError) {
|
|
1210
|
+
logger.error("\n" + err.message);
|
|
1211
|
+
logger.dim("Run `create-stackrjs --help` to see the available options.\n");
|
|
1212
|
+
} else {
|
|
1213
|
+
logger.error("\n" + (err instanceof Error ? err.stack ?? err.message : String(err)));
|
|
1214
|
+
}
|
|
1152
1215
|
process.exit(1);
|
|
1153
1216
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-stackrjs",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Scaffold a runnable full-stack Next.js project in one command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"template"
|
|
12
12
|
],
|
|
13
13
|
"engines": {
|
|
14
|
-
"node": ">=
|
|
14
|
+
"node": ">=20.9.0"
|
|
15
15
|
},
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "tsup",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:watch": "vitest",
|
|
23
23
|
"smoke": "tsx scripts/smoke.ts",
|
|
24
|
+
"gen:web": "tsx scripts/gen-web-registry.ts",
|
|
24
25
|
"release": "semantic-release",
|
|
25
26
|
"prepublishOnly": "npm run build"
|
|
26
27
|
},
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
|
|
4
|
-
/** Centered card shell shared by all auth pages. */
|
|
5
|
-
export function AuthShell({ title, children }: { title: string; children: ReactNode }) {
|
|
6
|
-
return (
|
|
7
|
-
<main className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
|
8
|
-
<div className="w-full max-w-sm rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
9
|
-
<h1 className="mb-6 text-2xl font-semibold tracking-tight">{title}</h1>
|
|
10
|
-
{children}
|
|
11
|
-
</div>
|
|
12
|
-
</main>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Field({ label, ...props }: { label: string } & ComponentProps<"input">) {
|
|
17
|
-
return (
|
|
18
|
-
<label className="mb-4 block">
|
|
19
|
-
<span className="mb-1 block text-sm font-medium text-gray-700">{label}</span>
|
|
20
|
-
<input
|
|
21
|
-
{...props}
|
|
22
|
-
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900"
|
|
23
|
-
/>
|
|
24
|
-
</label>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function SubmitButton({ children }: { children: ReactNode }) {
|
|
29
|
-
return (
|
|
30
|
-
<button
|
|
31
|
-
type="submit"
|
|
32
|
-
className="w-full rounded-md bg-gray-900 px-3 py-2 text-sm font-medium text-white transition hover:bg-gray-700"
|
|
33
|
-
>
|
|
34
|
-
{children}
|
|
35
|
-
</button>
|
|
36
|
-
);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function Alert({ kind, children }: { kind: "error" | "success"; children: ReactNode }) {
|
|
40
|
-
const styles =
|
|
41
|
-
kind === "error"
|
|
42
|
-
? "bg-red-50 text-red-700 border-red-200"
|
|
43
|
-
: "bg-green-50 text-green-700 border-green-200";
|
|
44
|
-
return <p className={`mb-4 rounded-md border px-3 py-2 text-sm ${styles}`}>{children}</p>;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function FooterLinks({ children }: { children: ReactNode }) {
|
|
48
|
-
return <div className="mt-6 space-y-1 text-center text-sm text-gray-500">{children}</div>;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export function TextLink({ href, children }: { href: string; children: ReactNode }) {
|
|
52
|
-
return (
|
|
53
|
-
<Link href={href} className="font-medium text-gray-900 underline-offset-2 hover:underline">
|
|
54
|
-
{children}
|
|
55
|
-
</Link>
|
|
56
|
-
);
|
|
57
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import type { ComponentProps, ReactNode } from "react";
|
|
2
|
-
import Link from "next/link";
|
|
3
|
-
|
|
4
|
-
/** Centered card shell shared by all auth pages. */
|
|
5
|
-
export function AuthShell({ title, children }: { title: string; children: ReactNode }) {
|
|
6
|
-
return (
|
|
7
|
-
<main className="flex min-h-screen items-center justify-center bg-gray-50 p-4">
|
|
8
|
-
<div className="w-full max-w-sm rounded-xl border border-gray-200 bg-white p-8 shadow-sm">
|
|
9
|
-
<h1 className="mb-6 text-2xl font-semibold tracking-tight">{title}</h1>
|
|
10
|
-
{children}
|
|
11
|
-
</div>
|
|
12
|
-
</main>
|
|
13
|
-
);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export function Field({ label, ...props }: { label: string } & ComponentProps<"input">) {
|
|
17
|
-
return (
|
|
18
|
-
<label className="mb-4 block">
|
|
19
|
-
<span className="mb-1 block text-sm font-medium text-gray-700">{label}</span>
|
|
20
|
-
<input
|
|
21
|
-
{...props}
|
|
22
|
-
className="w-full rounded-md border border-gray-300 px-3 py-2 text-sm outline-none focus:border-gray-900"
|
|
23
|
-
/>
|
|
24
|
-
</label>
|
|
25
|
-
);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
export function SubmitButton({ children, ...props }: ComponentProps<"button">) {
|
|
29
|
-
return (
|
|
30
|
-
<button
|
|
31
|
-
{...props}
|
|
32
|
-
type="submit"
|
|
33
|
-
className="w-full rounded-md bg-gray-900 px-3 py-2 text-sm font-medium text-white transition hover:bg-gray-700 disabled:opacity-60"
|
|
34
|
-
>
|
|
35
|
-
{children}
|
|
36
|
-
</button>
|
|
37
|
-
);
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export function Alert({ kind, children }: { kind: "error" | "success"; children: ReactNode }) {
|
|
41
|
-
const styles =
|
|
42
|
-
kind === "error"
|
|
43
|
-
? "bg-red-50 text-red-700 border-red-200"
|
|
44
|
-
: "bg-green-50 text-green-700 border-green-200";
|
|
45
|
-
return <p className={`mb-4 rounded-md border px-3 py-2 text-sm ${styles}`}>{children}</p>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function FooterLinks({ children }: { children: ReactNode }) {
|
|
49
|
-
return <div className="mt-6 space-y-1 text-center text-sm text-gray-500">{children}</div>;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export function TextLink({ href, children }: { href: string; children: ReactNode }) {
|
|
53
|
-
return (
|
|
54
|
-
<Link href={href} className="font-medium text-gray-900 underline-offset-2 hover:underline">
|
|
55
|
-
{children}
|
|
56
|
-
</Link>
|
|
57
|
-
);
|
|
58
|
-
}
|
|
File without changes
|