@warlock.js/core 5.14.0 → 5.16.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/CHANGELOG.md +22 -0
- package/esm/application/application-config-types.d.mts +9 -0
- package/esm/application/application-config-types.d.mts.map +1 -1
- package/esm/application/index.d.mts +2 -1
- package/esm/application/index.mjs +1 -0
- package/esm/application/public-url.d.mts +20 -0
- package/esm/application/public-url.d.mts.map +1 -0
- package/esm/application/public-url.mjs +26 -0
- package/esm/application/public-url.mjs.map +1 -0
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs +118 -0
- package/esm/generations/features/shared/merge-web-sitemap-config.mjs.map +1 -0
- package/esm/generations/features/sitemap.feature.mjs +77 -0
- package/esm/generations/features/sitemap.feature.mjs.map +1 -0
- package/esm/http/errors/errors.d.mts +16 -1
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +19 -1
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +2 -1
- package/esm/http/index.mjs +1 -1
- package/esm/http/parse-urlencoded-body.mjs +27 -0
- package/esm/http/parse-urlencoded-body.mjs.map +1 -0
- package/esm/http/plugins.d.mts.map +1 -1
- package/esm/http/plugins.mjs +8 -0
- package/esm/http/plugins.mjs.map +1 -1
- package/esm/http/request.d.mts +8 -0
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +13 -1
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts +9 -2
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +12 -4
- package/esm/http/response.mjs.map +1 -1
- package/esm/http/xmlable.d.mts +14 -0
- package/esm/http/xmlable.d.mts.map +1 -0
- package/esm/index.d.mts +4 -2
- package/esm/index.mjs +3 -2
- package/llms-full.txt +74 -3
- package/llms.txt +2 -2
- package/package.json +11 -11
- package/skills/configure-app/SKILL.md +29 -2
- package/skills/create-controller/SKILL.md +14 -0
- package/skills/health-checks/SKILL.md +8 -0
- package/skills/send-response/SKILL.md +17 -1
- package/skills/use-localization/SKILL.md +6 -0
package/llms-full.txt
CHANGED
|
@@ -964,7 +964,7 @@ Don't call `setBaseUrl` per request — it's process-global and races every othe
|
|
|
964
964
|
|
|
965
965
|
---
|
|
966
966
|
name: configure-app
|
|
967
|
-
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`,
|
|
967
|
+
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
|
|
968
968
|
---
|
|
969
969
|
|
|
970
970
|
# Warlock — configure the app
|
|
@@ -1069,7 +1069,7 @@ same nonce reaches the page's `<script>` tags.
|
|
|
1069
1069
|
| CLI commands (registered via `warlock <cmd>`) | `warlock.config.ts > cli` |
|
|
1070
1070
|
| HTTP server tuning per env (port, host, retry) | `warlock.config.ts > server` |
|
|
1071
1071
|
| HTTP runtime (CORS, cookies, rate limits, upload size) | `src/config/http.ts` |
|
|
1072
|
-
| App identity (name, baseUrl, timezone, locales)
|
|
1072
|
+
| App identity (name, baseUrl, publicUrl, timezone, locales) | `src/config/app.ts` |
|
|
1073
1073
|
| Subsystem configs (auth, mail, storage, cache, ai, …) | `src/config/<name>.ts` |
|
|
1074
1074
|
|
|
1075
1075
|
Heuristic: if the setting changes how the framework **boots, builds, or scaffolds**, it goes in `warlock.config.ts`. If it changes how a **subsystem behaves at runtime**, it goes in `src/config/`.
|
|
@@ -1203,6 +1203,33 @@ const config = {
|
|
|
1203
1203
|
|
|
1204
1204
|
Avoid scattering `process.env.NODE_ENV === "production"` checks — they don't get the same default-handling.
|
|
1205
1205
|
|
|
1206
|
+
### `app.publicUrl` — the app's public origin (5.15.0)
|
|
1207
|
+
|
|
1208
|
+
```ts title="src/config/app.ts"
|
|
1209
|
+
import type { AppConfigurations } from "@warlock.js/core";
|
|
1210
|
+
|
|
1211
|
+
const appConfigurations: AppConfigurations = {
|
|
1212
|
+
appName: "My App",
|
|
1213
|
+
publicUrl: "https://example.com",
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
export default appConfigurations;
|
|
1217
|
+
```
|
|
1218
|
+
|
|
1219
|
+
The one absolute-URL source every consumer that needs one — the sitemap
|
|
1220
|
+
route, canonical links, OG tags, absolute URLs in mail — reads instead of
|
|
1221
|
+
keeping its own copy. Optional in general (most apps have no consumer that
|
|
1222
|
+
needs it yet); read it with `getPublicUrl()`, which returns `app.publicUrl`,
|
|
1223
|
+
falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
|
|
1224
|
+
set.
|
|
1225
|
+
|
|
1226
|
+
`getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
|
|
1227
|
+
requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
|
|
1228
|
+
with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
|
|
1229
|
+
generation refuses (`MissingPublicUrlError`, naming both) rather than falling
|
|
1230
|
+
back to a request-derived host — a sitemap pointing at the wrong host is
|
|
1231
|
+
worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
|
|
1232
|
+
|
|
1206
1233
|
## Common patterns
|
|
1207
1234
|
|
|
1208
1235
|
### Adding a new subsystem
|
|
@@ -1282,6 +1309,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
|
|
|
1282
1309
|
|
|
1283
1310
|
Prefer `request.validated()` once a schema is attached — it's typed.
|
|
1284
1311
|
|
|
1312
|
+
### Request body content types
|
|
1313
|
+
|
|
1314
|
+
`request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
|
|
1315
|
+
|
|
1316
|
+
| Content type | Parsed by | Notes |
|
|
1317
|
+
| ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
|
|
1318
|
+
| `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
|
|
1319
|
+
| `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
|
|
1320
|
+
| `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
|
|
1321
|
+
|
|
1322
|
+
For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
|
|
1323
|
+
|
|
1324
|
+
All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
|
|
1325
|
+
|
|
1285
1326
|
## Returning output
|
|
1286
1327
|
|
|
1287
1328
|
Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
|
|
@@ -2389,6 +2430,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
|
|
|
2389
2430
|
### Config
|
|
2390
2431
|
|
|
2391
2432
|
```ts title="src/config/http.ts"
|
|
2433
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
2434
|
+
|
|
2392
2435
|
const httpConfigurations: HttpConfigurations = {
|
|
2393
2436
|
health: {
|
|
2394
2437
|
enabled: true, // default; set false to remove both endpoints
|
|
@@ -2396,6 +2439,8 @@ const httpConfigurations: HttpConfigurations = {
|
|
|
2396
2439
|
readinessPath: "/ready", // readiness path
|
|
2397
2440
|
},
|
|
2398
2441
|
};
|
|
2442
|
+
|
|
2443
|
+
export default httpConfigurations;
|
|
2399
2444
|
```
|
|
2400
2445
|
|
|
2401
2446
|
## Readiness checks
|
|
@@ -2436,12 +2481,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
|
|
|
2436
2481
|
3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
|
|
2437
2482
|
|
|
2438
2483
|
```ts title="src/config/http.ts"
|
|
2484
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
2485
|
+
|
|
2439
2486
|
const httpConfigurations: HttpConfigurations = {
|
|
2440
2487
|
gracefulShutdown: {
|
|
2441
2488
|
timeout: 10_000, // ms to wait for in-flight drain (default 10s)
|
|
2442
2489
|
forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
|
|
2443
2490
|
},
|
|
2444
2491
|
};
|
|
2492
|
+
|
|
2493
|
+
export default httpConfigurations;
|
|
2445
2494
|
```
|
|
2446
2495
|
|
|
2447
2496
|
`forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
|
|
@@ -4378,7 +4427,7 @@ await Mail.to(user.email)
|
|
|
4378
4427
|
|
|
4379
4428
|
---
|
|
4380
4429
|
name: send-response
|
|
4381
|
-
description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
|
|
4430
|
+
description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
|
|
4382
4431
|
---
|
|
4383
4432
|
|
|
4384
4433
|
# Warlock — send a response
|
|
@@ -4462,6 +4511,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
|
|
|
4462
4511
|
|
|
4463
4512
|
`SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
|
|
4464
4513
|
|
|
4514
|
+
`response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
|
|
4515
|
+
|
|
4465
4516
|
## Streams
|
|
4466
4517
|
|
|
4467
4518
|
```ts
|
|
@@ -4580,6 +4631,20 @@ Every response cookie gets `httpOnly: true`, `sameSite: "lax"`, and `secure: tru
|
|
|
4580
4631
|
|
|
4581
4632
|
Precedence, lowest to highest: **framework defaults → `http.cookies.options` → the per-call `options` argument.** Set an app-wide policy in config, override per cookie when a specific one genuinely needs different treatment.
|
|
4582
4633
|
|
|
4634
|
+
### Reading cookies back — `request.cookie()` / `request.hasCookie()` throw when the jar is unavailable (5.15.0)
|
|
4635
|
+
|
|
4636
|
+
`request.cookie(name)` and `request.hasCookie(name)` are a deliberate
|
|
4637
|
+
by-name assertion — "this cookie should be readable here" — so when
|
|
4638
|
+
`@fastify/cookie` is not registered on the Fastify instance, both now
|
|
4639
|
+
**throw `CookieJarUnavailableError`** naming the missing cookie, instead of
|
|
4640
|
+
silently returning `undefined` / `false`. Register the plugin (see core's
|
|
4641
|
+
`http/plugins.ts`) before reading cookies by name.
|
|
4642
|
+
|
|
4643
|
+
`request.cookies` (the plain getter, no by-name assertion) is unchanged and
|
|
4644
|
+
stays lenient — it returns `{}` when the jar is unavailable, because the
|
|
4645
|
+
framework's own opportunistic reads (e.g. locale resolution) must not throw
|
|
4646
|
+
on a request that simply has no jar.
|
|
4647
|
+
|
|
4583
4648
|
These are the flags whose absence never fails a test and is fatal in production: the app works perfectly and is simply insecure. Opting out is now explicit and visible in review.
|
|
4584
4649
|
|
|
4585
4650
|
These mutate the response in place; chain or call before the final `return response.<helper>()`.
|
|
@@ -6656,6 +6721,12 @@ All three lookups go through `@mongez/localization`'s `trans()` under the hood,
|
|
|
6656
6721
|
|
|
6657
6722
|
When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic groups/keys and placeholders are not inferred.
|
|
6658
6723
|
|
|
6724
|
+
`useTrans()` now works correctly across hydration (5.15.0): the hydration
|
|
6725
|
+
payload ships a `translations` key with the active locale's keywords, and
|
|
6726
|
+
`@warlock.js/web` registers them into this same lookup table before the
|
|
6727
|
+
client hydrates — see `@warlock.js/web/write-the-root/SKILL.md`'s "`useTrans()`
|
|
6728
|
+
survives hydration" section for the failure this fixed.
|
|
6729
|
+
|
|
6659
6730
|
### Locale on a specific lookup
|
|
6660
6731
|
|
|
6661
6732
|
```ts
|
package/llms.txt
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
- [benchmark-code](@warlock.js/core/benchmark-code/SKILL.md): Wrap a function with `measure(name, fn, options?)` to time it and classify the latency — onComplete/onError/onFinish hooks, `latencyRange` thresholds, `BenchmarkProfiler` for percentiles, `BenchmarkSnapshots` for raw captures. Triggers: `measure`, `BenchmarkProfiler`, `BenchmarkSnapshots`, `BenchmarkChannel`, `ConsoleChannel`, `latencyRange`, `shouldBenchmarkError`; "time this operation", "profile a slow service", "emit p50/p95/p99 metrics", "classify latency against thresholds"; typical import `import { measure, BenchmarkProfiler } from "@warlock.js/core"`. Skip: retry composition — `@warlock.js/core/retry-operation/SKILL.md`; benchmark config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `prom-client`, `pino`, `perf_hooks`, `console.time`.
|
|
11
11
|
- [build-restful](@warlock.js/core/build-restful/SKILL.md): Generate standard CRUD endpoints — via `router.route(path).list().show().create().update().destroy()` chain or the `Restful` base class. Pick the chain by default; reach for `Restful` when you want repository-bound defaults. Triggers: `router.route`, `Restful`, `router.restfulResource`, `RouteResource`, `.crud`, `.nest`, `beforeCreate`, `onCreate`; "build a CRUD API", "register list/show/create/update/destroy", "repository-bound default handlers", "override a single REST action"; typical import `import { router, Restful } from "@warlock.js/core"`. Skip: wider router surface — `@warlock.js/core/register-route/SKILL.md`; per-action controllers — `@warlock.js/core/create-controller/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing pattern: hand-rolled controllers, `@nestjs/swagger` decorator-driven CRUD.
|
|
12
12
|
- [build-url](@warlock.js/core/build-url/SKILL.md): HTTP URL helpers — `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, anchored at `app.baseUrl`. Use to render `src` / `href` / API URLs in resources and responses. `setBaseUrl` is wired by the HTTP connector from `config.get("app.baseUrl")`. Triggers: `url`, `publicUrl`, `assetsUrl`, `uploadsUrl`, `setBaseUrl`, `BASE_URL`; "render an avatar src URL", "absolute download link", "embed asset URL in email", "URL helpers vs path helpers"; typical import `import { url, publicUrl, uploadsUrl } from "@warlock.js/core"`. Skip: filesystem paths — `@warlock.js/core/resolve-path/SKILL.md`; signed CDN URLs — `@warlock.js/core/store-file/SKILL.md`; resource output — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: hand-rolled `${baseUrl}/...` template strings.
|
|
13
|
-
- [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`,
|
|
13
|
+
- [configure-app](@warlock.js/core/configure-app/SKILL.md): Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app's public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.
|
|
14
14
|
- [create-controller](@warlock.js/core/create-controller/SKILL.md): Author HTTP controllers in @warlock.js/core — RequestHandler signature, validated input via seal schemas, response helpers, attaching metadata. Controllers are thin functions; business logic moves to services or use-cases. Triggers: `RequestHandler`, `Request<TSchema>`, `GuardedRequestHandler`, `request.validated`, `request.input`, `controller.validation`, `response.success`, `response.successCreate`; "write a controller", "attach a schema to a handler", "thin controller pattern", "guarded request type"; typical import `import { type RequestHandler } from "@warlock.js/core"`. Skip: response helper menu — `@warlock.js/core/send-response/SKILL.md`; schema authoring — `@warlock.js/core/validate-input/SKILL.md`; URL wiring — `@warlock.js/core/register-route/SKILL.md`; competing patterns: `express` middleware functions, `@nestjs/common` `@Controller`/`@Get` decorators.
|
|
15
15
|
- [create-module](@warlock.js/core/create-module/SKILL.md): Scaffold a new feature module under `src/app/<name>/` via `warlock generate.module` and the follow-up generators for controllers, models, repositories, resources, and validation schemas. Triggers: `warlock generate.module`, `generate.controller`, `generate.service`, `generate.model`, `generate.repository`, `generate.resource`, `generate.migration`, `--minimal`, `gen.m`; "scaffold a new module", "create CRUD bootstrap", "add a controller to a module", "generate a model"; typical CLI `npx warlock generate.module <name>`. Skip: framework-wide layout rules — `@warlock.js/core/warlock-conventions/SKILL.md`; routes file shape — `@warlock.js/core/register-route/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing tooling: `@nestjs/cli`, `hygen`, hand-rolled folder layouts.
|
|
16
16
|
- [define-resource](@warlock.js/core/define-resource/SKILL.md): Map model fields to wire-shape via `defineResource()` or `Resource` subclasses. Output-only — never put business logic, hydration, or reconciliation in a resource. Triggers: `defineResource`, `Resource`, `RegisterResource`, `toJSON`, `"self"`, `"localized"`, `"uploadsUrl"`; "shape an API response", "nest related resources", "rename a field on output", "self-referential tree resource"; typical import `import { defineResource } from "@warlock.js/core"`. Skip: localized columns — `@warlock.js/core/use-localization/SKILL.md`; URL casting — `@warlock.js/core/build-url/SKILL.md`; controller side — `@warlock.js/core/create-controller/SKILL.md`; competing libs `@nestjs/swagger` `@ApiProperty`, `class-transformer`, hand-rolled DTO mappers.
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- [retry-operation](@warlock.js/core/retry-operation/SKILL.md): Wrap a flaky operation with `retry(fn, options)` — now provided by `@mongez/reinforcements` (not `@warlock.js/core`). `attempts` total tries, `delay` + `backoff` (linear/exponential/fn), `maxDelay`, `jitter`, `shouldRetry` to bail on permanent errors, `signal` to cancel, plus `retryable()` to pre-bind options. Triggers: `retry`, `retryable`, `RetryOptions`, `attempts`, `backoff`, `jitter`, `maxDelay`, `shouldRetry`, `signal`; "retry a flaky API call", "handle transient errors", "exponential backoff with jitter", "wrap an external request"; typical import `import { retry } from "@mongez/reinforcements"`. Skip: timing the retried op — `@warlock.js/core/benchmark-code/SKILL.md`; use-case-level `retry` option — `@warlock.js/core/write-use-case/SKILL.md`; competing libs `p-retry`, `async-retry`, `cockatiel`.
|
|
27
27
|
- [run-app](@warlock.js/core/run-app/SKILL.md): Three operational commands — `warlock dev` (HMR + type-gen + health checks), `warlock build` (esbuild bundle), `warlock start` (spawn the production bundle). All flags, all `warlock.config.ts` knobs that shape them. Triggers: `warlock dev`, `warlock build`, `warlock start`, `devServer`, `--fresh`, `--skip-typings`, `--skip-health`, `outdir`, `outFile`, `sourcemap`, `PortInUseError`, `assertPortIsAvailable`, `EADDRINUSE`, `EsbuildBinaryMissingError`; "start the dev server", "build for production", "run the bundle", "skip type generation", "tune watch globs", "dev server keyboard shortcuts", "press r to restart", "press q to quit", "restart the dev server", "port already in use"; typical config `warlock.config.ts > devServer / build`. Skip: writing a custom CLI — `@warlock.js/core/write-cli-command/SKILL.md`; config shape — `@warlock.js/core/configure-app/SKILL.md`; competing tooling `nodemon`, `tsx`, `ts-node-dev`, `esbuild` direct.
|
|
28
28
|
- [send-mail](@warlock.js/core/send-mail/SKILL.md): Send transactional email — `Mail` fluent builder, `sendMail()` direct call, React Email components. Test mode auto-captures into an in-memory mailbox; dev mode logs. Triggers: `Mail.to`, `sendMail`, `setMailMode`, `mailEvents`, `assertMailSent`, `getTestMailbox`, `wasMailSentTo`, `closeAllMailers`; "send a transactional email", "build a React Email template", "configure SMTP or SES", "assert an email was sent in tests"; typical import `import { Mail, sendMail } from "@warlock.js/core"`. Skip: per-config wiring — `@warlock.js/core/configure-app/SKILL.md`; layered service patterns — `@warlock.js/core/warlock-conventions/SKILL.md`; competing libs `nodemailer` direct, `@sendgrid/mail`, `resend`, `mailgun.js`.
|
|
29
|
-
- [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
|
|
29
|
+
- [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
|
|
30
30
|
- [store-file](@warlock.js/core/store-file/SKILL.md): Read/write/delete files via the `storage` singleton — disks, drivers (local/S3/R2/DO Spaces), `storage.use(name)`, `StorageFile` handles, presigned URLs. Triggers: `storage.put`, `storage.get`, `storage.use`, `StorageFile`, `storageConfigurations`, `getPresignedUrl`, `getPresignedUploadUrl`; "save an uploaded file", "switch between local and S3", "generate a presigned URL", "read file metadata"; typical import `import { storage } from "@warlock.js/core"`. Skip: multipart parsing + image chain — `@warlock.js/core/upload-file/SKILL.md`; image transforms — `@warlock.js/core/process-image/SKILL.md`; storage config shape — `@warlock.js/core/configure-app/SKILL.md`; competing libs `@aws-sdk/client-s3`, `multer`, `formidable`.
|
|
31
31
|
- [test-http](@warlock.js/core/test-http/SKILL.md): Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core/tests"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.
|
|
32
32
|
- [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps the framework with its own DB/cache connections so you can call your code directly, and `teardownTest()` closes it. Triggers: `setupTest`, `teardownTest`, `src/test-setup.ts`, `tests.connectors`, `tests.setupTimeout`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest, teardownTest } from "@warlock.js/core/tests"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.
|
package/package.json
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.4",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/cache": "5.
|
|
29
|
-
"@warlock.js/cascade": "5.
|
|
30
|
-
"@warlock.js/context": "5.
|
|
31
|
-
"@warlock.js/logger": "5.
|
|
32
|
-
"@warlock.js/seal": "5.
|
|
33
|
-
"@warlock.js/fs": "5.
|
|
28
|
+
"@warlock.js/cache": "5.16.0",
|
|
29
|
+
"@warlock.js/cascade": "5.16.0",
|
|
30
|
+
"@warlock.js/context": "5.16.0",
|
|
31
|
+
"@warlock.js/logger": "5.16.0",
|
|
32
|
+
"@warlock.js/seal": "5.16.0",
|
|
33
|
+
"@warlock.js/fs": "5.16.0",
|
|
34
34
|
"chokidar": "^5.0.0",
|
|
35
35
|
"dayjs": "^1.11.19",
|
|
36
36
|
"es-module-lexer": "^2.0.0",
|
|
@@ -56,10 +56,10 @@
|
|
|
56
56
|
"react": "^19.2.3",
|
|
57
57
|
"react-dom": "^19.2.3",
|
|
58
58
|
"@react-email/render": "^2.0.5",
|
|
59
|
-
"@warlock.js/herald": "5.
|
|
60
|
-
"@warlock.js/ai": "5.
|
|
61
|
-
"@warlock.js/access": "5.
|
|
62
|
-
"@warlock.js/notifications": "5.
|
|
59
|
+
"@warlock.js/herald": "5.16.0",
|
|
60
|
+
"@warlock.js/ai": "5.16.0",
|
|
61
|
+
"@warlock.js/access": "5.16.0",
|
|
62
|
+
"@warlock.js/notifications": "5.16.0"
|
|
63
63
|
},
|
|
64
64
|
"peerDependenciesMeta": {
|
|
65
65
|
"sharp": {
|
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
],
|
|
123
123
|
"author": "hassanzohdy",
|
|
124
124
|
"license": "MIT",
|
|
125
|
-
"version": "5.
|
|
125
|
+
"version": "5.16.0",
|
|
126
126
|
"type": "module",
|
|
127
127
|
"main": "./esm/index.mjs",
|
|
128
128
|
"module": "./esm/index.mjs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: configure-app
|
|
3
|
-
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`,
|
|
3
|
+
description: 'Configure a Warlock app — the two layers (`warlock.config.ts` for framework-level wiring, `src/config/*.ts` for subsystems), `.env` + `env()`, the `config()` getter for runtime reads, and `app.publicUrl`/`PUBLIC_APP_URL` (the app''s public origin). Triggers: `defineConfig`, `config.get`, `config.key`, `env`, `ConfigRegistry`, `HttpConfigurations`, `AppConfigurations`, `publicUrl`, `PUBLIC_APP_URL`, `getPublicUrl`; "add a new config file", "warlock.config.ts vs src/config", "read env values", "runtime config lookup", "app public origin/URL"; typical import `import { defineConfig, config, env } from "@warlock.js/core"`. Skip: cache driver registration — `@warlock.js/cache/cache-basics/SKILL.md`; mail config — `@warlock.js/core/send-mail/SKILL.md`; storage config — `@warlock.js/core/store-file/SKILL.md`; sitemap refusal on a missing origin — `@warlock.js/web/generate-sitemap/SKILL.md`; competing libs `dotenv` direct, `convict`, `node-config`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Warlock — configure the app
|
|
@@ -105,7 +105,7 @@ same nonce reaches the page's `<script>` tags.
|
|
|
105
105
|
| CLI commands (registered via `warlock <cmd>`) | `warlock.config.ts > cli` |
|
|
106
106
|
| HTTP server tuning per env (port, host, retry) | `warlock.config.ts > server` |
|
|
107
107
|
| HTTP runtime (CORS, cookies, rate limits, upload size) | `src/config/http.ts` |
|
|
108
|
-
| App identity (name, baseUrl, timezone, locales)
|
|
108
|
+
| App identity (name, baseUrl, publicUrl, timezone, locales) | `src/config/app.ts` |
|
|
109
109
|
| Subsystem configs (auth, mail, storage, cache, ai, …) | `src/config/<name>.ts` |
|
|
110
110
|
|
|
111
111
|
Heuristic: if the setting changes how the framework **boots, builds, or scaffolds**, it goes in `warlock.config.ts`. If it changes how a **subsystem behaves at runtime**, it goes in `src/config/`.
|
|
@@ -239,6 +239,33 @@ const config = {
|
|
|
239
239
|
|
|
240
240
|
Avoid scattering `process.env.NODE_ENV === "production"` checks — they don't get the same default-handling.
|
|
241
241
|
|
|
242
|
+
### `app.publicUrl` — the app's public origin (5.15.0)
|
|
243
|
+
|
|
244
|
+
```ts title="src/config/app.ts"
|
|
245
|
+
import type { AppConfigurations } from "@warlock.js/core";
|
|
246
|
+
|
|
247
|
+
const appConfigurations: AppConfigurations = {
|
|
248
|
+
appName: "My App",
|
|
249
|
+
publicUrl: "https://example.com",
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
export default appConfigurations;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The one absolute-URL source every consumer that needs one — the sitemap
|
|
256
|
+
route, canonical links, OG tags, absolute URLs in mail — reads instead of
|
|
257
|
+
keeping its own copy. Optional in general (most apps have no consumer that
|
|
258
|
+
needs it yet); read it with `getPublicUrl()`, which returns `app.publicUrl`,
|
|
259
|
+
falling back to the `PUBLIC_APP_URL` env var, or `undefined` when neither is
|
|
260
|
+
set.
|
|
261
|
+
|
|
262
|
+
`getPublicUrl()` never throws — it is a consumer's job to fail loudly when it
|
|
263
|
+
requires the value. `@warlock.js/web`'s sitemap is the first such consumer:
|
|
264
|
+
with `web.sitemap.enabled: true` and no `app.publicUrl`/`PUBLIC_APP_URL` set,
|
|
265
|
+
generation refuses (`MissingPublicUrlError`, naming both) rather than falling
|
|
266
|
+
back to a request-derived host — a sitemap pointing at the wrong host is
|
|
267
|
+
worse than none. See `@warlock.js/web/generate-sitemap/SKILL.md`.
|
|
268
|
+
|
|
242
269
|
## Common patterns
|
|
243
270
|
|
|
244
271
|
### Adding a new subsystem
|
|
@@ -41,6 +41,20 @@ Scaffold with: `npx warlock generate.controller <module>/<action>` (add `--with-
|
|
|
41
41
|
|
|
42
42
|
Prefer `request.validated()` once a schema is attached — it's typed.
|
|
43
43
|
|
|
44
|
+
### Request body content types
|
|
45
|
+
|
|
46
|
+
`request.input()` / `.all()` / `.validated()` read the same way regardless of how the body arrived — every content type below feeds the same parsed bag:
|
|
47
|
+
|
|
48
|
+
| Content type | Parsed by | Notes |
|
|
49
|
+
| ------------------------------------ | -------------------------------- | --------------------------------------------------------------------- |
|
|
50
|
+
| `application/json` | Fastify (built-in) | objects/arrays parsed as-is |
|
|
51
|
+
| `multipart/form-data` | `@fastify/multipart` | fields + files; see [`upload-file`](../upload-file/SKILL.md) |
|
|
52
|
+
| `application/x-www-form-urlencoded` | Warlock's own content-type parser (`http/parse-urlencoded-body.ts`) | plain HTML forms, OAuth `form_post` callbacks (e.g. Apple Sign in) |
|
|
53
|
+
|
|
54
|
+
For urlencoded bodies: fields decode via `URLSearchParams`. A key sent more than once (`tag=a&tag=b`) becomes an array (`request.input("tag")` → `["a", "b"]`); every other key is a plain string. Bracket-notation keys (`a[b]=1`) are **not** expanded by the urlencoded parser itself — nesting only happens through the same shared bracket-key logic every body type already goes through, so it behaves exactly like a JSON or query-string key of that shape, no differently than today.
|
|
55
|
+
|
|
56
|
+
All three content types are held to the same `http.bodyLimit` — an over-limit urlencoded body is rejected with the same `413` a JSON body would get.
|
|
57
|
+
|
|
44
58
|
## Returning output
|
|
45
59
|
|
|
46
60
|
Pick the helper that matches the outcome. Full surface in [send-response](../send-response/SKILL.md). Quick map:
|
|
@@ -27,6 +27,8 @@ GET /ready → 200 {"status":"ok","checks":{"db":true}}
|
|
|
27
27
|
### Config
|
|
28
28
|
|
|
29
29
|
```ts title="src/config/http.ts"
|
|
30
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
31
|
+
|
|
30
32
|
const httpConfigurations: HttpConfigurations = {
|
|
31
33
|
health: {
|
|
32
34
|
enabled: true, // default; set false to remove both endpoints
|
|
@@ -34,6 +36,8 @@ const httpConfigurations: HttpConfigurations = {
|
|
|
34
36
|
readinessPath: "/ready", // readiness path
|
|
35
37
|
},
|
|
36
38
|
};
|
|
39
|
+
|
|
40
|
+
export default httpConfigurations;
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
## Readiness checks
|
|
@@ -74,12 +78,16 @@ On SIGINT/SIGTERM the framework tears down in order: **app `onShutdown` hooks
|
|
|
74
78
|
3. Draining is bounded by a timeout so one stuck request can't hang the deploy — after it, the server force-closes and a warning is logged.
|
|
75
79
|
|
|
76
80
|
```ts title="src/config/http.ts"
|
|
81
|
+
import type { HttpConfigurations } from "@warlock.js/core";
|
|
82
|
+
|
|
77
83
|
const httpConfigurations: HttpConfigurations = {
|
|
78
84
|
gracefulShutdown: {
|
|
79
85
|
timeout: 10_000, // ms to wait for in-flight drain (default 10s)
|
|
80
86
|
forceCloseConnections: "idle", // close idle keep-alives, let active finish (default)
|
|
81
87
|
},
|
|
82
88
|
};
|
|
89
|
+
|
|
90
|
+
export default httpConfigurations;
|
|
83
91
|
```
|
|
84
92
|
|
|
85
93
|
`forceCloseConnections`: `"idle"` (default) closes idle keep-alive connections and lets active requests finish; `true` force-closes everything immediately; `false` waits for every connection.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: send-response
|
|
3
|
-
description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
|
|
3
|
+
description: 'Send HTTP responses via @warlock.js/core''s Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`, `request.cookie`, `request.hasCookie`, `CookieJarUnavailableError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Warlock — send a response
|
|
@@ -84,6 +84,8 @@ return response.sendBuffer(buffer, { contentType: "image/png" });
|
|
|
84
84
|
|
|
85
85
|
`SendFileOptions` lets you set `cacheTime`, `immutable`, `inline`, `filename` (download attachment name).
|
|
86
86
|
|
|
87
|
+
`response.xml(string | XMLable, statusCode?)` sends `application/xml` — pass a raw XML string, or anything with a `toXML(): string` method (structural, so core never depends on `@warlock.js/sitemap`). This is for BOUNDED values only: a raw string, or a single `@warlock.js/sitemap` `Sitemap` under the sitemaps.org 50,000-URL / 50MB ceiling. `SitemapIndex` has no `toXML()` — it is the streaming path for larger URL sets and does not fit in one response body. It writes shards plus a master index as FILES via `saveTo`/`publish`, and those files are served directly rather than passed through `xml()`. In Warlock web this is the `web.sitemap` config: `enabled`, `outputDir` (default `storagePath("sitemap")`), and `path` (default `/sitemap.xml`) — the app serves the generated files from `outputDir` at that route, it never builds a `SitemapIndex` response inline.
|
|
88
|
+
|
|
87
89
|
## Streams
|
|
88
90
|
|
|
89
91
|
```ts
|
|
@@ -202,6 +204,20 @@ Every response cookie gets `httpOnly: true`, `sameSite: "lax"`, and `secure: tru
|
|
|
202
204
|
|
|
203
205
|
Precedence, lowest to highest: **framework defaults → `http.cookies.options` → the per-call `options` argument.** Set an app-wide policy in config, override per cookie when a specific one genuinely needs different treatment.
|
|
204
206
|
|
|
207
|
+
### Reading cookies back — `request.cookie()` / `request.hasCookie()` throw when the jar is unavailable (5.15.0)
|
|
208
|
+
|
|
209
|
+
`request.cookie(name)` and `request.hasCookie(name)` are a deliberate
|
|
210
|
+
by-name assertion — "this cookie should be readable here" — so when
|
|
211
|
+
`@fastify/cookie` is not registered on the Fastify instance, both now
|
|
212
|
+
**throw `CookieJarUnavailableError`** naming the missing cookie, instead of
|
|
213
|
+
silently returning `undefined` / `false`. Register the plugin (see core's
|
|
214
|
+
`http/plugins.ts`) before reading cookies by name.
|
|
215
|
+
|
|
216
|
+
`request.cookies` (the plain getter, no by-name assertion) is unchanged and
|
|
217
|
+
stays lenient — it returns `{}` when the jar is unavailable, because the
|
|
218
|
+
framework's own opportunistic reads (e.g. locale resolution) must not throw
|
|
219
|
+
on a request that simply has no jar.
|
|
220
|
+
|
|
205
221
|
These are the flags whose absence never fails a test and is fatal in production: the app works perfectly and is simply insecure. Opting out is now explicit and visible in review.
|
|
206
222
|
|
|
207
223
|
These mutate the response in place; chain or call before the final `return response.<helper>()`.
|
|
@@ -100,6 +100,12 @@ All three lookups go through `@mongez/localization`'s `trans()` under the hood,
|
|
|
100
100
|
|
|
101
101
|
When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from literal `groupedTranslations("group", { key: ... })` registrations. It augments web's `TranslationKeyRegistry`, so `useTrans()("products.notFound")` is checked against registered keys and a typo fails TypeScript. Before the generated file exists, `useTrans()` accepts `string` for a non-breaking first boot. Dynamic groups/keys and placeholders are not inferred.
|
|
102
102
|
|
|
103
|
+
`useTrans()` now works correctly across hydration (5.15.0): the hydration
|
|
104
|
+
payload ships a `translations` key with the active locale's keywords, and
|
|
105
|
+
`@warlock.js/web` registers them into this same lookup table before the
|
|
106
|
+
client hydrates — see `@warlock.js/web/write-the-root/SKILL.md`'s "`useTrans()`
|
|
107
|
+
survives hydration" section for the failure this fixed.
|
|
108
|
+
|
|
103
109
|
### Locale on a specific lookup
|
|
104
110
|
|
|
105
111
|
```ts
|