create-daloy 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.
- package/README.md +68 -0
- package/bin/create-daloy.mjs +333 -0
- package/package.json +46 -0
- package/templates/cloudflare-worker/README.md +18 -0
- package/templates/cloudflare-worker/_gitignore +7 -0
- package/templates/cloudflare-worker/_npmrc +4 -0
- package/templates/cloudflare-worker/package.json +22 -0
- package/templates/cloudflare-worker/src/index.ts +36 -0
- package/templates/cloudflare-worker/tsconfig.json +18 -0
- package/templates/cloudflare-worker/wrangler.toml +3 -0
- package/templates/node-basic/README.md +41 -0
- package/templates/node-basic/_env.example +1 -0
- package/templates/node-basic/_gitignore +9 -0
- package/templates/node-basic/_npmrc +7 -0
- package/templates/node-basic/openapi-ts.config.ts +7 -0
- package/templates/node-basic/package.json +30 -0
- package/templates/node-basic/scripts/dump-openapi.ts +22 -0
- package/templates/node-basic/src/index.ts +63 -0
- package/templates/node-basic/tsconfig.json +21 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# create-daloy
|
|
2
|
+
|
|
3
|
+
Scaffold a new [DaloyJS](https://github.com/daloyjs/daloy) project in seconds.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# pick the package manager you actually use
|
|
7
|
+
pnpm create daloy@latest my-api
|
|
8
|
+
npm create daloy@latest my-api
|
|
9
|
+
yarn create daloy my-api
|
|
10
|
+
bun create daloy my-api
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The CLI is interactive when arguments are missing. It will ask you for:
|
|
14
|
+
|
|
15
|
+
- A project directory name (defaults to `my-daloy-app`)
|
|
16
|
+
- A template (`node-basic` or `cloudflare-worker`)
|
|
17
|
+
- Whether to install dependencies
|
|
18
|
+
- Whether to initialize a git repository
|
|
19
|
+
|
|
20
|
+
## Non-interactive usage
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pnpm create daloy@latest my-api \
|
|
24
|
+
--template node-basic \
|
|
25
|
+
--package-manager pnpm \
|
|
26
|
+
--install \
|
|
27
|
+
--git
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Flags
|
|
31
|
+
|
|
32
|
+
| Flag | Description |
|
|
33
|
+
| --- | --- |
|
|
34
|
+
| `--template <name>` | `node-basic` (default) or `cloudflare-worker`. |
|
|
35
|
+
| `--package-manager <pm>` | `pnpm` (default), `npm`, `yarn`, or `bun`. |
|
|
36
|
+
| `--install` / `--no-install` | Install dependencies after scaffolding. Defaults to interactive. |
|
|
37
|
+
| `--git` / `--no-git` | Initialize a git repository. Defaults to interactive. |
|
|
38
|
+
| `--force` | Overwrite an existing non-empty directory. |
|
|
39
|
+
| `--yes` | Accept all defaults; never prompt. |
|
|
40
|
+
| `--help` | Print usage and exit. |
|
|
41
|
+
| `--version` | Print version and exit. |
|
|
42
|
+
|
|
43
|
+
## Templates
|
|
44
|
+
|
|
45
|
+
### `node-basic`
|
|
46
|
+
|
|
47
|
+
A production-ready Node.js HTTP server using `@daloyjs/core` with:
|
|
48
|
+
|
|
49
|
+
- Strict TypeScript and `tsx` for instant dev runs.
|
|
50
|
+
- Hardened `.npmrc` for safer installs.
|
|
51
|
+
- `secureHeaders`, `requestId`, and `rateLimit` enabled by default.
|
|
52
|
+
- A sample `GET /healthz` and contract-first `GET /books/:id` route with Zod validation.
|
|
53
|
+
- `pnpm gen` wired to emit OpenAPI 3.1 + a typed Hey API client.
|
|
54
|
+
|
|
55
|
+
### `cloudflare-worker`
|
|
56
|
+
|
|
57
|
+
A minimal Cloudflare Worker bootstrap using `@daloyjs/core/cloudflare` with:
|
|
58
|
+
|
|
59
|
+
- `wrangler.toml` ready to deploy.
|
|
60
|
+
- Zod-validated route exposed as `fetch`.
|
|
61
|
+
- A sample test that exercises `app.request(...)`.
|
|
62
|
+
|
|
63
|
+
## What the CLI guarantees
|
|
64
|
+
|
|
65
|
+
- Zero runtime dependencies (uses only Node built-ins) for a clean supply-chain footprint.
|
|
66
|
+
- Templates are copied verbatim from this package's `templates/` directory.
|
|
67
|
+
- Files prefixed with `_` are renamed (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`) to survive npm packing.
|
|
68
|
+
- The CLI never executes template scripts and never makes network calls beyond the package manager you select.
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-daloy — scaffold a new DaloyJS project.
|
|
3
|
+
// Zero runtime dependencies; uses only Node built-ins.
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import { existsSync } from "node:fs";
|
|
7
|
+
import { mkdir, readdir, readFile, stat, writeFile, copyFile, rename } from "node:fs/promises";
|
|
8
|
+
import { createInterface } from "node:readline/promises";
|
|
9
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const PKG_ROOT = path.resolve(__dirname, "..");
|
|
15
|
+
const TEMPLATES_DIR = path.join(PKG_ROOT, "templates");
|
|
16
|
+
|
|
17
|
+
const TEMPLATES = ["node-basic", "cloudflare-worker"];
|
|
18
|
+
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"];
|
|
19
|
+
|
|
20
|
+
const RENAME_ON_COPY = new Map([
|
|
21
|
+
["_gitignore", ".gitignore"],
|
|
22
|
+
["_npmrc", ".npmrc"],
|
|
23
|
+
["_env.example", ".env.example"],
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
const COLORS = process.stdout.isTTY
|
|
27
|
+
? {
|
|
28
|
+
reset: "\x1b[0m",
|
|
29
|
+
bold: "\x1b[1m",
|
|
30
|
+
dim: "\x1b[2m",
|
|
31
|
+
cyan: "\x1b[36m",
|
|
32
|
+
green: "\x1b[32m",
|
|
33
|
+
red: "\x1b[31m",
|
|
34
|
+
yellow: "\x1b[33m",
|
|
35
|
+
}
|
|
36
|
+
: { reset: "", bold: "", dim: "", cyan: "", green: "", red: "", yellow: "" };
|
|
37
|
+
|
|
38
|
+
function color(code, s) {
|
|
39
|
+
return `${code}${s}${COLORS.reset}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp() {
|
|
43
|
+
console.log(`${color(COLORS.bold, "create-daloy")} — scaffold a DaloyJS project
|
|
44
|
+
|
|
45
|
+
Usage:
|
|
46
|
+
pnpm create daloy@latest [project-name] [options]
|
|
47
|
+
npm create daloy@latest [project-name] [options]
|
|
48
|
+
|
|
49
|
+
Options:
|
|
50
|
+
--template <name> ${TEMPLATES.join(" | ")} (default: node-basic)
|
|
51
|
+
--package-manager <pm> ${PACKAGE_MANAGERS.join(" | ")} (default: pnpm)
|
|
52
|
+
--install / --no-install Install dependencies after scaffolding.
|
|
53
|
+
--git / --no-git Initialize a git repository.
|
|
54
|
+
--force Overwrite an existing non-empty directory.
|
|
55
|
+
--yes, -y Accept all defaults; never prompt.
|
|
56
|
+
--help, -h Print this help.
|
|
57
|
+
--version, -v Print version.
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function readPkgVersion() {
|
|
62
|
+
try {
|
|
63
|
+
const raw = await readFile(path.join(PKG_ROOT, "package.json"), "utf8");
|
|
64
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
65
|
+
} catch {
|
|
66
|
+
return "0.0.0";
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function parseArgs(argv) {
|
|
71
|
+
const out = {
|
|
72
|
+
projectName: undefined,
|
|
73
|
+
template: undefined,
|
|
74
|
+
packageManager: undefined,
|
|
75
|
+
install: undefined,
|
|
76
|
+
git: undefined,
|
|
77
|
+
force: false,
|
|
78
|
+
yes: false,
|
|
79
|
+
help: false,
|
|
80
|
+
version: false,
|
|
81
|
+
};
|
|
82
|
+
const args = [...argv];
|
|
83
|
+
while (args.length) {
|
|
84
|
+
const a = args.shift();
|
|
85
|
+
if (a === "--help" || a === "-h") out.help = true;
|
|
86
|
+
else if (a === "--version" || a === "-v") out.version = true;
|
|
87
|
+
else if (a === "--yes" || a === "-y") out.yes = true;
|
|
88
|
+
else if (a === "--force") out.force = true;
|
|
89
|
+
else if (a === "--install") out.install = true;
|
|
90
|
+
else if (a === "--no-install") out.install = false;
|
|
91
|
+
else if (a === "--git") out.git = true;
|
|
92
|
+
else if (a === "--no-git") out.git = false;
|
|
93
|
+
else if (a === "--template") out.template = args.shift();
|
|
94
|
+
else if (a?.startsWith("--template=")) out.template = a.slice("--template=".length);
|
|
95
|
+
else if (a === "--package-manager" || a === "--pm") out.packageManager = args.shift();
|
|
96
|
+
else if (a?.startsWith("--package-manager=")) out.packageManager = a.slice("--package-manager=".length);
|
|
97
|
+
else if (a?.startsWith("--pm=")) out.packageManager = a.slice("--pm=".length);
|
|
98
|
+
else if (a && !a.startsWith("-") && out.projectName === undefined) out.projectName = a;
|
|
99
|
+
else if (a) {
|
|
100
|
+
console.error(color(COLORS.red, `Unknown argument: ${a}`));
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function detectPackageManager() {
|
|
108
|
+
const ua = process.env.npm_config_user_agent ?? "";
|
|
109
|
+
if (ua.startsWith("pnpm")) return "pnpm";
|
|
110
|
+
if (ua.startsWith("yarn")) return "yarn";
|
|
111
|
+
if (ua.startsWith("bun")) return "bun";
|
|
112
|
+
if (ua.startsWith("npm")) return "npm";
|
|
113
|
+
return "pnpm";
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const VALID_NAME = /^(?:@[a-z0-9-~][a-z0-9-._~]*\/)?[a-z0-9-~][a-z0-9-._~]*$/;
|
|
117
|
+
|
|
118
|
+
function validateProjectName(name) {
|
|
119
|
+
if (!name || !name.trim()) return "Project name cannot be empty.";
|
|
120
|
+
if (name === "." || name === "..") return "Use a real directory name.";
|
|
121
|
+
if (name.length > 214) return "Project name is too long (max 214 chars).";
|
|
122
|
+
if (!VALID_NAME.test(name)) {
|
|
123
|
+
return "Project name must be a valid npm package name (lowercase, no spaces, no leading dot/underscore).";
|
|
124
|
+
}
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function isDirEmpty(dir) {
|
|
129
|
+
try {
|
|
130
|
+
const entries = await readdir(dir);
|
|
131
|
+
return entries.length === 0;
|
|
132
|
+
} catch {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function copyTemplate(src, dest) {
|
|
138
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
139
|
+
await mkdir(dest, { recursive: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const renamed = RENAME_ON_COPY.get(entry.name) ?? entry.name;
|
|
142
|
+
const from = path.join(src, entry.name);
|
|
143
|
+
const to = path.join(dest, renamed);
|
|
144
|
+
if (entry.isDirectory()) {
|
|
145
|
+
await copyTemplate(from, to);
|
|
146
|
+
} else if (entry.isFile()) {
|
|
147
|
+
await copyFile(from, to);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function patchPackageJson(dir, projectName) {
|
|
153
|
+
const file = path.join(dir, "package.json");
|
|
154
|
+
if (!existsSync(file)) return;
|
|
155
|
+
const raw = await readFile(file, "utf8");
|
|
156
|
+
const json = JSON.parse(raw);
|
|
157
|
+
json.name = projectName.startsWith("@") ? projectName : projectName.toLowerCase();
|
|
158
|
+
await writeFile(file, JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function run(cmd, args, cwd) {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
const proc = spawn(cmd, args, { cwd, stdio: "inherit", shell: process.platform === "win32" });
|
|
164
|
+
proc.on("exit", (code) => resolve(code ?? 0));
|
|
165
|
+
proc.on("error", () => resolve(1));
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function ask(rl, question, defaultValue) {
|
|
170
|
+
const suffix = defaultValue !== undefined ? color(COLORS.dim, ` (${defaultValue})`) : "";
|
|
171
|
+
const answer = (await rl.question(`${color(COLORS.cyan, "?")} ${question}${suffix} `)).trim();
|
|
172
|
+
return answer.length === 0 ? defaultValue : answer;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
async function askYesNo(rl, question, defaultYes) {
|
|
176
|
+
const def = defaultYes ? "Y/n" : "y/N";
|
|
177
|
+
const answer = (await rl.question(`${color(COLORS.cyan, "?")} ${question} ${color(COLORS.dim, `(${def})`)} `))
|
|
178
|
+
.trim()
|
|
179
|
+
.toLowerCase();
|
|
180
|
+
if (answer.length === 0) return defaultYes;
|
|
181
|
+
return answer === "y" || answer === "yes";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async function askChoice(rl, question, choices, defaultChoice) {
|
|
185
|
+
const list = choices
|
|
186
|
+
.map((c, i) => ` ${color(COLORS.dim, `${i + 1})`)} ${c}${c === defaultChoice ? color(COLORS.dim, " (default)") : ""}`)
|
|
187
|
+
.join("\n");
|
|
188
|
+
console.log(`${color(COLORS.cyan, "?")} ${question}\n${list}`);
|
|
189
|
+
const raw = (await rl.question(` > `)).trim();
|
|
190
|
+
if (raw.length === 0) return defaultChoice;
|
|
191
|
+
const asNumber = Number.parseInt(raw, 10);
|
|
192
|
+
if (Number.isInteger(asNumber) && asNumber >= 1 && asNumber <= choices.length) {
|
|
193
|
+
return choices[asNumber - 1];
|
|
194
|
+
}
|
|
195
|
+
if (choices.includes(raw)) return raw;
|
|
196
|
+
console.error(color(COLORS.red, `Invalid choice. Pick one of: ${choices.join(", ")}`));
|
|
197
|
+
return askChoice(rl, question, choices, defaultChoice);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function main() {
|
|
201
|
+
const opts = parseArgs(process.argv.slice(2));
|
|
202
|
+
|
|
203
|
+
if (opts.help) {
|
|
204
|
+
printHelp();
|
|
205
|
+
process.exit(0);
|
|
206
|
+
}
|
|
207
|
+
if (opts.version) {
|
|
208
|
+
console.log(await readPkgVersion());
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
console.log(color(COLORS.bold, "\n create-daloy"));
|
|
213
|
+
console.log(color(COLORS.dim, ` https://daloyjs.dev\n`));
|
|
214
|
+
|
|
215
|
+
const detectedPm = detectPackageManager();
|
|
216
|
+
const interactive = !opts.yes && process.stdin.isTTY && process.stdout.isTTY;
|
|
217
|
+
const rl = interactive ? createInterface({ input, output }) : null;
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
let projectName = opts.projectName;
|
|
221
|
+
if (!projectName) {
|
|
222
|
+
if (rl) {
|
|
223
|
+
while (true) {
|
|
224
|
+
const candidate = await ask(rl, "Project name?", "my-daloy-app");
|
|
225
|
+
const valid = validateProjectName(candidate);
|
|
226
|
+
if (valid === true) {
|
|
227
|
+
projectName = candidate;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
console.error(color(COLORS.red, ` ${valid}`));
|
|
231
|
+
}
|
|
232
|
+
} else {
|
|
233
|
+
projectName = "my-daloy-app";
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const nameCheck = validateProjectName(projectName);
|
|
237
|
+
if (nameCheck !== true) {
|
|
238
|
+
console.error(color(COLORS.red, nameCheck));
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let template = opts.template;
|
|
243
|
+
if (!template) {
|
|
244
|
+
template = rl ? await askChoice(rl, "Pick a template:", TEMPLATES, "node-basic") : "node-basic";
|
|
245
|
+
}
|
|
246
|
+
if (!TEMPLATES.includes(template)) {
|
|
247
|
+
console.error(color(COLORS.red, `Unknown template "${template}". Available: ${TEMPLATES.join(", ")}`));
|
|
248
|
+
process.exit(1);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const templateDir = path.join(TEMPLATES_DIR, template);
|
|
252
|
+
if (!existsSync(templateDir)) {
|
|
253
|
+
console.error(color(COLORS.red, `Template "${template}" is missing from this CLI build.`));
|
|
254
|
+
process.exit(1);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
258
|
+
if (existsSync(targetDir)) {
|
|
259
|
+
const empty = await isDirEmpty(targetDir);
|
|
260
|
+
if (!empty && !opts.force) {
|
|
261
|
+
console.error(
|
|
262
|
+
color(
|
|
263
|
+
COLORS.red,
|
|
264
|
+
`Directory ${projectName} is not empty. Re-run with --force to overwrite.`,
|
|
265
|
+
),
|
|
266
|
+
);
|
|
267
|
+
process.exit(1);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let packageManager = opts.packageManager ?? detectedPm;
|
|
272
|
+
if (!PACKAGE_MANAGERS.includes(packageManager)) {
|
|
273
|
+
console.error(
|
|
274
|
+
color(COLORS.red, `Unknown --package-manager "${packageManager}". Use one of: ${PACKAGE_MANAGERS.join(", ")}`),
|
|
275
|
+
);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
let installDeps = opts.install;
|
|
280
|
+
if (installDeps === undefined) {
|
|
281
|
+
installDeps = rl ? await askYesNo(rl, `Install dependencies with ${packageManager}?`, true) : false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
let initGit = opts.git;
|
|
285
|
+
if (initGit === undefined) {
|
|
286
|
+
initGit = rl ? await askYesNo(rl, "Initialize a git repository?", true) : false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
rl?.close();
|
|
290
|
+
|
|
291
|
+
console.log(color(COLORS.dim, `\n Scaffolding ${color(COLORS.bold, projectName)} from template ${color(COLORS.bold, template)}...`));
|
|
292
|
+
|
|
293
|
+
await mkdir(targetDir, { recursive: true });
|
|
294
|
+
await copyTemplate(templateDir, targetDir);
|
|
295
|
+
await patchPackageJson(targetDir, projectName);
|
|
296
|
+
|
|
297
|
+
if (initGit) {
|
|
298
|
+
const code = await run("git", ["init", "--quiet"], targetDir);
|
|
299
|
+
if (code === 0) {
|
|
300
|
+
console.log(color(COLORS.green, " ✓ git repository initialized"));
|
|
301
|
+
} else {
|
|
302
|
+
console.warn(color(COLORS.yellow, " ! git init failed; continuing"));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (installDeps) {
|
|
307
|
+
console.log(color(COLORS.dim, ` Installing dependencies with ${packageManager}...`));
|
|
308
|
+
const code = await run(packageManager, ["install"], targetDir);
|
|
309
|
+
if (code !== 0) {
|
|
310
|
+
console.warn(
|
|
311
|
+
color(COLORS.yellow, ` ! ${packageManager} install exited with code ${code}; you can retry inside ${projectName}.`),
|
|
312
|
+
);
|
|
313
|
+
} else {
|
|
314
|
+
console.log(color(COLORS.green, " ✓ dependencies installed"));
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
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"));
|
|
326
|
+
} catch (err) {
|
|
327
|
+
rl?.close();
|
|
328
|
+
console.error(color(COLORS.red, `\n Failed: ${(err && err.message) || err}`));
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
await main();
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-daloy",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/daloyjs/daloy.git",
|
|
10
|
+
"directory": "packages/create-daloy"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/daloyjs/daloy/tree/main/packages/create-daloy#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/daloyjs/daloy/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"daloy",
|
|
18
|
+
"daloyjs",
|
|
19
|
+
"scaffold",
|
|
20
|
+
"create",
|
|
21
|
+
"init",
|
|
22
|
+
"starter",
|
|
23
|
+
"template",
|
|
24
|
+
"cli",
|
|
25
|
+
"rest",
|
|
26
|
+
"api",
|
|
27
|
+
"openapi"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=20.10.0"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"create-daloy": "bin/create-daloy.mjs"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin",
|
|
37
|
+
"templates",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"test": "node --test test/**/*.test.mjs"
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# my-daloy-worker
|
|
2
|
+
|
|
3
|
+
A [DaloyJS](https://daloyjs.dev) Cloudflare Workers starter.
|
|
4
|
+
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev # http://localhost:8787
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Deploy
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pnpm deploy
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
`@daloyjs/core/cloudflare` exposes `toFetchHandler(app)`, so the same `App` you would use on Node also runs on Workers.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-daloy-worker",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "wrangler dev",
|
|
8
|
+
"deploy": "wrangler deploy",
|
|
9
|
+
"typecheck": "tsc --noEmit",
|
|
10
|
+
"test": "node --import tsx --test tests/**/*.test.ts"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@daloyjs/core": "^0.1.0",
|
|
14
|
+
"zod": "^4.4.3"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@cloudflare/workers-types": "^4.20240909.0",
|
|
18
|
+
"tsx": "^4.22.0",
|
|
19
|
+
"typescript": "^6.0.3",
|
|
20
|
+
"wrangler": "^4.0.0"
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { App, NotFoundError, requestId, secureHeaders } from "@daloyjs/core";
|
|
3
|
+
import { toFetchHandler } from "@daloyjs/core/cloudflare";
|
|
4
|
+
|
|
5
|
+
const app = new App({
|
|
6
|
+
bodyLimitBytes: 256 * 1024,
|
|
7
|
+
requestTimeoutMs: 5_000,
|
|
8
|
+
production: true,
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
app.use(requestId());
|
|
12
|
+
app.use(secureHeaders());
|
|
13
|
+
|
|
14
|
+
const Book = z.object({ id: z.string(), title: z.string() });
|
|
15
|
+
const books = new Map<string, z.infer<typeof Book>>([
|
|
16
|
+
["1", { id: "1", title: "Noli Me Tangere" }],
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
app.route({
|
|
20
|
+
method: "GET",
|
|
21
|
+
path: "/books/:id",
|
|
22
|
+
operationId: "getBookById",
|
|
23
|
+
tags: ["Books"],
|
|
24
|
+
request: { params: z.object({ id: z.string() }) },
|
|
25
|
+
responses: {
|
|
26
|
+
200: { description: "Found", body: Book },
|
|
27
|
+
404: { description: "Not found" },
|
|
28
|
+
},
|
|
29
|
+
handler: async ({ params }) => {
|
|
30
|
+
const book = books.get(params.id);
|
|
31
|
+
if (!book) throw new NotFoundError(`Book ${params.id} not found`);
|
|
32
|
+
return { status: 200, body: book };
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export default { fetch: toFetchHandler(app) };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "Bundler",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["@cloudflare/workers-types"],
|
|
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": ["src/**/*"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# my-daloy-app
|
|
2
|
+
|
|
3
|
+
A [DaloyJS](https://daloyjs.dev) starter — runtime-portable, contract-first TypeScript REST API.
|
|
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
|
+
## Generate OpenAPI + typed client
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pnpm gen
|
|
23
|
+
# → generated/openapi.json
|
|
24
|
+
# → generated/client/ (typed Hey API client)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Build
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pnpm build
|
|
31
|
+
node dist/index.js
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What's included
|
|
35
|
+
|
|
36
|
+
- `@daloyjs/core` with `secureHeaders`, `requestId`, and `rateLimit` enabled.
|
|
37
|
+
- A health route and a contract-first `/books/:id` route with Zod validation.
|
|
38
|
+
- Hardened `.npmrc` for safer installs.
|
|
39
|
+
- Hey API codegen wired to `pnpm gen`.
|
|
40
|
+
|
|
41
|
+
Read the docs at <https://daloyjs.dev/docs>.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PORT=3000
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "my-daloy-app",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=20.10.0"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"dev": "node --import tsx --watch src/index.ts",
|
|
11
|
+
"start": "node --import tsx src/index.ts",
|
|
12
|
+
"build": "tsc -p tsconfig.json",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"test": "node --import tsx --test tests/**/*.test.ts",
|
|
15
|
+
"gen:openapi": "node --import tsx scripts/dump-openapi.ts",
|
|
16
|
+
"gen:client": "openapi-ts",
|
|
17
|
+
"gen": "pnpm gen:openapi && pnpm gen:client",
|
|
18
|
+
"audit": "pnpm audit --prod"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@daloyjs/core": "^0.1.0",
|
|
22
|
+
"zod": "^4.4.3"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@hey-api/openapi-ts": "^0.97.1",
|
|
26
|
+
"@types/node": "^25.7.0",
|
|
27
|
+
"tsx": "^4.22.0",
|
|
28
|
+
"typescript": "^6.0.3"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
2
|
+
import { generateOpenAPI } from "@daloyjs/core/openapi";
|
|
3
|
+
import { App } from "@daloyjs/core";
|
|
4
|
+
|
|
5
|
+
// Re-import the app definition, then write the spec.
|
|
6
|
+
// Keep this script deterministic so codegen output is stable in CI.
|
|
7
|
+
|
|
8
|
+
async function main() {
|
|
9
|
+
const { default: app } = (await import("../src/index.js")) as { default: App };
|
|
10
|
+
const doc = generateOpenAPI(app, {
|
|
11
|
+
info: { title: "My Daloy API", version: "0.0.1" },
|
|
12
|
+
servers: [{ url: "http://localhost:3000" }],
|
|
13
|
+
});
|
|
14
|
+
await mkdir("generated", { recursive: true });
|
|
15
|
+
await writeFile("generated/openapi.json", JSON.stringify(doc, null, 2));
|
|
16
|
+
console.log("wrote generated/openapi.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
main().catch((err) => {
|
|
20
|
+
console.error(err);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
App,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
rateLimit,
|
|
6
|
+
requestId,
|
|
7
|
+
secureHeaders,
|
|
8
|
+
} from "@daloyjs/core";
|
|
9
|
+
import { serve } from "@daloyjs/core/node";
|
|
10
|
+
|
|
11
|
+
const app = new App({
|
|
12
|
+
bodyLimitBytes: 1024 * 1024,
|
|
13
|
+
requestTimeoutMs: 5_000,
|
|
14
|
+
production: process.env.NODE_ENV === "production",
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
app.use(requestId());
|
|
18
|
+
app.use(secureHeaders());
|
|
19
|
+
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
20
|
+
|
|
21
|
+
app.route({
|
|
22
|
+
method: "GET",
|
|
23
|
+
path: "/healthz",
|
|
24
|
+
operationId: "healthz",
|
|
25
|
+
tags: ["Ops"],
|
|
26
|
+
responses: {
|
|
27
|
+
200: {
|
|
28
|
+
description: "Service is healthy",
|
|
29
|
+
body: z.object({ ok: z.literal(true), uptime: z.number() }),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
handler: async () => ({
|
|
33
|
+
status: 200,
|
|
34
|
+
body: { ok: true, uptime: process.uptime() },
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const Book = z.object({ id: z.string(), title: z.string() });
|
|
39
|
+
const books = new Map<string, z.infer<typeof Book>>([
|
|
40
|
+
["1", { id: "1", title: "Noli Me Tangere" }],
|
|
41
|
+
["2", { id: "2", title: "El Filibusterismo" }],
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
app.route({
|
|
45
|
+
method: "GET",
|
|
46
|
+
path: "/books/:id",
|
|
47
|
+
operationId: "getBookById",
|
|
48
|
+
tags: ["Books"],
|
|
49
|
+
request: { params: z.object({ id: z.string() }) },
|
|
50
|
+
responses: {
|
|
51
|
+
200: { description: "Found", body: Book },
|
|
52
|
+
404: { description: "Not found" },
|
|
53
|
+
},
|
|
54
|
+
handler: async ({ params }) => {
|
|
55
|
+
const book = books.get(params.id);
|
|
56
|
+
if (!book) throw new NotFoundError(`Book ${params.id} not found`);
|
|
57
|
+
return { status: 200, body: book };
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const port = Number(process.env.PORT ?? 3000);
|
|
62
|
+
serve(app, { port });
|
|
63
|
+
console.log(`DaloyJS listening on http://localhost:${port}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"lib": ["ES2022"],
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"sourceMap": true,
|
|
12
|
+
"outDir": "dist",
|
|
13
|
+
"rootDir": "./src",
|
|
14
|
+
"esModuleInterop": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"forceConsistentCasingInFileNames": true,
|
|
17
|
+
"resolveJsonModule": true
|
|
18
|
+
},
|
|
19
|
+
"include": ["src/**/*"],
|
|
20
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
21
|
+
}
|