create-pracht 0.0.1 → 0.1.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.
Files changed (2) hide show
  1. package/package.json +13 -1
  2. package/src/index.js +155 -21
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "create-pracht",
3
- "version": "0.0.1",
3
+ "version": "0.1.0",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "https://github.com/JoviDeCroock/pracht",
7
+ "directory": "packages/start"
8
+ },
9
+ "homepage": "https://github.com/JoviDeCroock/pracht/tree/main/packages/start",
10
+ "bugs": {
11
+ "url": "https://github.com/JoviDeCroock/pracht/issues"
12
+ },
4
13
  "bin": {
5
14
  "create-pracht": "./bin/create-pracht.js"
6
15
  },
@@ -10,6 +19,9 @@
10
19
  "README.md"
11
20
  ],
12
21
  "type": "module",
22
+ "publishConfig": {
23
+ "provenance": true
24
+ },
13
25
  "scripts": {
14
26
  "build": "node -e \"\""
15
27
  }
package/src/index.js CHANGED
@@ -4,6 +4,19 @@ import { mkdir, readdir, stat, writeFile } from "node:fs/promises";
4
4
  import { basename, dirname, resolve } from "node:path";
5
5
  import { createInterface } from "node:readline/promises";
6
6
 
7
+ async function fetchLatestVersion(packageName) {
8
+ const res = await fetch(
9
+ `https://registry.npmjs.org/${packageName}/latest`,
10
+ );
11
+ if (!res.ok) {
12
+ throw new Error(
13
+ `Failed to fetch version for ${packageName}: ${res.statusText}`,
14
+ );
15
+ }
16
+ const data = await res.json();
17
+ return data.version;
18
+ }
19
+
7
20
  const ADAPTERS = {
8
21
  node: {
9
22
  description: "Node.js server with pracht preview",
@@ -46,6 +59,7 @@ export async function run(argv = process.argv.slice(2)) {
46
59
  try {
47
60
  const dir = options.dir ?? (await promptForDirectory(readline));
48
61
  const adapterId = options.adapter ?? (await promptForAdapter(readline));
62
+ const router = options.router ?? (await promptForRouter(readline));
49
63
  const targetDir = resolve(process.cwd(), dir);
50
64
 
51
65
  await ensureTargetDirectory(targetDir);
@@ -53,6 +67,7 @@ export async function run(argv = process.argv.slice(2)) {
53
67
  await scaffoldProject({
54
68
  adapter: ADAPTERS[adapterId],
55
69
  packageManager,
70
+ router,
56
71
  targetDir,
57
72
  });
58
73
 
@@ -75,12 +90,13 @@ export async function run(argv = process.argv.slice(2)) {
75
90
  }
76
91
  }
77
92
 
78
- export async function scaffoldProject({ adapter, packageManager, targetDir }) {
93
+ export async function scaffoldProject({ adapter, packageManager, router = "manifest", targetDir }) {
79
94
  const packageName = toPackageName(basename(targetDir));
80
- const files = buildProjectFiles({
95
+ const files = await buildProjectFiles({
81
96
  adapter,
82
97
  packageManager,
83
98
  projectName: packageName,
99
+ router,
84
100
  });
85
101
 
86
102
  await mkdir(targetDir, { recursive: true });
@@ -103,6 +119,7 @@ export function parseArgs(argv) {
103
119
  const options = {
104
120
  adapter: undefined,
105
121
  dir: undefined,
122
+ router: undefined,
106
123
  skipInstall: false,
107
124
  };
108
125
 
@@ -117,6 +134,11 @@ export function parseArgs(argv) {
117
134
  continue;
118
135
  }
119
136
 
137
+ if (arg.startsWith("--router=")) {
138
+ options.router = normalizeRouter(arg.slice("--router=".length));
139
+ continue;
140
+ }
141
+
120
142
  if (arg === "--help" || arg === "-h") {
121
143
  printHelp();
122
144
  process.exit(0);
@@ -166,6 +188,23 @@ async function promptForAdapter(readline) {
166
188
  }
167
189
  }
168
190
 
191
+ async function promptForRouter(readline) {
192
+ console.log("Router:");
193
+ console.log(" 1. Manifest (explicit routes.ts)");
194
+ console.log(" 2. Pages (file-system routing)");
195
+
196
+ while (true) {
197
+ const answer = await readline.question("Router (1): ");
198
+ const normalized = normalizeRouter(answer.trim() || "1");
199
+
200
+ if (normalized) {
201
+ return normalized;
202
+ }
203
+
204
+ console.log("Choose 1/2 or manifest/pages.");
205
+ }
206
+ }
207
+
169
208
  async function ensureTargetDirectory(targetDir) {
170
209
  const error = await validateTargetDirectory(targetDir);
171
210
 
@@ -192,6 +231,20 @@ async function validateTargetDirectory(targetDir) {
192
231
  return null;
193
232
  }
194
233
 
234
+ function normalizeRouter(value) {
235
+ const normalized = value.toLowerCase();
236
+
237
+ if (normalized === "1" || normalized === "manifest") {
238
+ return "manifest";
239
+ }
240
+
241
+ if (normalized === "2" || normalized === "pages") {
242
+ return "pages";
243
+ }
244
+
245
+ return null;
246
+ }
247
+
195
248
  function normalizeAdapter(value) {
196
249
  const normalized = value.toLowerCase();
197
250
 
@@ -215,18 +268,46 @@ function normalizeAdapter(value) {
215
268
  return null;
216
269
  }
217
270
 
218
- function buildProjectFiles({ adapter, packageManager, projectName }) {
271
+ async function resolveVersions(packageNames) {
272
+ const entries = await Promise.all(
273
+ packageNames.map(async (name) => [
274
+ name,
275
+ `^${await fetchLatestVersion(name)}`,
276
+ ]),
277
+ );
278
+ return Object.fromEntries(entries);
279
+ }
280
+
281
+ async function buildProjectFiles({ adapter, packageManager, projectName, router }) {
282
+ const packagesToResolve = [
283
+ "@pracht/cli",
284
+ "@pracht/vite-plugin",
285
+ "@pracht/core",
286
+ adapter.packageName,
287
+ ];
288
+ if (adapter.id === "vercel") {
289
+ packagesToResolve.push("vercel");
290
+ }
291
+
292
+ const versions = await resolveVersions(packagesToResolve);
293
+
219
294
  const files = {
220
295
  ".gitignore": "dist\nnode_modules\n.wrangler\n.vercel\n",
221
- "README.md": createReadme({ adapter, packageManager, projectName }),
222
- "package.json": createPackageJson({ adapter, projectName }),
296
+ "README.md": createReadme({ adapter, packageManager, projectName, router }),
297
+ "package.json": createPackageJson({ adapter, projectName, versions }),
223
298
  "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),
299
+ "vite.config.ts": createViteConfig(adapter, router),
228
300
  };
229
301
 
302
+ if (router === "pages") {
303
+ files["src/pages/_app.tsx"] = createShellFile(projectName);
304
+ files["src/pages/index.tsx"] = createPagesHomeRoute(adapter);
305
+ } else {
306
+ files["src/routes.ts"] = createRoutesFile();
307
+ files["src/routes/home.tsx"] = createHomeRoute(adapter);
308
+ files["src/shells/public.tsx"] = createShellFile(projectName);
309
+ }
310
+
230
311
  if (adapter.id === "cloudflare") {
231
312
  files["wrangler.jsonc"] = createWranglerConfig(projectName);
232
313
  files["src/env.d.ts"] = createCloudflareEnvDeclaration();
@@ -235,7 +316,7 @@ function buildProjectFiles({ adapter, packageManager, projectName }) {
235
316
  return files;
236
317
  }
237
318
 
238
- function createPackageJson({ adapter, projectName }) {
319
+ function createPackageJson({ adapter, projectName, versions }) {
239
320
  const scripts = {
240
321
  build: "pracht build",
241
322
  dev: "pracht dev",
@@ -243,8 +324,8 @@ function createPackageJson({ adapter, projectName }) {
243
324
  };
244
325
 
245
326
  const devDependencies = {
246
- "@pracht/cli": "latest",
247
- "@pracht/vite-plugin": "latest",
327
+ "@pracht/cli": versions["@pracht/cli"],
328
+ "@pracht/vite-plugin": versions["@pracht/vite-plugin"],
248
329
  preact: "^10.26.9",
249
330
  "preact-render-to-string": "^6.5.13",
250
331
  vite: "^8.0.0",
@@ -257,14 +338,14 @@ function createPackageJson({ adapter, projectName }) {
257
338
 
258
339
  if (adapter.id === "vercel") {
259
340
  scripts.deploy = "pracht build && vercel deploy --prebuilt";
260
- devDependencies.vercel = "latest";
341
+ devDependencies.vercel = versions["vercel"];
261
342
  }
262
343
 
263
344
  return `${JSON.stringify(
264
345
  {
265
346
  dependencies: {
266
- [adapter.packageName]: "latest",
267
- pracht: "latest",
347
+ [adapter.packageName]: versions[adapter.packageName],
348
+ "@pracht/core": versions["@pracht/core"],
268
349
  },
269
350
  devDependencies,
270
351
  name: projectName,
@@ -278,7 +359,7 @@ function createPackageJson({ adapter, projectName }) {
278
359
  )}\n`;
279
360
  }
280
361
 
281
- function createViteConfig(adapter) {
362
+ function createViteConfig(adapter, router) {
282
363
  const ADAPTER_IMPORTS = {
283
364
  node: { fn: "nodeAdapter", pkg: "@pracht/adapter-node" },
284
365
  cloudflare: { fn: "cloudflareAdapter", pkg: "@pracht/adapter-cloudflare" },
@@ -287,13 +368,18 @@ function createViteConfig(adapter) {
287
368
 
288
369
  const info = ADAPTER_IMPORTS[adapter.id] ?? ADAPTER_IMPORTS.node;
289
370
 
371
+ const prachtOptions =
372
+ router === "pages"
373
+ ? `{ pagesDir: "/src/pages", adapter: ${info.fn}() }`
374
+ : `{ adapter: ${info.fn}() }`;
375
+
290
376
  return [
291
377
  'import { defineConfig } from "vite";',
292
378
  'import { pracht } from "@pracht/vite-plugin";',
293
379
  `import { ${info.fn} } from "${info.pkg}";`,
294
380
  "",
295
381
  "export default defineConfig({",
296
- ` plugins: [pracht({ adapter: ${info.fn}() })],`,
382
+ ` plugins: [pracht(${prachtOptions})],`,
297
383
  "});",
298
384
  "",
299
385
  ].join("\n");
@@ -379,6 +465,46 @@ function createHomeRoute(adapter) {
379
465
  ].join("\n");
380
466
  }
381
467
 
468
+ function createPagesHomeRoute(adapter) {
469
+ return [
470
+ 'import type { LoaderArgs, RouteComponentProps } from "@pracht/core";',
471
+ "",
472
+ 'export const RENDER_MODE = "ssg";',
473
+ "",
474
+ "export async function loader(_args: LoaderArgs) {",
475
+ " return {",
476
+ ` adapter: ${JSON.stringify(adapter.label)},`,
477
+ " steps: [",
478
+ ' "Edit src/pages/index.tsx to change this page.",',
479
+ ' "Add more pages in src/pages/.",',
480
+ ' "Add API handlers in src/api/*.ts.",',
481
+ " ],",
482
+ " };",
483
+ "}",
484
+ "",
485
+ "export function Component({ data }: RouteComponentProps<typeof loader>) {",
486
+ " return (",
487
+ " <section>",
488
+ ' <p style={{ color: "#555", marginBottom: "8px" }}>Starter ready.</p>',
489
+ ' <h1 style={{ fontSize: "2.5rem", lineHeight: 1.1, margin: "0 0 16px" }}>Your pracht app is up and running.</h1>',
490
+ ' <p style={{ fontSize: "1.1rem", lineHeight: 1.6, marginBottom: "24px" }}>',
491
+ " This starter is configured for <strong>{data.adapter}</strong>.",
492
+ " </p>",
493
+ ' <ul style={{ lineHeight: 1.8, paddingLeft: "20px" }}>',
494
+ " {data.steps.map((step) => (",
495
+ " <li key={step}>{step}</li>",
496
+ " ))}",
497
+ " </ul>",
498
+ ' <p style={{ marginTop: "24px" }}>',
499
+ " Check <code>/api/health</code> for a simple API route.",
500
+ " </p>",
501
+ " </section>",
502
+ " );",
503
+ "}",
504
+ "",
505
+ ].join("\n");
506
+ }
507
+
382
508
  function createHealthRoute(adapter) {
383
509
  return [
384
510
  "export function GET() {",
@@ -426,7 +552,7 @@ function createCloudflareEnvDeclaration() {
426
552
  ].join("\n");
427
553
  }
428
554
 
429
- function createReadme({ adapter, packageManager, projectName }) {
555
+ function createReadme({ adapter, packageManager, projectName, router }) {
430
556
  const installCommand = packageManager === "npm" ? "npm install" : `${packageManager} install`;
431
557
  const devCommand = packageManager === "npm" ? "npm run dev" : `${packageManager} dev`;
432
558
  const previewCommand = packageManager === "npm" ? "npm run preview" : `${packageManager} preview`;
@@ -461,8 +587,16 @@ function createReadme({ adapter, packageManager, projectName }) {
461
587
  lines.push("");
462
588
  lines.push("## Files");
463
589
  lines.push("");
464
- lines.push("- `src/routes.ts` defines your app manifest.");
465
- lines.push("- `src/routes/home.tsx` is the first page.");
590
+
591
+ if (router === "pages") {
592
+ lines.push("- `src/pages/` contains your file-system routes.");
593
+ lines.push("- `src/pages/_app.tsx` is the app shell.");
594
+ lines.push("- `src/pages/index.tsx` is the home page.");
595
+ } else {
596
+ lines.push("- `src/routes.ts` defines your app manifest.");
597
+ lines.push("- `src/routes/home.tsx` is the first page.");
598
+ }
599
+
466
600
  lines.push("- `src/api/health.ts` is a sample API route.");
467
601
 
468
602
  return `${lines.join("\n")}\n`;
@@ -514,7 +648,7 @@ function printHelp() {
514
648
  console.log(`create-pracht
515
649
 
516
650
  Usage:
517
- create-pracht [directory] [--adapter=node|cf|vercel] [--skip-install]
651
+ create-pracht [directory] [--adapter=node|cf|vercel] [--router=manifest|pages] [--skip-install]
518
652
  `);
519
653
  }
520
654