@warlock.js/core 5.25.0 → 5.26.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 +34 -0
- package/esm/cli/commands/doctor/checks/index.mjs +2 -0
- package/esm/cli/commands/doctor/checks/index.mjs.map +1 -1
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs +170 -0
- package/esm/cli/commands/doctor/checks/locales-ts-deprecated.check.mjs.map +1 -0
- package/esm/cli/commands/doctor/doctor-command.action.mjs +7 -2
- package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
- package/esm/cli/commands/doctor.command.mjs +5 -0
- package/esm/cli/commands/doctor.command.mjs.map +1 -1
- package/esm/cli/commands/generate/generators/module.generator.mjs +1 -12
- package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +4 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +2 -2
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/dev-server/legacy-locales-warning.mjs +16 -0
- package/esm/dev-server/legacy-locales-warning.mjs.map +1 -0
- package/esm/dev-server/module-loader.mjs +16 -0
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/special-file-patterns.mjs +2 -2
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/translation-keys-sources.mjs +7 -3
- package/esm/dev-server/translation-keys-sources.mjs.map +1 -1
- package/esm/dev-server/type-generator.mjs +27 -4
- package/esm/dev-server/type-generator.mjs.map +1 -1
- package/esm/generations/features/access.feature.mjs +38 -18
- package/esm/generations/features/access.feature.mjs.map +1 -1
- package/esm/generations/features/bull-board.feature.mjs +2 -2
- package/esm/generations/features/bull-board.feature.mjs.map +1 -1
- package/esm/http/build-rate-limit-options.d.mts +8 -2
- package/esm/http/build-rate-limit-options.d.mts.map +1 -1
- package/esm/http/build-rate-limit-options.mjs +1 -0
- package/esm/http/build-rate-limit-options.mjs.map +1 -1
- package/esm/http/errors/errors.d.mts +1 -17
- package/esm/http/errors/errors.d.mts.map +1 -1
- package/esm/http/errors/errors.mjs +1 -20
- package/esm/http/errors/errors.mjs.map +1 -1
- package/esm/http/index.d.mts +2 -2
- package/esm/http/index.mjs +1 -1
- package/esm/http/middleware/inject-request-context.d.mts +1 -1
- package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
- package/esm/http/middleware/inject-request-context.mjs.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
- package/esm/http/middleware/rate-limit.middleware.mjs +5 -1
- package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
- package/esm/http/request.d.mts +7 -24
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +1 -22
- package/esm/http/request.mjs.map +1 -1
- package/esm/http/response.d.mts.map +1 -1
- package/esm/http/response.mjs +4 -2
- package/esm/http/response.mjs.map +1 -1
- package/esm/index.d.mts +5 -3
- package/esm/index.mjs +5 -2
- package/esm/localization/index.mjs +4 -0
- package/esm/localization/parse-locale-dictionary.d.mts +23 -0
- package/esm/localization/parse-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/parse-locale-dictionary.mjs +215 -0
- package/esm/localization/parse-locale-dictionary.mjs.map +1 -0
- package/esm/localization/register-locale-dictionary.d.mts +12 -0
- package/esm/localization/register-locale-dictionary.d.mts.map +1 -0
- package/esm/localization/register-locale-dictionary.mjs +58 -0
- package/esm/localization/register-locale-dictionary.mjs.map +1 -0
- package/esm/production/production-builder.mjs +11 -2
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/router/types.d.mts +8 -0
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.d.mts +10 -0
- package/esm/tests/start-http-development-server.d.mts.map +1 -1
- package/esm/tests/start-http-development-server.mjs +16 -2
- package/esm/tests/start-http-development-server.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +48 -1
- package/esm/validation/validateAll.mjs.map +1 -1
- package/llms-full.txt +118 -75
- package/llms.txt +1 -1
- package/package.json +20 -20
- package/skills/README.md +1 -1
- package/skills/create-controller/SKILL.md +4 -0
- package/skills/test-http/SKILL.md +9 -1
- package/skills/use-localization/SKILL.md +39 -26
package/llms-full.txt
CHANGED
|
@@ -1197,6 +1197,8 @@ Same shape — TS validates the union; runtime picks the `default` key.
|
|
|
1197
1197
|
- `socket.adapter` — adapter factory for cross-server broadcast.
|
|
1198
1198
|
- `cache.silenceSingleServerWarning`, `storage.silenceSingleServerWarning`, `socket.silenceSingleServerWarning` — silence the production warnings for in-memory cache, local storage, and adapter-less sockets.
|
|
1199
1199
|
|
|
1200
|
+
`http.modelValidationErrorStatus` sets the status for a Cascade model-write validation failure (a model failing its own schema on save). It defaults to `500` with an opaque body, because request input is already validated with a `422` before the model is touched. Set a `4xx` (e.g. `422`) to treat it as a client error instead; the response then carries `{ errors }`.
|
|
1201
|
+
|
|
1200
1202
|
## Per-env knobs
|
|
1201
1203
|
|
|
1202
1204
|
For one-off switches gated on environment, the canonical pattern is `Application.isProduction` (also `isDevelopment`, `isTest`):
|
|
@@ -1380,6 +1382,10 @@ Two pieces:
|
|
|
1380
1382
|
|
|
1381
1383
|
If validation fails, the framework returns a 400 with an `errors` payload and your handler never runs.
|
|
1382
1384
|
|
|
1385
|
+
### Validating route params
|
|
1386
|
+
|
|
1387
|
+
`handler.validation = { params: v.object({ id: v.int().min(1) }) }` validates path segments (e.g. `/posts/:id`). Number and boolean validators convert the string segments; the app's schema object is never changed. Parsed values replace `request.params` (`request.input("id")` is a number) and merge into `request.validated()`, with params winning over body fields. Failures answer like a `schema` failure.
|
|
1388
|
+
|
|
1383
1389
|
### Typing a guarded handler
|
|
1384
1390
|
|
|
1385
1391
|
Routes behind `authMiddleware` need `request.locals.user` typed. Project conventions add a `GuardedRequest<TSchema>` (overriding `locals` to `RequestLocals & { user: User }`) and a paired `GuardedRequestHandler<TSchema>` alias in `app/auth/requests/guarded.request`:
|
|
@@ -1934,6 +1940,10 @@ export class Product extends Model {
|
|
|
1934
1940
|
|
|
1935
1941
|
This is convention, not a framework requirement. Controllers/services that import the resource explicitly work the same way.
|
|
1936
1942
|
|
|
1943
|
+
### `id` and Mongo `_id`
|
|
1944
|
+
|
|
1945
|
+
The output `id` falls back to `_id` when the record has no `id`; an explicit `id` always wins. If a collection uses native ObjectIds, set `public primaryKey = "_id"` on the model so `Model.find(hex)` resolves the `id` your resource emits.
|
|
1946
|
+
|
|
1937
1947
|
## Output-only rule
|
|
1938
1948
|
|
|
1939
1949
|
Resources map fields. They do not:
|
|
@@ -3786,6 +3796,7 @@ export default defineConfig({
|
|
|
3786
3796
|
sourcemap: true, // default — true | false | "inline" | "linked"
|
|
3787
3797
|
singleBundle: false, // default — one runnable file, deps inlined
|
|
3788
3798
|
esmShim: true, // default — require/__filename/__dirname for bundled CJS
|
|
3799
|
+
routeRegistrationTimeoutMs: 30_000, // default — route-registration child deadline
|
|
3789
3800
|
},
|
|
3790
3801
|
});
|
|
3791
3802
|
```
|
|
@@ -3799,6 +3810,7 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
|
|
|
3799
3810
|
- **`sourcemap: false`** — skip source maps entirely. Smaller artifact, but stack traces in production logs lose their file:line precision (and `warlock start` will not enable `--enable-source-maps` since there's nothing to map).
|
|
3800
3811
|
- **`singleBundle: true`** — inline dependencies so `node dist/app.js` runs with no `node_modules` and no launcher. Sets `packages: "bundle"` + `splitting: false` as **defaults you can override**. ⚠ Native `.node` addons are still emitted alongside — "single bundle" is one JS file _plus_ any native addons. Do NOT reach for it as the default: keeping deps external is right when you deploy the folder.
|
|
3801
3812
|
- **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
|
|
3813
|
+
- **`routeRegistrationTimeoutMs`** — increase on busy CI runners when static route registration needs more than 30 seconds. `WARLOCK_ROUTE_REGISTRATION_TIMEOUT_MS` overrides this value for a build; invalid values warn and use 30,000ms.
|
|
3802
3814
|
|
|
3803
3815
|
⚠ **The trap this replaced.** Setting `packages: "bundle"` by hand used to produce a clean build whose process died on start with `Error: Dynamic require of "node:assert" is not supported`. The output is ESM; bundled CJS deps call `require(...)` and read `__dirname`, and neither exists in an ES module, so the bundler substitutes a throwing stub. **A green `warlock build` was not evidence the bundle ran.** `esmShim` now injects `createRequire(import.meta.url)` and friends automatically for any ESM build, so both `singleBundle` and a hand-written `packages: "bundle"` work. An existing hand-written `banner` is preserved — the shim is prepended, not substituted.
|
|
3804
3816
|
|
|
@@ -5265,7 +5277,7 @@ Some tests need the full stack: route matching, middleware chain, validation, co
|
|
|
5265
5277
|
|
|
5266
5278
|
`startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
|
|
5267
5279
|
|
|
5268
|
-
⚠ **Changed in 4.13.0 — the import is a subpath now.** These helpers used to be re-exported from the package root; they are not any more, because that put the test helpers into every application's production module graph. `import { testGet } from "@warlock.js/core"` now fails with
|
|
5280
|
+
⚠ **Changed in 4.13.0 — the import is a subpath now.** These helpers used to be re-exported from the package root; they are not any more, because that put the test helpers into every application's production module graph. `import { testGet } from "@warlock.js/core"` now fails with _"has no exported member"_ — **add `/tests` to the specifier and nothing else changes.**
|
|
5269
5281
|
|
|
5270
5282
|
## The shape
|
|
5271
5283
|
|
|
@@ -5306,7 +5318,7 @@ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests"
|
|
|
5306
5318
|
- Loads `warlock.config.ts` + bootstrap + `filesOrchestrator` (without file watching).
|
|
5307
5319
|
- Loads `src/config/*.ts`.
|
|
5308
5320
|
- Loads every module (`routes.ts`, `main.ts`, `events/*.ts`).
|
|
5309
|
-
- Starts **ALL** connectors, including HTTP.
|
|
5321
|
+
- Starts **ALL** connectors, including HTTP, by default.
|
|
5310
5322
|
|
|
5311
5323
|
Unlike the dev server, it doesn't watch files, doesn't do HMR, doesn't run health checkers. Just a working HTTP endpoint listening on the configured port.
|
|
5312
5324
|
|
|
@@ -5326,6 +5338,14 @@ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`
|
|
|
5326
5338
|
|
|
5327
5339
|
The request helpers follow with no extra wiring: the bound port is published as `WARLOCK_TEST_SERVER_PORT`, which the workers inherit, and `getTestServerUrl()` prefers it over their own `.env`-resolved `http.port`. `stopHttpTestServer()` withdraws it.
|
|
5328
5340
|
|
|
5341
|
+
### API-only suites — `startHttpTestServer({ web: false })`
|
|
5342
|
+
|
|
5343
|
+
```ts
|
|
5344
|
+
await startHttpTestServer({ web: false });
|
|
5345
|
+
```
|
|
5346
|
+
|
|
5347
|
+
Use this in `globalSetup` for a suite that exercises API routes only. It still boots the HTTP connector and every other configured connector, but excludes the optional `web` connector before its lifecycle begins. That means web's lazy delegate never constructs `WebConnector`, and Vite is never created. Omitting `web` keeps the default full-stack behavior.
|
|
5348
|
+
|
|
5329
5349
|
### The port is preflighted
|
|
5330
5350
|
|
|
5331
5351
|
Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
|
|
@@ -5381,8 +5401,8 @@ import { defineConfig } from "vitest/config";
|
|
|
5381
5401
|
export default defineConfig({
|
|
5382
5402
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
5383
5403
|
test: {
|
|
5384
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
5385
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
5404
|
+
globalSetup: "./src/test-global-setup.ts", // ← starts the HTTP server
|
|
5405
|
+
setupFiles: ["./src/test-setup.ts"], // ← setupTest + afterAll(teardownTest), per test file
|
|
5386
5406
|
environment: "node",
|
|
5387
5407
|
globals: false,
|
|
5388
5408
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -5418,7 +5438,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
|
|
|
5418
5438
|
```ts
|
|
5419
5439
|
import { getTestServerUrl } from "@warlock.js/core/tests";
|
|
5420
5440
|
|
|
5421
|
-
const url = getTestServerUrl();
|
|
5441
|
+
const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
|
|
5422
5442
|
```
|
|
5423
5443
|
|
|
5424
5444
|
Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config — unless `startHttpTestServer` published a port, which wins, since a worker's own config never sees the `{ port }` option passed in `globalSetup`. If you change the HTTP config, helpers follow automatically.
|
|
@@ -5456,9 +5476,9 @@ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
|
|
|
5456
5476
|
const body = await parseJsonResponse<MyShape>(response);
|
|
5457
5477
|
|
|
5458
5478
|
// Assert status + parse in one call
|
|
5459
|
-
const body = await expectJson<MyShape>(response);
|
|
5460
|
-
const body = await expectJson<MyShape>(response, 201);
|
|
5461
|
-
const body = await expectJson<MyShape>(response, 404);
|
|
5479
|
+
const body = await expectJson<MyShape>(response); // expects 200
|
|
5480
|
+
const body = await expectJson<MyShape>(response, 201); // expects 201
|
|
5481
|
+
const body = await expectJson<MyShape>(response, 404); // expects 404 (testing error paths)
|
|
5462
5482
|
```
|
|
5463
5483
|
|
|
5464
5484
|
`expectJson` throws with the actual status + response body when the assertion fails — no chasing "got 500" messages without context.
|
|
@@ -5494,7 +5514,7 @@ describe("Products API — happy path", () => {
|
|
|
5494
5514
|
|
|
5495
5515
|
```ts
|
|
5496
5516
|
it("rejects missing required fields with 400", async () => {
|
|
5497
|
-
const response = await testPost("/products", { name: "" });
|
|
5517
|
+
const response = await testPost("/products", { name: "" }); // price missing
|
|
5498
5518
|
const body = await expectJson<{ errors: Array<{ key: string }> }>(response, 400);
|
|
5499
5519
|
|
|
5500
5520
|
expect(body.errors.some((e) => e.key === "price")).toBe(true);
|
|
@@ -5619,13 +5639,13 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
|
|
|
5619
5639
|
|
|
5620
5640
|
⚠ **Corrected in 4.14.0 — `setupTest` is CALLED once per TEST FILE, not once per worker.** Every version of this skill through 4.13.0 said "per worker", the generated `src/test-setup.ts` carries a `Per-Worker Test Setup` comment saying the same thing, and **both were wrong.** Vitest runs `setupFiles` before **each test file**, and the setup module's registry is rebuilt every time — measured across all four `pool` × `isolate` combinations.
|
|
5621
5641
|
|
|
5622
|
-
⛔ **If your project was generated before 4.14.0, fix BOTH the comment and the call.** The comment is false, **and** the generated `setupTest({ connectors: true })` is now an
|
|
5642
|
+
⛔ **If your project was generated before 4.14.0, fix BOTH the comment and the call.** The comment is false, **and** the generated `setupTest({ connectors: true })` is now an _explicit_ value that overrides your `src/config/tests.ts`. **Bare `setupTest()` is the correct call.**
|
|
5623
5643
|
|
|
5624
5644
|
**The lifetime is FILE-SCOPED, on purpose.** Your setup file bootstraps the framework and its `afterAll(teardownTest)` closes it, once per test file. **One owner, one pairing — correct under every pool, every isolation setting, and watch mode.**
|
|
5625
5645
|
|
|
5626
|
-
⚠ **A worker-scoped lifetime is possible and is deliberately not shipped yet.** Lifecycle state now lives in the worker runtime, so leaving the framework running would let every file in a worker share one bootstrap. Two things block claiming it: under `pool: "threads"` we cannot observe whether Node reclaims a torn-down thread's sockets and pools, and **in watch mode Vitest reuses workers between reruns, so there is no recycle and no cleanup owner.** It gets taken when the real cost is measured and the integration is chosen, not inherited. See
|
|
5646
|
+
⚠ **A worker-scoped lifetime is possible and is deliberately not shipped yet.** Lifecycle state now lives in the worker runtime, so leaving the framework running would let every file in a worker share one bootstrap. Two things block claiming it: under `pool: "threads"` we cannot observe whether Node reclaims a torn-down thread's sockets and pools, and **in watch mode Vitest reuses workers between reruns, so there is no recycle and no cleanup owner.** It gets taken when the real cost is measured and the integration is chosen, not inherited. See _Lifecycle and repeated calls_ below.
|
|
5627
5647
|
|
|
5628
|
-
⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with
|
|
5648
|
+
⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with _"has no exported member"_ — **add `/tests` to the specifier and nothing else changes.**
|
|
5629
5649
|
|
|
5630
5650
|
## The shape
|
|
5631
5651
|
|
|
@@ -5642,7 +5662,7 @@ describe("registerUserService", () => {
|
|
|
5642
5662
|
});
|
|
5643
5663
|
|
|
5644
5664
|
expect(user.get("email")).toBe("test@example.com");
|
|
5645
|
-
expect(user.get("password")).not.toBe("secret");
|
|
5665
|
+
expect(user.get("password")).not.toBe("secret"); // hashed by useHashedPassword()
|
|
5646
5666
|
|
|
5647
5667
|
const found = await usersRepository.first({ email: "test@example.com" });
|
|
5648
5668
|
expect(found).toBeDefined();
|
|
@@ -5667,18 +5687,18 @@ What it does (in order):
|
|
|
5667
5687
|
3. Runs `bootstrap()` — env, app, prestart hooks.
|
|
5668
5688
|
4. Initializes the `filesOrchestrator` (module/route/config discovery, no file watching).
|
|
5669
5689
|
5. Loads all `src/config/*.ts` files.
|
|
5670
|
-
6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See
|
|
5690
|
+
6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See _Selecting connectors_ below.
|
|
5671
5691
|
7. Starts the chosen connectors — but **never `http`** when you pass a boolean. HTTP is the global-setup's job.
|
|
5672
5692
|
|
|
5673
5693
|
The result: DB/cache/logger/storage connections your code can use. Models save, repositories query, services run. Same code as production, just isolated to the test process.
|
|
5674
5694
|
|
|
5675
5695
|
### The `connectors` parameter
|
|
5676
5696
|
|
|
5677
|
-
| Value
|
|
5678
|
-
|
|
|
5679
|
-
| `true`
|
|
5680
|
-
| `false`
|
|
5681
|
-
| `["database", "cache"]` | Just those, in that order
|
|
5697
|
+
| Value | Boots | Use when |
|
|
5698
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
5699
|
+
| `true` _(default)_ | Every connector except `http` — via `startWithout(["http"])` (db, cache, logger, storage, and socket if configured) | Most service / repository / model tests. |
|
|
5700
|
+
| `false` | None | Pure logic tests with no DB / cache touches (parsers, validators, util functions). |
|
|
5701
|
+
| `["database", "cache"]` | Just those, in that order | A test that only needs DB but not, say, the storage driver. |
|
|
5682
5702
|
|
|
5683
5703
|
The default `true` is the sane choice. Reach for `false` when the unit you're testing genuinely doesn't talk to any framework subsystem — pulling up a DB connection **for every file** just to test a string parser is wasted setup time.
|
|
5684
5704
|
|
|
@@ -5694,25 +5714,25 @@ export default testsConfigurations;
|
|
|
5694
5714
|
|
|
5695
5715
|
⚠ **BREAKING in 4.14.0 — the precedence flipped.**
|
|
5696
5716
|
|
|
5697
|
-
|
|
|
5698
|
-
|
|
5699
|
-
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true`
|
|
5700
|
-
| **4.14.0 onward**
|
|
5717
|
+
| | Order |
|
|
5718
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------- |
|
|
5719
|
+
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true` |
|
|
5720
|
+
| **4.14.0 onward** | **explicit `setupTest({ connectors })` parameter** **>** `tests.connectors` config **>** `true` |
|
|
5701
5721
|
|
|
5702
|
-
**An explicit call-site value now beats project config.** If your project sets `tests.connectors`
|
|
5722
|
+
**An explicit call-site value now beats project config.** If your project sets `tests.connectors` _and_ some test file passes `connectors` explicitly, **that file will start a different connector set after upgrading.** Search for `setupTest({` across your tests before you upgrade — a call passing `connectors` was previously ignored and is now honoured.
|
|
5703
5723
|
|
|
5704
5724
|
**"Explicit" means you supplied a non-`undefined` value.** Both of these fall through to config:
|
|
5705
5725
|
|
|
5706
5726
|
```ts
|
|
5707
|
-
await setupTest();
|
|
5708
|
-
await setupTest({});
|
|
5727
|
+
await setupTest(); // → tests.connectors, else true
|
|
5728
|
+
await setupTest({}); // → tests.connectors, else true
|
|
5709
5729
|
await setupTest({ connectors: undefined }); // → tests.connectors, else true — NOT "start none"
|
|
5710
5730
|
```
|
|
5711
5731
|
|
|
5712
5732
|
The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase your project config.
|
|
5713
5733
|
|
|
5714
5734
|
```ts
|
|
5715
|
-
await setupTest({ connectors: false });
|
|
5735
|
+
await setupTest({ connectors: false }); // → none, even if config says otherwise
|
|
5716
5736
|
await setupTest({ connectors: ["database"] }); // → exactly that, even if config differs
|
|
5717
5737
|
```
|
|
5718
5738
|
|
|
@@ -5739,7 +5759,7 @@ afterAll(teardownTest);
|
|
|
5739
5759
|
⛔ **Three things changed here in 4.14.0. If you generated this file earlier, replace all three — it is not a comment fix.**
|
|
5740
5760
|
|
|
5741
5761
|
1. **`afterAll(teardownTest)` is new and mandatory.** Nothing else closes the framework your tests started. This is what makes the lifetime file-scoped and owned rather than left running.
|
|
5742
|
-
2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an
|
|
5762
|
+
2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an _explicit_ value and would override `tests.connectors` for **every file in the project.** Bare means "whatever this project configured, else the default".
|
|
5743
5763
|
3. **The comment used to say `Per-Worker Test Setup` / "Runs in EACH Vitest worker thread".** False — see the top of this skill.
|
|
5744
5764
|
|
|
5745
5765
|
```ts title="vite.config.ts"
|
|
@@ -5750,8 +5770,8 @@ import { defineConfig } from "vitest/config";
|
|
|
5750
5770
|
export default defineConfig({
|
|
5751
5771
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
5752
5772
|
test: {
|
|
5753
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
5754
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
5773
|
+
globalSetup: "./src/test-global-setup.ts", // ← HTTP server (see test-http skill)
|
|
5774
|
+
setupFiles: ["./src/test-setup.ts"], // ← runs setupTest before EACH test file
|
|
5755
5775
|
environment: "node",
|
|
5756
5776
|
globals: false,
|
|
5757
5777
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -5797,9 +5817,9 @@ describe("createProductService", () => {
|
|
|
5797
5817
|
it("rejects duplicate names", async () => {
|
|
5798
5818
|
await Product.create({ name: "Existing", price: 10 });
|
|
5799
5819
|
|
|
5800
|
-
await expect(
|
|
5801
|
-
|
|
5802
|
-
)
|
|
5820
|
+
await expect(createProductService({ name: "Existing", price: 20 })).rejects.toThrow(
|
|
5821
|
+
/already exists/i,
|
|
5822
|
+
);
|
|
5803
5823
|
});
|
|
5804
5824
|
});
|
|
5805
5825
|
```
|
|
@@ -5869,7 +5889,7 @@ afterEach(async () => {
|
|
|
5869
5889
|
|
|
5870
5890
|
Vitest runs the tests within one file sequentially, so an `afterEach` truncate gives each test a clean slate.
|
|
5871
5891
|
|
|
5872
|
-
⚠ **Cross
|
|
5892
|
+
⚠ **Cross-_file_ isolation is not solved by this.** Separate workers get separate **connections**, not separate **rows** — two files pointed at the same database see each other's committed data regardless of pool or worker count. **Truncate what your file wrote; don't assume the worker boundary did it for you.** Real data isolation (DB-per-worker, transaction-per-test) is a separate piece of work and is not in this release.
|
|
5873
5893
|
|
|
5874
5894
|
### Skipping connectors for pure logic tests
|
|
5875
5895
|
|
|
@@ -5879,7 +5899,7 @@ import { setupTest } from "@warlock.js/core/tests";
|
|
|
5879
5899
|
import { slugify } from "../utils/slugify";
|
|
5880
5900
|
|
|
5881
5901
|
beforeAll(async () => {
|
|
5882
|
-
await setupTest({ connectors: false });
|
|
5902
|
+
await setupTest({ connectors: false }); // starts no connectors at all
|
|
5883
5903
|
});
|
|
5884
5904
|
|
|
5885
5905
|
describe("slugify", () => {
|
|
@@ -5906,7 +5926,7 @@ beforeAll(async () => {
|
|
|
5906
5926
|
});
|
|
5907
5927
|
|
|
5908
5928
|
afterAll(async () => {
|
|
5909
|
-
await teardownTest();
|
|
5929
|
+
await teardownTest(); // ← REQUIRED, see below
|
|
5910
5930
|
});
|
|
5911
5931
|
```
|
|
5912
5932
|
|
|
@@ -5927,16 +5947,16 @@ afterAll(async () => {
|
|
|
5927
5947
|
|
|
5928
5948
|
`setupTest` / `teardownTest` are a pair. **The harness that calls one owns calling the other in the same context.**
|
|
5929
5949
|
|
|
5930
|
-
| Call
|
|
5931
|
-
|
|
5932
|
-
| `setupTest(x)` while idle
|
|
5933
|
-
| `setupTest(x)` while already ready with the **same** effective options
|
|
5934
|
-
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested
|
|
5935
|
-
| two concurrent `setupTest(x)` calls
|
|
5936
|
-
| `setupTest` after a failed setup
|
|
5937
|
-
| `teardownTest()` while idle
|
|
5938
|
-
| two concurrent `teardownTest()` calls
|
|
5939
|
-
| `setupTest(y)` after a **successful** teardown
|
|
5950
|
+
| Call | Behaviour |
|
|
5951
|
+
| --------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
5952
|
+
| `setupTest(x)` while idle | bootstraps |
|
|
5953
|
+
| `setupTest(x)` while already ready with the **same** effective options | no-op |
|
|
5954
|
+
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested |
|
|
5955
|
+
| two concurrent `setupTest(x)` calls | share one startup |
|
|
5956
|
+
| `setupTest` after a failed setup | allowed — a failed setup unwinds and returns to idle |
|
|
5957
|
+
| `teardownTest()` while idle | no-op |
|
|
5958
|
+
| two concurrent `teardownTest()` calls | share one shutdown |
|
|
5959
|
+
| `setupTest(y)` after a **successful** teardown | allowed, different options fine |
|
|
5940
5960
|
|
|
5941
5961
|
**"Same options" is compared by meaning, not by literal value** — connector arrays are deduplicated and compared as sets, so `["cache", "database"]` and `["database", "cache", "cache"]` are the same selection.
|
|
5942
5962
|
|
|
@@ -5951,7 +5971,7 @@ afterAll(async () => {
|
|
|
5951
5971
|
```ts title="src/config/tests.ts"
|
|
5952
5972
|
const testsConfigurations = {
|
|
5953
5973
|
connectors: ["database", "logger"],
|
|
5954
|
-
setupTimeout: 120000,
|
|
5974
|
+
setupTimeout: 120000, // milliseconds — this is the default
|
|
5955
5975
|
};
|
|
5956
5976
|
|
|
5957
5977
|
export default testsConfigurations;
|
|
@@ -5970,9 +5990,9 @@ in `src/config/tests.ts` — milliseconds, default 120000.
|
|
|
5970
5990
|
1. **It bounds the setup ATTEMPT, not teardown separately.** `teardownTest()` awaits the same attempt, so it inherits the bound — **one timer, not two.** A second teardown-side deadline was tried and rejected: it re-introduced the unbounded re-entry this whole guard exists to remove.
|
|
5971
5991
|
2. **Expiry poisons the lifecycle**, it does not return to `idle`. The attempt may have started connectors nobody can now account for, so pretending the runtime is clean would be worse than refusing.
|
|
5972
5992
|
3. **⛔ An invalid `setupTimeout` throws, naming the bad value.** Zero, negative and non-numeric all fail loudly rather than falling back to the default — a silent fallback would hide a typo behind a working suite.
|
|
5973
|
-
4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens
|
|
5993
|
+
4. **The bound is measured from when the attempt started**, not from when config was read. `tests.setupTimeout` is only readable after `loadConfigFiles()`, which happens _inside_ the window being bounded; re-arming naively would give you the default plus your configured value.
|
|
5974
5994
|
|
|
5975
|
-
⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default
|
|
5995
|
+
⚠ **What is proven and what is not.** The nine guards above were each seen to fail under their own mutation. **But every spec injects its scheduler**, so the default _value_ is tested while the production timer — and whether its `unref` actually releases the worker — is not. **And no spec observes a real hang**: the stuck attempt is a mock gate, not a socket that never returns. These prove what the lifecycle _decides_, not what a genuinely wedged connector does.
|
|
5976
5996
|
|
|
5977
5997
|
### State is per worker runtime, not per module
|
|
5978
5998
|
|
|
@@ -6003,6 +6023,16 @@ In both cases the guard's scope matches the resource's scope, which is the point
|
|
|
6003
6023
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — where tests live in a module (`tests/*.test.ts`).
|
|
6004
6024
|
- [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — `warlock add test` for the initial scaffold.
|
|
6005
6025
|
|
|
6026
|
+
## App modules and `importModule`
|
|
6027
|
+
|
|
6028
|
+
Under Vitest, test files run through vite-node while Warlock's own native `import()` of app modules (config, routes, events) does not, so app classes such as `User` can exist twice in one worker and `instanceof` fails. Pass `importModule` to `setupTest` so Warlock loads app modules through the test's own module graph:
|
|
6029
|
+
|
|
6030
|
+
```ts
|
|
6031
|
+
setupTest({ importModule: (file) => import(file) });
|
|
6032
|
+
```
|
|
6033
|
+
|
|
6034
|
+
The generated `src/test-setup.ts` already does this. Without it, `setupTest()` under Vitest warns once.
|
|
6035
|
+
|
|
6006
6036
|
|
|
6007
6037
|
## update-packages `@warlock.js/core/update-packages/SKILL.md`
|
|
6008
6038
|
|
|
@@ -6764,7 +6794,7 @@ cors: {
|
|
|
6764
6794
|
|
|
6765
6795
|
---
|
|
6766
6796
|
name: use-localization
|
|
6767
|
-
description: 'Multi-locale translations via `groupedTranslations` (declare keys), `t()`
|
|
6797
|
+
description: 'Multi-locale translations via `groupedTranslations` (declare keys), `t()` (look up; always use it in app code, never `request.t()`), `request.getLocaleCode()` (detect locale from headers/query), `getLocalized` (pick the right value from a localized-array column). Triggers: `groupedTranslations`, `t`, `request.t`, `request.trans`, `request.transFrom`, `request.getLocaleCode`, `request.setLocaleCode`, `getLocalized`; "add a translation key", "resolve a localized error message", "detect request locale", "pick the right per-locale column value"; typical import `import { t, getLocalized } from "@warlock.js/core"`. Skip: resource output — `@warlock.js/core/define-resource/SKILL.md`; module scaffold — `@warlock.js/core/create-module/SKILL.md`; competing libs `i18next`, `react-intl`, raw `@mongez/localization`.'
|
|
6768
6798
|
---
|
|
6769
6799
|
|
|
6770
6800
|
# Warlock — translate keys + pick localized values
|
|
@@ -6779,7 +6809,7 @@ Both pivot on **the request's locale** — auto-detected from headers / query st
|
|
|
6779
6809
|
## The shape
|
|
6780
6810
|
|
|
6781
6811
|
```ts
|
|
6782
|
-
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.
|
|
6812
|
+
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.json)
|
|
6783
6813
|
import { groupedTranslations } from "@mongez/localization";
|
|
6784
6814
|
|
|
6785
6815
|
groupedTranslations("products", {
|
|
@@ -6791,8 +6821,7 @@ groupedTranslations("products", {
|
|
|
6791
6821
|
import { t } from "@warlock.js/core";
|
|
6792
6822
|
|
|
6793
6823
|
throw new ResourceNotFoundError(t("products.notFound"));
|
|
6794
|
-
|
|
6795
|
-
return response.success({ message: request.t("products.created") });
|
|
6824
|
+
return response.success({ message: t("products.created") });
|
|
6796
6825
|
|
|
6797
6826
|
// 3. Pick a value from a localized-array column
|
|
6798
6827
|
import { getLocalized } from "@warlock.js/core";
|
|
@@ -6801,7 +6830,26 @@ const name = getLocalized(product.get("name_translations"));
|
|
|
6801
6830
|
// → reads the current request's locale, returns the matching value
|
|
6802
6831
|
```
|
|
6803
6832
|
|
|
6804
|
-
## Declaring translations — `
|
|
6833
|
+
## Declaring translations — module `locales.json`
|
|
6834
|
+
|
|
6835
|
+
Prefer a data-only dictionary in `src/app/<module>/utils/locales.json`. The
|
|
6836
|
+
module folder is its default group, so this example registers
|
|
6837
|
+
`products.notFound`; a root `$group` string replaces that group when a module
|
|
6838
|
+
needs a different namespace. The module loader validates the strict JSON
|
|
6839
|
+
shape, flattens nested keys, and rejects duplicate ownership. Do not import the
|
|
6840
|
+
file yourself.
|
|
6841
|
+
|
|
6842
|
+
```json title="src/app/products/utils/locales.json"
|
|
6843
|
+
{
|
|
6844
|
+
"notFound": { "en": "Product not found", "ar": "المنتج غير موجود" },
|
|
6845
|
+
"errors": { "outOfStock": { "en": "Product out of stock", "ar": "المنتج غير متوفر" } }
|
|
6846
|
+
}
|
|
6847
|
+
```
|
|
6848
|
+
|
|
6849
|
+
`utils/locales.ts` with `groupedTranslations` remains supported but is
|
|
6850
|
+
deprecated and will be removed in v6. Migrate it with `warlock doctor --fix`.
|
|
6851
|
+
|
|
6852
|
+
### Legacy `groupedTranslations`
|
|
6805
6853
|
|
|
6806
6854
|
Every module owns its translation namespace under `src/app/<module>/utils/locales.ts`. The file is auto-loaded at boot — you don't import it from anywhere; the framework picks it up via the module loader.
|
|
6807
6855
|
|
|
@@ -6838,31 +6886,25 @@ t("products.outOfStock", { name: "Pen", count: 0 });
|
|
|
6838
6886
|
|
|
6839
6887
|
Placeholders use the `:name` syntax. The lookup helper substitutes them from the second argument.
|
|
6840
6888
|
|
|
6841
|
-
## Looking up translations —
|
|
6889
|
+
## Looking up translations — always `t()`
|
|
6842
6890
|
|
|
6843
|
-
|
|
6891
|
+
**Rule: app code translates with `t()` from `@warlock.js/core`.** It works everywhere: controllers, services, page and layout loaders, `metadata`, events and jobs. It returns a `string`.
|
|
6844
6892
|
|
|
6845
6893
|
```ts
|
|
6846
|
-
// 1. Top-level helper — works inside or outside a request.
|
|
6847
|
-
// Inside a request: uses request's locale.
|
|
6848
|
-
// Outside: uses the global default locale.
|
|
6849
6894
|
import { t } from "@warlock.js/core";
|
|
6850
6895
|
|
|
6851
6896
|
t("products.notFound");
|
|
6852
6897
|
t("products.outOfStock", { name: "Pen", count: 0 });
|
|
6853
|
-
|
|
6854
|
-
// 2. On the request object — explicit, scoped to that request.
|
|
6855
|
-
request.t("products.notFound");
|
|
6856
|
-
|
|
6857
|
-
// 3. Alias for request.t — same behavior.
|
|
6858
|
-
request.trans("products.notFound");
|
|
6859
6898
|
```
|
|
6860
6899
|
|
|
6861
|
-
|
|
6900
|
+
- Inside a request (a controller or a page loader) it uses the request's locale. In a page it first checks the route's `locales.json` snapshot, then falls back to module translations (`groupedTranslations` or module `locales.json`).
|
|
6901
|
+
- Outside a request (jobs, CLI) it uses the configured default locale.
|
|
6902
|
+
- `request.t()` / `request.trans()` are the per-request binding that `t()` delegates to. Don't call them in app code: `t()` gives the same result without threading `request` through services.
|
|
6903
|
+
- Don't wrap it in `String(...)`; it is already typed `string`.
|
|
6862
6904
|
|
|
6863
6905
|
### Web `useTrans()` key checking
|
|
6864
6906
|
|
|
6865
|
-
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.
|
|
6907
|
+
When an app uses `@warlock.js/web`, `warlock dev` writes `.warlock/typings/translations.d.ts` from module `locales.json` dictionaries and 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 TypeScript groups/keys and placeholders are not inferred.
|
|
6866
6908
|
|
|
6867
6909
|
`useTrans()` now works correctly across hydration (5.15.0): the hydration
|
|
6868
6910
|
payload ships a `translations` key with the active locale's keywords, and
|
|
@@ -6908,7 +6950,7 @@ export default {
|
|
|
6908
6950
|
request.setLocaleCode("ar");
|
|
6909
6951
|
```
|
|
6910
6952
|
|
|
6911
|
-
After this, every `
|
|
6953
|
+
After this, every `t(...)` and `getLocalized(...)` in the same request returns Arabic. Useful for user-preference overrides (e.g. "this user has set their language to ar in their profile — switch the request's locale on auth").
|
|
6912
6954
|
|
|
6913
6955
|
## Localized columns — `getLocalized`
|
|
6914
6956
|
|
|
@@ -7005,6 +7047,7 @@ The error's message is locale-aware — the `inject-request-context` middleware
|
|
|
7005
7047
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
7006
7048
|
import { type CreateProductSchema } from "../schema/create-product.schema";
|
|
7007
7049
|
import { createProductService } from "../services/create-product.service";
|
|
7050
|
+
import { t } from "@warlock.js/core";
|
|
7008
7051
|
|
|
7009
7052
|
export const createProductController: GuardedRequestHandler<CreateProductSchema> = async ({
|
|
7010
7053
|
request,
|
|
@@ -7012,7 +7055,7 @@ export const createProductController: GuardedRequestHandler<CreateProductSchema>
|
|
|
7012
7055
|
}) => {
|
|
7013
7056
|
const product = await createProductService(request.validated());
|
|
7014
7057
|
return response.success({
|
|
7015
|
-
message:
|
|
7058
|
+
message: t("products.created"),
|
|
7016
7059
|
product,
|
|
7017
7060
|
});
|
|
7018
7061
|
};
|
|
@@ -7052,11 +7095,11 @@ await Product.create({
|
|
|
7052
7095
|
GET /products/42?locale=ar
|
|
7053
7096
|
```
|
|
7054
7097
|
|
|
7055
|
-
`request.getLocaleCode()` returns `"ar"` — every `
|
|
7098
|
+
`request.getLocaleCode()` returns `"ar"` — every `t(...)` and resource-output `getLocalized` returns Arabic. No app code changes required.
|
|
7056
7099
|
|
|
7057
7100
|
## Gotchas
|
|
7058
7101
|
|
|
7059
|
-
- **Locales files are auto-loaded — don't import them.** `src/app/<module>/utils/locales.
|
|
7102
|
+
- **Locales files are auto-loaded — don't import them.** `src/app/<module>/utils/locales.json` is picked up by the module loader on boot. Importing legacy `locales.ts` manually causes double-registration warnings.
|
|
7060
7103
|
- **Group names should match module URL slugs.** `groupedTranslations("products", ...)` for the `/products` module. Mismatch breaks the convention but doesn't throw.
|
|
7061
7104
|
- **Every key needs every locale.** If a key is missing for the active locale, the lookup falls back to the key string itself (`"products.notFound"` literally appears in the response). Always ship all locales together when adding a key.
|
|
7062
7105
|
- **`t()` outside a request uses the global default.** `Application.environment === "test"` or background jobs without a request context get the configured `app.localeCode`, not whatever the most recent request used.
|
|
@@ -7069,8 +7112,8 @@ GET /products/42?locale=ar
|
|
|
7069
7112
|
|
|
7070
7113
|
- [`send-response/SKILL.md`](../send-response/SKILL.md) — error helpers that pair with translated messages (`response.notFound({ error: t("...") })`).
|
|
7071
7114
|
- [`define-resource/SKILL.md`](../define-resource/SKILL.md) — using `getLocalized` inside resource output for clean per-locale responses.
|
|
7072
|
-
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.
|
|
7073
|
-
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.
|
|
7115
|
+
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.json` file is part of the generated module scaffold.
|
|
7116
|
+
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.json` is auto-loaded; the suffix is mandatory.
|
|
7074
7117
|
|
|
7075
7118
|
## Route-owned web translations (5.17)
|
|
7076
7119
|
|
|
@@ -7084,9 +7127,9 @@ GET /products/42?locale=ar
|
|
|
7084
7127
|
}
|
|
7085
7128
|
```
|
|
7086
7129
|
|
|
7087
|
-
This produces `account.title` and `account.actions.save`. Keep every leaf complete for the configured locale list. The page pipeline selects an immutable snapshot for its route source and locale. `useTrans()
|
|
7130
|
+
This produces `account.title` and `account.actions.save`. Keep every leaf complete for the configured locale list. The page pipeline selects an immutable snapshot for its route source and locale. `useTrans()` and `t()` use that snapshot; a missing scoped key returns the key itself. With no route-locales manifest, the existing global `@mongez/localization` registry remains the legacy fallback path.
|
|
7088
7131
|
|
|
7089
|
-
When a route-locales snapshot applies, `
|
|
7132
|
+
When a route-locales snapshot applies, `t()` (through the request binding) and `request.transFrom(locale, key, placeholders?)` resolve through it; without one, they retain the existing global-registry behavior. The underlying `transFromKeywords(locale, keywords, key, placeholders?, converter?)` is supplied by `@mongez/localization` 3.5.0. Error pages rebind to their own snapshot, while framework fallback uses the app/root snapshot. Development HMR add/edit/delete publishes a new snapshot, and the snapshot revision keeps page-cache entries from reusing old JSON. Generated `.warlock` typings contain the flattened route keys; do not maintain a second handwritten key union.
|
|
7090
7133
|
|
|
7091
7134
|
|
|
7092
7135
|
## use-middleware `@warlock.js/core/use-middleware/SKILL.md`
|
package/llms.txt
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
- [update-packages](@warlock.js/core/update-packages/SKILL.md): Keep a project current with `warlock update` — bump every `@warlock.js/*` dependency in package.json to its latest published version (range operator preserved), then run the lockfile-detected package manager install. Also covers the `warlock dev` update notice, its `u` update-and-restart keyboard shortcut, and the `devServer.checkForUpdates` toggle. Triggers: `warlock update`, `--no-install`, `--dry-run`, `--check`, `checkForUpdates`, `fetchLatestVersion`, `isNewerVersion`; "update warlock packages", "upgrade the framework", "is there a new warlock version", "update notice in the dev server", "press u to update", "dev server keyboard shortcut", "update check offline", "bump @warlock.js/* to latest"; typical CLI `warlock update`. Skip: dev/build/start runtime — `@warlock.js/core/run-app/SKILL.md`; writing a custom command — `@warlock.js/core/write-cli-command/SKILL.md`; installing a NEW feature package (auth, mail, storage) — that is `warlock add`; releasing/publishing the framework — workspace release tooling, not this command.
|
|
34
34
|
- [upload-file](@warlock.js/core/upload-file/SKILL.md): Handle multipart file uploads — read via `request.file()` or `request.validated()`, validate with `v.file()`, save via `UploadedFile.save()` or the storage layer, transform images inline. Triggers: `UploadedFile`, `request.file`, `v.file`, `.save`, `.saveAs`, `.resize`, `.format`, `.quality`, `.image`, `.mimeType`, `.maxSize`; "accept a file upload", "validate file size and mime", "save to S3 or local disk", "resize an uploaded image on save"; typical import `import type { UploadedFile, RequestHandler } from "@warlock.js/core"`. Skip: storage drivers + presigned URLs — `@warlock.js/core/store-file/SKILL.md`; image-only transforms — `@warlock.js/core/process-image/SKILL.md`; schema rules — `@warlock.js/core/validate-input/SKILL.md`; competing libs `multer`, `formidable`, `busboy`.
|
|
35
35
|
- [use-app-context](@warlock.js/core/use-app-context/SKILL.md): Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy, boot lifecycle) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `Application.onceBooted`, `Application.whenBooted`, `Application.isBooted`, `Application.onShutdown`, `Application.isShuttingDown`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check", "run code once the app is fully booted", "after all connectors started", "app booted hook", "run cleanup before shutdown", "graceful shutdown hook"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.
|
|
36
|
-
- [use-localization](@warlock.js/core/use-localization/SKILL.md): Multi-locale translations via `groupedTranslations` (declare keys), `t()`
|
|
36
|
+
- [use-localization](@warlock.js/core/use-localization/SKILL.md): Multi-locale translations via `groupedTranslations` (declare keys), `t()` (look up; always use it in app code, never `request.t()`), `request.getLocaleCode()` (detect locale from headers/query), `getLocalized` (pick the right value from a localized-array column). Triggers: `groupedTranslations`, `t`, `request.t`, `request.trans`, `request.transFrom`, `request.getLocaleCode`, `request.setLocaleCode`, `getLocalized`; "add a translation key", "resolve a localized error message", "detect request locale", "pick the right per-locale column value"; typical import `import { t, getLocalized } from "@warlock.js/core"`. Skip: resource output — `@warlock.js/core/define-resource/SKILL.md`; module scaffold — `@warlock.js/core/create-module/SKILL.md`; competing libs `i18next`, `react-intl`, raw `@mongez/localization`.
|
|
37
37
|
- [use-middleware](@warlock.js/core/use-middleware/SKILL.md): Attach built-in HTTP middleware to routes via the `middleware` namespace from `@warlock.js/core` — rateLimit, concurrencyLimit, maxBodySize, idempotency, maintenance, ipFilter, cache. Plus `X-Request-Id` correlation, wired automatically. Triggers: `middleware.rateLimit`, `middleware.concurrencyLimit`, `middleware.maxBodySize`, `middleware.idempotency`, `middleware.maintenance`, `middleware.ipFilter`, `middleware.cache`, `X-Request-Id`, `Idempotency-Key`; "add rate limiting", "dedupe writes by idempotency key", "cap concurrent requests", "block IPs", "cache a GET response"; typical import `import { middleware } from "@warlock.js/core"`. Skip: author custom middleware — `@warlock.js/core/write-middleware/SKILL.md`; cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; competing libs `@fastify/rate-limit` direct, `express-rate-limit`, `helmet`.
|
|
38
38
|
- [use-model-transformers](@warlock.js/core/use-model-transformers/SKILL.md): Three schema-side helpers — `useHashedPassword()` (bcrypt on save) attaches via `.addTransformer(...)`; `useComputedSlug(field?, scope?)` (auto-slug from another field) and `useComputedModel(callback)` (arbitrary computed-on-save value) attach via `v.computed(...)`. Triggers: `useHashedPassword`, `useComputedSlug`, `useComputedModel`, `.addTransformer`, `v.computed`, `ComputedCallback`; "auto-hash a password field", "auto-slug from title on save", "derive a value at write time", "declarative model transformers"; typical import `import { useHashedPassword, useComputedSlug } from "@warlock.js/core"`. Skip: bcrypt setup details — `@warlock.js/core/hash-password/SKILL.md`; repository writes — `@warlock.js/core/use-repository/SKILL.md`; output filtering — `@warlock.js/core/define-resource/SKILL.md`; competing patterns: manual `await hashPassword(input)` in services, ORM lifecycle hooks.
|
|
39
39
|
- [use-repository](@warlock.js/core/use-repository/SKILL.md): Subclass `RepositoryManager` for data access — declare `source`, `filterBy`, `defaultOptions`, then call `list()`/`listCached()`/`find()`/`create()`/`update()`/`delete()`, the active/cached/cursor variants, and the `filterBy`-aware aggregates `sum()`/`avg()`/`min()`/`max()`/`groupBy()`/`aggregate()`. Triggers: `RepositoryManager`, `FilterRules`, `RepositoryOptions`, `.list`, `.listCached`, `.find`, `.findCached`, `.create`, `.update`, `.delete`, `.sum`, `.avg`, `.min`, `.max`, `.groupBy`, `.aggregate`, `simpleSelectColumns`; "create a repository", "filter rules for a list endpoint", "cursor vs page pagination", "cached vs uncached read", "sum/avg/group-by with filters"; typical import `import { RepositoryManager } from "@warlock.js/core"`. Skip: cache singleton — `@warlock.js/cache/cache-basics/SKILL.md`; use-case pipelines — `@warlock.js/core/write-use-case/SKILL.md`; wire mapping — `@warlock.js/core/define-resource/SKILL.md`; competing libs `typeorm` Repository, `prisma.client.<model>`, `@nestjs/typeorm`.
|
package/package.json
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
"warlock": "bin/warlock.js"
|
|
9
9
|
},
|
|
10
10
|
"dependencies": {
|
|
11
|
-
"@fastify/cookie": "^
|
|
12
|
-
"@fastify/cors": "^11.
|
|
13
|
-
"@fastify/http-proxy": "^11.
|
|
14
|
-
"@fastify/multipart": "^
|
|
15
|
-
"@fastify/rate-limit": "^
|
|
16
|
-
"@fastify/static": "^
|
|
11
|
+
"@fastify/cookie": "^11.1.2",
|
|
12
|
+
"@fastify/cors": "^11.3.0",
|
|
13
|
+
"@fastify/http-proxy": "^11.6.3",
|
|
14
|
+
"@fastify/multipart": "^10.1.2",
|
|
15
|
+
"@fastify/rate-limit": "^11.2.0",
|
|
16
|
+
"@fastify/static": "^10.1.5",
|
|
17
17
|
"@mongez/concat-route": "^1.2.1",
|
|
18
18
|
"@mongez/config": "^1.2.2",
|
|
19
19
|
"@mongez/copper": "^2.1.3",
|
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.5",
|
|
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/fs": "5.
|
|
32
|
-
"@warlock.js/logger": "5.
|
|
33
|
-
"@warlock.js/seal": "5.
|
|
28
|
+
"@warlock.js/cache": "5.26.0",
|
|
29
|
+
"@warlock.js/cascade": "5.26.0",
|
|
30
|
+
"@warlock.js/context": "5.26.0",
|
|
31
|
+
"@warlock.js/fs": "5.26.0",
|
|
32
|
+
"@warlock.js/logger": "5.26.0",
|
|
33
|
+
"@warlock.js/seal": "5.26.0",
|
|
34
34
|
"bcryptjs": "^3.0.3",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"dayjs": "^1.11.19",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"esbuild": "^0.27.2",
|
|
39
39
|
"eslint": "^9.18.0",
|
|
40
40
|
"fast-glob": "^3.3.3",
|
|
41
|
-
"fastify": "^5.
|
|
42
|
-
"find-my-way": "^9.
|
|
41
|
+
"fastify": "^5.12.5",
|
|
42
|
+
"find-my-way": "^9.9.0",
|
|
43
43
|
"get-tsconfig": "^5.0.0-beta.5",
|
|
44
44
|
"import-meta-resolve": "^4.2.0",
|
|
45
45
|
"mime": "^4.1.0",
|
|
@@ -52,11 +52,11 @@
|
|
|
52
52
|
"@aws-sdk/lib-storage": "^3.955.0",
|
|
53
53
|
"@aws-sdk/s3-request-presigner": "^3.955.0",
|
|
54
54
|
"@react-email/render": "^2.0.5",
|
|
55
|
-
"@warlock.js/access": "5.
|
|
56
|
-
"@warlock.js/ai": "5.
|
|
57
|
-
"@warlock.js/herald": "5.
|
|
58
|
-
"@warlock.js/notifications": "5.
|
|
59
|
-
"@warlock.js/devtools": "5.
|
|
55
|
+
"@warlock.js/access": "5.26.0",
|
|
56
|
+
"@warlock.js/ai": "5.26.0",
|
|
57
|
+
"@warlock.js/herald": "5.26.0",
|
|
58
|
+
"@warlock.js/notifications": "5.26.0",
|
|
59
|
+
"@warlock.js/devtools": "5.26.0",
|
|
60
60
|
"nodemailer": "^8.0.5",
|
|
61
61
|
"react": "^19.2.3",
|
|
62
62
|
"react-dom": "^19.2.3",
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
],
|
|
128
128
|
"author": "hassanzohdy",
|
|
129
129
|
"license": "MIT",
|
|
130
|
-
"version": "5.
|
|
130
|
+
"version": "5.26.0",
|
|
131
131
|
"type": "module",
|
|
132
132
|
"main": "./esm/index.mjs",
|
|
133
133
|
"module": "./esm/index.mjs",
|
package/skills/README.md
CHANGED
|
@@ -34,7 +34,7 @@ Each folder holds one `SKILL.md` (an agent-facing how-to for a single task). The
|
|
|
34
34
|
- [use-app-context](use-app-context/SKILL.md) — read app-wide context via the `Application` static class and the `app` runtime accessor.
|
|
35
35
|
- [add-connector](add-connector/SKILL.md) — extend the lifecycle with a `BaseConnector` subclass (`start`/`shutdown`/`watchedFiles`).
|
|
36
36
|
- [wire-socket](wire-socket/SKILL.md) — configure Socket.IO, reach the live server via `getSocketServer()` / `app.socket`.
|
|
37
|
-
- [use-localization](use-localization/SKILL.md) — multi-locale translations: `groupedTranslations`, `t()`
|
|
37
|
+
- [use-localization](use-localization/SKILL.md) — multi-locale translations: `groupedTranslations`, `t()` (always, never `request.t()`), locale resolution.
|
|
38
38
|
|
|
39
39
|
## Use-cases & services
|
|
40
40
|
|
|
@@ -102,6 +102,10 @@ Two pieces:
|
|
|
102
102
|
|
|
103
103
|
If validation fails, the framework returns a 400 with an `errors` payload and your handler never runs.
|
|
104
104
|
|
|
105
|
+
### Validating route params
|
|
106
|
+
|
|
107
|
+
`handler.validation = { params: v.object({ id: v.int().min(1) }) }` validates path segments (e.g. `/posts/:id`). Number and boolean validators convert the string segments; the app's schema object is never changed. Parsed values replace `request.params` (`request.input("id")` is a number) and merge into `request.validated()`, with params winning over body fields. Failures answer like a `schema` failure.
|
|
108
|
+
|
|
105
109
|
### Typing a guarded handler
|
|
106
110
|
|
|
107
111
|
Routes behind `authMiddleware` need `request.locals.user` typed. Project conventions add a `GuardedRequest<TSchema>` (overriding `locals` to `RequestLocals & { user: User }`) and a paired `GuardedRequestHandler<TSchema>` alias in `app/auth/requests/guarded.request`:
|