create-daloy 0.39.1 → 0.42.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/bin/create-daloy.mjs +13 -1
- package/package.json +1 -1
- package/sbom.cdx.json +9 -9
- package/sbom.spdx.json +5 -5
- package/templates/bun-basic/README.md +14 -0
- package/templates/bun-basic/_githooks/pre-push +16 -0
- package/templates/bun-basic/package.json +4 -2
- package/templates/bun-basic/tests/contract.test.ts +33 -0
- package/templates/bun-basic/tests/healthz.test.ts +7 -0
- package/templates/cloudflare-worker/README.md +14 -0
- package/templates/cloudflare-worker/_githooks/pre-push +17 -0
- package/templates/cloudflare-worker/package.json +4 -2
- package/templates/cloudflare-worker/src/index.ts +5 -0
- package/templates/cloudflare-worker/tests/app.test.ts +19 -0
- package/templates/cloudflare-worker/tests/contract.test.ts +38 -0
- package/templates/deno-basic/README.md +14 -0
- package/templates/deno-basic/_githooks/pre-push +16 -0
- package/templates/deno-basic/deno.json +8 -5
- package/templates/deno-basic/tests/contract_test.ts +37 -0
- package/templates/deno-basic/tests/healthz_test.ts +7 -0
- package/templates/node-basic/README.md +14 -0
- package/templates/node-basic/_githooks/pre-push +17 -0
- package/templates/node-basic/package.json +4 -2
- package/templates/node-basic/tests/contract.test.ts +38 -0
- package/templates/node-basic/tests/healthz.test.ts +19 -0
- package/templates/vercel/README.md +14 -0
- package/templates/vercel/_githooks/pre-push +17 -0
- package/templates/vercel/package.json +4 -2
- package/templates/vercel/tests/app.test.ts +6 -0
- package/templates/vercel/tests/contract.test.ts +38 -0
package/bin/create-daloy.mjs
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { spawn } from "node:child_process";
|
|
6
6
|
import { existsSync, openSync } from "node:fs";
|
|
7
|
-
import { mkdir, readdir, readFile, writeFile, copyFile, rm } from "node:fs/promises";
|
|
7
|
+
import { mkdir, readdir, readFile, writeFile, copyFile, rm, stat, chmod } 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 { ReadStream as TtyReadStream } from "node:tty";
|
|
@@ -79,6 +79,10 @@ const RENAME_ON_COPY = new Map([
|
|
|
79
79
|
// Templates author this as `_vscode/` so npm pack does not drop the
|
|
80
80
|
// dotfolder during publish.
|
|
81
81
|
["_vscode", ".vscode"],
|
|
82
|
+
// Directory: holds the opt-in `pre-push` contract gate. `core.hooksPath`
|
|
83
|
+
// points here once the user runs the `hooks:install` script. Authored as
|
|
84
|
+
// `_githooks/` so npm pack does not drop the dotfolder during publish.
|
|
85
|
+
["_githooks", ".githooks"],
|
|
82
86
|
]);
|
|
83
87
|
|
|
84
88
|
// Templates that target a runtime instead of an npm package manager.
|
|
@@ -499,6 +503,12 @@ async function copyTemplate(src, dest) {
|
|
|
499
503
|
await copyTemplate(from, to);
|
|
500
504
|
} else if (entry.isFile()) {
|
|
501
505
|
await copyFile(from, to);
|
|
506
|
+
// Preserve the source file's mode so executable templates (e.g.
|
|
507
|
+
// `.githooks/pre-push`, the localhost contract gate) stay executable in
|
|
508
|
+
// the scaffold. `fs.copyFile` does not guarantee mode preservation on
|
|
509
|
+
// every platform (notably Linux `copy_file_range`), so set it explicitly.
|
|
510
|
+
const { mode } = await stat(from);
|
|
511
|
+
await chmod(to, mode);
|
|
502
512
|
}
|
|
503
513
|
}
|
|
504
514
|
}
|
|
@@ -549,6 +559,8 @@ function rewritePackageManagerText(raw, packageManager) {
|
|
|
549
559
|
.replace(/\bpnpm deploy\b/g, `${packageManager} run deploy`)
|
|
550
560
|
.replace(/\bpnpm dev\b/g, `${packageManager} run dev`)
|
|
551
561
|
.replace(/\bpnpm gen\b/g, `${packageManager} run gen`)
|
|
562
|
+
.replace(/\bpnpm contract\b/g, `${packageManager} run contract`)
|
|
563
|
+
.replace(/\bpnpm hooks:install\b/g, `${packageManager} run hooks:install`)
|
|
552
564
|
.replace(/\bpnpm test\b/g, `${packageManager} test`)
|
|
553
565
|
.replace(/\bpnpm audit\b/g, `${packageManager} audit`)
|
|
554
566
|
.replace(
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"bomFormat": "CycloneDX",
|
|
3
3
|
"specVersion": "1.5",
|
|
4
|
-
"serialNumber": "urn:uuid:
|
|
4
|
+
"serialNumber": "urn:uuid:d2c825aa-a918-58f1-9fed-b01fd3b46439",
|
|
5
5
|
"version": 1,
|
|
6
6
|
"metadata": {
|
|
7
|
-
"timestamp": "2026-06-
|
|
7
|
+
"timestamp": "2026-06-19T08:56:45.353Z",
|
|
8
8
|
"tools": [
|
|
9
9
|
{
|
|
10
10
|
"vendor": "DaloyJS",
|
|
11
11
|
"name": "daloy-generate-sbom",
|
|
12
|
-
"version": "0.
|
|
12
|
+
"version": "0.42.0"
|
|
13
13
|
}
|
|
14
14
|
],
|
|
15
15
|
"authors": [],
|
|
16
16
|
"component": {
|
|
17
17
|
"type": "library",
|
|
18
|
-
"bom-ref": "pkg:npm/create-daloy@0.
|
|
18
|
+
"bom-ref": "pkg:npm/create-daloy@0.42.0",
|
|
19
19
|
"name": "create-daloy",
|
|
20
|
-
"version": "0.
|
|
20
|
+
"version": "0.42.0",
|
|
21
21
|
"description": "Scaffold a new DaloyJS project. Run with `pnpm create daloy`, `npm create daloy@latest`, `yarn create daloy`, or `bun create daloy`.",
|
|
22
|
-
"purl": "pkg:npm/create-daloy@0.
|
|
22
|
+
"purl": "pkg:npm/create-daloy@0.42.0",
|
|
23
23
|
"licenses": [
|
|
24
24
|
{
|
|
25
25
|
"license": {
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
}
|
|
43
43
|
],
|
|
44
44
|
"swid": {
|
|
45
|
-
"tagId": "swidtag-create-daloy-0.
|
|
45
|
+
"tagId": "swidtag-create-daloy-0.42.0",
|
|
46
46
|
"name": "create-daloy",
|
|
47
|
-
"version": "0.
|
|
47
|
+
"version": "0.42.0",
|
|
48
48
|
"tagVersion": 0,
|
|
49
49
|
"patch": false
|
|
50
50
|
}
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"components": [],
|
|
54
54
|
"dependencies": [
|
|
55
55
|
{
|
|
56
|
-
"ref": "pkg:npm/create-daloy@0.
|
|
56
|
+
"ref": "pkg:npm/create-daloy@0.42.0",
|
|
57
57
|
"dependsOn": []
|
|
58
58
|
}
|
|
59
59
|
]
|
package/sbom.spdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"spdxVersion": "SPDX-2.3",
|
|
3
3
|
"dataLicense": "CC0-1.0",
|
|
4
4
|
"SPDXID": "SPDXRef-DOCUMENT",
|
|
5
|
-
"name": "create-daloy-0.
|
|
6
|
-
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.
|
|
5
|
+
"name": "create-daloy-0.42.0",
|
|
6
|
+
"documentNamespace": "https://github.com/daloyjs/daloy/sbom/create-daloy-0.42.0-d2c825aa-a918-58f1-9fed-b01fd3b46439",
|
|
7
7
|
"creationInfo": {
|
|
8
|
-
"created": "2026-06-
|
|
8
|
+
"created": "2026-06-19T08:56:45.353Z",
|
|
9
9
|
"creators": [
|
|
10
10
|
"Tool: daloy-generate-sbom",
|
|
11
11
|
"Organization: DaloyJS"
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
{
|
|
17
17
|
"SPDXID": "SPDXRef-Package-create-daloy",
|
|
18
18
|
"name": "create-daloy",
|
|
19
|
-
"versionInfo": "0.
|
|
19
|
+
"versionInfo": "0.42.0",
|
|
20
20
|
"downloadLocation": "https://github.com/daloyjs/daloy",
|
|
21
21
|
"filesAnalyzed": false,
|
|
22
22
|
"licenseConcluded": "MIT",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
{
|
|
28
28
|
"referenceCategory": "PACKAGE-MANAGER",
|
|
29
29
|
"referenceType": "purl",
|
|
30
|
-
"referenceLocator": "pkg:npm/create-daloy@0.
|
|
30
|
+
"referenceLocator": "pkg:npm/create-daloy@0.42.0"
|
|
31
31
|
}
|
|
32
32
|
]
|
|
33
33
|
}
|
|
@@ -44,6 +44,20 @@ bun run gen:client
|
|
|
44
44
|
bun test
|
|
45
45
|
```
|
|
46
46
|
|
|
47
|
+
## Contract gate
|
|
48
|
+
|
|
49
|
+
Check that your OpenAPI contract is internally consistent (operationIds present and unique, response examples matching their schemas, no dead routes). It ships as `tests/contract.test.ts` (run under `bun test`) and as a focused script:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
bun run contract # bun test tests/contract.test.ts
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
For a localhost-only gate that runs before code leaves your machine, enable the bundled pre-push hook (opt-in; bypass once with `git push --no-verify`):
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
bun run hooks:install # points core.hooksPath at .githooks
|
|
59
|
+
```
|
|
60
|
+
|
|
47
61
|
## Imports
|
|
48
62
|
|
|
49
63
|
This project uses TypeScript with `"moduleResolution": "Bundler"` and `"allowImportingTsExtensions": true`. Relative imports use the **`.ts` extension** directly:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# daloyjs-pre-push-contract-hook v1
|
|
3
|
+
#
|
|
4
|
+
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, response examples that
|
|
6
|
+
# don't match their schema, accidental body schemas on safe methods, or routes
|
|
7
|
+
# with no declared responses. Catches the class of drift `tsc` can't see before
|
|
8
|
+
# it leaves your machine, with CI as the backstop.
|
|
9
|
+
#
|
|
10
|
+
# Enable once per clone: bun run hooks:install (sets core.hooksPath here)
|
|
11
|
+
# Bypass once: git push --no-verify
|
|
12
|
+
if ! command -v bun >/dev/null 2>&1; then
|
|
13
|
+
echo "pre-push: bun not found on PATH. Skipping contract gate." >&2
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
exec bun test tests/contract.test.ts
|
|
@@ -11,13 +11,15 @@
|
|
|
11
11
|
"start": "bun src/index.ts",
|
|
12
12
|
"typecheck": "tsc --noEmit",
|
|
13
13
|
"test": "bun test",
|
|
14
|
+
"contract": "bun test tests/contract.test.ts",
|
|
14
15
|
"gen:openapi": "bun run scripts/dump-openapi.ts",
|
|
15
16
|
"gen:client": "openapi-ts",
|
|
16
17
|
"gen": "pnpm gen:openapi && pnpm gen:client",
|
|
17
|
-
"audit": "pnpm audit --prod"
|
|
18
|
+
"audit": "pnpm audit --prod",
|
|
19
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
18
20
|
},
|
|
19
21
|
"dependencies": {
|
|
20
|
-
"@daloyjs/core": "^0.
|
|
22
|
+
"@daloyjs/core": "^0.42.0",
|
|
21
23
|
"zod": "^4.4.3"
|
|
22
24
|
},
|
|
23
25
|
"devDependencies": {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { App } from "@daloyjs/core";
|
|
4
|
+
import { runContractTests } from "@daloyjs/core/contract";
|
|
5
|
+
import { buildApp } from "../src/build-app.ts";
|
|
6
|
+
|
|
7
|
+
// Contract gate. `runContractTests` re-derives the OpenAPI contract from the
|
|
8
|
+
// live route table and fails on the defects `tsc` can't see: missing or
|
|
9
|
+
// duplicate operationIds, response examples that don't match their schema, and
|
|
10
|
+
// routes that declare no responses. Running it under `bun test` means a broken
|
|
11
|
+
// contract fails CI before it can ship a misleading OpenAPI spec or generate a
|
|
12
|
+
// wrong typed client.
|
|
13
|
+
describe("contract", () => {
|
|
14
|
+
test("the app's OpenAPI contract is internally consistent", async () => {
|
|
15
|
+
const report = await runContractTests(buildApp());
|
|
16
|
+
expect(report.ok).toBe(true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Unhappy path: prove the gate actually rejects a broken contract. A route
|
|
20
|
+
// without an operationId can't generate a stable client method name, so the
|
|
21
|
+
// runner reports an error (operationId is required by default).
|
|
22
|
+
test("the contract gate rejects a route missing its operationId", async () => {
|
|
23
|
+
const broken = new App();
|
|
24
|
+
broken.route({
|
|
25
|
+
method: "GET",
|
|
26
|
+
path: "/missing-op-id",
|
|
27
|
+
responses: { 200: { description: "ok", body: z.object({ ok: z.boolean() }) } },
|
|
28
|
+
handler: async () => ({ status: 200 as const, body: { ok: true } }),
|
|
29
|
+
});
|
|
30
|
+
const report = await runContractTests(broken);
|
|
31
|
+
expect(report.ok).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
@@ -10,4 +10,11 @@ describe("buildApp", () => {
|
|
|
10
10
|
expect(body.ok).toBe(true);
|
|
11
11
|
expect(body.runtime).toBe("bun");
|
|
12
12
|
});
|
|
13
|
+
|
|
14
|
+
// Unhappy path: an unregistered route is rejected with 404 (problem+json).
|
|
15
|
+
test("unknown route returns 404", async () => {
|
|
16
|
+
const app = buildApp();
|
|
17
|
+
const res = await app.request("/__not_a_route__");
|
|
18
|
+
expect(res.status).toBe(404);
|
|
19
|
+
});
|
|
13
20
|
});
|
|
@@ -17,6 +17,20 @@ pnpm deploy
|
|
|
17
17
|
|
|
18
18
|
`@daloyjs/core/cloudflare` exposes `toFetchHandler(app)`, so the same `App` you would use on Node also runs on Workers.
|
|
19
19
|
|
|
20
|
+
## Contract gate
|
|
21
|
+
|
|
22
|
+
Check that your OpenAPI contract is internally consistent (operationIds present and unique, response examples matching their schemas, no dead routes). It ships as `tests/contract.test.ts` (run under `pnpm test`) and as a focused script:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm contract # daloy inspect --check src/index.ts
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a localhost-only gate that runs before code leaves your machine, enable the bundled pre-push hook (opt-in; bypass once with `git push --no-verify`):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pnpm hooks:install # points core.hooksPath at .githooks
|
|
32
|
+
```
|
|
33
|
+
|
|
20
34
|
## What's included
|
|
21
35
|
|
|
22
36
|
- `@daloyjs/core/cloudflare` with starter security middleware: `secureHeaders` and `requestId`.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# daloyjs-pre-push-contract-hook v1
|
|
3
|
+
#
|
|
4
|
+
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, response examples that
|
|
6
|
+
# don't match their schema, accidental body schemas on safe methods, or routes
|
|
7
|
+
# with no declared responses. Catches the class of drift `tsc` can't see before
|
|
8
|
+
# it leaves your machine, with CI as the backstop.
|
|
9
|
+
#
|
|
10
|
+
# Enable once per clone: npm run hooks:install (sets core.hooksPath here)
|
|
11
|
+
# Bypass once: git push --no-verify
|
|
12
|
+
DALOY="./node_modules/.bin/daloy"
|
|
13
|
+
if [ ! -x "$DALOY" ]; then
|
|
14
|
+
echo "pre-push: daloy CLI not found — run your package manager's install. Skipping contract gate." >&2
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
exec "$DALOY" inspect --check src/index.ts
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"deploy": "wrangler deploy",
|
|
9
9
|
"typecheck": "tsc --noEmit",
|
|
10
10
|
"test": "node --import tsx/esm --test tests/**/*.test.ts",
|
|
11
|
-
"
|
|
11
|
+
"contract": "daloy inspect --check src/index.ts",
|
|
12
|
+
"audit": "pnpm audit --prod",
|
|
13
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
|
-
"@daloyjs/core": "^0.
|
|
16
|
+
"@daloyjs/core": "^0.42.0",
|
|
15
17
|
"zod": "^4.4.3"
|
|
16
18
|
},
|
|
17
19
|
"devDependencies": {
|
|
@@ -68,4 +68,9 @@ app.route({
|
|
|
68
68
|
});
|
|
69
69
|
// daloy-minimal:strip-end books
|
|
70
70
|
|
|
71
|
+
// Cloudflare invokes the default export's `fetch`. The `app` instance is also
|
|
72
|
+
// exported (a no-op for the Worker runtime) so `tests/contract.test.ts` can
|
|
73
|
+
// contract-check the route table in-process without booting the handler.
|
|
74
|
+
export { app };
|
|
75
|
+
|
|
71
76
|
export default toFetchHandler(app);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import handler from "../src/index.ts";
|
|
4
|
+
|
|
5
|
+
// Cloudflare Workers invoke the default export's `fetch(request, env?, ctx?)`.
|
|
6
|
+
// env / ctx are optional, so a plain Request is enough for in-process tests.
|
|
7
|
+
test("GET /healthz returns 200", async () => {
|
|
8
|
+
const res = await handler.fetch(new Request("https://example.test/healthz"));
|
|
9
|
+
assert.equal(res.status, 200);
|
|
10
|
+
const body = (await res.json()) as { ok: boolean; runtime: string };
|
|
11
|
+
assert.equal(body.ok, true);
|
|
12
|
+
assert.equal(body.runtime, "cloudflare-worker");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Unhappy path: an unregistered route is rejected with 404 (problem+json).
|
|
16
|
+
test("unknown route returns 404", async () => {
|
|
17
|
+
const res = await handler.fetch(new Request("https://example.test/__not_a_route__"));
|
|
18
|
+
assert.equal(res.status, 404);
|
|
19
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { App } from "@daloyjs/core";
|
|
5
|
+
import { runContractTests } from "@daloyjs/core/contract";
|
|
6
|
+
import { app } from "../src/index.ts";
|
|
7
|
+
|
|
8
|
+
// Contract gate. `runContractTests` re-derives the OpenAPI contract from the
|
|
9
|
+
// live route table and fails on the defects `tsc` can't see: missing or
|
|
10
|
+
// duplicate operationIds, response examples that don't match their schema, and
|
|
11
|
+
// routes that declare no responses. Running it under `pnpm test` means a broken
|
|
12
|
+
// contract fails CI before it can ship a misleading OpenAPI spec or generate a
|
|
13
|
+
// wrong typed client.
|
|
14
|
+
test("the app's OpenAPI contract is internally consistent", async () => {
|
|
15
|
+
const report = await runContractTests(app);
|
|
16
|
+
assert.equal(
|
|
17
|
+
report.ok,
|
|
18
|
+
true,
|
|
19
|
+
`contract issues:\n${report.issues
|
|
20
|
+
.map((i) => ` [${i.level}] ${i.route}: ${i.message}`)
|
|
21
|
+
.join("\n")}`,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Unhappy path: prove the gate actually rejects a broken contract. A route
|
|
26
|
+
// without an operationId can't generate a stable client method name, so the
|
|
27
|
+
// runner reports an error (operationId is required by default).
|
|
28
|
+
test("the contract gate rejects a route missing its operationId", async () => {
|
|
29
|
+
const broken = new App();
|
|
30
|
+
broken.route({
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: "/missing-op-id",
|
|
33
|
+
responses: { 200: { description: "ok", body: z.object({ ok: z.boolean() }) } },
|
|
34
|
+
handler: async () => ({ status: 200 as const, body: { ok: true } }),
|
|
35
|
+
});
|
|
36
|
+
const report = await runContractTests(broken);
|
|
37
|
+
assert.equal(report.ok, false);
|
|
38
|
+
});
|
|
@@ -47,6 +47,20 @@ first-class Deno entry point.
|
|
|
47
47
|
deno task test
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
## Contract gate
|
|
51
|
+
|
|
52
|
+
Check that your OpenAPI contract is internally consistent (operationIds present and unique, response examples matching their schemas, no dead routes). It ships as `tests/contract_test.ts` (run under `deno task test`) and as a focused task:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
deno task contract # deno test --allow-net --allow-env tests/contract_test.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
For a localhost-only gate that runs before code leaves your machine, enable the bundled pre-push hook (opt-in; bypass once with `git push --no-verify`):
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
deno task hooks:install # points core.hooksPath at .githooks
|
|
62
|
+
```
|
|
63
|
+
|
|
50
64
|
## What's included
|
|
51
65
|
|
|
52
66
|
- `@daloyjs/core` (loaded via `jsr:` specifiers in `deno.json`).
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# daloyjs-pre-push-contract-hook v1
|
|
3
|
+
#
|
|
4
|
+
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, response examples that
|
|
6
|
+
# don't match their schema, accidental body schemas on safe methods, or routes
|
|
7
|
+
# with no declared responses. Catches the class of drift the type-checker can't
|
|
8
|
+
# see before it leaves your machine, with CI as the backstop.
|
|
9
|
+
#
|
|
10
|
+
# Enable once per clone: deno task hooks:install (sets core.hooksPath here)
|
|
11
|
+
# Bypass once: git push --no-verify
|
|
12
|
+
if ! command -v deno >/dev/null 2>&1; then
|
|
13
|
+
echo "pre-push: deno not found on PATH. Skipping contract gate." >&2
|
|
14
|
+
exit 0
|
|
15
|
+
fi
|
|
16
|
+
exec deno test --allow-net --allow-env tests/contract_test.ts
|
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
"start": "deno run --allow-net --allow-env --allow-read src/main.ts",
|
|
6
6
|
"typecheck": "deno check src/main.ts",
|
|
7
7
|
"test": "deno test --allow-net --allow-env tests/",
|
|
8
|
-
"
|
|
8
|
+
"contract": "deno test --allow-net --allow-env tests/contract_test.ts",
|
|
9
|
+
"gen:openapi": "deno run --allow-net --allow-env --allow-read --allow-write scripts/dump-openapi.ts",
|
|
10
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
9
11
|
},
|
|
10
12
|
"imports": {
|
|
11
|
-
"@daloyjs/core": "jsr:@daloyjs/daloy@^0.
|
|
12
|
-
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.
|
|
13
|
-
"@daloyjs/core/
|
|
14
|
-
"@daloyjs/core/
|
|
13
|
+
"@daloyjs/core": "jsr:@daloyjs/daloy@^0.42.0",
|
|
14
|
+
"@daloyjs/core/banner": "jsr:@daloyjs/daloy@^0.42.0/banner",
|
|
15
|
+
"@daloyjs/core/contract": "jsr:@daloyjs/daloy@^0.42.0/contract",
|
|
16
|
+
"@daloyjs/core/deno": "jsr:@daloyjs/daloy@^0.42.0/deno",
|
|
17
|
+
"@daloyjs/core/openapi": "jsr:@daloyjs/daloy@^0.42.0/openapi",
|
|
15
18
|
"zod": "npm:zod@^4.4.3"
|
|
16
19
|
},
|
|
17
20
|
"compilerOptions": {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { assertEquals } from "jsr:@std/assert@^1.0.0";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { App } from "@daloyjs/core";
|
|
4
|
+
import { runContractTests } from "@daloyjs/core/contract";
|
|
5
|
+
import { buildApp } from "../src/build-app.ts";
|
|
6
|
+
|
|
7
|
+
// Contract gate. `runContractTests` re-derives the OpenAPI contract from the
|
|
8
|
+
// live route table and fails on the defects the type-checker can't see: missing
|
|
9
|
+
// or duplicate operationIds, response examples that don't match their schema,
|
|
10
|
+
// and routes that declare no responses. Running it under `deno task test` means
|
|
11
|
+
// a broken contract fails CI before it can ship a misleading OpenAPI spec or
|
|
12
|
+
// generate a wrong typed client.
|
|
13
|
+
Deno.test("the app's OpenAPI contract is internally consistent", async () => {
|
|
14
|
+
const report = await runContractTests(buildApp());
|
|
15
|
+
assertEquals(
|
|
16
|
+
report.ok,
|
|
17
|
+
true,
|
|
18
|
+
`contract issues:\n${report.issues
|
|
19
|
+
.map((i) => ` [${i.level}] ${i.route}: ${i.message}`)
|
|
20
|
+
.join("\n")}`,
|
|
21
|
+
);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Unhappy path: prove the gate actually rejects a broken contract. A route
|
|
25
|
+
// without an operationId can't generate a stable client method name, so the
|
|
26
|
+
// runner reports an error (operationId is required by default).
|
|
27
|
+
Deno.test("the contract gate rejects a route missing its operationId", async () => {
|
|
28
|
+
const broken = new App();
|
|
29
|
+
broken.route({
|
|
30
|
+
method: "GET",
|
|
31
|
+
path: "/missing-op-id",
|
|
32
|
+
responses: { 200: { description: "ok", body: z.object({ ok: z.boolean() }) } },
|
|
33
|
+
handler: async () => ({ status: 200 as const, body: { ok: true } }),
|
|
34
|
+
});
|
|
35
|
+
const report = await runContractTests(broken);
|
|
36
|
+
assertEquals(report.ok, false);
|
|
37
|
+
});
|
|
@@ -9,3 +9,10 @@ Deno.test("GET /healthz returns 200", async () => {
|
|
|
9
9
|
assertEquals(body.ok, true);
|
|
10
10
|
assertEquals(body.runtime, "deno");
|
|
11
11
|
});
|
|
12
|
+
|
|
13
|
+
// Unhappy path: an unregistered route is rejected with 404 (problem+json).
|
|
14
|
+
Deno.test("unknown route returns 404", async () => {
|
|
15
|
+
const app = buildApp();
|
|
16
|
+
const res = await app.request("/__not_a_route__");
|
|
17
|
+
assertEquals(res.status, 404);
|
|
18
|
+
});
|
|
@@ -39,6 +39,20 @@ pnpm gen
|
|
|
39
39
|
# → generated/client/ (typed Hey API client)
|
|
40
40
|
```
|
|
41
41
|
|
|
42
|
+
## Contract gate
|
|
43
|
+
|
|
44
|
+
Check that your OpenAPI contract is internally consistent (operationIds present and unique, response examples matching their schemas, no dead routes):
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pnpm contract # daloy inspect --check src/build-app.ts
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
It also runs as `tests/contract.test.ts` under `pnpm test`. For a localhost-only gate that runs before code leaves your machine, enable the bundled pre-push hook (opt-in; bypass once with `git push --no-verify`):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pnpm hooks:install # points core.hooksPath at .githooks
|
|
54
|
+
```
|
|
55
|
+
|
|
42
56
|
## Build
|
|
43
57
|
|
|
44
58
|
```bash
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# daloyjs-pre-push-contract-hook v1
|
|
3
|
+
#
|
|
4
|
+
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, response examples that
|
|
6
|
+
# don't match their schema, accidental body schemas on safe methods, or routes
|
|
7
|
+
# with no declared responses. Catches the class of drift `tsc` can't see before
|
|
8
|
+
# it leaves your machine, with CI as the backstop.
|
|
9
|
+
#
|
|
10
|
+
# Enable once per clone: npm run hooks:install (sets core.hooksPath here)
|
|
11
|
+
# Bypass once: git push --no-verify
|
|
12
|
+
DALOY="./node_modules/.bin/daloy"
|
|
13
|
+
if [ ! -x "$DALOY" ]; then
|
|
14
|
+
echo "pre-push: daloy CLI not found — run your package manager's install. Skipping contract gate." >&2
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
exec "$DALOY" inspect --check src/build-app.ts
|
|
@@ -12,13 +12,15 @@
|
|
|
12
12
|
"build": "tsc -p tsconfig.build.json",
|
|
13
13
|
"typecheck": "tsc --noEmit",
|
|
14
14
|
"test": "node --import tsx/esm --test tests/**/*.test.ts",
|
|
15
|
+
"contract": "daloy inspect --check src/build-app.ts",
|
|
15
16
|
"gen:openapi": "node --import tsx/esm scripts/dump-openapi.ts",
|
|
16
17
|
"gen:client": "openapi-ts",
|
|
17
18
|
"gen": "pnpm gen:openapi && pnpm gen:client",
|
|
18
|
-
"audit": "pnpm audit --prod"
|
|
19
|
+
"audit": "pnpm audit --prod",
|
|
20
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
19
21
|
},
|
|
20
22
|
"dependencies": {
|
|
21
|
-
"@daloyjs/core": "^0.
|
|
23
|
+
"@daloyjs/core": "^0.42.0",
|
|
22
24
|
"zod": "^4.4.3"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { App } from "@daloyjs/core";
|
|
5
|
+
import { runContractTests } from "@daloyjs/core/contract";
|
|
6
|
+
import { buildApp } from "../src/build-app.ts";
|
|
7
|
+
|
|
8
|
+
// Contract gate. `runContractTests` re-derives the OpenAPI contract from the
|
|
9
|
+
// live route table and fails on the defects `tsc` can't see: missing or
|
|
10
|
+
// duplicate operationIds, response examples that don't match their schema, and
|
|
11
|
+
// routes that declare no responses. Running it under `pnpm test` means a broken
|
|
12
|
+
// contract fails CI before it can ship a misleading OpenAPI spec or generate a
|
|
13
|
+
// wrong typed client.
|
|
14
|
+
test("the app's OpenAPI contract is internally consistent", async () => {
|
|
15
|
+
const report = await runContractTests(buildApp());
|
|
16
|
+
assert.equal(
|
|
17
|
+
report.ok,
|
|
18
|
+
true,
|
|
19
|
+
`contract issues:\n${report.issues
|
|
20
|
+
.map((i) => ` [${i.level}] ${i.route}: ${i.message}`)
|
|
21
|
+
.join("\n")}`,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Unhappy path: prove the gate actually rejects a broken contract. A route
|
|
26
|
+
// without an operationId can't generate a stable client method name, so the
|
|
27
|
+
// runner reports an error (operationId is required by default).
|
|
28
|
+
test("the contract gate rejects a route missing its operationId", async () => {
|
|
29
|
+
const broken = new App();
|
|
30
|
+
broken.route({
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: "/missing-op-id",
|
|
33
|
+
responses: { 200: { description: "ok", body: z.object({ ok: z.boolean() }) } },
|
|
34
|
+
handler: async () => ({ status: 200 as const, body: { ok: true } }),
|
|
35
|
+
});
|
|
36
|
+
const report = await runContractTests(broken);
|
|
37
|
+
assert.equal(report.ok, false);
|
|
38
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { buildApp } from "../src/build-app.ts";
|
|
4
|
+
|
|
5
|
+
// In-process tests via `app.request(...)` — no port, no network.
|
|
6
|
+
test("GET /healthz returns 200", async () => {
|
|
7
|
+
const app = buildApp();
|
|
8
|
+
const res = await app.request("/healthz");
|
|
9
|
+
assert.equal(res.status, 200);
|
|
10
|
+
const body = (await res.json()) as { ok: boolean };
|
|
11
|
+
assert.equal(body.ok, true);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Unhappy path: an unregistered route is rejected with 404 (problem+json).
|
|
15
|
+
test("unknown route returns 404", async () => {
|
|
16
|
+
const app = buildApp();
|
|
17
|
+
const res = await app.request("/__not_a_route__");
|
|
18
|
+
assert.equal(res.status, 404);
|
|
19
|
+
});
|
|
@@ -31,6 +31,20 @@ To brand Scalar, change `docs: true` in `api/index.ts` to `docs: { scalar: { the
|
|
|
31
31
|
|
|
32
32
|
<!-- daloy-minimal:strip-end docs -->
|
|
33
33
|
|
|
34
|
+
## Contract gate
|
|
35
|
+
|
|
36
|
+
Check that your OpenAPI contract is internally consistent (operationIds present and unique, response examples matching their schemas, no dead routes). It ships as `tests/contract.test.ts` (run under `pnpm test`) and as a focused script:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pnpm contract # daloy inspect --check api/index.ts
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
For a localhost-only gate that runs before code leaves your machine, enable the bundled pre-push hook (opt-in; bypass once with `git push --no-verify`):
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pnpm hooks:install # points core.hooksPath at .githooks
|
|
46
|
+
```
|
|
47
|
+
|
|
34
48
|
## Deploy
|
|
35
49
|
|
|
36
50
|
```bash
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# daloyjs-pre-push-contract-hook v1
|
|
3
|
+
#
|
|
4
|
+
# Localhost-only contract gate. Rejects a push whose OpenAPI contract has
|
|
5
|
+
# error-level issues: missing or duplicate operationIds, response examples that
|
|
6
|
+
# don't match their schema, accidental body schemas on safe methods, or routes
|
|
7
|
+
# with no declared responses. Catches the class of drift `tsc` can't see before
|
|
8
|
+
# it leaves your machine, with CI as the backstop.
|
|
9
|
+
#
|
|
10
|
+
# Enable once per clone: npm run hooks:install (sets core.hooksPath here)
|
|
11
|
+
# Bypass once: git push --no-verify
|
|
12
|
+
DALOY="./node_modules/.bin/daloy"
|
|
13
|
+
if [ ! -x "$DALOY" ]; then
|
|
14
|
+
echo "pre-push: daloy CLI not found — run your package manager's install. Skipping contract gate." >&2
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
exec "$DALOY" inspect --check api/index.ts
|
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
"deploy": "vercel deploy",
|
|
9
9
|
"typecheck": "tsc --noEmit",
|
|
10
10
|
"test": "node --import tsx/esm --test tests/**/*.test.ts",
|
|
11
|
-
"
|
|
11
|
+
"contract": "daloy inspect --check api/index.ts",
|
|
12
|
+
"audit": "pnpm audit --prod",
|
|
13
|
+
"hooks:install": "git config core.hooksPath .githooks"
|
|
12
14
|
},
|
|
13
15
|
"dependencies": {
|
|
14
|
-
"@daloyjs/core": "^0.
|
|
16
|
+
"@daloyjs/core": "^0.42.0",
|
|
15
17
|
"zod": "^4.4.3"
|
|
16
18
|
},
|
|
17
19
|
"devDependencies": {
|
|
@@ -8,3 +8,9 @@ test("Vercel Node.js handler responds through DaloyJS", async () => {
|
|
|
8
8
|
assert.equal(response.status, 200);
|
|
9
9
|
assert.equal((await response.json()).runtime, "vercel");
|
|
10
10
|
});
|
|
11
|
+
|
|
12
|
+
// Unhappy path: an unregistered route is rejected with 404 (problem+json).
|
|
13
|
+
test("unknown route returns 404", async () => {
|
|
14
|
+
const response = await handler.fetch(new Request("https://example.test/__not_a_route__"));
|
|
15
|
+
assert.equal(response.status, 404);
|
|
16
|
+
});
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { App } from "@daloyjs/core";
|
|
5
|
+
import { runContractTests } from "@daloyjs/core/contract";
|
|
6
|
+
import { app } from "../api/index.ts";
|
|
7
|
+
|
|
8
|
+
// Contract gate. `runContractTests` re-derives the OpenAPI contract from the
|
|
9
|
+
// live route table and fails on the defects `tsc` can't see: missing or
|
|
10
|
+
// duplicate operationIds, response examples that don't match their schema, and
|
|
11
|
+
// routes that declare no responses. Running it under `pnpm test` means a broken
|
|
12
|
+
// contract fails CI before it can ship a misleading OpenAPI spec or generate a
|
|
13
|
+
// wrong typed client.
|
|
14
|
+
test("the app's OpenAPI contract is internally consistent", async () => {
|
|
15
|
+
const report = await runContractTests(app);
|
|
16
|
+
assert.equal(
|
|
17
|
+
report.ok,
|
|
18
|
+
true,
|
|
19
|
+
`contract issues:\n${report.issues
|
|
20
|
+
.map((i) => ` [${i.level}] ${i.route}: ${i.message}`)
|
|
21
|
+
.join("\n")}`,
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Unhappy path: prove the gate actually rejects a broken contract. A route
|
|
26
|
+
// without an operationId can't generate a stable client method name, so the
|
|
27
|
+
// runner reports an error (operationId is required by default).
|
|
28
|
+
test("the contract gate rejects a route missing its operationId", async () => {
|
|
29
|
+
const broken = new App();
|
|
30
|
+
broken.route({
|
|
31
|
+
method: "GET",
|
|
32
|
+
path: "/missing-op-id",
|
|
33
|
+
responses: { 200: { description: "ok", body: z.object({ ok: z.boolean() }) } },
|
|
34
|
+
handler: async () => ({ status: 200 as const, body: { ok: true } }),
|
|
35
|
+
});
|
|
36
|
+
const report = await runContractTests(broken);
|
|
37
|
+
assert.equal(report.ok, false);
|
|
38
|
+
});
|