@shaferllc/keel 0.66.0 → 0.68.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.
Files changed (132) hide show
  1. package/AGENTS.md +167 -0
  2. package/README.md +30 -1
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/core/application.d.ts +5 -5
  5. package/dist/core/application.js +2 -2
  6. package/dist/core/cache.d.ts +82 -5
  7. package/dist/core/cache.js +181 -23
  8. package/dist/core/cli/stubs.d.ts +12 -0
  9. package/dist/core/cli/stubs.js +120 -0
  10. package/dist/core/events.d.ts +129 -5
  11. package/dist/core/events.js +165 -7
  12. package/dist/core/health.d.ts +141 -0
  13. package/dist/core/health.js +226 -0
  14. package/dist/core/helpers.d.ts +9 -3
  15. package/dist/core/helpers.js +11 -3
  16. package/dist/core/index.d.ts +15 -10
  17. package/dist/core/index.js +7 -4
  18. package/dist/core/lock.d.ts +139 -0
  19. package/dist/core/lock.js +215 -0
  20. package/dist/core/logger.d.ts +82 -4
  21. package/dist/core/logger.js +141 -23
  22. package/dist/core/mail.d.ts +128 -7
  23. package/dist/core/mail.js +264 -16
  24. package/dist/core/queue.d.ts +134 -9
  25. package/dist/core/queue.js +304 -14
  26. package/dist/core/storage.d.ts +159 -6
  27. package/dist/core/storage.js +287 -7
  28. package/dist/mcp/server.d.ts +19 -0
  29. package/dist/mcp/server.js +355 -0
  30. package/docs/ai-manifest.json +2472 -0
  31. package/docs/ai.md +128 -0
  32. package/docs/architecture.md +331 -0
  33. package/docs/authentication.md +453 -0
  34. package/docs/authorization.md +167 -0
  35. package/docs/broadcasting.md +137 -0
  36. package/docs/broker.md +500 -0
  37. package/docs/cache.md +558 -0
  38. package/docs/configuration.md +311 -0
  39. package/docs/console.md +356 -0
  40. package/docs/container.md +467 -0
  41. package/docs/controllers.md +265 -0
  42. package/docs/cors.md +51 -0
  43. package/docs/database.md +530 -0
  44. package/docs/debugging.md +129 -0
  45. package/docs/decorators.md +127 -0
  46. package/docs/errors.md +395 -0
  47. package/docs/events.md +496 -0
  48. package/docs/examples/architecture-app.ts +27 -0
  49. package/docs/examples/authentication.ts +61 -0
  50. package/docs/examples/authorization.ts +79 -0
  51. package/docs/examples/broadcasting.ts +60 -0
  52. package/docs/examples/broker-cache-validate.ts +34 -0
  53. package/docs/examples/broker-fault-tolerance.ts +29 -0
  54. package/docs/examples/broker-middleware.ts +27 -0
  55. package/docs/examples/broker.ts +203 -0
  56. package/docs/examples/cache.ts +222 -0
  57. package/docs/examples/configuration.ts +81 -0
  58. package/docs/examples/container.ts +134 -0
  59. package/docs/examples/controllers.ts +86 -0
  60. package/docs/examples/database.ts +118 -0
  61. package/docs/examples/debugging.ts +41 -0
  62. package/docs/examples/decorators.ts +40 -0
  63. package/docs/examples/errors.ts +121 -0
  64. package/docs/examples/events.ts +204 -0
  65. package/docs/examples/factories.ts +84 -0
  66. package/docs/examples/hashing.ts +71 -0
  67. package/docs/examples/health.ts +94 -0
  68. package/docs/examples/helpers.ts +171 -0
  69. package/docs/examples/hooks.ts +54 -0
  70. package/docs/examples/inertia.ts +81 -0
  71. package/docs/examples/locks.ts +120 -0
  72. package/docs/examples/logger.ts +92 -0
  73. package/docs/examples/mail.ts +160 -0
  74. package/docs/examples/middleware.ts +119 -0
  75. package/docs/examples/migrations.ts +126 -0
  76. package/docs/examples/models.ts +239 -0
  77. package/docs/examples/notification.ts +124 -0
  78. package/docs/examples/providers.ts +123 -0
  79. package/docs/examples/queues.ts +254 -0
  80. package/docs/examples/rate-limiting.ts +42 -0
  81. package/docs/examples/redis.ts +99 -0
  82. package/docs/examples/request-response.ts +197 -0
  83. package/docs/examples/routing.ts +186 -0
  84. package/docs/examples/scheduling.ts +62 -0
  85. package/docs/examples/sessions.ts +102 -0
  86. package/docs/examples/static-files.ts +63 -0
  87. package/docs/examples/storage.ts +132 -0
  88. package/docs/examples/templates.ts +58 -0
  89. package/docs/examples/testing.ts +66 -0
  90. package/docs/examples/transformer.ts +141 -0
  91. package/docs/examples/transformers.ts +49 -0
  92. package/docs/examples/url-builder.ts +86 -0
  93. package/docs/examples/validation.ts +102 -0
  94. package/docs/examples/views.tsx +62 -0
  95. package/docs/examples/vite.ts +106 -0
  96. package/docs/factories.md +166 -0
  97. package/docs/getting-started.md +290 -0
  98. package/docs/hashing.md +259 -0
  99. package/docs/health.md +225 -0
  100. package/docs/helpers.md +347 -0
  101. package/docs/hono.md +186 -0
  102. package/docs/hooks.md +118 -0
  103. package/docs/inertia.md +241 -0
  104. package/docs/locks.md +323 -0
  105. package/docs/logger.md +290 -0
  106. package/docs/mail.md +678 -0
  107. package/docs/middleware.md +425 -0
  108. package/docs/migrations.md +476 -0
  109. package/docs/models.md +810 -0
  110. package/docs/notifications.md +474 -0
  111. package/docs/providers.md +363 -0
  112. package/docs/queues.md +679 -0
  113. package/docs/rate-limiting.md +155 -0
  114. package/docs/redis.md +178 -0
  115. package/docs/request-response.md +953 -0
  116. package/docs/routing.md +804 -0
  117. package/docs/scheduling.md +110 -0
  118. package/docs/security.md +85 -0
  119. package/docs/sessions.md +354 -0
  120. package/docs/social-auth.md +174 -0
  121. package/docs/static-files.md +211 -0
  122. package/docs/storage.md +450 -0
  123. package/docs/templates.md +315 -0
  124. package/docs/testing.md +125 -0
  125. package/docs/transformers.md +381 -0
  126. package/docs/url-builder.md +295 -0
  127. package/docs/validation.md +288 -0
  128. package/docs/views.md +267 -0
  129. package/docs/vite.md +434 -0
  130. package/llms-full.txt +17694 -0
  131. package/llms.txt +116 -0
  132. package/package.json +26 -7
@@ -0,0 +1,315 @@
1
+ # Templates
2
+
3
+ A string templating engine in the spirit of AdonisJS **Edge** — `{{ }}`
4
+ interpolation and `@`-prefixed tags for logic, includes, layouts, and
5
+ components. Reach for it when you want plain-text templates instead of (or
6
+ alongside) [JSX views](./views.md).
7
+
8
+ Unlike engines that compile a template to a function with `eval` /
9
+ `new Function`, Keel **interprets** templates against a small, safe expression
10
+ evaluator. No dynamic code generation, so the same templates run on Node **and**
11
+ on Cloudflare Workers (where `eval` is forbidden).
12
+
13
+ ## Rendering
14
+
15
+ Register a template by name, then render it with a state object:
16
+
17
+ ```ts
18
+ import { templates, render } from "@shaferllc/keel/core";
19
+
20
+ templates().register("greeting", "Hello, {{ name }}!");
21
+
22
+ await render("greeting", { name: "Ada" }); // "Hello, Ada!"
23
+ ```
24
+
25
+ `render()` returns a `Promise<string>` — hand it to a response or a [view](./views.md):
26
+
27
+ ```ts
28
+ import { html, render } from "@shaferllc/keel/core";
29
+ return html(await render("greeting", { name: "Ada" }));
30
+ ```
31
+
32
+ Register many at once (e.g. a Node loader reads `.html` files and passes them in):
33
+
34
+ ```ts
35
+ templates().registerAll({
36
+ layout: await readFile("views/layout.html", "utf8"),
37
+ home: await readFile("views/home.html", "utf8"),
38
+ });
39
+ ```
40
+
41
+ ## Interpolation
42
+
43
+ ```html
44
+ {{ user.name }} {{-- escaped: HTML-safe --}}
45
+ {{{ post.body }}} {{-- raw: unescaped, for trusted HTML --}}
46
+ {{-- this is a comment; it renders nothing --}}
47
+ ```
48
+
49
+ Escaped `{{ }}` is the default and encodes `& < > " '`. Use raw `{{{ }}}` only for
50
+ HTML you trust. A `null`/`undefined` value renders as an empty string.
51
+
52
+ ## Expressions
53
+
54
+ Interpolation and tag conditions accept a practical subset of JavaScript —
55
+ enough for real templates, without `eval`:
56
+
57
+ ```html
58
+ {{ user.name }} {{-- property + index access --}}
59
+ {{ items[0] }}
60
+ {{ title.toUpperCase() }} {{-- method calls --}}
61
+ {{ items.join(", ") }}
62
+ {{ price * qty }} {{-- + - * / % --}}
63
+ {{ n > 3 && n < 10 }} {{-- comparisons, && || ! ?? --}}
64
+ {{ admin ? "Admin" : "User" }} {{-- ternary --}}
65
+ {{ [1, 2, 3].length }} {{-- array / object literals --}}
66
+ {{ { role: "admin" }.role }}
67
+ ```
68
+
69
+ Not supported: assignment, arrow functions, and other statement-level JS. Keep
70
+ logic in your controller and pass results in as state.
71
+
72
+ ### Filters
73
+
74
+ Pipe a value through a filter with `|`:
75
+
76
+ ```html
77
+ {{ name | upper }}
78
+ {{ name | capitalize }}
79
+ {{ items | length }}
80
+ {{ price | currency("USD") }} {{-- filters take arguments --}}
81
+ ```
82
+
83
+ Built-in filters: `upper`, `lower`, `capitalize`, `json`, `length`. Register your
84
+ own on the engine:
85
+
86
+ ```ts
87
+ templates().filter("currency", (v, code) =>
88
+ new Intl.NumberFormat("en-US", { style: "currency", currency: String(code) }).format(Number(v)),
89
+ );
90
+ ```
91
+
92
+ ## Conditionals
93
+
94
+ ```html
95
+ @if(user.admin)
96
+ <span>Admin</span>
97
+ @elseif(user.member)
98
+ <span>Member</span>
99
+ @else
100
+ <span>Guest</span>
101
+ @end
102
+ ```
103
+
104
+ ## Loops
105
+
106
+ `@each` iterates arrays (or the values of an object). A `$loop` variable exposes
107
+ positional info, and you can capture the index:
108
+
109
+ ```html
110
+ <ul>
111
+ @each(post in posts)
112
+ <li>{{ $loop.iteration }}. {{ post.title }}</li>
113
+ @end
114
+ </ul>
115
+
116
+ @each(item, i in items)
117
+ {{ i }}: {{ item }}
118
+ @end
119
+ ```
120
+
121
+ `$loop` fields: `index` (0-based), `iteration` (1-based), `first`, `last`,
122
+ `count`, `even`, `odd`.
123
+
124
+ ## Partials
125
+
126
+ Pull one template into another with `@include` — it shares the current state:
127
+
128
+ ```html
129
+ {{-- list.html --}}
130
+ <ul>@each(item in items)@include("row")@end</ul>
131
+
132
+ {{-- row.html --}}
133
+ <li>{{ item }}</li>
134
+ ```
135
+
136
+ `@includeIf(condition, "name")` includes only when the condition is truthy.
137
+
138
+ ## Layouts
139
+
140
+ A page declares its layout and fills the layout's `@yield` slots with `@section`:
141
+
142
+ ```html
143
+ {{-- layout.html --}}
144
+ <!doctype html>
145
+ <title>@yield("title")Keel@end</title>
146
+ <body>@yield("body")@end</body>
147
+
148
+ {{-- page.html --}}
149
+ @layout("layout")
150
+ @section("title"){{ page.title }} · Keel@end
151
+ @section("body")<h1>{{ page.title }}</h1>@end
152
+ ```
153
+
154
+ `@yield("name") … @end` renders the matching section, falling back to the content
155
+ between `@yield` and `@end` when the page defines no such section.
156
+
157
+ ## Components
158
+
159
+ Components are reusable templates rendered with props and slots. The content
160
+ between `@component` and its `@end` becomes the `main` slot; `@slot("name")`
161
+ defines named slots. Inside the component, slots arrive as pre-rendered HTML
162
+ strings on a `slots` object:
163
+
164
+ ```html
165
+ {{-- card.html --}}
166
+ <div class="card">
167
+ <header>{{{ slots.header }}}</header>
168
+ <main>{{{ slots.main }}}</main>
169
+ <footer>{{ title }}</footer>
170
+ </div>
171
+
172
+ {{-- usage --}}
173
+ @component("card", { title: "Welcome" })
174
+ @slot("header")<h2>Hi</h2>@end
175
+ <p>Body content goes to the main slot.</p>
176
+ @end
177
+ ```
178
+
179
+ Props are any expression evaluating to an object; they become the component's
180
+ state (merged with globals).
181
+
182
+ ## Globals
183
+
184
+ Expose values or helpers to every template:
185
+
186
+ ```ts
187
+ templates()
188
+ .global("appName", "Keel")
189
+ .global("asset", (path: string) => `/static/${path}`);
190
+ ```
191
+
192
+ ```html
193
+ <title>{{ appName }}</title>
194
+ <img src="{{ asset('logo.svg') }}" />
195
+ ```
196
+
197
+ ## Debugging
198
+
199
+ `@dump(value)` renders a `<pre>` of the value's JSON — handy while building a
200
+ template.
201
+
202
+ ```html
203
+ @dump(user)
204
+ ```
205
+
206
+ ## Escaping & safety
207
+
208
+ - Escaped `{{ }}` encodes HTML; only use raw `{{{ }}}` for trusted content.
209
+ - The evaluator blocks access to `__proto__`, `constructor`, and `prototype`, so
210
+ template state can't be used to reach the prototype chain.
211
+ - There's no `eval`: a template can't execute arbitrary JavaScript, only the
212
+ expression subset above.
213
+
214
+ ## API reference
215
+
216
+ ### `templates()`
217
+
218
+ `templates(): TemplateEngine`
219
+
220
+ Returns the default engine — register templates, globals, and filters on it.
221
+
222
+ ```ts
223
+ templates().register("home", "…");
224
+ ```
225
+
226
+ **Notes:** module-global and shared. Swap it with `setTemplateEngine()` (e.g. for
227
+ an isolated engine in a test).
228
+
229
+ ### `render(name, state?)`
230
+
231
+ `render(name: string, state?: Record<string, unknown>): Promise<string>`
232
+
233
+ Renders a registered template on the default engine.
234
+
235
+ ```ts
236
+ await render("home", { user });
237
+ ```
238
+
239
+ **Notes:** throws if `name` isn't registered. Async because includes, components,
240
+ and layouts compose asynchronously.
241
+
242
+ ### `setTemplateEngine(engine)`
243
+
244
+ `setTemplateEngine(engine: TemplateEngine): TemplateEngine`
245
+
246
+ Replaces the default engine and returns it.
247
+
248
+ **Notes:** the last call wins; useful to reset state between tests.
249
+
250
+ ### `escapeHtml(value)`
251
+
252
+ `escapeHtml(value: unknown): string`
253
+
254
+ HTML-escapes a value (`& < > " '`); `null`/`undefined` become `""`. This is what
255
+ `{{ }}` uses internally.
256
+
257
+ ### `TemplateEngine`
258
+
259
+ The engine class. Construct your own for isolation, or use `templates()`.
260
+
261
+ #### `register(name, source)`
262
+
263
+ `register(name: string, source: string): this`
264
+
265
+ Parses and registers a template. Chainable.
266
+
267
+ **Notes:** parsing happens here, so a malformed template throws at registration,
268
+ not at render.
269
+
270
+ #### `registerAll(sources)`
271
+
272
+ `registerAll(sources: Record<string, string>): this`
273
+
274
+ Registers many templates at once from a `name → source` map.
275
+
276
+ #### `has(name)`
277
+
278
+ `has(name: string): boolean`
279
+
280
+ Whether a template is registered.
281
+
282
+ #### `global(name, value)`
283
+
284
+ `global(name: string, value: unknown): this`
285
+
286
+ Exposes a value or function to every template as a global variable.
287
+
288
+ #### `filter(name, fn)`
289
+
290
+ `filter(name: string, fn: Filter): this`
291
+
292
+ Registers a `{{ value | name }}` filter. `Filter` is
293
+ `(value: unknown, ...args: unknown[]) => unknown`.
294
+
295
+ #### `render(name, state?)`
296
+
297
+ `render(name: string, state?: Record<string, unknown>): Promise<string>`
298
+
299
+ Renders a registered template. Throws for an unknown template, tag, or filter.
300
+
301
+ ### Interfaces & types
302
+
303
+ #### `Filter`
304
+
305
+ `type Filter = (value: unknown, ...args: unknown[]) => unknown`
306
+
307
+ A pipe filter: receives the piped value plus any `filter(arg)` arguments, returns
308
+ the transformed value.
309
+
310
+ #### `RenderContext`
311
+
312
+ `interface RenderContext { sections: Record<string, string>; slots: Record<string, string> }`
313
+
314
+ Internal per-render state threaded through layouts and components — you won't
315
+ construct it directly.
@@ -0,0 +1,125 @@
1
+ # Testing
2
+
3
+ Test your app by **injecting requests** — no server, no port, no network — and
4
+ asserting on the response. `testClient()` wraps your app's Hono instance (which
5
+ already does fetch-style injection) with verb helpers and fluent assertions, the
6
+ way Fastify's `inject()` works.
7
+
8
+ ## The client
9
+
10
+ Build a client from an `Application` and fire requests:
11
+
12
+ ```ts
13
+ import { test } from "node:test";
14
+ import { Application, Router, json, testClient } from "@shaferllc/keel/core";
15
+
16
+ async function makeApp() {
17
+ const app = new Application();
18
+ await app.boot([], { discoverConfig: false, config: { app: {} } });
19
+ app.make(Router).get("/health", () => json({ ok: true }));
20
+ return app;
21
+ }
22
+
23
+ test("health check", async () => {
24
+ const client = testClient(await makeApp());
25
+ const res = await client.get("/health");
26
+ res.assertStatus(200).assertJson({ ok: true });
27
+ });
28
+ ```
29
+
30
+ `testClient()` accepts an **`Application`** (built through a fresh kernel), an
31
+ **`HttpKernel`** (use this if you need global middleware registered with
32
+ `kernel.use(...)`), or anything with a `request()` (a built Hono instance).
33
+
34
+ ## Requests
35
+
36
+ Verb helpers cover the common methods; `post` / `put` / `patch` take a body that's
37
+ JSON-encoded automatically:
38
+
39
+ ```ts
40
+ await client.get("/users");
41
+ await client.get("/users?active=true");
42
+ await client.post("/users", { email: "a@b.com", name: "Ada" }); // sends JSON
43
+ await client.put("/users/1", { name: "Grace" });
44
+ await client.delete("/users/1");
45
+
46
+ // full control — pass a RequestInit for headers, custom bodies, etc.
47
+ await client.request("/users", { method: "POST", headers: { authorization: "Bearer x" }, body });
48
+ ```
49
+
50
+ ## The response
51
+
52
+ Every call resolves to a `TestResponse`. The body is **pre-buffered**, so reads
53
+ are synchronous and repeatable (no "body already consumed"):
54
+
55
+ ```ts
56
+ const res = await client.get("/user");
57
+ res.status; // 200
58
+ res.header("content-type");
59
+ res.text(); // the raw body
60
+ res.json<User>(); // parsed (sync — the body is already read)
61
+ ```
62
+
63
+ ## Assertions
64
+
65
+ Assertions are chainable and throw a descriptive error (including the body) on
66
+ mismatch:
67
+
68
+ ```ts
69
+ res.assertStatus(201);
70
+ res.assertOk(); // any 2xx
71
+ res.assertJson({ id: 1, email }); // deep-equals the JSON body
72
+ res.assertText("pong");
73
+ res.assertHeader("content-type", "application/json");
74
+ res.assertRedirect("/login"); // 3xx (+ optional Location)
75
+
76
+ // chain them:
77
+ (await client.post("/users", body)).assertStatus(201).assertJson({ id: 2, ...body });
78
+ ```
79
+
80
+ ## Testing with middleware
81
+
82
+ When your test needs global middleware (sessions, request logging, auth), build
83
+ the kernel yourself and hand it to `testClient`:
84
+
85
+ ```ts
86
+ const app = await makeApp();
87
+ const kernel = new HttpKernel(app);
88
+ kernel.use(sessionMiddleware());
89
+ kernel.use(requestLogger());
90
+ const client = testClient(kernel);
91
+ ```
92
+
93
+ ## API reference
94
+
95
+ ### `testClient(target)`
96
+
97
+ `testClient(target: Application | HttpKernel | { request(...) }): TestClient`
98
+
99
+ Builds a `TestClient`. An `Application` is built through a fresh `HttpKernel`; pass
100
+ a kernel to register global middleware first.
101
+
102
+ ### `TestClient`
103
+
104
+ | Method | Signature |
105
+ |--------|-----------|
106
+ | `get` / `delete` | `(path, init?) => Promise<TestResponse>` |
107
+ | `post` / `put` / `patch` | `(path, body?, init?) => Promise<TestResponse>` — body JSON-encoded |
108
+ | `request` | `(path, init?) => Promise<TestResponse>` — the low-level form |
109
+
110
+ ### `TestResponse`
111
+
112
+ Body pre-buffered; reads are synchronous.
113
+
114
+ | Member | Notes |
115
+ |--------|-------|
116
+ | `status` | the response status |
117
+ | `header(name)` | a response header, or `null` |
118
+ | `text()` / `json<T>()` | the body (raw / parsed) |
119
+ | `assertStatus(n)` / `assertOk()` | status is `n` / any 2xx |
120
+ | `assertJson(v)` | JSON body deep-equals `v` |
121
+ | `assertText(s)` / `assertHeader(n, v)` | exact body / header match |
122
+ | `assertRedirect(location?)` | 3xx, optionally to `location` |
123
+ | `raw` | the underlying `Response` |
124
+
125
+ All assertions return `this` (chainable) and throw on mismatch.