create-daloy 0.1.7 → 0.1.8

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
@@ -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.
@@ -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.7",
3
+ "version": "0.1.8",
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",
@@ -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
- # Optional, pnpm 10+:
6
- # minimum-release-age=1440
7
- # ignore-scripts=true
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 { App } from "@daloyjs/core";
3
+ import { buildApp } from "../src/build-app.js";
4
4
 
5
- // Re-import the app definition, then write the spec.
6
- // Keep this script deterministic so codegen output is stable in CI.
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 { default: app } = (await import("../src/index.js")) as { default: App };
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 { generateOpenAPI } from "@daloyjs/core/openapi";
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: http://localhost:${port}/docs`);
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