create-daloy 0.1.7 → 0.1.10
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 +2 -1
- package/bin/create-daloy.mjs +58 -2
- package/package.json +2 -2
- package/templates/cloudflare-worker/_npmrc +12 -0
- package/templates/cloudflare-worker/package.json +1 -1
- package/templates/node-basic/_npmrc +17 -3
- package/templates/node-basic/package.json +1 -1
- package/templates/node-basic/scripts/dump-openapi.ts +5 -4
- package/templates/node-basic/src/build-app.ts +110 -0
- package/templates/node-basic/src/index.ts +5 -97
- package/templates/vercel-edge/_npmrc +12 -0
- package/templates/vercel-edge/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,7 @@ A production-ready Node.js HTTP server using `@daloyjs/core` with:
|
|
|
52
52
|
|
|
53
53
|
- Strict TypeScript and `tsx` for instant dev runs.
|
|
54
54
|
- Hardened `.npmrc` for safer installs.
|
|
55
|
-
- `secureHeaders`, `requestId`, and `rateLimit` enabled by default.
|
|
55
|
+
- `secureHeaders`, `requestId`, and `rateLimit` enabled by default (`rateLimit` is global until you configure `keyGenerator` or trusted proxy headers).
|
|
56
56
|
- A sample `GET /healthz` and contract-first `GET /books/:id` route with Zod validation.
|
|
57
57
|
- `pnpm gen` wired to emit OpenAPI 3.1 + a typed Hey API client.
|
|
58
58
|
|
|
@@ -79,4 +79,5 @@ A Vercel Edge API bootstrap using `@daloyjs/core/vercel` with:
|
|
|
79
79
|
- Templates are copied verbatim from this package's `templates/` directory.
|
|
80
80
|
- Files prefixed with `_` are renamed (`_gitignore` → `.gitignore`, `_npmrc` → `.npmrc`) to survive npm packing.
|
|
81
81
|
- pnpm-specific `.npmrc` hardening is kept only when you choose `pnpm`; other package managers get a clean project without unsupported config warnings.
|
|
82
|
+
- pnpm projects ship with `ignore-scripts=true`, `minimum-release-age=1440`, `verify-store-integrity=true`, `prefer-frozen-lockfile=true`, and `strict-peer-dependencies=true` by default.
|
|
82
83
|
- The CLI never executes template scripts and never makes network calls beyond the package manager you select.
|
package/bin/create-daloy.mjs
CHANGED
|
@@ -185,15 +185,70 @@ async function copyTemplate(src, dest) {
|
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
188
|
-
async function patchPackageJson(dir, projectName) {
|
|
188
|
+
async function patchPackageJson(dir, projectName, packageManager) {
|
|
189
189
|
const file = path.join(dir, "package.json");
|
|
190
190
|
if (!existsSync(file)) return;
|
|
191
191
|
const raw = await readFile(file, "utf8");
|
|
192
192
|
const json = JSON.parse(raw);
|
|
193
193
|
json.name = projectName.startsWith("@") ? projectName : projectName.toLowerCase();
|
|
194
|
+
if (json.scripts && packageManager && packageManager !== "pnpm") {
|
|
195
|
+
json.scripts = rewriteScriptsForPackageManager(json.scripts, packageManager);
|
|
196
|
+
}
|
|
194
197
|
await writeFile(file, JSON.stringify(json, null, 2) + "\n", "utf8");
|
|
195
198
|
}
|
|
196
199
|
|
|
200
|
+
async function patchReadme(dir, packageManager) {
|
|
201
|
+
if (packageManager === "pnpm") return;
|
|
202
|
+
const file = path.join(dir, "README.md");
|
|
203
|
+
if (!existsSync(file)) return;
|
|
204
|
+
const raw = await readFile(file, "utf8");
|
|
205
|
+
const next = raw
|
|
206
|
+
.replace(/\bpnpm install\b/g, `${packageManager} install`)
|
|
207
|
+
.replace(/\bpnpm dev\b/g, `${packageManager} run dev`)
|
|
208
|
+
.replace(/\bpnpm gen\b/g, `${packageManager} run gen`)
|
|
209
|
+
.replace(/\bpnpm build\b/g, `${packageManager} run build`)
|
|
210
|
+
.replace(
|
|
211
|
+
"- Hardened `.npmrc` for safer installs.",
|
|
212
|
+
`- Package-manager scripts adjusted for ${packageManager}.`,
|
|
213
|
+
)
|
|
214
|
+
.replace(
|
|
215
|
+
"- Hey API codegen wired to `pnpm gen`.",
|
|
216
|
+
`- Hey API codegen wired to \`${packageManager} run gen\`.`,
|
|
217
|
+
);
|
|
218
|
+
await writeFile(file, next, "utf8");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Rewrite scaffolded package.json scripts so they work under the user's
|
|
223
|
+
* chosen package manager. Templates are authored with `pnpm` because that
|
|
224
|
+
* is the recommended manager, but `pnpm <subscript>` and `pnpm audit` will
|
|
225
|
+
* fail under npm/yarn/bun. We rewrite both forms to the equivalent that
|
|
226
|
+
* the chosen manager understands.
|
|
227
|
+
*/
|
|
228
|
+
function rewriteScriptsForPackageManager(scripts, pm) {
|
|
229
|
+
// `pnpm audit` → `<pm> audit`. yarn/bun also expose an `audit` command;
|
|
230
|
+
// npm of course does too. Keep flags intact.
|
|
231
|
+
// `pnpm <subscript>` (where subscript is another script in the same
|
|
232
|
+
// package.json) → `<pm> run <subscript>` so cross-script chains work
|
|
233
|
+
// everywhere. Both yarn and bun also accept `<pm> run <name>`.
|
|
234
|
+
const out = {};
|
|
235
|
+
const subscriptNames = new Set(Object.keys(scripts));
|
|
236
|
+
for (const [name, command] of Object.entries(scripts)) {
|
|
237
|
+
if (typeof command !== "string") {
|
|
238
|
+
out[name] = command;
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
let next = command.replace(/\bpnpm\s+audit\b/g, `${pm} audit`);
|
|
242
|
+
next = next.replace(/\bpnpm\s+([a-zA-Z0-9_:-]+)/g, (match, sub) => {
|
|
243
|
+
if (sub === "audit") return match; // already handled above
|
|
244
|
+
if (!subscriptNames.has(sub)) return match;
|
|
245
|
+
return `${pm} run ${sub}`;
|
|
246
|
+
});
|
|
247
|
+
out[name] = next;
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
197
252
|
async function normalizePackageManagerFiles(dir, packageManager) {
|
|
198
253
|
if (packageManager === "pnpm") return;
|
|
199
254
|
const npmrcPath = path.join(dir, ".npmrc");
|
|
@@ -387,8 +442,9 @@ async function main() {
|
|
|
387
442
|
await mkdir(targetDir, { recursive: true });
|
|
388
443
|
await copyTemplate(templateDir, targetDir);
|
|
389
444
|
logStep("Template copied", template);
|
|
390
|
-
await patchPackageJson(targetDir, projectName);
|
|
445
|
+
await patchPackageJson(targetDir, projectName, packageManager);
|
|
391
446
|
logStep("Package metadata written", projectName);
|
|
447
|
+
await patchReadme(targetDir, packageManager);
|
|
392
448
|
await normalizePackageManagerFiles(targetDir, packageManager);
|
|
393
449
|
if (packageManager !== "pnpm") {
|
|
394
450
|
logStep("Package-manager config normalized", packageManager);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-daloy",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.10",
|
|
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",
|
|
@@ -43,4 +43,4 @@
|
|
|
43
43
|
"scripts": {
|
|
44
44
|
"test": "node --test test/**/*.test.mjs"
|
|
45
45
|
}
|
|
46
|
-
}
|
|
46
|
+
}
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
# DaloyJS supply-chain hardening defaults — see the "Supply chain" docs.
|
|
2
|
+
|
|
1
3
|
auto-install-peers=true
|
|
2
4
|
strict-peer-dependencies=true
|
|
3
5
|
prefer-frozen-lockfile=true
|
|
4
6
|
verify-store-integrity=true
|
|
7
|
+
|
|
8
|
+
# 24h cooldown on freshly published versions. Drops you off the hot path
|
|
9
|
+
# of npm worm campaigns, which are typically detected and unpublished
|
|
10
|
+
# within hours.
|
|
11
|
+
minimum-release-age=1440
|
|
12
|
+
|
|
13
|
+
# Block postinstall/preinstall/prepare hooks from transitive deps —
|
|
14
|
+
# the primary execution channel for chalk/debug, node-ipc, Shai-Hulud.
|
|
15
|
+
# Allowlist trusted builds via package.json `pnpm.onlyBuiltDependencies`.
|
|
16
|
+
ignore-scripts=true
|
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
# DaloyJS supply-chain hardening defaults.
|
|
2
|
+
#
|
|
3
|
+
# See the DaloyJS "Supply chain" docs and the 2026-05-11 TanStack incident
|
|
4
|
+
# postmortem (https://tanstack.com/blog/npm-supply-chain-compromise-postmortem)
|
|
5
|
+
# for context on why every line below is on by default.
|
|
6
|
+
|
|
1
7
|
auto-install-peers=true
|
|
2
8
|
strict-peer-dependencies=true
|
|
3
9
|
prefer-frozen-lockfile=true
|
|
4
10
|
verify-store-integrity=true
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
#
|
|
11
|
+
|
|
12
|
+
# Wait 24h before resolving a freshly published version. Most npm worm
|
|
13
|
+
# campaigns are detected and unpublished within hours; this window keeps
|
|
14
|
+
# you off the early-installer hot path. Set to 0 only for a real hotfix.
|
|
15
|
+
minimum-release-age=1440
|
|
16
|
+
|
|
17
|
+
# postinstall / preinstall / prepare hooks from transitive deps are the
|
|
18
|
+
# main execution channel used by chalk/debug, node-ipc, and Shai-Hulud
|
|
19
|
+
# malware. Allowlist the few packages you actually trust to build via
|
|
20
|
+
# package.json `pnpm.onlyBuiltDependencies` instead of turning this off.
|
|
21
|
+
ignore-scripts=true
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { writeFile, mkdir } from "node:fs/promises";
|
|
2
2
|
import { generateOpenAPI } from "@daloyjs/core/openapi";
|
|
3
|
-
import {
|
|
3
|
+
import { buildApp } from "../src/build-app.js";
|
|
4
4
|
|
|
5
|
-
//
|
|
6
|
-
// Keep this script deterministic so codegen output
|
|
5
|
+
// Build a fresh app from the factory so the spec dump never starts the HTTP
|
|
6
|
+
// listener as a side effect. Keep this script deterministic so codegen output
|
|
7
|
+
// is stable in CI.
|
|
7
8
|
|
|
8
9
|
async function main() {
|
|
9
|
-
const
|
|
10
|
+
const app = buildApp();
|
|
10
11
|
const doc = generateOpenAPI(app, {
|
|
11
12
|
info: { title: "My Daloy API", version: "0.0.1" },
|
|
12
13
|
servers: [{ url: "http://localhost:3000" }],
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {
|
|
3
|
+
App,
|
|
4
|
+
NotFoundError,
|
|
5
|
+
rateLimit,
|
|
6
|
+
requestId,
|
|
7
|
+
secureHeaders,
|
|
8
|
+
} from "@daloyjs/core";
|
|
9
|
+
import { generateOpenAPI } from "@daloyjs/core/openapi";
|
|
10
|
+
import { htmlResponse, swaggerUiHtml } from "@daloyjs/core/docs";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build the application as a pure factory.
|
|
14
|
+
*
|
|
15
|
+
* Keeping construction separate from `serve(app, ...)` lets tooling
|
|
16
|
+
* (`scripts/dump-openapi.ts`, contract tests, in-process tests via
|
|
17
|
+
* `app.request(...)`) import and reuse the app without booting an
|
|
18
|
+
* HTTP listener as a side effect.
|
|
19
|
+
*/
|
|
20
|
+
export function buildApp(): App {
|
|
21
|
+
const app = new App({
|
|
22
|
+
bodyLimitBytes: 1024 * 1024,
|
|
23
|
+
requestTimeoutMs: 5_000,
|
|
24
|
+
production: process.env.NODE_ENV === "production",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
app.use(requestId());
|
|
28
|
+
app.use(secureHeaders());
|
|
29
|
+
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
30
|
+
|
|
31
|
+
app.route({
|
|
32
|
+
method: "GET",
|
|
33
|
+
path: "/healthz",
|
|
34
|
+
operationId: "healthz",
|
|
35
|
+
tags: ["Ops"],
|
|
36
|
+
responses: {
|
|
37
|
+
200: {
|
|
38
|
+
description: "Service is healthy",
|
|
39
|
+
body: z.object({ ok: z.literal(true), uptime: z.number() }),
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
handler: async () => ({
|
|
43
|
+
status: 200,
|
|
44
|
+
body: { ok: true as const, uptime: process.uptime() },
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const Book = z.object({ id: z.string(), title: z.string() });
|
|
49
|
+
const books = new Map<string, z.infer<typeof Book>>([
|
|
50
|
+
["1", { id: "1", title: "Noli Me Tangere" }],
|
|
51
|
+
["2", { id: "2", title: "El Filibusterismo" }],
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
app.route({
|
|
55
|
+
method: "GET",
|
|
56
|
+
path: "/books/:id",
|
|
57
|
+
operationId: "getBookById",
|
|
58
|
+
tags: ["Books"],
|
|
59
|
+
request: { params: z.object({ id: z.string() }) },
|
|
60
|
+
responses: {
|
|
61
|
+
200: { description: "Found", body: Book },
|
|
62
|
+
404: { description: "Not found" },
|
|
63
|
+
},
|
|
64
|
+
handler: async ({ params }) => {
|
|
65
|
+
const book = books.get(params.id);
|
|
66
|
+
if (!book) throw new NotFoundError(`Book ${params.id} not found`);
|
|
67
|
+
return { status: 200, body: book };
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// --- API documentation ---------------------------------------------------
|
|
72
|
+
// `/openapi.json` returns the live OpenAPI 3.1 spec generated from the
|
|
73
|
+
// routes defined above. `/docs` serves a Swagger UI page that loads it.
|
|
74
|
+
|
|
75
|
+
app.route({
|
|
76
|
+
method: "GET",
|
|
77
|
+
path: "/openapi.json",
|
|
78
|
+
operationId: "getOpenAPI",
|
|
79
|
+
tags: ["Docs"],
|
|
80
|
+
responses: { 200: { description: "OpenAPI 3.1 document" } },
|
|
81
|
+
handler: async () => ({
|
|
82
|
+
status: 200 as const,
|
|
83
|
+
body: generateOpenAPI(app, {
|
|
84
|
+
info: { title: "My Daloy API", version: "0.0.1" },
|
|
85
|
+
servers: [{ url: `http://localhost:${process.env.PORT ?? 3000}` }],
|
|
86
|
+
}),
|
|
87
|
+
}),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
app.route({
|
|
91
|
+
method: "GET",
|
|
92
|
+
path: "/docs",
|
|
93
|
+
operationId: "docs",
|
|
94
|
+
tags: ["Docs"],
|
|
95
|
+
responses: { 200: { description: "API reference UI" } },
|
|
96
|
+
handler: async () => {
|
|
97
|
+
const html = swaggerUiHtml({ specUrl: "/openapi.json", title: "My Daloy API" });
|
|
98
|
+
const res = htmlResponse(html);
|
|
99
|
+
return {
|
|
100
|
+
status: 200 as const,
|
|
101
|
+
body: html,
|
|
102
|
+
headers: Object.fromEntries(res.headers),
|
|
103
|
+
};
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
return app;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export default buildApp;
|
|
@@ -1,105 +1,13 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import {
|
|
3
|
-
App,
|
|
4
|
-
NotFoundError,
|
|
5
|
-
rateLimit,
|
|
6
|
-
requestId,
|
|
7
|
-
secureHeaders,
|
|
8
|
-
} from "@daloyjs/core";
|
|
9
1
|
import { serve } from "@daloyjs/core/node";
|
|
10
|
-
import {
|
|
11
|
-
import { htmlResponse, swaggerUiHtml } from "@daloyjs/core/docs";
|
|
12
|
-
|
|
13
|
-
const app = new App({
|
|
14
|
-
bodyLimitBytes: 1024 * 1024,
|
|
15
|
-
requestTimeoutMs: 5_000,
|
|
16
|
-
production: process.env.NODE_ENV === "production",
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
app.use(requestId());
|
|
20
|
-
app.use(secureHeaders());
|
|
21
|
-
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
|
|
22
|
-
|
|
23
|
-
app.route({
|
|
24
|
-
method: "GET",
|
|
25
|
-
path: "/healthz",
|
|
26
|
-
operationId: "healthz",
|
|
27
|
-
tags: ["Ops"],
|
|
28
|
-
responses: {
|
|
29
|
-
200: {
|
|
30
|
-
description: "Service is healthy",
|
|
31
|
-
body: z.object({ ok: z.literal(true), uptime: z.number() }),
|
|
32
|
-
},
|
|
33
|
-
},
|
|
34
|
-
handler: async () => ({
|
|
35
|
-
status: 200,
|
|
36
|
-
body: { ok: true as const, uptime: process.uptime() },
|
|
37
|
-
}),
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
const Book = z.object({ id: z.string(), title: z.string() });
|
|
41
|
-
const books = new Map<string, z.infer<typeof Book>>([
|
|
42
|
-
["1", { id: "1", title: "Noli Me Tangere" }],
|
|
43
|
-
["2", { id: "2", title: "El Filibusterismo" }],
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
app.route({
|
|
47
|
-
method: "GET",
|
|
48
|
-
path: "/books/:id",
|
|
49
|
-
operationId: "getBookById",
|
|
50
|
-
tags: ["Books"],
|
|
51
|
-
request: { params: z.object({ id: z.string() }) },
|
|
52
|
-
responses: {
|
|
53
|
-
200: { description: "Found", body: Book },
|
|
54
|
-
404: { description: "Not found" },
|
|
55
|
-
},
|
|
56
|
-
handler: async ({ params }) => {
|
|
57
|
-
const book = books.get(params.id);
|
|
58
|
-
if (!book) throw new NotFoundError(`Book ${params.id} not found`);
|
|
59
|
-
return { status: 200, body: book };
|
|
60
|
-
},
|
|
61
|
-
});
|
|
2
|
+
import { buildApp } from "./build-app.js";
|
|
62
3
|
|
|
4
|
+
const app = buildApp();
|
|
63
5
|
const port = Number(process.env.PORT ?? 3000);
|
|
64
6
|
|
|
65
|
-
// --- API documentation -----------------------------------------------------
|
|
66
|
-
// `/openapi.json` returns the live OpenAPI 3.1 spec generated from the routes
|
|
67
|
-
// defined above. `/docs` serves a Swagger UI page that loads that spec.
|
|
68
|
-
|
|
69
|
-
app.route({
|
|
70
|
-
method: "GET",
|
|
71
|
-
path: "/openapi.json",
|
|
72
|
-
operationId: "getOpenAPI",
|
|
73
|
-
tags: ["Docs"],
|
|
74
|
-
responses: { 200: { description: "OpenAPI 3.1 document" } },
|
|
75
|
-
handler: async () => ({
|
|
76
|
-
status: 200 as const,
|
|
77
|
-
body: generateOpenAPI(app, {
|
|
78
|
-
info: { title: "My Daloy API", version: "0.0.1" },
|
|
79
|
-
servers: [{ url: `http://localhost:${port}` }],
|
|
80
|
-
}),
|
|
81
|
-
}),
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
app.route({
|
|
85
|
-
method: "GET",
|
|
86
|
-
path: "/docs",
|
|
87
|
-
operationId: "docs",
|
|
88
|
-
tags: ["Docs"],
|
|
89
|
-
responses: { 200: { description: "API reference UI" } },
|
|
90
|
-
handler: async () => {
|
|
91
|
-
const html = swaggerUiHtml({ specUrl: "/openapi.json", title: "My Daloy API" });
|
|
92
|
-
const res = htmlResponse(html);
|
|
93
|
-
return {
|
|
94
|
-
status: 200 as const,
|
|
95
|
-
body: html,
|
|
96
|
-
headers: Object.fromEntries(res.headers),
|
|
97
|
-
};
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
7
|
serve(app, { port });
|
|
102
8
|
console.log(`DaloyJS listening on http://localhost:${port}`);
|
|
103
|
-
console.log(` Swagger UI:
|
|
9
|
+
console.log(` Swagger UI: http://localhost:${port}/docs`);
|
|
104
10
|
console.log(` OpenAPI JSON: http://localhost:${port}/openapi.json`);
|
|
105
11
|
console.log(` Health: http://localhost:${port}/healthz`);
|
|
12
|
+
|
|
13
|
+
export default app;
|
|
@@ -1,4 +1,16 @@
|
|
|
1
|
+
# DaloyJS supply-chain hardening defaults — see the "Supply chain" docs.
|
|
2
|
+
|
|
1
3
|
auto-install-peers=true
|
|
2
4
|
strict-peer-dependencies=true
|
|
3
5
|
prefer-frozen-lockfile=true
|
|
4
6
|
verify-store-integrity=true
|
|
7
|
+
|
|
8
|
+
# 24h cooldown on freshly published versions. Drops you off the hot path
|
|
9
|
+
# of npm worm campaigns, which are typically detected and unpublished
|
|
10
|
+
# within hours.
|
|
11
|
+
minimum-release-age=1440
|
|
12
|
+
|
|
13
|
+
# Block postinstall/preinstall/prepare hooks from transitive deps —
|
|
14
|
+
# the primary execution channel for chalk/debug, node-ipc, Shai-Hulud.
|
|
15
|
+
# Allowlist trusted builds via package.json `pnpm.onlyBuiltDependencies`.
|
|
16
|
+
ignore-scripts=true
|