create-pracht 0.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 ADDED
@@ -0,0 +1,60 @@
1
+ # create-pracht
2
+
3
+ Interactive starter CLI for bootstrapping a new pracht app.
4
+
5
+ ## Quickstart
6
+
7
+ ```bash
8
+ npm create pracht@latest my-app
9
+ cd my-app
10
+ npm install
11
+ npm run dev
12
+ ```
13
+
14
+ ## What It Does
15
+
16
+ - Prompts for the target folder.
17
+ - Detects the active package manager from the current environment.
18
+ - Lets the user choose between the Node.js, Cloudflare, and Vercel adapters.
19
+ - Scaffolds a minimal app with a route manifest, shell, home route, sample API route, and runnable project README.
20
+
21
+ ## Usage
22
+
23
+ ```bash
24
+ node ./packages/start/bin/create-pracht.js
25
+ node ./packages/start/bin/create-pracht.js my-app --adapter=node --skip-install
26
+ node ./packages/start/bin/create-pracht.js my-app --adapter=vercel --skip-install
27
+ ```
28
+
29
+ ## Generated Files
30
+
31
+ - `package.json`
32
+ - `vite.config.ts`
33
+ - `src/routes.ts`
34
+ - `src/routes/home.tsx`
35
+ - `src/shells/public.tsx`
36
+ - `src/api/health.ts`
37
+
38
+ Cloudflare scaffolds also include:
39
+
40
+ - `wrangler.jsonc`
41
+
42
+ ## Generated Scripts
43
+
44
+ - `dev` -> `pracht dev`
45
+ - `build` -> `pracht build`
46
+ - `preview` -> `pracht preview`
47
+
48
+ Cloudflare starters also include:
49
+
50
+ - `deploy` -> `pracht build && wrangler deploy`
51
+
52
+ Vercel starters also include:
53
+
54
+ - `deploy` -> `pracht build && vercel deploy --prebuilt`
55
+
56
+ Node starters can be run in production with:
57
+
58
+ ```bash
59
+ node dist/server/server.js
60
+ ```
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { run } from "../src/index.js";
4
+
5
+ run().catch((error) => {
6
+ console.error("Failed to create a pracht app.");
7
+ console.error(error instanceof Error ? error.message : error);
8
+ process.exit(1);
9
+ });
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "create-pracht",
3
+ "version": "0.0.0",
4
+ "bin": {
5
+ "create-pracht": "./bin/create-pracht.js"
6
+ },
7
+ "files": [
8
+ "bin",
9
+ "src",
10
+ "README.md"
11
+ ],
12
+ "type": "module",
13
+ "scripts": {
14
+ "build": "node -e \"\""
15
+ }
16
+ }
package/src/index.js ADDED
@@ -0,0 +1,529 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, resolve } from "node:path";
5
+ import { createInterface } from "node:readline/promises";
6
+
7
+ const ADAPTERS = {
8
+ node: {
9
+ description: "Node.js server with pracht preview",
10
+ id: "node",
11
+ label: "Node.js",
12
+ packageName: "@pracht/adapter-node",
13
+ short: "node",
14
+ },
15
+ cloudflare: {
16
+ description: "Cloudflare Workers with wrangler deploy",
17
+ id: "cloudflare",
18
+ label: "Cloudflare Workers",
19
+ packageName: "@pracht/adapter-cloudflare",
20
+ short: "cf",
21
+ },
22
+ vercel: {
23
+ description: "Vercel Edge Functions with prebuilt deploy",
24
+ id: "vercel",
25
+ label: "Vercel",
26
+ packageName: "@pracht/adapter-vercel",
27
+ short: "vercel",
28
+ },
29
+ };
30
+
31
+ const DEFAULT_DIRECTORY = "pracht-app";
32
+
33
+ export async function run(argv = process.argv.slice(2)) {
34
+ const options = parseArgs(argv);
35
+ const packageManager = getPackageManager();
36
+
37
+ console.log("create-pracht");
38
+ console.log(`Using ${packageManager} for this scaffold.`);
39
+ console.log("");
40
+
41
+ const readline = createInterface({
42
+ input: process.stdin,
43
+ output: process.stdout,
44
+ });
45
+
46
+ try {
47
+ const dir = options.dir ?? (await promptForDirectory(readline));
48
+ const adapterId = options.adapter ?? (await promptForAdapter(readline));
49
+ const targetDir = resolve(process.cwd(), dir);
50
+
51
+ await ensureTargetDirectory(targetDir);
52
+
53
+ await scaffoldProject({
54
+ adapter: ADAPTERS[adapterId],
55
+ packageManager,
56
+ targetDir,
57
+ });
58
+
59
+ let installSucceeded = false;
60
+ if (!options.skipInstall) {
61
+ console.log("");
62
+ console.log(`Installing dependencies with ${packageManager}...`);
63
+ installSucceeded = await installDependencies(targetDir, packageManager);
64
+ }
65
+
66
+ printNextSteps({
67
+ adapter: ADAPTERS[adapterId],
68
+ dir,
69
+ installSucceeded,
70
+ packageManager,
71
+ skipInstall: options.skipInstall,
72
+ });
73
+ } finally {
74
+ readline.close();
75
+ }
76
+ }
77
+
78
+ export async function scaffoldProject({ adapter, packageManager, targetDir }) {
79
+ const packageName = toPackageName(basename(targetDir));
80
+ const files = buildProjectFiles({
81
+ adapter,
82
+ packageManager,
83
+ projectName: packageName,
84
+ });
85
+
86
+ await mkdir(targetDir, { recursive: true });
87
+
88
+ for (const [relativePath, content] of Object.entries(files)) {
89
+ const filePath = resolve(targetDir, relativePath);
90
+ await mkdir(dirname(filePath), { recursive: true });
91
+ await writeFile(filePath, content, "utf-8");
92
+ }
93
+ }
94
+
95
+ export function getPackageManager(userAgent = process.env.npm_config_user_agent ?? "") {
96
+ if (userAgent.startsWith("pnpm")) return "pnpm";
97
+ if (userAgent.startsWith("yarn")) return "yarn";
98
+ if (userAgent.startsWith("bun") || process.versions.bun) return "bun";
99
+ return "npm";
100
+ }
101
+
102
+ export function parseArgs(argv) {
103
+ const options = {
104
+ adapter: undefined,
105
+ dir: undefined,
106
+ skipInstall: false,
107
+ };
108
+
109
+ for (const arg of argv) {
110
+ if (arg === "--skip-install") {
111
+ options.skipInstall = true;
112
+ continue;
113
+ }
114
+
115
+ if (arg.startsWith("--adapter=")) {
116
+ options.adapter = normalizeAdapter(arg.slice("--adapter=".length));
117
+ continue;
118
+ }
119
+
120
+ if (arg === "--help" || arg === "-h") {
121
+ printHelp();
122
+ process.exit(0);
123
+ }
124
+
125
+ if (!arg.startsWith("-") && !options.dir) {
126
+ options.dir = arg;
127
+ continue;
128
+ }
129
+
130
+ throw new Error(`Unknown argument: ${arg}`);
131
+ }
132
+
133
+ return options;
134
+ }
135
+
136
+ async function promptForDirectory(readline) {
137
+ while (true) {
138
+ const answer = await readline.question(`Project directory (${DEFAULT_DIRECTORY}): `);
139
+ const dir = answer.trim() || DEFAULT_DIRECTORY;
140
+ const targetDir = resolve(process.cwd(), dir);
141
+ const error = await validateTargetDirectory(targetDir);
142
+
143
+ if (!error) {
144
+ return dir;
145
+ }
146
+
147
+ console.log(error);
148
+ }
149
+ }
150
+
151
+ async function promptForAdapter(readline) {
152
+ console.log("Adapters:");
153
+ console.log(" 1. Node.js");
154
+ console.log(" 2. Cloudflare Workers");
155
+ console.log(" 3. Vercel");
156
+
157
+ while (true) {
158
+ const answer = await readline.question("Adapter (1): ");
159
+ const normalized = normalizeAdapter(answer.trim() || "1");
160
+
161
+ if (normalized) {
162
+ return normalized;
163
+ }
164
+
165
+ console.log("Choose 1/2/3 or node/cf/vercel.");
166
+ }
167
+ }
168
+
169
+ async function ensureTargetDirectory(targetDir) {
170
+ const error = await validateTargetDirectory(targetDir);
171
+
172
+ if (error) {
173
+ throw new Error(error);
174
+ }
175
+ }
176
+
177
+ async function validateTargetDirectory(targetDir) {
178
+ if (!existsSync(targetDir)) {
179
+ return null;
180
+ }
181
+
182
+ const targetStat = await stat(targetDir);
183
+ if (!targetStat.isDirectory()) {
184
+ return "Target path already exists and is not a directory.";
185
+ }
186
+
187
+ const entries = await readdir(targetDir);
188
+ if (entries.length > 0) {
189
+ return "Target directory already exists and is not empty.";
190
+ }
191
+
192
+ return null;
193
+ }
194
+
195
+ function normalizeAdapter(value) {
196
+ const normalized = value.toLowerCase();
197
+
198
+ if (normalized === "1" || normalized === "node") {
199
+ return "node";
200
+ }
201
+
202
+ if (
203
+ normalized === "2" ||
204
+ normalized === "cf" ||
205
+ normalized === "cloudflare" ||
206
+ normalized === "cloudflare-workers"
207
+ ) {
208
+ return "cloudflare";
209
+ }
210
+
211
+ if (normalized === "3" || normalized === "vc" || normalized === "vercel") {
212
+ return "vercel";
213
+ }
214
+
215
+ return null;
216
+ }
217
+
218
+ function buildProjectFiles({ adapter, packageManager, projectName }) {
219
+ const files = {
220
+ ".gitignore": "dist\nnode_modules\n.wrangler\n.vercel\n",
221
+ "README.md": createReadme({ adapter, packageManager, projectName }),
222
+ "package.json": createPackageJson({ adapter, projectName }),
223
+ "src/api/health.ts": createHealthRoute(adapter),
224
+ "src/routes.ts": createRoutesFile(),
225
+ "src/routes/home.tsx": createHomeRoute(adapter),
226
+ "src/shells/public.tsx": createShellFile(projectName),
227
+ "vite.config.ts": createViteConfig(adapter),
228
+ };
229
+
230
+ if (adapter.id === "cloudflare") {
231
+ files["wrangler.jsonc"] = createWranglerConfig(projectName);
232
+ files["src/env.d.ts"] = createCloudflareEnvDeclaration();
233
+ }
234
+
235
+ return files;
236
+ }
237
+
238
+ function createPackageJson({ adapter, projectName }) {
239
+ const scripts = {
240
+ build: "pracht build",
241
+ dev: "pracht dev",
242
+ preview: "pracht preview",
243
+ };
244
+
245
+ const devDependencies = {
246
+ "@pracht/cli": "latest",
247
+ "@pracht/vite-plugin": "latest",
248
+ preact: "^10.26.9",
249
+ "preact-render-to-string": "^6.5.13",
250
+ vite: "^8.0.0",
251
+ };
252
+
253
+ if (adapter.id === "cloudflare") {
254
+ scripts.deploy = "pracht build && wrangler deploy";
255
+ devDependencies.wrangler = "^4.81.0";
256
+ }
257
+
258
+ if (adapter.id === "vercel") {
259
+ scripts.deploy = "pracht build && vercel deploy --prebuilt";
260
+ devDependencies.vercel = "latest";
261
+ }
262
+
263
+ return `${JSON.stringify(
264
+ {
265
+ dependencies: {
266
+ [adapter.packageName]: "latest",
267
+ pracht: "latest",
268
+ },
269
+ devDependencies,
270
+ name: projectName,
271
+ private: true,
272
+ scripts,
273
+ type: "module",
274
+ version: "0.0.0",
275
+ },
276
+ null,
277
+ 2,
278
+ )}\n`;
279
+ }
280
+
281
+ function createViteConfig(adapter) {
282
+ const ADAPTER_IMPORTS = {
283
+ node: { fn: "nodeAdapter", pkg: "@pracht/adapter-node" },
284
+ cloudflare: { fn: "cloudflareAdapter", pkg: "@pracht/adapter-cloudflare" },
285
+ vercel: { fn: "vercelAdapter", pkg: "@pracht/adapter-vercel" },
286
+ };
287
+
288
+ const info = ADAPTER_IMPORTS[adapter.id] ?? ADAPTER_IMPORTS.node;
289
+
290
+ return [
291
+ 'import { defineConfig } from "vite";',
292
+ 'import { pracht } from "@pracht/vite-plugin";',
293
+ `import { ${info.fn} } from "${info.pkg}";`,
294
+ "",
295
+ "export default defineConfig({",
296
+ ` plugins: [pracht({ adapter: ${info.fn}() })],`,
297
+ "});",
298
+ "",
299
+ ].join("\n");
300
+ }
301
+
302
+ function createRoutesFile() {
303
+ return [
304
+ 'import { defineApp, route } from "pracht";',
305
+ "",
306
+ "export const app = defineApp({",
307
+ " shells: {",
308
+ ' public: "./shells/public.tsx",',
309
+ " },",
310
+ " routes: [",
311
+ ' route("/", "./routes/home.tsx", { id: "home", render: "ssg", shell: "public" }),',
312
+ " ],",
313
+ "});",
314
+ "",
315
+ ].join("\n");
316
+ }
317
+
318
+ function createShellFile(projectName) {
319
+ return [
320
+ 'import type { ShellProps } from "pracht";',
321
+ "",
322
+ "export function Shell({ children }: ShellProps) {",
323
+ " return (",
324
+ ' <div style={{ fontFamily: "Inter, system-ui, sans-serif", margin: "0 auto", maxWidth: "720px", padding: "48px 20px" }}>',
325
+ ' <header style={{ marginBottom: "32px" }}>',
326
+ ` <strong>${projectName}</strong>`,
327
+ ' <p style={{ color: "#555", margin: "8px 0 0" }}>A new pracht app.</p>',
328
+ " </header>",
329
+ " <main>{children}</main>",
330
+ " </div>",
331
+ " );",
332
+ "}",
333
+ "",
334
+ "export function head() {",
335
+ " return {",
336
+ ' meta: [{ content: "width=device-width, initial-scale=1", name: "viewport" }],',
337
+ ` title: ${JSON.stringify(projectName)},`,
338
+ " };",
339
+ "}",
340
+ "",
341
+ ].join("\n");
342
+ }
343
+
344
+ function createHomeRoute(adapter) {
345
+ return [
346
+ 'import type { LoaderArgs, RouteComponentProps } from "pracht";',
347
+ "",
348
+ "export async function loader(_args: LoaderArgs) {",
349
+ " return {",
350
+ ` adapter: ${JSON.stringify(adapter.label)},`,
351
+ " steps: [",
352
+ ' "Edit src/routes/home.tsx to change this page.",',
353
+ ' "Add more routes in src/routes.ts.",',
354
+ ' "Add API handlers in src/api/*.ts.",',
355
+ " ],",
356
+ " };",
357
+ "}",
358
+ "",
359
+ "export function Component({ data }: RouteComponentProps<typeof loader>) {",
360
+ " return (",
361
+ " <section>",
362
+ ' <p style={{ color: "#555", marginBottom: "8px" }}>Starter ready.</p>',
363
+ ' <h1 style={{ fontSize: "2.5rem", lineHeight: 1.1, margin: "0 0 16px" }}>Your pracht app is up and running.</h1>',
364
+ ' <p style={{ fontSize: "1.1rem", lineHeight: 1.6, marginBottom: "24px" }}>',
365
+ " This starter is configured for <strong>{data.adapter}</strong>.",
366
+ " </p>",
367
+ ' <ul style={{ lineHeight: 1.8, paddingLeft: "20px" }}>',
368
+ " {data.steps.map((step) => (",
369
+ " <li key={step}>{step}</li>",
370
+ " ))}",
371
+ " </ul>",
372
+ ' <p style={{ marginTop: "24px" }}>',
373
+ " Check <code>/api/health</code> for a simple API route.",
374
+ " </p>",
375
+ " </section>",
376
+ " );",
377
+ "}",
378
+ "",
379
+ ].join("\n");
380
+ }
381
+
382
+ function createHealthRoute(adapter) {
383
+ return [
384
+ "export function GET() {",
385
+ " return Response.json({",
386
+ ` adapter: ${JSON.stringify(adapter.short)},`,
387
+ " ok: true,",
388
+ ' service: "pracht",',
389
+ " });",
390
+ "}",
391
+ "",
392
+ ].join("\n");
393
+ }
394
+
395
+ function createWranglerConfig(projectName) {
396
+ const compatibilityDate = new Date().toISOString().slice(0, 10);
397
+
398
+ return [
399
+ "{",
400
+ ' "$schema": "node_modules/wrangler/config-schema.json",',
401
+ ` "name": ${JSON.stringify(projectName)},`,
402
+ ' "main": "dist/server/server.js",',
403
+ ` "compatibility_date": ${JSON.stringify(compatibilityDate)},`,
404
+ ' "assets": {',
405
+ ' "binding": "ASSETS",',
406
+ ' "directory": "dist/client",',
407
+ ' "run_worker_first": true',
408
+ " }",
409
+ "}",
410
+ "",
411
+ ].join("\n");
412
+ }
413
+
414
+ function createCloudflareEnvDeclaration() {
415
+ return [
416
+ 'import "pracht";',
417
+ 'declare module "pracht" {',
418
+ " interface Register {",
419
+ " context: {",
420
+ " env: Env;",
421
+ " executionContext: ExecutionContext;",
422
+ " };",
423
+ " }",
424
+ "}",
425
+ "",
426
+ ].join("\n");
427
+ }
428
+
429
+ function createReadme({ adapter, packageManager, projectName }) {
430
+ const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
431
+ const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
432
+ const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
433
+ const deployCommand = packageManager === "npm" ? "npm run deploy" : `${packageManager} deploy`;
434
+
435
+ const lines = [
436
+ `# ${projectName}`,
437
+ "",
438
+ `This pracht starter is configured for ${adapter.label}.`,
439
+ "",
440
+ "## Commands",
441
+ "",
442
+ `- \`${installCommand}\``,
443
+ `- \`${devCommand}\``,
444
+ `- \`${previewCommand}\``,
445
+ ];
446
+
447
+ if (adapter.id === "cloudflare") {
448
+ lines.push(`- \`${deployCommand}\``);
449
+ lines.push("");
450
+ lines.push(
451
+ "Edit `wrangler.jsonc` to add KV, D1, R2, cron triggers, or other Cloudflare bindings.",
452
+ );
453
+ }
454
+
455
+ if (adapter.id === "vercel") {
456
+ lines.push(`- \`${deployCommand}\``);
457
+ lines.push("");
458
+ lines.push("Run the deploy command after linking or logging into your Vercel account.");
459
+ }
460
+
461
+ lines.push("");
462
+ lines.push("## Files");
463
+ lines.push("");
464
+ lines.push("- `src/routes.ts` defines your app manifest.");
465
+ lines.push("- `src/routes/home.tsx` is the first page.");
466
+ lines.push("- `src/api/health.ts` is a sample API route.");
467
+
468
+ return `${lines.join("\n")}\n`;
469
+ }
470
+
471
+ async function installDependencies(targetDir, packageManager) {
472
+ const args = packageManager === "yarn" ? ["install"] : ["install"];
473
+
474
+ return await new Promise((resolveInstall) => {
475
+ const child = spawn(packageManager, args, {
476
+ cwd: targetDir,
477
+ stdio: "inherit",
478
+ });
479
+
480
+ child.on("close", (code) => {
481
+ resolveInstall(code === 0);
482
+ });
483
+
484
+ child.on("error", () => {
485
+ resolveInstall(false);
486
+ });
487
+ });
488
+ }
489
+
490
+ function printNextSteps({ adapter, dir, installSucceeded, packageManager, skipInstall }) {
491
+ const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
492
+ const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
493
+
494
+ console.log("");
495
+ console.log(`Created a pracht app in ${dir}.`);
496
+ console.log(`Adapter: ${adapter.label}`);
497
+ console.log("");
498
+ console.log("Next steps:");
499
+ console.log(` cd ${dir}`);
500
+
501
+ if (skipInstall || !installSucceeded) {
502
+ console.log(` ${installCommand}`);
503
+ }
504
+
505
+ console.log(` ${devCommand}`);
506
+
507
+ if (!skipInstall && !installSucceeded) {
508
+ console.log("");
509
+ console.log("Dependency installation did not complete. The project files were still created.");
510
+ }
511
+ }
512
+
513
+ function printHelp() {
514
+ console.log(`create-pracht
515
+
516
+ Usage:
517
+ create-pracht [directory] [--adapter=node|cf|vercel] [--skip-install]
518
+ `);
519
+ }
520
+
521
+ function toPackageName(value) {
522
+ const normalized = value
523
+ .trim()
524
+ .toLowerCase()
525
+ .replace(/[^a-z0-9-]+/g, "-")
526
+ .replace(/^-+|-+$/g, "");
527
+
528
+ return normalized || DEFAULT_DIRECTORY;
529
+ }