create-daloy 0.1.0 → 0.1.2

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 CHANGED
@@ -13,7 +13,8 @@ bun create daloy my-api
13
13
  The CLI is interactive when arguments are missing. It will ask you for:
14
14
 
15
15
  - A project directory name (defaults to `my-daloy-app`)
16
- - A template (`node-basic` or `cloudflare-worker`)
16
+ - A template (`node-basic`, `vercel-edge`, or `cloudflare-worker`)
17
+ - A package manager (`pnpm`, `npm`, `yarn`, or `bun`)
17
18
  - Whether to install dependencies
18
19
  - Whether to initialize a git repository
19
20
 
@@ -31,8 +32,9 @@ pnpm create daloy@latest my-api \
31
32
 
32
33
  | Flag | Description |
33
34
  | --- | --- |
34
- | `--template <name>` | `node-basic` (default) or `cloudflare-worker`. |
35
+ | `--template <name>` | `node-basic` (default), `vercel-edge`, or `cloudflare-worker`. |
35
36
  | `--package-manager <pm>` | `pnpm` (default), `npm`, `yarn`, or `bun`. |
37
+ | `--list-templates` | Print available templates with descriptions. |
36
38
  | `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to interactive. |
37
39
  | `--git` / `--no-git` | Initialize a git repository. Defaults to interactive. |
38
40
  | `--force` | Overwrite an existing non-empty directory. |
@@ -42,6 +44,8 @@ pnpm create daloy@latest my-api \
42
44
 
43
45
  ## Templates
44
46
 
47
+ Use `create-daloy --list-templates` to inspect available templates without creating a project.
48
+
45
49
  ### `node-basic`
46
50
 
47
51
  A production-ready Node.js HTTP server using `@daloyjs/core` with:
@@ -60,6 +64,15 @@ A minimal Cloudflare Worker bootstrap using `@daloyjs/core/cloudflare` with:
60
64
  - Zod-validated route exposed as `fetch`.
61
65
  - A sample test that exercises `app.request(...)`.
62
66
 
67
+ ### `vercel-edge`
68
+
69
+ A Vercel Edge API bootstrap using `@daloyjs/core/vercel` with:
70
+
71
+ - `api/[...path].ts` catch-all routing so DaloyJS owns the API surface.
72
+ - `export const config = { runtime: "edge" }` ready for Vercel Edge.
73
+ - `vercel dev` / `vercel deploy` scripts.
74
+ - A health route and bookstore route mirroring the Node starter.
75
+
63
76
  ## What the CLI guarantees
64
77
 
65
78
  - Zero runtime dependencies (uses only Node built-ins) for a clean supply-chain footprint.
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { spawn } from "node:child_process";
6
6
  import { existsSync } from "node:fs";
7
- import { mkdir, readdir, readFile, stat, writeFile, copyFile, rename } from "node:fs/promises";
7
+ import { mkdir, readdir, readFile, writeFile, copyFile } from "node:fs/promises";
8
8
  import { createInterface } from "node:readline/promises";
9
9
  import { stdin as input, stdout as output } from "node:process";
10
10
  import path from "node:path";
@@ -14,8 +14,33 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
14
14
  const PKG_ROOT = path.resolve(__dirname, "..");
15
15
  const TEMPLATES_DIR = path.join(PKG_ROOT, "templates");
16
16
 
17
- const TEMPLATES = ["node-basic", "cloudflare-worker"];
18
- const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
17
+ const TEMPLATE_OPTIONS = [
18
+ {
19
+ value: "node-basic",
20
+ title: "Node API",
21
+ description: "Traditional REST API with secure defaults and Hey API codegen",
22
+ },
23
+ {
24
+ value: "vercel-edge",
25
+ title: "Vercel Edge",
26
+ description: "Catch-all Edge API route using @daloyjs/core/vercel",
27
+ },
28
+ {
29
+ value: "cloudflare-worker",
30
+ title: "Cloudflare Workers",
31
+ description: "Worker entrypoint with wrangler dev/deploy scripts",
32
+ },
33
+ ];
34
+
35
+ const PACKAGE_MANAGER_OPTIONS = [
36
+ { value: "pnpm", title: "pnpm", description: "Recommended for DaloyJS projects" },
37
+ { value: "npm", title: "npm", description: "Use the npm client you already have" },
38
+ { value: "yarn", title: "Yarn", description: "Classic create workflow" },
39
+ { value: "bun", title: "Bun", description: "Fast install and runtime tooling" },
40
+ ];
41
+
42
+ const TEMPLATES = TEMPLATE_OPTIONS.map((option) => option.value);
43
+ const PACKAGE_MANAGERS = PACKAGE_MANAGER_OPTIONS.map((option) => option.value);
19
44
 
20
45
  const RENAME_ON_COPY = new Map([
21
46
  ["_gitignore", ".gitignore"],
@@ -40,7 +65,7 @@ function color(code, s) {
40
65
  }
41
66
 
42
67
  function printHelp() {
43
- console.log(`${color(COLORS.bold, "create-daloy")} scaffold a DaloyJS project
68
+ console.log(`${color(COLORS.bold, "create-daloy")} - scaffold a DaloyJS project
44
69
 
45
70
  Usage:
46
71
  pnpm create daloy@latest [project-name] [options]
@@ -49,6 +74,7 @@ Usage:
49
74
  Options:
50
75
  --template <name> ${TEMPLATES.join(" | ")} (default: node-basic)
51
76
  --package-manager <pm> ${PACKAGE_MANAGERS.join(" | ")} (default: pnpm)
77
+ --list-templates Print available templates and exit.
52
78
  --install / --no-install Install dependencies after scaffolding.
53
79
  --git / --no-git Initialize a git repository.
54
80
  --force Overwrite an existing non-empty directory.
@@ -58,6 +84,14 @@ Options:
58
84
  `);
59
85
  }
60
86
 
87
+ function printTemplates() {
88
+ console.log(color(COLORS.bold, "Available DaloyJS templates\n"));
89
+ for (const option of TEMPLATE_OPTIONS) {
90
+ console.log(` ${color(COLORS.cyan, option.value.padEnd(18))} ${option.title}`);
91
+ console.log(color(COLORS.dim, ` ${" ".repeat(18)} ${option.description}\n`));
92
+ }
93
+ }
94
+
61
95
  async function readPkgVersion() {
62
96
  try {
63
97
  const raw = await readFile(path.join(PKG_ROOT, "package.json"), "utf8");
@@ -78,12 +112,14 @@ function parseArgs(argv) {
78
112
  yes: false,
79
113
  help: false,
80
114
  version: false,
115
+ listTemplates: false,
81
116
  };
82
117
  const args = [...argv];
83
118
  while (args.length) {
84
119
  const a = args.shift();
85
120
  if (a === "--help" || a === "-h") out.help = true;
86
121
  else if (a === "--version" || a === "-v") out.version = true;
122
+ else if (a === "--list-templates") out.listTemplates = true;
87
123
  else if (a === "--yes" || a === "-y") out.yes = true;
88
124
  else if (a === "--force") out.force = true;
89
125
  else if (a === "--install") out.install = true;
@@ -181,22 +217,63 @@ async function askYesNo(rl, question, defaultYes) {
181
217
  return answer === "y" || answer === "yes";
182
218
  }
183
219
 
220
+ function optionValue(option) {
221
+ return typeof option === "string" ? option : option.value;
222
+ }
223
+
224
+ function optionLabel(option) {
225
+ return typeof option === "string" ? option : `${option.title} ${color(COLORS.dim, `(${option.value})`)}`;
226
+ }
227
+
184
228
  async function askChoice(rl, question, choices, defaultChoice) {
229
+ const nameWidth = Math.max(...choices.map((choice) => optionLabel(choice).replace(/\x1b\[[0-9;]*m/g, "").length));
185
230
  const list = choices
186
- .map((c, i) => ` ${color(COLORS.dim, `${i + 1})`)} ${c}${c === defaultChoice ? color(COLORS.dim, " (default)") : ""}`)
231
+ .map((choice, i) => {
232
+ const value = optionValue(choice);
233
+ const label = optionLabel(choice).padEnd(nameWidth);
234
+ const description = typeof choice === "string" ? "" : color(COLORS.dim, ` ${choice.description}`);
235
+ const defaultMarker = value === defaultChoice ? color(COLORS.green, " recommended") : "";
236
+ return ` ${color(COLORS.dim, `${i + 1})`)} ${label}${description}${defaultMarker}`;
237
+ })
187
238
  .join("\n");
188
239
  console.log(`${color(COLORS.cyan, "?")} ${question}\n${list}`);
189
240
  const raw = (await rl.question(` > `)).trim();
190
241
  if (raw.length === 0) return defaultChoice;
191
242
  const asNumber = Number.parseInt(raw, 10);
192
243
  if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= choices.length) {
193
- return choices[asNumber - 1];
244
+ return optionValue(choices[asNumber - 1]);
194
245
  }
195
- if (choices.includes(raw)) return raw;
196
- console.error(color(COLORS.red, `Invalid choice. Pick one of: ${choices.join(", ")}`));
246
+ if (choices.some((choice) => optionValue(choice) === raw)) return raw;
247
+ console.error(color(COLORS.red, `Invalid choice. Pick one of: ${choices.map(optionValue).join(", ")}`));
197
248
  return askChoice(rl, question, choices, defaultChoice);
198
249
  }
199
250
 
251
+ function logStep(message, detail) {
252
+ const suffix = detail ? color(COLORS.dim, ` ${detail}`) : "";
253
+ console.log(`${color(COLORS.green, " [ok]")} ${message}${suffix}`);
254
+ }
255
+
256
+ function logWarn(message) {
257
+ console.warn(`${color(COLORS.yellow, " [warn]")} ${message}`);
258
+ }
259
+
260
+ function printSummary({ projectName, template, packageManager, installDeps }) {
261
+ console.log(color(COLORS.green, "\nCreated a new DaloyJS project."));
262
+ console.log(`\n ${color(COLORS.bold, "Project")} ${projectName}`);
263
+ console.log(` ${color(COLORS.bold, "Template")} ${template}`);
264
+ console.log(` ${color(COLORS.bold, "Manager")} ${packageManager}`);
265
+ console.log(`\n ${color(COLORS.bold, "Next steps")}`);
266
+ console.log(` cd ${projectName}`);
267
+ if (!installDeps) console.log(` ${packageManager} install`);
268
+ console.log(` ${packageManager} run dev`);
269
+ console.log(`\n ${color(COLORS.bold, "Useful commands")}`);
270
+ console.log(` ${packageManager} run typecheck`);
271
+ console.log(` ${packageManager} test`);
272
+ if (template === "node-basic") console.log(` ${packageManager} run gen`);
273
+ console.log(`\n ${color(COLORS.dim, "Docs: https://daloyjs.dev/docs")}`);
274
+ console.log(color(COLORS.dim, " Issues: https://github.com/daloyjs/daloy/issues\n"));
275
+ }
276
+
200
277
  async function main() {
201
278
  const opts = parseArgs(process.argv.slice(2));
202
279
 
@@ -208,9 +285,14 @@ async function main() {
208
285
  console.log(await readPkgVersion());
209
286
  process.exit(0);
210
287
  }
288
+ if (opts.listTemplates) {
289
+ printTemplates();
290
+ process.exit(0);
291
+ }
211
292
 
212
- console.log(color(COLORS.bold, "\n create-daloy"));
213
- console.log(color(COLORS.dim, ` https://daloyjs.dev\n`));
293
+ console.log(color(COLORS.bold, "\ncreate-daloy"));
294
+ console.log(color(COLORS.dim, "Contract-first REST APIs for Node, Vercel Edge, and Workers"));
295
+ console.log(color(COLORS.dim, "https://daloyjs.dev\n"));
214
296
 
215
297
  const detectedPm = detectPackageManager();
216
298
  const interactive = !opts.yes && process.stdin.isTTY && process.stdout.isTTY;
@@ -241,7 +323,7 @@ async function main() {
241
323
 
242
324
  let template = opts.template;
243
325
  if (!template) {
244
- template = rl ? await askChoice(rl, "Pick a template:", TEMPLATES, "node-basic") : "node-basic";
326
+ template = rl ? await askChoice(rl, "Choose a starter template:", TEMPLATE_OPTIONS, "node-basic") : "node-basic";
245
327
  }
246
328
  if (!TEMPLATES.includes(template)) {
247
329
  console.error(color(COLORS.red, `Unknown template "${template}". Available: ${TEMPLATES.join(", ")}`));
@@ -268,7 +350,12 @@ async function main() {
268
350
  }
269
351
  }
270
352
 
271
- let packageManager = opts.packageManager ?? detectedPm;
353
+ let packageManager = opts.packageManager;
354
+ if (!packageManager) {
355
+ packageManager = rl
356
+ ? await askChoice(rl, "Choose a package manager:", PACKAGE_MANAGER_OPTIONS, detectedPm)
357
+ : detectedPm;
358
+ }
272
359
  if (!PACKAGE_MANAGERS.includes(packageManager)) {
273
360
  console.error(
274
361
  color(COLORS.red, `Unknown --package-manager "${packageManager}". Use one of: ${PACKAGE_MANAGERS.join(", ")}`),
@@ -288,18 +375,20 @@ async function main() {
288
375
 
289
376
  rl?.close();
290
377
 
291
- console.log(color(COLORS.dim, `\n Scaffolding ${color(COLORS.bold, projectName)} from template ${color(COLORS.bold, template)}...`));
378
+ console.log(color(COLORS.bold, "\nScaffolding"));
292
379
 
293
380
  await mkdir(targetDir, { recursive: true });
294
381
  await copyTemplate(templateDir, targetDir);
382
+ logStep("Template copied", template);
295
383
  await patchPackageJson(targetDir, projectName);
384
+ logStep("Package metadata written", projectName);
296
385
 
297
386
  if (initGit) {
298
387
  const code = await run("git", ["init", "--quiet"], targetDir);
299
388
  if (code === 0) {
300
- console.log(color(COLORS.green, " git repository initialized"));
389
+ logStep("Git repository initialized");
301
390
  } else {
302
- console.warn(color(COLORS.yellow, " ! git init failed; continuing"));
391
+ logWarn("git init failed; continuing");
303
392
  }
304
393
  }
305
394
 
@@ -307,22 +396,13 @@ async function main() {
307
396
  console.log(color(COLORS.dim, ` Installing dependencies with ${packageManager}...`));
308
397
  const code = await run(packageManager, ["install"], targetDir);
309
398
  if (code !== 0) {
310
- console.warn(
311
- color(COLORS.yellow, ` ! ${packageManager} install exited with code ${code}; you can retry inside ${projectName}.`),
312
- );
399
+ logWarn(`${packageManager} install exited with code ${code}; you can retry inside ${projectName}.`);
313
400
  } else {
314
- console.log(color(COLORS.green, " dependencies installed"));
401
+ logStep("Dependencies installed", packageManager);
315
402
  }
316
403
  }
317
404
 
318
- console.log(color(COLORS.green, `\n Done.`));
319
- console.log(`\n Next steps:\n`);
320
- console.log(` cd ${projectName}`);
321
- if (!installDeps) console.log(` ${packageManager} install`);
322
- console.log(` ${packageManager} run dev`);
323
- console.log("");
324
- console.log(color(COLORS.dim, " Docs: https://daloyjs.dev/docs"));
325
- console.log(color(COLORS.dim, " Issues: https://github.com/daloyjs/daloy/issues\n"));
405
+ printSummary({ projectName, template, packageManager, installDeps });
326
406
  } catch (err) {
327
407
  rl?.close();
328
408
  console.error(color(COLORS.red, `\n Failed: ${(err && err.message) || err}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-daloy",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -0,0 +1,32 @@
1
+ # my-daloy-vercel-api
2
+
3
+ A [DaloyJS](https://daloyjs.dev) Vercel Edge API starter.
4
+
5
+ ## Develop
6
+
7
+ ```bash
8
+ pnpm install
9
+ pnpm dev # http://localhost:3000
10
+ ```
11
+
12
+ Try it:
13
+
14
+ ```bash
15
+ curl http://localhost:3000/healthz
16
+ curl http://localhost:3000/books/1
17
+ ```
18
+
19
+ ## Deploy
20
+
21
+ ```bash
22
+ pnpm deploy
23
+ ```
24
+
25
+ The API entry lives at `api/[...path].ts` and uses `@daloyjs/core/vercel`:
26
+
27
+ ```ts
28
+ export const config = { runtime: "edge" };
29
+ export default toEdgeHandler(app);
30
+ ```
31
+
32
+ That catch-all API route lets DaloyJS own routing while Vercel handles the Edge runtime.
@@ -0,0 +1 @@
1
+ # Add Vercel project env vars here.
@@ -0,0 +1,9 @@
1
+ node_modules/
2
+ .vercel/
3
+ dist/
4
+ coverage/
5
+ .DS_Store
6
+ *.log
7
+ .env
8
+ .env.*
9
+ !.env.example
@@ -0,0 +1,4 @@
1
+ auto-install-peers=true
2
+ strict-peer-dependencies=true
3
+ prefer-frozen-lockfile=true
4
+ verify-store-integrity=true
@@ -0,0 +1,55 @@
1
+ import { z } from "zod";
2
+ import { App, NotFoundError, requestId, secureHeaders } from "@daloyjs/core";
3
+ import { toEdgeHandler } from "@daloyjs/core/vercel";
4
+
5
+ export const config = { runtime: "edge" };
6
+
7
+ const app = new App({
8
+ bodyLimitBytes: 256 * 1024,
9
+ requestTimeoutMs: 5_000,
10
+ production: process.env.NODE_ENV === "production",
11
+ });
12
+
13
+ app.use(requestId());
14
+ app.use(secureHeaders());
15
+
16
+ app.route({
17
+ method: "GET",
18
+ path: "/healthz",
19
+ operationId: "healthz",
20
+ tags: ["Ops"],
21
+ responses: {
22
+ 200: {
23
+ description: "Service is healthy",
24
+ body: z.object({ ok: z.literal(true), runtime: z.literal("vercel-edge") }),
25
+ },
26
+ },
27
+ handler: async () => ({
28
+ status: 200,
29
+ body: { ok: true, runtime: "vercel-edge" as const },
30
+ }),
31
+ });
32
+
33
+ const Book = z.object({ id: z.string(), title: z.string() });
34
+ const books = new Map<string, z.infer<typeof Book>>([
35
+ ["1", { id: "1", title: "Noli Me Tangere" }],
36
+ ]);
37
+
38
+ app.route({
39
+ method: "GET",
40
+ path: "/books/:id",
41
+ operationId: "getBookById",
42
+ tags: ["Books"],
43
+ request: { params: z.object({ id: z.string() }) },
44
+ responses: {
45
+ 200: { description: "Found", body: Book },
46
+ 404: { description: "Not found" },
47
+ },
48
+ handler: async ({ params }) => {
49
+ const book = books.get(params.id);
50
+ if (!book) throw new NotFoundError(`Book ${params.id} not found`);
51
+ return { status: 200, body: book };
52
+ },
53
+ });
54
+
55
+ export default toEdgeHandler(app);
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "my-daloy-vercel-api",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vercel dev",
8
+ "deploy": "vercel deploy",
9
+ "typecheck": "tsc --noEmit",
10
+ "test": "node --import tsx --test tests/**/*.test.ts"
11
+ },
12
+ "dependencies": {
13
+ "@daloyjs/core": "^0.1.1",
14
+ "zod": "^4.4.3"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^25.7.0",
18
+ "tsx": "^4.22.0",
19
+ "typescript": "^6.0.3",
20
+ "vercel": "^48.0.0"
21
+ }
22
+ }
@@ -0,0 +1,9 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+ import handler from "../api/[...path].js";
4
+
5
+ test("Vercel Edge handler responds through DaloyJS", async () => {
6
+ const response = await handler(new Request("https://example.test/healthz"));
7
+ assert.equal(response.status, 200);
8
+ assert.equal((await response.json()).runtime, "vercel-edge");
9
+ });
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "lib": ["ES2022", "DOM"],
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "isolatedModules": true,
15
+ "noEmit": true
16
+ },
17
+ "include": ["api/**/*.ts", "tests/**/*.ts"]
18
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "version": 2,
3
+ "cleanUrls": true
4
+ }