create-pracht 0.2.5 → 0.3.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 +28 -0
- package/package.json +1 -1
- package/src/index.js +332 -29
package/README.md
CHANGED
|
@@ -16,7 +16,9 @@ npm run dev
|
|
|
16
16
|
- Prompts for the target folder.
|
|
17
17
|
- Detects the active package manager from the current environment.
|
|
18
18
|
- Lets the user choose between the Node.js, Cloudflare, and Vercel adapters.
|
|
19
|
+
- Optionally wires up Tailwind CSS (`tailwindcss` + `@tailwindcss/vite`, a global stylesheet, and the shell import).
|
|
19
20
|
- Scaffolds a minimal app with a route manifest or pages router, shell, home route, sample API route, runnable project README, and agent instructions.
|
|
21
|
+
- Initializes a git repository with an initial commit (skipped with `--no-git`, when git is unavailable, or when the target is already inside a repository).
|
|
20
22
|
- `--dry-run` uses pinned fallback versions and does not require npm registry access.
|
|
21
23
|
|
|
22
24
|
## Usage
|
|
@@ -25,8 +27,22 @@ npm run dev
|
|
|
25
27
|
node ./packages/start/bin/create-pracht.js
|
|
26
28
|
node ./packages/start/bin/create-pracht.js my-app --adapter=node --skip-install
|
|
27
29
|
node ./packages/start/bin/create-pracht.js my-app --adapter=vercel --skip-install
|
|
30
|
+
node ./packages/start/bin/create-pracht.js my-app --template=tailwind --yes
|
|
31
|
+
node ./packages/start/bin/create-pracht.js my-app --adapter=node --no-tailwind --no-git --yes
|
|
28
32
|
```
|
|
29
33
|
|
|
34
|
+
## Options
|
|
35
|
+
|
|
36
|
+
- `--adapter=node|cf|vercel` — choose the hosting adapter (default: node).
|
|
37
|
+
- `--router=manifest|pages` — choose the routing system (default: manifest).
|
|
38
|
+
- `--template=minimal|tailwind` — non-interactive template selection; `minimal` is the default output, `tailwind` is minimal plus Tailwind CSS wiring.
|
|
39
|
+
- `--tailwind` / `--no-tailwind` — enable or disable Tailwind CSS without going through the prompt.
|
|
40
|
+
- `--no-git` — skip `git init` and the initial commit.
|
|
41
|
+
- `--skip-install` — skip dependency installation.
|
|
42
|
+
- `--yes`, `-y` — accept defaults (node adapter, manifest router, no Tailwind) and skip all prompts.
|
|
43
|
+
- `--json` — output a JSON summary instead of prose.
|
|
44
|
+
- `--dry-run` — list the files that would be created without writing anything.
|
|
45
|
+
|
|
30
46
|
## Generated Files
|
|
31
47
|
|
|
32
48
|
- `package.json`
|
|
@@ -35,6 +51,16 @@ node ./packages/start/bin/create-pracht.js my-app --adapter=vercel --skip-instal
|
|
|
35
51
|
- `src/routes/home.tsx`
|
|
36
52
|
- `src/shells/public.tsx`
|
|
37
53
|
- `src/api/health.ts`
|
|
54
|
+
- `.gitignore`
|
|
55
|
+
|
|
56
|
+
Node scaffolds also include:
|
|
57
|
+
|
|
58
|
+
- `Dockerfile` — multi-stage build (install → build → slim runtime) that runs `node dist/server/server.js`
|
|
59
|
+
- `.dockerignore`
|
|
60
|
+
|
|
61
|
+
Tailwind scaffolds also include:
|
|
62
|
+
|
|
63
|
+
- `src/styles/global.css` — the Tailwind entry stylesheet, imported by the shell
|
|
38
64
|
|
|
39
65
|
Cloudflare scaffolds also include:
|
|
40
66
|
|
|
@@ -47,10 +73,12 @@ Cloudflare scaffolds also include:
|
|
|
47
73
|
|
|
48
74
|
Node starters also include:
|
|
49
75
|
|
|
76
|
+
- `preview` -> `pracht preview`
|
|
50
77
|
- `start` -> `node dist/server/server.js`
|
|
51
78
|
|
|
52
79
|
Cloudflare starters also include:
|
|
53
80
|
|
|
81
|
+
- `preview` -> `pracht preview`
|
|
54
82
|
- `deploy` -> `pracht build && wrangler deploy`
|
|
55
83
|
|
|
56
84
|
Vercel starters also include:
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -18,6 +18,8 @@ const FALLBACK_VERSION_RANGES = {
|
|
|
18
18
|
"@pracht/cli": "^1.3.1",
|
|
19
19
|
"@pracht/core": "^0.5.0",
|
|
20
20
|
"@pracht/vite-plugin": "^0.3.2",
|
|
21
|
+
"@tailwindcss/vite": "^4.1.0",
|
|
22
|
+
tailwindcss: "^4.1.0",
|
|
21
23
|
vercel: "latest",
|
|
22
24
|
};
|
|
23
25
|
|
|
@@ -68,12 +70,19 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
68
70
|
const dir = options.dir ?? (options.yes ? DEFAULT_DIRECTORY : null);
|
|
69
71
|
const adapterId = options.adapter ?? (options.yes ? "node" : null);
|
|
70
72
|
const router = options.router ?? (options.yes ? "manifest" : null);
|
|
73
|
+
const tailwind = options.tailwind ?? (options.yes ? false : null);
|
|
71
74
|
|
|
72
75
|
let resolvedDir = dir;
|
|
73
76
|
let resolvedAdapter = adapterId;
|
|
74
77
|
let resolvedRouter = router;
|
|
78
|
+
let resolvedTailwind = tailwind;
|
|
75
79
|
|
|
76
|
-
if (
|
|
80
|
+
if (
|
|
81
|
+
resolvedDir == null ||
|
|
82
|
+
resolvedAdapter == null ||
|
|
83
|
+
resolvedRouter == null ||
|
|
84
|
+
resolvedTailwind == null
|
|
85
|
+
) {
|
|
77
86
|
const readline = createInterface({
|
|
78
87
|
input: process.stdin,
|
|
79
88
|
output: process.stdout,
|
|
@@ -83,6 +92,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
83
92
|
resolvedDir = resolvedDir ?? (await promptForDirectory(readline));
|
|
84
93
|
resolvedAdapter = resolvedAdapter ?? (await promptForAdapter(readline));
|
|
85
94
|
resolvedRouter = resolvedRouter ?? (await promptForRouter(readline));
|
|
95
|
+
resolvedTailwind = resolvedTailwind ?? (await promptForTailwind(readline));
|
|
86
96
|
} finally {
|
|
87
97
|
readline.close();
|
|
88
98
|
}
|
|
@@ -99,6 +109,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
99
109
|
projectName: toPackageName(basename(targetDir)),
|
|
100
110
|
resolveRemoteVersions: false,
|
|
101
111
|
router: resolvedRouter,
|
|
112
|
+
tailwind: resolvedTailwind,
|
|
102
113
|
});
|
|
103
114
|
|
|
104
115
|
const fileList = Object.keys(files).sort();
|
|
@@ -111,6 +122,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
111
122
|
dryRun: true,
|
|
112
123
|
files: fileList,
|
|
113
124
|
router: resolvedRouter,
|
|
125
|
+
tailwind: resolvedTailwind,
|
|
114
126
|
}),
|
|
115
127
|
);
|
|
116
128
|
} else {
|
|
@@ -128,6 +140,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
128
140
|
adapter: ADAPTERS[resolvedAdapter],
|
|
129
141
|
packageManager,
|
|
130
142
|
router: resolvedRouter,
|
|
143
|
+
tailwind: resolvedTailwind,
|
|
131
144
|
targetDir,
|
|
132
145
|
});
|
|
133
146
|
|
|
@@ -138,6 +151,23 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
138
151
|
installSucceeded = await installDependencies(targetDir, packageManager);
|
|
139
152
|
}
|
|
140
153
|
|
|
154
|
+
let gitInitialized = false;
|
|
155
|
+
if (options.git) {
|
|
156
|
+
const gitResult = await initGitRepository(targetDir);
|
|
157
|
+
gitInitialized = gitResult.initialized;
|
|
158
|
+
|
|
159
|
+
if (gitResult.initialized) {
|
|
160
|
+
log("");
|
|
161
|
+
log("Initialized a git repository with an initial commit.");
|
|
162
|
+
} else if (gitResult.reason === "existing-repo") {
|
|
163
|
+
log("");
|
|
164
|
+
log("Skipped git init — the target directory is already inside a git repository.");
|
|
165
|
+
} else if (gitResult.reason === "git-not-found") {
|
|
166
|
+
log("");
|
|
167
|
+
log("Skipped git init — git is not available on this machine.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
141
171
|
if (options.json) {
|
|
142
172
|
const files = await buildProjectFiles({
|
|
143
173
|
adapter: ADAPTERS[resolvedAdapter],
|
|
@@ -145,6 +175,7 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
145
175
|
projectName: toPackageName(basename(targetDir)),
|
|
146
176
|
resolveRemoteVersions: false,
|
|
147
177
|
router: resolvedRouter,
|
|
178
|
+
tailwind: resolvedTailwind,
|
|
148
179
|
});
|
|
149
180
|
|
|
150
181
|
console.log(
|
|
@@ -152,8 +183,10 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
152
183
|
adapter: resolvedAdapter,
|
|
153
184
|
directory: resolvedDir,
|
|
154
185
|
files: Object.keys(files).sort(),
|
|
186
|
+
gitInitialized,
|
|
155
187
|
installed: options.skipInstall ? false : installSucceeded,
|
|
156
188
|
router: resolvedRouter,
|
|
189
|
+
tailwind: resolvedTailwind,
|
|
157
190
|
}),
|
|
158
191
|
);
|
|
159
192
|
} else {
|
|
@@ -167,13 +200,20 @@ export async function run(argv = process.argv.slice(2)) {
|
|
|
167
200
|
}
|
|
168
201
|
}
|
|
169
202
|
|
|
170
|
-
export async function scaffoldProject({
|
|
203
|
+
export async function scaffoldProject({
|
|
204
|
+
adapter,
|
|
205
|
+
packageManager,
|
|
206
|
+
router = "manifest",
|
|
207
|
+
tailwind = false,
|
|
208
|
+
targetDir,
|
|
209
|
+
}) {
|
|
171
210
|
const packageName = toPackageName(basename(targetDir));
|
|
172
211
|
const files = await buildProjectFiles({
|
|
173
212
|
adapter,
|
|
174
213
|
packageManager,
|
|
175
214
|
projectName: packageName,
|
|
176
215
|
router,
|
|
216
|
+
tailwind,
|
|
177
217
|
});
|
|
178
218
|
|
|
179
219
|
await mkdir(targetDir, { recursive: true });
|
|
@@ -207,9 +247,11 @@ export function parseArgs(argv) {
|
|
|
207
247
|
adapter: undefined,
|
|
208
248
|
dir: undefined,
|
|
209
249
|
dryRun: false,
|
|
250
|
+
git: true,
|
|
210
251
|
json: false,
|
|
211
252
|
router: undefined,
|
|
212
253
|
skipInstall: false,
|
|
254
|
+
tailwind: undefined,
|
|
213
255
|
yes: false,
|
|
214
256
|
};
|
|
215
257
|
|
|
@@ -219,6 +261,32 @@ export function parseArgs(argv) {
|
|
|
219
261
|
continue;
|
|
220
262
|
}
|
|
221
263
|
|
|
264
|
+
if (arg === "--tailwind") {
|
|
265
|
+
options.tailwind = true;
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (arg === "--no-tailwind") {
|
|
270
|
+
options.tailwind = false;
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (arg === "--no-git") {
|
|
275
|
+
options.git = false;
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (arg.startsWith("--template=")) {
|
|
280
|
+
const value = normalizeTemplate(arg.slice("--template=".length));
|
|
281
|
+
if (!value) {
|
|
282
|
+
throw new ValidationError(
|
|
283
|
+
`Invalid template: ${arg.slice("--template=".length)}. Use minimal or tailwind.`,
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
options.tailwind = value === "tailwind";
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
222
290
|
if (arg === "--yes" || arg === "-y") {
|
|
223
291
|
options.yes = true;
|
|
224
292
|
continue;
|
|
@@ -322,6 +390,33 @@ async function promptForRouter(readline) {
|
|
|
322
390
|
}
|
|
323
391
|
}
|
|
324
392
|
|
|
393
|
+
async function promptForTailwind(readline) {
|
|
394
|
+
while (true) {
|
|
395
|
+
const answer = await readline.question("Use Tailwind CSS? (y/N): ");
|
|
396
|
+
const normalized = normalizeYesNo(answer.trim() || "no");
|
|
397
|
+
|
|
398
|
+
if (normalized != null) {
|
|
399
|
+
return normalized;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
console.log("Answer y/yes or n/no.");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function normalizeYesNo(value) {
|
|
407
|
+
const normalized = value.toLowerCase();
|
|
408
|
+
|
|
409
|
+
if (normalized === "y" || normalized === "yes") {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (normalized === "n" || normalized === "no") {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
|
|
325
420
|
async function ensureTargetDirectory(targetDir) {
|
|
326
421
|
const error = await validateTargetDirectory(targetDir);
|
|
327
422
|
|
|
@@ -348,6 +443,20 @@ async function validateTargetDirectory(targetDir) {
|
|
|
348
443
|
return null;
|
|
349
444
|
}
|
|
350
445
|
|
|
446
|
+
function normalizeTemplate(value) {
|
|
447
|
+
const normalized = value.toLowerCase();
|
|
448
|
+
|
|
449
|
+
if (normalized === "minimal") {
|
|
450
|
+
return "minimal";
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if (normalized === "tailwind") {
|
|
454
|
+
return "tailwind";
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
|
|
351
460
|
function normalizeRouter(value) {
|
|
352
461
|
const normalized = value.toLowerCase();
|
|
353
462
|
|
|
@@ -406,6 +515,7 @@ async function buildProjectFiles({
|
|
|
406
515
|
projectName,
|
|
407
516
|
resolveRemoteVersions = true,
|
|
408
517
|
router,
|
|
518
|
+
tailwind = false,
|
|
409
519
|
}) {
|
|
410
520
|
const packagesToResolve = [
|
|
411
521
|
"@pracht/cli",
|
|
@@ -416,26 +526,33 @@ async function buildProjectFiles({
|
|
|
416
526
|
if (adapter.id === "vercel") {
|
|
417
527
|
packagesToResolve.push("vercel");
|
|
418
528
|
}
|
|
529
|
+
if (tailwind) {
|
|
530
|
+
packagesToResolve.push("tailwindcss", "@tailwindcss/vite");
|
|
531
|
+
}
|
|
419
532
|
|
|
420
533
|
const versions = await resolveVersions(packagesToResolve, { remote: resolveRemoteVersions });
|
|
421
534
|
|
|
422
535
|
const files = {
|
|
423
|
-
".gitignore": "dist\nnode_modules\n.wrangler\n.vercel\n",
|
|
424
|
-
"README.md": createReadme({ adapter, packageManager, projectName, router }),
|
|
425
|
-
"package.json": createPackageJson({ adapter, projectName, versions }),
|
|
536
|
+
".gitignore": "dist\nnode_modules\n.wrangler\n.vercel\n.env*\n!.env.example\n.dev.vars\n",
|
|
537
|
+
"README.md": createReadme({ adapter, packageManager, projectName, router, tailwind }),
|
|
538
|
+
"package.json": createPackageJson({ adapter, projectName, tailwind, versions }),
|
|
426
539
|
"src/api/health.ts": createHealthRoute(adapter),
|
|
427
|
-
"vite.config.ts": createViteConfig(adapter, router),
|
|
540
|
+
"vite.config.ts": createViteConfig(adapter, router, tailwind),
|
|
428
541
|
"tsconfig.json": createBaseTSConfig(adapter),
|
|
429
|
-
"AGENTS.md": createAgentInstructions({ adapter, packageManager, router }),
|
|
542
|
+
"AGENTS.md": createAgentInstructions({ adapter, packageManager, router, tailwind }),
|
|
430
543
|
};
|
|
431
544
|
|
|
432
545
|
if (router === "pages") {
|
|
433
|
-
files["src/pages/_app.tsx"] = createShellFile(projectName);
|
|
546
|
+
files["src/pages/_app.tsx"] = createShellFile(projectName, tailwind);
|
|
434
547
|
files["src/pages/index.tsx"] = createPagesHomeRoute(adapter);
|
|
435
548
|
} else {
|
|
436
549
|
files["src/routes.ts"] = createRoutesFile();
|
|
437
550
|
files["src/routes/home.tsx"] = createHomeRoute(adapter);
|
|
438
|
-
files["src/shells/public.tsx"] = createShellFile(projectName);
|
|
551
|
+
files["src/shells/public.tsx"] = createShellFile(projectName, tailwind);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (tailwind) {
|
|
555
|
+
files["src/styles/global.css"] = '@import "tailwindcss";\n';
|
|
439
556
|
}
|
|
440
557
|
|
|
441
558
|
if (adapter.id === "cloudflare") {
|
|
@@ -443,16 +560,22 @@ async function buildProjectFiles({
|
|
|
443
560
|
files["src/env.d.ts"] = createCloudflareEnvDeclaration();
|
|
444
561
|
}
|
|
445
562
|
|
|
563
|
+
if (adapter.id === "node") {
|
|
564
|
+
files["Dockerfile"] = createDockerfile(packageManager);
|
|
565
|
+
files[".dockerignore"] = createDockerignore();
|
|
566
|
+
}
|
|
567
|
+
|
|
446
568
|
return files;
|
|
447
569
|
}
|
|
448
570
|
|
|
449
|
-
function createPackageJson({ adapter, projectName, versions }) {
|
|
571
|
+
function createPackageJson({ adapter, projectName, tailwind, versions }) {
|
|
450
572
|
const scripts = {
|
|
451
573
|
build: "pracht build",
|
|
452
574
|
dev: "pracht dev",
|
|
453
575
|
};
|
|
454
576
|
|
|
455
577
|
if (adapter.id === "node") {
|
|
578
|
+
scripts.preview = "pracht preview";
|
|
456
579
|
scripts.start = "node dist/server/server.js";
|
|
457
580
|
}
|
|
458
581
|
|
|
@@ -466,6 +589,7 @@ function createPackageJson({ adapter, projectName, versions }) {
|
|
|
466
589
|
|
|
467
590
|
if (adapter.id === "cloudflare") {
|
|
468
591
|
scripts.deploy = "pracht build && wrangler deploy";
|
|
592
|
+
scripts.preview = "pracht preview";
|
|
469
593
|
devDependencies.wrangler = "^4.81.0";
|
|
470
594
|
}
|
|
471
595
|
|
|
@@ -474,6 +598,11 @@ function createPackageJson({ adapter, projectName, versions }) {
|
|
|
474
598
|
devDependencies.vercel = versions["vercel"];
|
|
475
599
|
}
|
|
476
600
|
|
|
601
|
+
if (tailwind) {
|
|
602
|
+
devDependencies["@tailwindcss/vite"] = versions["@tailwindcss/vite"];
|
|
603
|
+
devDependencies.tailwindcss = versions["tailwindcss"];
|
|
604
|
+
}
|
|
605
|
+
|
|
477
606
|
return `${JSON.stringify(
|
|
478
607
|
{
|
|
479
608
|
dependencies: {
|
|
@@ -492,7 +621,7 @@ function createPackageJson({ adapter, projectName, versions }) {
|
|
|
492
621
|
)}\n`;
|
|
493
622
|
}
|
|
494
623
|
|
|
495
|
-
function createViteConfig(adapter, router) {
|
|
624
|
+
function createViteConfig(adapter, router, tailwind) {
|
|
496
625
|
const ADAPTER_IMPORTS = {
|
|
497
626
|
node: { fn: "nodeAdapter", pkg: "@pracht/adapter-node" },
|
|
498
627
|
cloudflare: { fn: "cloudflareAdapter", pkg: "@pracht/adapter-cloudflare" },
|
|
@@ -506,16 +635,23 @@ function createViteConfig(adapter, router) {
|
|
|
506
635
|
? `{ pagesDir: "/src/pages", adapter: ${info.fn}() }`
|
|
507
636
|
: `{ adapter: ${info.fn}() }`;
|
|
508
637
|
|
|
509
|
-
|
|
638
|
+
const plugins = tailwind
|
|
639
|
+
? `[pracht(${prachtOptions}), tailwindcss()]`
|
|
640
|
+
: `[pracht(${prachtOptions})]`;
|
|
641
|
+
|
|
642
|
+
const lines = [
|
|
510
643
|
'import { defineConfig } from "vite";',
|
|
511
644
|
'import { pracht } from "@pracht/vite-plugin";',
|
|
512
645
|
`import { ${info.fn} } from "${info.pkg}";`,
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
"
|
|
517
|
-
|
|
518
|
-
|
|
646
|
+
];
|
|
647
|
+
|
|
648
|
+
if (tailwind) {
|
|
649
|
+
lines.push('import tailwindcss from "@tailwindcss/vite";');
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
lines.push("", "export default defineConfig({", ` plugins: ${plugins},`, "});", "");
|
|
653
|
+
|
|
654
|
+
return lines.join("\n");
|
|
519
655
|
}
|
|
520
656
|
|
|
521
657
|
function createRoutesFile() {
|
|
@@ -534,9 +670,15 @@ function createRoutesFile() {
|
|
|
534
670
|
].join("\n");
|
|
535
671
|
}
|
|
536
672
|
|
|
537
|
-
function createShellFile(projectName) {
|
|
673
|
+
function createShellFile(projectName, tailwind = false) {
|
|
674
|
+
const lines = ['import type { ShellProps } from "@pracht/core";'];
|
|
675
|
+
|
|
676
|
+
if (tailwind) {
|
|
677
|
+
lines.push('import "../styles/global.css";');
|
|
678
|
+
}
|
|
679
|
+
|
|
538
680
|
return [
|
|
539
|
-
|
|
681
|
+
...lines,
|
|
540
682
|
"",
|
|
541
683
|
"export function Shell({ children }: ShellProps) {",
|
|
542
684
|
" return (",
|
|
@@ -705,7 +847,80 @@ function createCloudflareEnvDeclaration() {
|
|
|
705
847
|
].join("\n");
|
|
706
848
|
}
|
|
707
849
|
|
|
708
|
-
function
|
|
850
|
+
function createDockerfile(packageManager) {
|
|
851
|
+
const COMMANDS = {
|
|
852
|
+
npm: {
|
|
853
|
+
build: "npm run build",
|
|
854
|
+
install: "npm install",
|
|
855
|
+
lockfile: "package-lock.json*",
|
|
856
|
+
prune: "npm prune --omit=dev",
|
|
857
|
+
setup: null,
|
|
858
|
+
},
|
|
859
|
+
pnpm: {
|
|
860
|
+
build: "pnpm build",
|
|
861
|
+
install: "pnpm install",
|
|
862
|
+
lockfile: "pnpm-lock.yaml*",
|
|
863
|
+
prune: "pnpm prune --prod",
|
|
864
|
+
setup: "corepack enable pnpm",
|
|
865
|
+
},
|
|
866
|
+
yarn: {
|
|
867
|
+
build: "yarn build",
|
|
868
|
+
install: "yarn install",
|
|
869
|
+
lockfile: "yarn.lock*",
|
|
870
|
+
prune: "yarn install --production --ignore-scripts --prefer-offline",
|
|
871
|
+
setup: "corepack enable yarn",
|
|
872
|
+
},
|
|
873
|
+
};
|
|
874
|
+
|
|
875
|
+
// The runtime image ships Node.js, so bun scaffolds fall back to npm inside Docker.
|
|
876
|
+
const commands = COMMANDS[packageManager] ?? COMMANDS.npm;
|
|
877
|
+
|
|
878
|
+
const lines = ["# syntax=docker/dockerfile:1", "", "FROM node:22-alpine AS base", "WORKDIR /app"];
|
|
879
|
+
|
|
880
|
+
if (commands.setup) {
|
|
881
|
+
lines.push(`RUN ${commands.setup}`);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
lines.push(
|
|
885
|
+
"",
|
|
886
|
+
"FROM base AS deps",
|
|
887
|
+
`COPY package.json ${commands.lockfile} ./`,
|
|
888
|
+
`RUN ${commands.install}`,
|
|
889
|
+
"",
|
|
890
|
+
"FROM deps AS build",
|
|
891
|
+
"COPY . .",
|
|
892
|
+
`RUN ${commands.build}`,
|
|
893
|
+
`RUN ${commands.prune}`,
|
|
894
|
+
"",
|
|
895
|
+
"FROM node:22-alpine AS runtime",
|
|
896
|
+
"WORKDIR /app",
|
|
897
|
+
"ENV NODE_ENV=production",
|
|
898
|
+
"ENV PORT=3000",
|
|
899
|
+
"COPY --from=build /app/package.json ./package.json",
|
|
900
|
+
"COPY --from=build /app/node_modules ./node_modules",
|
|
901
|
+
"COPY --from=build /app/dist ./dist",
|
|
902
|
+
"EXPOSE 3000",
|
|
903
|
+
'CMD ["node", "dist/server/server.js"]',
|
|
904
|
+
"",
|
|
905
|
+
);
|
|
906
|
+
|
|
907
|
+
return lines.join("\n");
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
function createDockerignore() {
|
|
911
|
+
return [
|
|
912
|
+
"node_modules",
|
|
913
|
+
"dist",
|
|
914
|
+
".git",
|
|
915
|
+
".env*",
|
|
916
|
+
"!.env.example",
|
|
917
|
+
"Dockerfile",
|
|
918
|
+
".dockerignore",
|
|
919
|
+
"",
|
|
920
|
+
].join("\n");
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
function createAgentInstructions({ adapter, packageManager, router, tailwind }) {
|
|
709
924
|
const runCmd = packageManager === "npm" ? "npm run" : packageManager;
|
|
710
925
|
|
|
711
926
|
const lines = [
|
|
@@ -717,6 +932,10 @@ function createAgentInstructions({ adapter, packageManager, router }) {
|
|
|
717
932
|
`- \`${runCmd} build\` — production build`,
|
|
718
933
|
];
|
|
719
934
|
|
|
935
|
+
if (adapter.id === "node" || adapter.id === "cloudflare") {
|
|
936
|
+
lines.push(`- \`${runCmd} preview\` — build and serve the production build locally`);
|
|
937
|
+
}
|
|
938
|
+
|
|
720
939
|
if (adapter.id === "node") {
|
|
721
940
|
lines.push(`- \`${runCmd} start\` — run the built server`);
|
|
722
941
|
}
|
|
@@ -756,6 +975,14 @@ function createAgentInstructions({ adapter, packageManager, router }) {
|
|
|
756
975
|
lines.push("- `src/api/` — API route handlers");
|
|
757
976
|
lines.push(`- \`vite.config.ts\` — Vite config with the ${adapter.label} adapter`);
|
|
758
977
|
|
|
978
|
+
if (tailwind) {
|
|
979
|
+
lines.push("- `src/styles/global.css` — Tailwind CSS entry stylesheet, imported by the shell");
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (adapter.id === "node") {
|
|
983
|
+
lines.push("- `Dockerfile` — multi-stage container build that runs the built server");
|
|
984
|
+
}
|
|
985
|
+
|
|
759
986
|
if (adapter.id === "cloudflare") {
|
|
760
987
|
lines.push("- `wrangler.jsonc` — Cloudflare Workers configuration");
|
|
761
988
|
lines.push("- `src/env.d.ts` — TypeScript types for Cloudflare bindings");
|
|
@@ -766,9 +993,10 @@ function createAgentInstructions({ adapter, packageManager, router }) {
|
|
|
766
993
|
return lines.join("\n");
|
|
767
994
|
}
|
|
768
995
|
|
|
769
|
-
function createReadme({ adapter, packageManager, projectName, router }) {
|
|
996
|
+
function createReadme({ adapter, packageManager, projectName, router, tailwind }) {
|
|
770
997
|
const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
|
|
771
998
|
const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
|
|
999
|
+
const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
|
|
772
1000
|
const startCommand = packageManager === "npm" ? "npm run start" : `${packageManager} start`;
|
|
773
1001
|
const deployCommand = packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
|
|
774
1002
|
|
|
@@ -784,10 +1012,12 @@ function createReadme({ adapter, packageManager, projectName, router }) {
|
|
|
784
1012
|
];
|
|
785
1013
|
|
|
786
1014
|
if (adapter.id === "node") {
|
|
1015
|
+
lines.push(`- \`${previewCommand}\``);
|
|
787
1016
|
lines.push(`- \`${startCommand}\``);
|
|
788
1017
|
}
|
|
789
1018
|
|
|
790
1019
|
if (adapter.id === "cloudflare") {
|
|
1020
|
+
lines.push(`- \`${previewCommand}\``);
|
|
791
1021
|
lines.push(`- \`${deployCommand}\``);
|
|
792
1022
|
lines.push("");
|
|
793
1023
|
lines.push(
|
|
@@ -816,9 +1046,79 @@ function createReadme({ adapter, packageManager, projectName, router }) {
|
|
|
816
1046
|
|
|
817
1047
|
lines.push("- `src/api/health.ts` is a sample API route.");
|
|
818
1048
|
|
|
1049
|
+
if (tailwind) {
|
|
1050
|
+
lines.push("- `src/styles/global.css` is the Tailwind CSS entry, imported by the shell.");
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (adapter.id === "node") {
|
|
1054
|
+
lines.push("");
|
|
1055
|
+
lines.push("## Docker");
|
|
1056
|
+
lines.push("");
|
|
1057
|
+
lines.push("A multi-stage `Dockerfile` builds the app and runs the Node server:");
|
|
1058
|
+
lines.push("");
|
|
1059
|
+
lines.push("```bash");
|
|
1060
|
+
lines.push(`docker build -t ${projectName} .`);
|
|
1061
|
+
lines.push(`docker run -p 3000:3000 ${projectName}`);
|
|
1062
|
+
lines.push("```");
|
|
1063
|
+
}
|
|
1064
|
+
|
|
819
1065
|
return `${lines.join("\n")}\n`;
|
|
820
1066
|
}
|
|
821
1067
|
|
|
1068
|
+
export async function initGitRepository(targetDir) {
|
|
1069
|
+
if (!(await execCommand("git", ["--version"]))) {
|
|
1070
|
+
return { initialized: false, reason: "git-not-found" };
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
if (await execCommand("git", ["rev-parse", "--is-inside-work-tree"], targetDir)) {
|
|
1074
|
+
return { initialized: false, reason: "existing-repo" };
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
if (!(await execCommand("git", ["init"], targetDir))) {
|
|
1078
|
+
return { initialized: false, reason: "init-failed" };
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
if (!(await execCommand("git", ["add", "-A"], targetDir))) {
|
|
1082
|
+
return { initialized: false, reason: "commit-failed" };
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Fall back to a scoped identity when the user has no git identity configured,
|
|
1086
|
+
// so the initial commit still succeeds (e.g. on fresh machines or CI).
|
|
1087
|
+
const hasIdentity = await execCommand("git", ["config", "user.email"], targetDir);
|
|
1088
|
+
const identityArgs = hasIdentity
|
|
1089
|
+
? []
|
|
1090
|
+
: ["-c", "user.name=create-pracht", "-c", "user.email=create-pracht@localhost"];
|
|
1091
|
+
|
|
1092
|
+
const committed = await execCommand(
|
|
1093
|
+
"git",
|
|
1094
|
+
[...identityArgs, "commit", "-m", "Initial commit from create-pracht"],
|
|
1095
|
+
targetDir,
|
|
1096
|
+
);
|
|
1097
|
+
|
|
1098
|
+
if (!committed) {
|
|
1099
|
+
return { initialized: false, reason: "commit-failed" };
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
return { initialized: true };
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
function execCommand(command, args, cwd) {
|
|
1106
|
+
return new Promise((resolveExec) => {
|
|
1107
|
+
const child = spawn(command, args, {
|
|
1108
|
+
cwd,
|
|
1109
|
+
stdio: "ignore",
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
child.on("close", (code) => {
|
|
1113
|
+
resolveExec(code === 0);
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
child.on("error", () => {
|
|
1117
|
+
resolveExec(false);
|
|
1118
|
+
});
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
822
1122
|
async function installDependencies(targetDir, packageManager) {
|
|
823
1123
|
const args = packageManager === "yarn" ? ["install"] : ["install"];
|
|
824
1124
|
|
|
@@ -868,13 +1168,16 @@ Usage:
|
|
|
868
1168
|
create-pracht [directory] [options]
|
|
869
1169
|
|
|
870
1170
|
Options:
|
|
871
|
-
--adapter=node|cf|vercel
|
|
872
|
-
--router=manifest|pages
|
|
873
|
-
--
|
|
874
|
-
--
|
|
875
|
-
--
|
|
876
|
-
--
|
|
877
|
-
-
|
|
1171
|
+
--adapter=node|cf|vercel Choose hosting adapter (default: node)
|
|
1172
|
+
--router=manifest|pages Choose routing system (default: manifest)
|
|
1173
|
+
--template=minimal|tailwind Choose starter template (minimal, or minimal + Tailwind CSS)
|
|
1174
|
+
--tailwind / --no-tailwind Enable or disable Tailwind CSS wiring (default: prompt)
|
|
1175
|
+
--no-git Skip git init and the initial commit
|
|
1176
|
+
--skip-install Skip dependency installation
|
|
1177
|
+
--yes, -y Accept defaults, skip all prompts
|
|
1178
|
+
--json Output JSON summary instead of prose
|
|
1179
|
+
--dry-run Show which files would be created without writing
|
|
1180
|
+
-h, --help Show this help message
|
|
878
1181
|
`);
|
|
879
1182
|
}
|
|
880
1183
|
|