@shaferllc/keel 0.66.0 → 0.74.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 (239) hide show
  1. package/AGENTS.md +169 -0
  2. package/README.md +44 -6
  3. package/bin/keel-mcp.mjs +9 -0
  4. package/dist/api/api.config.stub +9 -0
  5. package/dist/api/config.d.ts +13 -0
  6. package/dist/api/config.js +14 -0
  7. package/dist/api/index.d.ts +16 -0
  8. package/dist/api/index.js +13 -0
  9. package/dist/api/provider.d.ts +10 -0
  10. package/dist/api/provider.js +17 -0
  11. package/dist/api/query.d.ts +35 -0
  12. package/dist/api/query.js +42 -0
  13. package/dist/api/resource.d.ts +91 -0
  14. package/dist/api/resource.js +188 -0
  15. package/dist/core/application.d.ts +5 -5
  16. package/dist/core/application.js +8 -2
  17. package/dist/core/cache.d.ts +81 -5
  18. package/dist/core/cache.js +188 -23
  19. package/dist/core/cli/stubs.d.ts +26 -0
  20. package/dist/core/cli/stubs.js +225 -0
  21. package/dist/core/console-prompt.d.ts +79 -0
  22. package/dist/core/console-prompt.js +239 -0
  23. package/dist/core/console-ui.d.ts +96 -0
  24. package/dist/core/console-ui.js +187 -0
  25. package/dist/core/console.d.ts +188 -0
  26. package/dist/core/console.js +395 -0
  27. package/dist/core/database.d.ts +70 -1
  28. package/dist/core/database.js +174 -15
  29. package/dist/core/env.d.ts +96 -0
  30. package/dist/core/env.js +140 -0
  31. package/dist/core/events.d.ts +129 -5
  32. package/dist/core/events.js +165 -7
  33. package/dist/core/health.d.ts +141 -0
  34. package/dist/core/health.js +226 -0
  35. package/dist/core/helpers.d.ts +9 -3
  36. package/dist/core/helpers.js +11 -3
  37. package/dist/core/http/kernel.d.ts +2 -0
  38. package/dist/core/http/kernel.js +48 -0
  39. package/dist/core/http/router.d.ts +5 -5
  40. package/dist/core/http/router.js +5 -5
  41. package/dist/core/i18n.d.ts +162 -0
  42. package/dist/core/i18n.js +472 -0
  43. package/dist/core/index.d.ts +39 -13
  44. package/dist/core/index.js +19 -6
  45. package/dist/core/instrumentation.d.ts +113 -0
  46. package/dist/core/instrumentation.js +52 -0
  47. package/dist/core/lock.d.ts +139 -0
  48. package/dist/core/lock.js +215 -0
  49. package/dist/core/logger.d.ts +89 -4
  50. package/dist/core/logger.js +167 -22
  51. package/dist/core/mail.d.ts +128 -7
  52. package/dist/core/mail.js +264 -16
  53. package/dist/core/notification.js +10 -1
  54. package/dist/core/package.d.ts +120 -0
  55. package/dist/core/package.js +169 -0
  56. package/dist/core/pages.d.ts +108 -0
  57. package/dist/core/pages.js +199 -0
  58. package/dist/core/queue.d.ts +134 -9
  59. package/dist/core/queue.js +325 -14
  60. package/dist/core/repl.d.ts +33 -0
  61. package/dist/core/repl.js +88 -0
  62. package/dist/core/scheduler.js +6 -0
  63. package/dist/core/social.d.ts +4 -4
  64. package/dist/core/social.js +4 -4
  65. package/dist/core/storage.d.ts +159 -6
  66. package/dist/core/storage.js +299 -7
  67. package/dist/core/telemetry.d.ts +208 -0
  68. package/dist/core/telemetry.js +383 -0
  69. package/dist/core/template.d.ts +2 -3
  70. package/dist/core/template.js +2 -3
  71. package/dist/core/testing.d.ts +170 -1
  72. package/dist/core/testing.js +504 -2
  73. package/dist/db/d1.js +13 -0
  74. package/dist/db/pg.d.ts +13 -0
  75. package/dist/db/pg.js +46 -4
  76. package/dist/mcp/server.d.ts +19 -0
  77. package/dist/mcp/server.js +355 -0
  78. package/dist/openapi/config.d.ts +28 -0
  79. package/dist/openapi/config.js +25 -0
  80. package/dist/openapi/doc.d.ts +40 -0
  81. package/dist/openapi/doc.js +20 -0
  82. package/dist/openapi/export.d.ts +8 -0
  83. package/dist/openapi/export.js +19 -0
  84. package/dist/openapi/gate.d.ts +15 -0
  85. package/dist/openapi/gate.js +27 -0
  86. package/dist/openapi/index.d.ts +19 -0
  87. package/dist/openapi/index.js +15 -0
  88. package/dist/openapi/openapi.config.stub +29 -0
  89. package/dist/openapi/provider.d.ts +18 -0
  90. package/dist/openapi/provider.js +35 -0
  91. package/dist/openapi/routes.d.ts +9 -0
  92. package/dist/openapi/routes.js +23 -0
  93. package/dist/openapi/spec.d.ts +23 -0
  94. package/dist/openapi/spec.js +132 -0
  95. package/dist/openapi/ui.d.ts +8 -0
  96. package/dist/openapi/ui.js +31 -0
  97. package/dist/openapi/zod.d.ts +12 -0
  98. package/dist/openapi/zod.js +46 -0
  99. package/dist/watch/config.d.ts +33 -0
  100. package/dist/watch/config.js +38 -0
  101. package/dist/watch/entry.d.ts +53 -0
  102. package/dist/watch/entry.js +105 -0
  103. package/dist/watch/gate.d.ts +20 -0
  104. package/dist/watch/gate.js +32 -0
  105. package/dist/watch/index.d.ts +21 -0
  106. package/dist/watch/index.js +17 -0
  107. package/dist/watch/migration.d.ts +7 -0
  108. package/dist/watch/migration.js +20 -0
  109. package/dist/watch/provider.d.ts +22 -0
  110. package/dist/watch/provider.js +58 -0
  111. package/dist/watch/prune.d.ts +11 -0
  112. package/dist/watch/prune.js +20 -0
  113. package/dist/watch/recorder.d.ts +24 -0
  114. package/dist/watch/recorder.js +39 -0
  115. package/dist/watch/routes.d.ts +13 -0
  116. package/dist/watch/routes.js +55 -0
  117. package/dist/watch/store.d.ts +54 -0
  118. package/dist/watch/store.js +158 -0
  119. package/dist/watch/ui/dist/watch.css +1 -0
  120. package/dist/watch/ui/dist/watch.js +555 -0
  121. package/dist/watch/ui-shell.d.ts +12 -0
  122. package/dist/watch/ui-shell.js +24 -0
  123. package/dist/watch/watch.config.stub +47 -0
  124. package/dist/watch/watchers.d.ts +12 -0
  125. package/dist/watch/watchers.js +156 -0
  126. package/docs/ai-manifest.json +3206 -0
  127. package/docs/ai.md +128 -0
  128. package/docs/api-resources.md +118 -0
  129. package/docs/architecture.md +331 -0
  130. package/docs/authentication.md +453 -0
  131. package/docs/authorization.md +167 -0
  132. package/docs/broadcasting.md +137 -0
  133. package/docs/broker.md +500 -0
  134. package/docs/cache.md +558 -0
  135. package/docs/configuration.md +385 -0
  136. package/docs/console.md +536 -0
  137. package/docs/container.md +467 -0
  138. package/docs/controllers.md +265 -0
  139. package/docs/cors.md +51 -0
  140. package/docs/database.md +631 -0
  141. package/docs/debugging.md +129 -0
  142. package/docs/decorators.md +127 -0
  143. package/docs/errors.md +395 -0
  144. package/docs/events.md +496 -0
  145. package/docs/examples/architecture-app.ts +27 -0
  146. package/docs/examples/authentication.ts +61 -0
  147. package/docs/examples/authorization.ts +79 -0
  148. package/docs/examples/broadcasting.ts +60 -0
  149. package/docs/examples/broker-cache-validate.ts +34 -0
  150. package/docs/examples/broker-fault-tolerance.ts +29 -0
  151. package/docs/examples/broker-middleware.ts +27 -0
  152. package/docs/examples/broker.ts +203 -0
  153. package/docs/examples/cache.ts +222 -0
  154. package/docs/examples/configuration.ts +121 -0
  155. package/docs/examples/console.ts +134 -0
  156. package/docs/examples/container.ts +134 -0
  157. package/docs/examples/controllers.ts +86 -0
  158. package/docs/examples/database.ts +208 -0
  159. package/docs/examples/debugging.ts +41 -0
  160. package/docs/examples/decorators.ts +40 -0
  161. package/docs/examples/errors.ts +121 -0
  162. package/docs/examples/events.ts +204 -0
  163. package/docs/examples/factories.ts +84 -0
  164. package/docs/examples/hashing.ts +71 -0
  165. package/docs/examples/health.ts +94 -0
  166. package/docs/examples/helpers.ts +171 -0
  167. package/docs/examples/hooks.ts +54 -0
  168. package/docs/examples/i18n.ts +117 -0
  169. package/docs/examples/inertia.ts +81 -0
  170. package/docs/examples/locks.ts +120 -0
  171. package/docs/examples/logger.ts +166 -0
  172. package/docs/examples/mail.ts +263 -0
  173. package/docs/examples/middleware.ts +119 -0
  174. package/docs/examples/migrations.ts +126 -0
  175. package/docs/examples/models.ts +239 -0
  176. package/docs/examples/notification.ts +124 -0
  177. package/docs/examples/pages.ts +82 -0
  178. package/docs/examples/providers.ts +123 -0
  179. package/docs/examples/queues.ts +254 -0
  180. package/docs/examples/rate-limiting.ts +42 -0
  181. package/docs/examples/redis.ts +99 -0
  182. package/docs/examples/request-response.ts +197 -0
  183. package/docs/examples/routing.ts +186 -0
  184. package/docs/examples/scheduling.ts +62 -0
  185. package/docs/examples/sessions.ts +102 -0
  186. package/docs/examples/static-files.ts +63 -0
  187. package/docs/examples/storage.ts +132 -0
  188. package/docs/examples/telemetry.ts +127 -0
  189. package/docs/examples/templates.ts +58 -0
  190. package/docs/examples/testing.ts +215 -0
  191. package/docs/examples/transformer.ts +141 -0
  192. package/docs/examples/transformers.ts +49 -0
  193. package/docs/examples/url-builder.ts +86 -0
  194. package/docs/examples/validation.ts +102 -0
  195. package/docs/examples/views.tsx +62 -0
  196. package/docs/examples/vite.ts +106 -0
  197. package/docs/factories.md +166 -0
  198. package/docs/getting-started.md +290 -0
  199. package/docs/hashing.md +259 -0
  200. package/docs/health.md +225 -0
  201. package/docs/helpers.md +347 -0
  202. package/docs/hono.md +186 -0
  203. package/docs/hooks.md +118 -0
  204. package/docs/i18n.md +302 -0
  205. package/docs/inertia.md +241 -0
  206. package/docs/locks.md +323 -0
  207. package/docs/logger.md +436 -0
  208. package/docs/mail.md +751 -0
  209. package/docs/middleware.md +425 -0
  210. package/docs/migrations.md +476 -0
  211. package/docs/models.md +810 -0
  212. package/docs/notifications.md +474 -0
  213. package/docs/openapi.md +111 -0
  214. package/docs/packages.md +118 -0
  215. package/docs/pages.md +217 -0
  216. package/docs/providers.md +363 -0
  217. package/docs/queues.md +679 -0
  218. package/docs/rate-limiting.md +155 -0
  219. package/docs/redis.md +178 -0
  220. package/docs/request-response.md +953 -0
  221. package/docs/routing.md +804 -0
  222. package/docs/scheduling.md +110 -0
  223. package/docs/security.md +85 -0
  224. package/docs/sessions.md +354 -0
  225. package/docs/social-auth.md +174 -0
  226. package/docs/static-files.md +211 -0
  227. package/docs/storage.md +452 -0
  228. package/docs/telemetry.md +263 -0
  229. package/docs/templates.md +314 -0
  230. package/docs/testing.md +376 -0
  231. package/docs/transformers.md +381 -0
  232. package/docs/url-builder.md +295 -0
  233. package/docs/validation.md +288 -0
  234. package/docs/views.md +267 -0
  235. package/docs/vite.md +434 -0
  236. package/docs/watch.md +118 -0
  237. package/llms-full.txt +19816 -0
  238. package/llms.txt +127 -0
  239. package/package.json +42 -7
@@ -0,0 +1,263 @@
1
+ # Telemetry
2
+
3
+ Distributed tracing — spans, W3C trace context, and an OTLP exporter — with **no
4
+ SDK**.
5
+
6
+ ```ts
7
+ import { setTelemetry, Tracer, otlpExporter, tracing, trace } from "@shaferllc/keel/core";
8
+
9
+ setTelemetry(
10
+ new Tracer({
11
+ serviceName: "api",
12
+ exporter: otlpExporter({ url: "http://localhost:4318/v1/traces" }),
13
+ sampleRatio: 0.1, // 10% of traces in production
14
+ }),
15
+ );
16
+
17
+ // in your HTTP kernel
18
+ this.use(tracing()); // a server span per request
19
+ ```
20
+
21
+ ```ts
22
+ // your own spans, anywhere
23
+ await trace("charge", async (span) => {
24
+ span.setAttributes({ "order.id": order.id });
25
+ await stripe.charge(order);
26
+ });
27
+ ```
28
+
29
+ ## Why there's no SDK here
30
+
31
+ The OpenTelemetry Node SDK is a large tree of packages that assumes a Node
32
+ process. What a trace actually **is**, though, is small: an id, a parent, a start
33
+ and an end, some attributes — and a documented JSON shape to POST them in.
34
+
35
+ That's what this is. It speaks **OTLP/HTTP over `fetch`**, so it runs on Workers as
36
+ happily as on Node, and any OTLP collector accepts it — Jaeger, Tempo, Honeycomb,
37
+ Grafana, Datadog. You don't get the SDK's auto-instrumentation of every library
38
+ under the sun; you get the part that matters, in about 400 lines you can read.
39
+
40
+ ## Spans
41
+
42
+ `trace(name, fn)` opens a span, runs your function inside it, and closes it — even
43
+ if the function throws, in which case the error is recorded on the span and
44
+ rethrown.
45
+
46
+ ```ts
47
+ const receipt = await trace("charge", async (span) => {
48
+ span.setAttributes({ "order.id": id, currency: "USD" });
49
+ span.addEvent("calling stripe");
50
+
51
+ return stripe.charge(id); // a throw here marks the span failed, then propagates
52
+ });
53
+ ```
54
+
55
+ **Spans nest automatically.** A `trace()` inside another `trace()` becomes its
56
+ child, sharing the trace id — you don't thread anything through:
57
+
58
+ ```ts
59
+ await trace("checkout", async () => {
60
+ await trace("reserve-stock", async () => { … }); // child
61
+ await trace("charge", async () => { … }); // sibling of the above
62
+ });
63
+ ```
64
+
65
+ That works across `await` boundaries, and across **concurrent** traces, because
66
+ the current span is tracked in `AsyncLocalStorage` rather than a global. Two
67
+ requests in flight at once don't get tangled.
68
+
69
+ From anywhere inside a span:
70
+
71
+ ```ts
72
+ setAttributes({ tenant: "acme" }); // add to the current span
73
+ addEvent("cache miss", { key }); // a timestamped annotation
74
+ currentSpan(); // the Span itself, or undefined
75
+ ```
76
+
77
+ All of these are **no-ops outside a span**, so instrumented code stays safe to call
78
+ from a script or a test.
79
+
80
+ ## HTTP requests
81
+
82
+ `tracing()` opens a **server span** per request, records the method, path, and
83
+ status, and closes it when the response is sent.
84
+
85
+ ```ts
86
+ this.use(tracing());
87
+ ```
88
+
89
+ A 5xx marks the span failed; a 404 doesn't — that's a valid answer, not a fault.
90
+
91
+ It also writes a `traceparent` header onto the **response**, so when a user says
92
+ "this page was slow", you can look up their exact trace.
93
+
94
+ `/health/*`, `/metrics`, and `/favicon.ico` are ignored by default — they're noise.
95
+ Change that with `ignore`, and name spans yourself with `name`:
96
+
97
+ ```ts
98
+ this.use(
99
+ tracing({
100
+ ignore: (path) => path.startsWith("/internal"),
101
+ name: (method, path) => `${method} ${path.replace(/\/\d+/, "/:id")}`,
102
+ }),
103
+ );
104
+ ```
105
+
106
+ ## Following a trace between services
107
+
108
+ This is the point of tracing: one id spanning every service a request touches.
109
+
110
+ **Incoming.** `tracing()` reads the caller's `traceparent` header and makes your
111
+ span a **child of theirs**, so both land in the same trace. A missing or malformed
112
+ header just starts a fresh trace — never an error.
113
+
114
+ **Outgoing.** `injectTraceContext()` puts the current context on your request
115
+ headers, so the service you call joins this trace instead of starting its own:
116
+
117
+ ```ts
118
+ await fetch(url, {
119
+ headers: injectTraceContext({ accept: "application/json" }),
120
+ });
121
+ ```
122
+
123
+ `parseTraceparent()` and `traceparent()` are the two halves on their own, if you
124
+ need to carry the context somewhere odd — a queue payload, say.
125
+
126
+ ## Connecting logs to traces
127
+
128
+ `traceIds()` returns the current `trace_id` and `span_id`. Bind them to your logger
129
+ and every line becomes a jumping-off point into the trace it came from:
130
+
131
+ ```ts
132
+ const log = logger().child(traceIds());
133
+ log.info("charging card", { orderId }); // carries trace_id + span_id
134
+ ```
135
+
136
+ ## Sampling
137
+
138
+ Recording every trace in production is expensive. `sampleRatio` records a fraction:
139
+
140
+ ```ts
141
+ new Tracer({ sampleRatio: 0.1 }); // 10%
142
+ ```
143
+
144
+ The decision is made **once, at the root span, and inherited by every child** —
145
+ because half a trace is worse than no trace. An unsampled span still runs (your
146
+ code is unaffected); it just isn't exported.
147
+
148
+ ## Exporters
149
+
150
+ | Exporter | Use |
151
+ |----------|-----|
152
+ | `otlpExporter({ url, headers, resource })` | Any OTLP/HTTP collector. Production. |
153
+ | `consoleExporter()` | Prints each span. Local development, no collector needed. |
154
+ | `MemoryExporter` | Collects spans in memory. Tests. |
155
+
156
+ ```ts
157
+ otlpExporter({
158
+ url: "https://api.honeycomb.io/v1/traces",
159
+ headers: { "x-honeycomb-team": env("HONEYCOMB_KEY") },
160
+ resource: { "service.name": "api", "deployment.environment": "prod" },
161
+ });
162
+ ```
163
+
164
+ Spans are **buffered** and sent in batches (100 by default, via `batchSize`).
165
+
166
+ **Flush before the process — or the isolate — goes away**, or the last few spans
167
+ die with it:
168
+
169
+ ```ts
170
+ onShutdown(() => flushTelemetry());
171
+ ```
172
+
173
+ On Workers, call it at the end of a request (ideally inside `waitUntil`).
174
+
175
+ ## Testing
176
+
177
+ `MemoryExporter` collects spans so you can assert on them:
178
+
179
+ ```ts
180
+ import { Tracer, MemoryExporter, setTelemetry, trace } from "@shaferllc/keel/core";
181
+
182
+ const exporter = new MemoryExporter();
183
+ setTelemetry(new Tracer({ exporter, batchSize: 1 })); // batchSize 1 = export immediately
184
+
185
+ await trace("charge", async (span) => span.setAttributes({ "order.id": 1 }));
186
+
187
+ const span = exporter.named("charge")[0];
188
+ assert.equal(span.status, "unset");
189
+ assert.equal(span.attributes["order.id"], 1);
190
+ ```
191
+
192
+ `exporter.trace(traceId)` returns every span in one trace; `exporter.clear()`
193
+ empties it between tests.
194
+
195
+ ---
196
+
197
+ ## API reference
198
+
199
+ ### `trace(name, fn, options?)`
200
+
201
+ `trace<T>(name: string, fn: (span: Span) => Promise<T> | T, options?: SpanOptions): Promise<T>`
202
+
203
+ Run `fn` inside a span. The span is current for the duration, ends when `fn`
204
+ settles, and records a throw before rethrowing it.
205
+
206
+ ### `currentSpan()` / `setAttributes(attrs)` / `addEvent(name, attrs?)`
207
+
208
+ Reach the span in scope. All no-ops outside one.
209
+
210
+ ### `traceIds()`
211
+
212
+ `traceIds(): { trace_id?: string; span_id?: string }` — the ids to hang on a log
213
+ line.
214
+
215
+ ### `tracing(options?)`
216
+
217
+ `tracing(options?: TracingOptions): MiddlewareHandler`
218
+
219
+ A server span per request. Options: `ignore(path)` (default: `/health`, `/metrics`,
220
+ `/favicon.ico`), `name(method, path)`.
221
+
222
+ ### `Tracer`
223
+
224
+ `new Tracer(options: TracerOptions)`
225
+
226
+ | Option | Meaning |
227
+ |--------|---------|
228
+ | `serviceName` | Added to every span as `service.name` |
229
+ | `exporter` | Where finished spans go. Omit and nothing is exported |
230
+ | `sampleRatio` | 0–1, decided once at the root. Default 1 |
231
+ | `enabled` | `false` turns tracing off |
232
+ | `resource` | Attributes describing the service, sent with each batch |
233
+ | `batchSize` | Export once this many spans are buffered. Default 100 |
234
+
235
+ Methods: `startSpan(name, options?)`, `trace(name, fn, options?)`, `flush()`.
236
+
237
+ ### `Span`
238
+
239
+ `setAttribute(k, v)` / `setAttributes(attrs)` / `addEvent(name, attrs?)` /
240
+ `setStatus(status, message?)` / `recordException(error)` / `end()`, plus a
241
+ `context` (`{ traceId, spanId, sampled }`).
242
+
243
+ ### `setTelemetry(tracer)` / `telemetry()` / `flushTelemetry()`
244
+
245
+ Register the active tracer, read it, and drain its buffer.
246
+
247
+ ### Trace context
248
+
249
+ `parseTraceparent(header)` → `SpanContext | null` (null on anything malformed).
250
+ `traceparent(context)` → the header string.
251
+ `injectTraceContext(headers?)` → headers with the current context added.
252
+
253
+ ### Exporters
254
+
255
+ `otlpExporter({ url, headers?, resource? })`, `consoleExporter()`, and
256
+ `MemoryExporter` (`.spans`, `.named(name)`, `.trace(traceId)`, `.clear()`).
257
+
258
+ ### Interfaces & types
259
+
260
+ `SpanData`, `SpanContext`, `SpanEvent`, `SpanKind`
261
+ (`internal | server | client | producer | consumer`), `SpanStatus`
262
+ (`unset | ok | error`), `SpanAttributes`, `SpanExporter`, `TracerOptions`,
263
+ `TracingOptions`, `OtlpOptions`.
@@ -0,0 +1,314 @@
1
+ # Templates
2
+
3
+ A string templating engine — `{{ }}` interpolation and `@`-prefixed tags for
4
+ logic, includes, layouts, and components. Reach for it when you want plain-text
5
+ templates instead of (or alongside) [JSX views](./views.md).
6
+
7
+ Unlike engines that compile a template to a function with `eval` /
8
+ `new Function`, Keel **interprets** templates against a small, safe expression
9
+ evaluator. No dynamic code generation, so the same templates run on Node **and**
10
+ on Cloudflare Workers (where `eval` is forbidden).
11
+
12
+ ## Rendering
13
+
14
+ Register a template by name, then render it with a state object:
15
+
16
+ ```ts
17
+ import { templates, render } from "@shaferllc/keel/core";
18
+
19
+ templates().register("greeting", "Hello, {{ name }}!");
20
+
21
+ await render("greeting", { name: "Ada" }); // "Hello, Ada!"
22
+ ```
23
+
24
+ `render()` returns a `Promise<string>` — hand it to a response or a [view](./views.md):
25
+
26
+ ```ts
27
+ import { html, render } from "@shaferllc/keel/core";
28
+ return html(await render("greeting", { name: "Ada" }));
29
+ ```
30
+
31
+ Register many at once (e.g. a Node loader reads `.html` files and passes them in):
32
+
33
+ ```ts
34
+ templates().registerAll({
35
+ layout: await readFile("views/layout.html", "utf8"),
36
+ home: await readFile("views/home.html", "utf8"),
37
+ });
38
+ ```
39
+
40
+ ## Interpolation
41
+
42
+ ```html
43
+ {{ user.name }} {{-- escaped: HTML-safe --}}
44
+ {{{ post.body }}} {{-- raw: unescaped, for trusted HTML --}}
45
+ {{-- this is a comment; it renders nothing --}}
46
+ ```
47
+
48
+ Escaped `{{ }}` is the default and encodes `& < > " '`. Use raw `{{{ }}}` only for
49
+ HTML you trust. A `null`/`undefined` value renders as an empty string.
50
+
51
+ ## Expressions
52
+
53
+ Interpolation and tag conditions accept a practical subset of JavaScript —
54
+ enough for real templates, without `eval`:
55
+
56
+ ```html
57
+ {{ user.name }} {{-- property + index access --}}
58
+ {{ items[0] }}
59
+ {{ title.toUpperCase() }} {{-- method calls --}}
60
+ {{ items.join(", ") }}
61
+ {{ price * qty }} {{-- + - * / % --}}
62
+ {{ n > 3 && n < 10 }} {{-- comparisons, && || ! ?? --}}
63
+ {{ admin ? "Admin" : "User" }} {{-- ternary --}}
64
+ {{ [1, 2, 3].length }} {{-- array / object literals --}}
65
+ {{ { role: "admin" }.role }}
66
+ ```
67
+
68
+ Not supported: assignment, arrow functions, and other statement-level JS. Keep
69
+ logic in your controller and pass results in as state.
70
+
71
+ ### Filters
72
+
73
+ Pipe a value through a filter with `|`:
74
+
75
+ ```html
76
+ {{ name | upper }}
77
+ {{ name | capitalize }}
78
+ {{ items | length }}
79
+ {{ price | currency("USD") }} {{-- filters take arguments --}}
80
+ ```
81
+
82
+ Built-in filters: `upper`, `lower`, `capitalize`, `json`, `length`. Register your
83
+ own on the engine:
84
+
85
+ ```ts
86
+ templates().filter("currency", (v, code) =>
87
+ new Intl.NumberFormat("en-US", { style: "currency", currency: String(code) }).format(Number(v)),
88
+ );
89
+ ```
90
+
91
+ ## Conditionals
92
+
93
+ ```html
94
+ @if(user.admin)
95
+ <span>Admin</span>
96
+ @elseif(user.member)
97
+ <span>Member</span>
98
+ @else
99
+ <span>Guest</span>
100
+ @end
101
+ ```
102
+
103
+ ## Loops
104
+
105
+ `@each` iterates arrays (or the values of an object). A `$loop` variable exposes
106
+ positional info, and you can capture the index:
107
+
108
+ ```html
109
+ <ul>
110
+ @each(post in posts)
111
+ <li>{{ $loop.iteration }}. {{ post.title }}</li>
112
+ @end
113
+ </ul>
114
+
115
+ @each(item, i in items)
116
+ {{ i }}: {{ item }}
117
+ @end
118
+ ```
119
+
120
+ `$loop` fields: `index` (0-based), `iteration` (1-based), `first`, `last`,
121
+ `count`, `even`, `odd`.
122
+
123
+ ## Partials
124
+
125
+ Pull one template into another with `@include` — it shares the current state:
126
+
127
+ ```html
128
+ {{-- list.html --}}
129
+ <ul>@each(item in items)@include("row")@end</ul>
130
+
131
+ {{-- row.html --}}
132
+ <li>{{ item }}</li>
133
+ ```
134
+
135
+ `@includeIf(condition, "name")` includes only when the condition is truthy.
136
+
137
+ ## Layouts
138
+
139
+ A page declares its layout and fills the layout's `@yield` slots with `@section`:
140
+
141
+ ```html
142
+ {{-- layout.html --}}
143
+ <!doctype html>
144
+ <title>@yield("title")Keel@end</title>
145
+ <body>@yield("body")@end</body>
146
+
147
+ {{-- page.html --}}
148
+ @layout("layout")
149
+ @section("title"){{ page.title }} · Keel@end
150
+ @section("body")<h1>{{ page.title }}</h1>@end
151
+ ```
152
+
153
+ `@yield("name") … @end` renders the matching section, falling back to the content
154
+ between `@yield` and `@end` when the page defines no such section.
155
+
156
+ ## Components
157
+
158
+ Components are reusable templates rendered with props and slots. The content
159
+ between `@component` and its `@end` becomes the `main` slot; `@slot("name")`
160
+ defines named slots. Inside the component, slots arrive as pre-rendered HTML
161
+ strings on a `slots` object:
162
+
163
+ ```html
164
+ {{-- card.html --}}
165
+ <div class="card">
166
+ <header>{{{ slots.header }}}</header>
167
+ <main>{{{ slots.main }}}</main>
168
+ <footer>{{ title }}</footer>
169
+ </div>
170
+
171
+ {{-- usage --}}
172
+ @component("card", { title: "Welcome" })
173
+ @slot("header")<h2>Hi</h2>@end
174
+ <p>Body content goes to the main slot.</p>
175
+ @end
176
+ ```
177
+
178
+ Props are any expression evaluating to an object; they become the component's
179
+ state (merged with globals).
180
+
181
+ ## Globals
182
+
183
+ Expose values or helpers to every template:
184
+
185
+ ```ts
186
+ templates()
187
+ .global("appName", "Keel")
188
+ .global("asset", (path: string) => `/static/${path}`);
189
+ ```
190
+
191
+ ```html
192
+ <title>{{ appName }}</title>
193
+ <img src="{{ asset('logo.svg') }}" />
194
+ ```
195
+
196
+ ## Debugging
197
+
198
+ `@dump(value)` renders a `<pre>` of the value's JSON — handy while building a
199
+ template.
200
+
201
+ ```html
202
+ @dump(user)
203
+ ```
204
+
205
+ ## Escaping & safety
206
+
207
+ - Escaped `{{ }}` encodes HTML; only use raw `{{{ }}}` for trusted content.
208
+ - The evaluator blocks access to `__proto__`, `constructor`, and `prototype`, so
209
+ template state can't be used to reach the prototype chain.
210
+ - There's no `eval`: a template can't execute arbitrary JavaScript, only the
211
+ expression subset above.
212
+
213
+ ## API reference
214
+
215
+ ### `templates()`
216
+
217
+ `templates(): TemplateEngine`
218
+
219
+ Returns the default engine — register templates, globals, and filters on it.
220
+
221
+ ```ts
222
+ templates().register("home", "…");
223
+ ```
224
+
225
+ **Notes:** module-global and shared. Swap it with `setTemplateEngine()` (e.g. for
226
+ an isolated engine in a test).
227
+
228
+ ### `render(name, state?)`
229
+
230
+ `render(name: string, state?: Record<string, unknown>): Promise<string>`
231
+
232
+ Renders a registered template on the default engine.
233
+
234
+ ```ts
235
+ await render("home", { user });
236
+ ```
237
+
238
+ **Notes:** throws if `name` isn't registered. Async because includes, components,
239
+ and layouts compose asynchronously.
240
+
241
+ ### `setTemplateEngine(engine)`
242
+
243
+ `setTemplateEngine(engine: TemplateEngine): TemplateEngine`
244
+
245
+ Replaces the default engine and returns it.
246
+
247
+ **Notes:** the last call wins; useful to reset state between tests.
248
+
249
+ ### `escapeHtml(value)`
250
+
251
+ `escapeHtml(value: unknown): string`
252
+
253
+ HTML-escapes a value (`& < > " '`); `null`/`undefined` become `""`. This is what
254
+ `{{ }}` uses internally.
255
+
256
+ ### `TemplateEngine`
257
+
258
+ The engine class. Construct your own for isolation, or use `templates()`.
259
+
260
+ #### `register(name, source)`
261
+
262
+ `register(name: string, source: string): this`
263
+
264
+ Parses and registers a template. Chainable.
265
+
266
+ **Notes:** parsing happens here, so a malformed template throws at registration,
267
+ not at render.
268
+
269
+ #### `registerAll(sources)`
270
+
271
+ `registerAll(sources: Record<string, string>): this`
272
+
273
+ Registers many templates at once from a `name → source` map.
274
+
275
+ #### `has(name)`
276
+
277
+ `has(name: string): boolean`
278
+
279
+ Whether a template is registered.
280
+
281
+ #### `global(name, value)`
282
+
283
+ `global(name: string, value: unknown): this`
284
+
285
+ Exposes a value or function to every template as a global variable.
286
+
287
+ #### `filter(name, fn)`
288
+
289
+ `filter(name: string, fn: Filter): this`
290
+
291
+ Registers a `{{ value | name }}` filter. `Filter` is
292
+ `(value: unknown, ...args: unknown[]) => unknown`.
293
+
294
+ #### `render(name, state?)`
295
+
296
+ `render(name: string, state?: Record<string, unknown>): Promise<string>`
297
+
298
+ Renders a registered template. Throws for an unknown template, tag, or filter.
299
+
300
+ ### Interfaces & types
301
+
302
+ #### `Filter`
303
+
304
+ `type Filter = (value: unknown, ...args: unknown[]) => unknown`
305
+
306
+ A pipe filter: receives the piped value plus any `filter(arg)` arguments, returns
307
+ the transformed value.
308
+
309
+ #### `RenderContext`
310
+
311
+ `interface RenderContext { sections: Record<string, string>; slots: Record<string, string> }`
312
+
313
+ Internal per-render state threaded through layouts and components — you won't
314
+ construct it directly.