@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.
- package/AGENTS.md +169 -0
- package/README.md +44 -6
- package/bin/keel-mcp.mjs +9 -0
- package/dist/api/api.config.stub +9 -0
- package/dist/api/config.d.ts +13 -0
- package/dist/api/config.js +14 -0
- package/dist/api/index.d.ts +16 -0
- package/dist/api/index.js +13 -0
- package/dist/api/provider.d.ts +10 -0
- package/dist/api/provider.js +17 -0
- package/dist/api/query.d.ts +35 -0
- package/dist/api/query.js +42 -0
- package/dist/api/resource.d.ts +91 -0
- package/dist/api/resource.js +188 -0
- package/dist/core/application.d.ts +5 -5
- package/dist/core/application.js +8 -2
- package/dist/core/cache.d.ts +81 -5
- package/dist/core/cache.js +188 -23
- package/dist/core/cli/stubs.d.ts +26 -0
- package/dist/core/cli/stubs.js +225 -0
- package/dist/core/console-prompt.d.ts +79 -0
- package/dist/core/console-prompt.js +239 -0
- package/dist/core/console-ui.d.ts +96 -0
- package/dist/core/console-ui.js +187 -0
- package/dist/core/console.d.ts +188 -0
- package/dist/core/console.js +395 -0
- package/dist/core/database.d.ts +70 -1
- package/dist/core/database.js +174 -15
- package/dist/core/env.d.ts +96 -0
- package/dist/core/env.js +140 -0
- package/dist/core/events.d.ts +129 -5
- package/dist/core/events.js +165 -7
- package/dist/core/health.d.ts +141 -0
- package/dist/core/health.js +226 -0
- package/dist/core/helpers.d.ts +9 -3
- package/dist/core/helpers.js +11 -3
- package/dist/core/http/kernel.d.ts +2 -0
- package/dist/core/http/kernel.js +48 -0
- package/dist/core/http/router.d.ts +5 -5
- package/dist/core/http/router.js +5 -5
- package/dist/core/i18n.d.ts +162 -0
- package/dist/core/i18n.js +472 -0
- package/dist/core/index.d.ts +39 -13
- package/dist/core/index.js +19 -6
- package/dist/core/instrumentation.d.ts +113 -0
- package/dist/core/instrumentation.js +52 -0
- package/dist/core/lock.d.ts +139 -0
- package/dist/core/lock.js +215 -0
- package/dist/core/logger.d.ts +89 -4
- package/dist/core/logger.js +167 -22
- package/dist/core/mail.d.ts +128 -7
- package/dist/core/mail.js +264 -16
- package/dist/core/notification.js +10 -1
- package/dist/core/package.d.ts +120 -0
- package/dist/core/package.js +169 -0
- package/dist/core/pages.d.ts +108 -0
- package/dist/core/pages.js +199 -0
- package/dist/core/queue.d.ts +134 -9
- package/dist/core/queue.js +325 -14
- package/dist/core/repl.d.ts +33 -0
- package/dist/core/repl.js +88 -0
- package/dist/core/scheduler.js +6 -0
- package/dist/core/social.d.ts +4 -4
- package/dist/core/social.js +4 -4
- package/dist/core/storage.d.ts +159 -6
- package/dist/core/storage.js +299 -7
- package/dist/core/telemetry.d.ts +208 -0
- package/dist/core/telemetry.js +383 -0
- package/dist/core/template.d.ts +2 -3
- package/dist/core/template.js +2 -3
- package/dist/core/testing.d.ts +170 -1
- package/dist/core/testing.js +504 -2
- package/dist/db/d1.js +13 -0
- package/dist/db/pg.d.ts +13 -0
- package/dist/db/pg.js +46 -4
- package/dist/mcp/server.d.ts +19 -0
- package/dist/mcp/server.js +355 -0
- package/dist/openapi/config.d.ts +28 -0
- package/dist/openapi/config.js +25 -0
- package/dist/openapi/doc.d.ts +40 -0
- package/dist/openapi/doc.js +20 -0
- package/dist/openapi/export.d.ts +8 -0
- package/dist/openapi/export.js +19 -0
- package/dist/openapi/gate.d.ts +15 -0
- package/dist/openapi/gate.js +27 -0
- package/dist/openapi/index.d.ts +19 -0
- package/dist/openapi/index.js +15 -0
- package/dist/openapi/openapi.config.stub +29 -0
- package/dist/openapi/provider.d.ts +18 -0
- package/dist/openapi/provider.js +35 -0
- package/dist/openapi/routes.d.ts +9 -0
- package/dist/openapi/routes.js +23 -0
- package/dist/openapi/spec.d.ts +23 -0
- package/dist/openapi/spec.js +132 -0
- package/dist/openapi/ui.d.ts +8 -0
- package/dist/openapi/ui.js +31 -0
- package/dist/openapi/zod.d.ts +12 -0
- package/dist/openapi/zod.js +46 -0
- package/dist/watch/config.d.ts +33 -0
- package/dist/watch/config.js +38 -0
- package/dist/watch/entry.d.ts +53 -0
- package/dist/watch/entry.js +105 -0
- package/dist/watch/gate.d.ts +20 -0
- package/dist/watch/gate.js +32 -0
- package/dist/watch/index.d.ts +21 -0
- package/dist/watch/index.js +17 -0
- package/dist/watch/migration.d.ts +7 -0
- package/dist/watch/migration.js +20 -0
- package/dist/watch/provider.d.ts +22 -0
- package/dist/watch/provider.js +58 -0
- package/dist/watch/prune.d.ts +11 -0
- package/dist/watch/prune.js +20 -0
- package/dist/watch/recorder.d.ts +24 -0
- package/dist/watch/recorder.js +39 -0
- package/dist/watch/routes.d.ts +13 -0
- package/dist/watch/routes.js +55 -0
- package/dist/watch/store.d.ts +54 -0
- package/dist/watch/store.js +158 -0
- package/dist/watch/ui/dist/watch.css +1 -0
- package/dist/watch/ui/dist/watch.js +555 -0
- package/dist/watch/ui-shell.d.ts +12 -0
- package/dist/watch/ui-shell.js +24 -0
- package/dist/watch/watch.config.stub +47 -0
- package/dist/watch/watchers.d.ts +12 -0
- package/dist/watch/watchers.js +156 -0
- package/docs/ai-manifest.json +3206 -0
- package/docs/ai.md +128 -0
- package/docs/api-resources.md +118 -0
- package/docs/architecture.md +331 -0
- package/docs/authentication.md +453 -0
- package/docs/authorization.md +167 -0
- package/docs/broadcasting.md +137 -0
- package/docs/broker.md +500 -0
- package/docs/cache.md +558 -0
- package/docs/configuration.md +385 -0
- package/docs/console.md +536 -0
- package/docs/container.md +467 -0
- package/docs/controllers.md +265 -0
- package/docs/cors.md +51 -0
- package/docs/database.md +631 -0
- package/docs/debugging.md +129 -0
- package/docs/decorators.md +127 -0
- package/docs/errors.md +395 -0
- package/docs/events.md +496 -0
- package/docs/examples/architecture-app.ts +27 -0
- package/docs/examples/authentication.ts +61 -0
- package/docs/examples/authorization.ts +79 -0
- package/docs/examples/broadcasting.ts +60 -0
- package/docs/examples/broker-cache-validate.ts +34 -0
- package/docs/examples/broker-fault-tolerance.ts +29 -0
- package/docs/examples/broker-middleware.ts +27 -0
- package/docs/examples/broker.ts +203 -0
- package/docs/examples/cache.ts +222 -0
- package/docs/examples/configuration.ts +121 -0
- package/docs/examples/console.ts +134 -0
- package/docs/examples/container.ts +134 -0
- package/docs/examples/controllers.ts +86 -0
- package/docs/examples/database.ts +208 -0
- package/docs/examples/debugging.ts +41 -0
- package/docs/examples/decorators.ts +40 -0
- package/docs/examples/errors.ts +121 -0
- package/docs/examples/events.ts +204 -0
- package/docs/examples/factories.ts +84 -0
- package/docs/examples/hashing.ts +71 -0
- package/docs/examples/health.ts +94 -0
- package/docs/examples/helpers.ts +171 -0
- package/docs/examples/hooks.ts +54 -0
- package/docs/examples/i18n.ts +117 -0
- package/docs/examples/inertia.ts +81 -0
- package/docs/examples/locks.ts +120 -0
- package/docs/examples/logger.ts +166 -0
- package/docs/examples/mail.ts +263 -0
- package/docs/examples/middleware.ts +119 -0
- package/docs/examples/migrations.ts +126 -0
- package/docs/examples/models.ts +239 -0
- package/docs/examples/notification.ts +124 -0
- package/docs/examples/pages.ts +82 -0
- package/docs/examples/providers.ts +123 -0
- package/docs/examples/queues.ts +254 -0
- package/docs/examples/rate-limiting.ts +42 -0
- package/docs/examples/redis.ts +99 -0
- package/docs/examples/request-response.ts +197 -0
- package/docs/examples/routing.ts +186 -0
- package/docs/examples/scheduling.ts +62 -0
- package/docs/examples/sessions.ts +102 -0
- package/docs/examples/static-files.ts +63 -0
- package/docs/examples/storage.ts +132 -0
- package/docs/examples/telemetry.ts +127 -0
- package/docs/examples/templates.ts +58 -0
- package/docs/examples/testing.ts +215 -0
- package/docs/examples/transformer.ts +141 -0
- package/docs/examples/transformers.ts +49 -0
- package/docs/examples/url-builder.ts +86 -0
- package/docs/examples/validation.ts +102 -0
- package/docs/examples/views.tsx +62 -0
- package/docs/examples/vite.ts +106 -0
- package/docs/factories.md +166 -0
- package/docs/getting-started.md +290 -0
- package/docs/hashing.md +259 -0
- package/docs/health.md +225 -0
- package/docs/helpers.md +347 -0
- package/docs/hono.md +186 -0
- package/docs/hooks.md +118 -0
- package/docs/i18n.md +302 -0
- package/docs/inertia.md +241 -0
- package/docs/locks.md +323 -0
- package/docs/logger.md +436 -0
- package/docs/mail.md +751 -0
- package/docs/middleware.md +425 -0
- package/docs/migrations.md +476 -0
- package/docs/models.md +810 -0
- package/docs/notifications.md +474 -0
- package/docs/openapi.md +111 -0
- package/docs/packages.md +118 -0
- package/docs/pages.md +217 -0
- package/docs/providers.md +363 -0
- package/docs/queues.md +679 -0
- package/docs/rate-limiting.md +155 -0
- package/docs/redis.md +178 -0
- package/docs/request-response.md +953 -0
- package/docs/routing.md +804 -0
- package/docs/scheduling.md +110 -0
- package/docs/security.md +85 -0
- package/docs/sessions.md +354 -0
- package/docs/social-auth.md +174 -0
- package/docs/static-files.md +211 -0
- package/docs/storage.md +452 -0
- package/docs/telemetry.md +263 -0
- package/docs/templates.md +314 -0
- package/docs/testing.md +376 -0
- package/docs/transformers.md +381 -0
- package/docs/url-builder.md +295 -0
- package/docs/validation.md +288 -0
- package/docs/views.md +267 -0
- package/docs/vite.md +434 -0
- package/docs/watch.md +118 -0
- package/llms-full.txt +19816 -0
- package/llms.txt +127 -0
- 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.
|