@warlock.js/core 5.24.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 +64 -1
- package/bin/warlock.js +0 -0
- package/esm/application/application.d.mts +21 -0
- package/esm/application/application.d.mts.map +1 -1
- package/esm/application/application.mjs +29 -1
- package/esm/application/application.mjs.map +1 -1
- package/esm/application/boot-signal.d.mts +2 -1
- package/esm/application/boot-signal.d.mts.map +1 -1
- package/esm/application/boot-signal.mjs.map +1 -1
- package/esm/application/index.d.mts +1 -0
- package/esm/application/index.mjs +1 -0
- package/esm/application/roles.d.mts +72 -0
- package/esm/application/roles.d.mts.map +1 -0
- package/esm/application/roles.mjs +124 -0
- package/esm/application/roles.mjs.map +1 -0
- package/esm/cli/commands/add.command.mjs.map +1 -1
- package/esm/cli/commands/create-database.command.mjs.map +1 -1
- package/esm/cli/commands/dev-server.command.mjs.map +1 -1
- package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
- 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/drop-tables.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/generate/utils/path-resolver.mjs.map +1 -1
- package/esm/cli/commands/migrate.command.mjs.map +1 -1
- package/esm/cli/commands/routes.command.mjs.map +1 -1
- package/esm/cli/commands/seed.command.mjs.map +1 -1
- package/esm/cli/commands/start-production.command.mjs +54 -5
- package/esm/cli/commands/start-production.command.mjs.map +1 -1
- package/esm/cli/commands/storage-put.action.mjs.map +1 -1
- package/esm/cli/commands/storage-put.command.mjs.map +1 -1
- package/esm/cli/commands/typings-generator.command.mjs +20 -1
- package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
- package/esm/cli/commands/update.command.mjs.map +1 -1
- package/esm/cli/commands-loader.mjs.map +1 -1
- package/esm/config/config-loader.d.mts.map +1 -1
- package/esm/config/config-loader.mjs +3 -1
- package/esm/config/config-loader.mjs.map +1 -1
- package/esm/connectors/connectors-manager.mjs.map +1 -1
- package/esm/connectors/database-connector.mjs.map +1 -1
- package/esm/connectors/herald-connector.mjs.map +1 -1
- package/esm/connectors/http-connector.d.mts.map +1 -1
- package/esm/connectors/http-connector.mjs +7 -0
- package/esm/connectors/http-connector.mjs.map +1 -1
- package/esm/connectors/logger-connector.mjs.map +1 -1
- package/esm/connectors/mail-connector.mjs.map +1 -1
- package/esm/connectors/types.d.mts +8 -0
- package/esm/connectors/types.d.mts.map +1 -1
- package/esm/connectors/types.mjs.map +1 -1
- package/esm/database/create-database-action.mjs.map +1 -1
- package/esm/database/drop-tables-action.mjs.map +1 -1
- package/esm/database/migrate-action.mjs +4 -3
- package/esm/database/migrate-action.mjs.map +1 -1
- package/esm/database/models/database-log/database-log.mjs.map +1 -1
- package/esm/database/seed-command-action.mjs +2 -1
- package/esm/database/seed-command-action.mjs.map +1 -1
- package/esm/dev-server/dependency-graph.mjs.map +1 -1
- package/esm/dev-server/development-server.mjs +2 -0
- package/esm/dev-server/development-server.mjs.map +1 -1
- package/esm/dev-server/files-orchestrator.mjs.map +1 -1
- package/esm/dev-server/layer-executor.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/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +26 -5
- package/esm/dev-server/module-loader.mjs.map +1 -1
- package/esm/dev-server/register-installed-devtools.mjs +47 -0
- package/esm/dev-server/register-installed-devtools.mjs.map +1 -0
- package/esm/dev-server/run-connector-typings.mjs +28 -0
- package/esm/dev-server/run-connector-typings.mjs.map +1 -0
- package/esm/dev-server/special-file-patterns.mjs +5 -3
- package/esm/dev-server/special-file-patterns.mjs.map +1 -1
- package/esm/dev-server/special-files-collector.d.mts +2 -2
- package/esm/dev-server/special-files-collector.d.mts.map +1 -1
- package/esm/dev-server/special-files-collector.mjs +7 -4
- package/esm/dev-server/special-files-collector.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/encryption/password.mjs.map +1 -1
- package/esm/generations/add-command.action.mjs +3 -1
- package/esm/generations/add-command.action.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/ai-anthropic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
- package/esm/generations/features/ai-google.feature.mjs.map +1 -1
- package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
- package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
- package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
- package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
- package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
- package/esm/generations/features/ai.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/generations/features/devtools.feature.mjs +23 -0
- package/esm/generations/features/devtools.feature.mjs.map +1 -0
- package/esm/generations/features/herald.feature.mjs.map +1 -1
- package/esm/generations/features/image.feature.mjs.map +1 -1
- package/esm/generations/features/index.mjs +2 -0
- package/esm/generations/features/index.mjs.map +1 -1
- package/esm/generations/features/mail.feature.mjs.map +1 -1
- package/esm/generations/features/mongodb.feature.mjs.map +1 -1
- package/esm/generations/features/notifications.feature.mjs.map +1 -1
- package/esm/generations/features/postgres.feature.mjs.map +1 -1
- package/esm/generations/features/react-email.feature.mjs.map +1 -1
- package/esm/generations/features/react.feature.mjs.map +1 -1
- package/esm/generations/features/redis.feature.mjs.map +1 -1
- package/esm/generations/features/s3.feature.mjs.map +1 -1
- package/esm/generations/features/scheduler.feature.mjs.map +1 -1
- package/esm/generations/features/ses.feature.mjs.map +1 -1
- package/esm/generations/features/socket.feature.mjs.map +1 -1
- package/esm/generations/features/test.feature.mjs +6 -2
- package/esm/generations/features/test.feature.mjs.map +1 -1
- package/esm/generations/features/types.mjs.map +1 -1
- package/esm/generations/features/web.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/database/RequestLog.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 +4 -3
- package/esm/http/index.mjs +3 -2
- 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 +9 -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 +5 -5
- 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/http/server-rebuilt.d.mts +20 -0
- package/esm/http/server-rebuilt.d.mts.map +1 -0
- package/esm/http/server-rebuilt.mjs +21 -0
- package/esm/http/server-rebuilt.mjs.map +1 -0
- package/esm/http/tracing/index.mjs +1 -1
- package/esm/http/tracing/tracing-dispatcher.d.mts +13 -6
- package/esm/http/tracing/tracing-dispatcher.d.mts.map +1 -1
- package/esm/http/tracing/tracing-dispatcher.mjs +46 -14
- package/esm/http/tracing/tracing-dispatcher.mjs.map +1 -1
- package/esm/http/tracing/tracing.type.d.mts +2 -1
- package/esm/http/tracing/tracing.type.d.mts.map +1 -1
- package/esm/http/types.d.mts +9 -0
- package/esm/http/types.d.mts.map +1 -1
- package/esm/image/image.mjs.map +1 -1
- package/esm/index.d.mts +8 -4
- package/esm/index.mjs +8 -3
- package/esm/loader/app-module-importer.d.mts +17 -0
- package/esm/loader/app-module-importer.d.mts.map +1 -0
- package/esm/loader/app-module-importer.mjs +20 -0
- package/esm/loader/app-module-importer.mjs.map +1 -0
- 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/logger/logger.mjs.map +1 -1
- package/esm/mail/events.d.mts +7 -0
- package/esm/mail/events.d.mts.map +1 -1
- package/esm/mail/events.mjs +7 -0
- package/esm/mail/events.mjs.map +1 -1
- package/esm/mail/send-mail.d.mts.map +1 -1
- package/esm/mail/send-mail.mjs +23 -2
- package/esm/mail/send-mail.mjs.map +1 -1
- package/esm/mail/types.d.mts +5 -1
- package/esm/mail/types.d.mts.map +1 -1
- package/esm/mail/types.mjs.map +1 -1
- package/esm/manifest/manifest-manager.mjs.map +1 -1
- package/esm/production/build-contributions.mjs +19 -3
- package/esm/production/build-contributions.mjs.map +1 -1
- package/esm/production/production-builder.mjs +39 -10
- package/esm/production/production-builder.mjs.map +1 -1
- package/esm/production/production-supervisor.mjs +6 -0
- package/esm/production/production-supervisor.mjs.map +1 -1
- package/esm/production/resolve-build-config.d.mts +1 -1
- package/esm/production/resolve-build-config.mjs +15 -0
- package/esm/production/resolve-build-config.mjs.map +1 -1
- package/esm/production/route-registration-child.mjs +14 -2
- package/esm/production/route-registration-child.mjs.map +1 -1
- package/esm/production/route-registration-snapshot.mjs +9 -2
- package/esm/production/route-registration-snapshot.mjs.map +1 -1
- package/esm/resource/resource-field-builder.mjs +1 -1
- package/esm/resource/resource-field-builder.mjs.map +1 -1
- package/esm/resource/resource.d.mts +5 -0
- package/esm/resource/resource.d.mts.map +1 -1
- package/esm/resource/resource.mjs +17 -6
- package/esm/resource/resource.mjs.map +1 -1
- package/esm/router/router.d.mts.map +1 -1
- package/esm/router/router.mjs +1 -0
- package/esm/router/router.mjs.map +1 -1
- package/esm/router/types.d.mts +10 -1
- package/esm/router/types.d.mts.map +1 -1
- package/esm/tests/index.d.mts +3 -2
- package/esm/tests/index.mjs +3 -2
- 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/tests/vitest-setup.d.mts +16 -1
- package/esm/tests/vitest-setup.d.mts.map +1 -1
- package/esm/tests/vitest-setup.mjs +20 -2
- package/esm/tests/vitest-setup.mjs.map +1 -1
- package/esm/use-cases/use-case-events.mjs.map +1 -1
- package/esm/utils/is-test-file.mjs +15 -0
- package/esm/utils/is-test-file.mjs.map +1 -0
- package/esm/utils/normalized-path.d.mts +3 -1
- package/esm/utils/normalized-path.d.mts.map +1 -1
- package/esm/utils/normalized-path.mjs +4 -2
- package/esm/utils/normalized-path.mjs.map +1 -1
- package/esm/utils/paths.mjs.map +1 -1
- package/esm/validation/plugins/database-plugin.mjs.map +1 -1
- package/esm/validation/validateAll.mjs +57 -4
- package/esm/validation/validateAll.mjs.map +1 -1
- package/esm/warlock-config/default-configurations.mjs +1 -0
- package/esm/warlock-config/default-configurations.mjs.map +1 -1
- package/esm/warlock-config/types.d.mts +7 -0
- package/esm/warlock-config/types.d.mts.map +1 -1
- package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
- package/llms-full.txt +382 -214
- package/llms.txt +1 -1
- package/package.json +23 -19
- package/skills/README.md +1 -1
- package/skills/configure-app/SKILL.md +2 -0
- package/skills/create-controller/SKILL.md +4 -0
- package/skills/define-resource/SKILL.md +4 -0
- package/skills/request-tracing/SKILL.md +38 -15
- package/skills/run-app/SKILL.md +71 -34
- package/skills/send-mail/SKILL.md +55 -25
- package/skills/test-http/SKILL.md +28 -9
- package/skills/test-service/SKILL.md +61 -40
- package/skills/use-localization/SKILL.md +39 -26
- package/skills/warlock-conventions/SKILL.md +26 -18
- package/skills/write-cli-command/SKILL.md +50 -45
|
@@ -9,7 +9,7 @@ Some tests need the full stack: route matching, middleware chain, validation, co
|
|
|
9
9
|
|
|
10
10
|
`startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
|
|
11
11
|
|
|
12
|
-
⚠ **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
|
|
12
|
+
⚠ **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.**
|
|
13
13
|
|
|
14
14
|
## The shape
|
|
15
15
|
|
|
@@ -50,7 +50,7 @@ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests"
|
|
|
50
50
|
- Loads `warlock.config.ts` + bootstrap + `filesOrchestrator` (without file watching).
|
|
51
51
|
- Loads `src/config/*.ts`.
|
|
52
52
|
- Loads every module (`routes.ts`, `main.ts`, `events/*.ts`).
|
|
53
|
-
- Starts **ALL** connectors, including HTTP.
|
|
53
|
+
- Starts **ALL** connectors, including HTTP, by default.
|
|
54
54
|
|
|
55
55
|
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.
|
|
56
56
|
|
|
@@ -70,6 +70,14 @@ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`
|
|
|
70
70
|
|
|
71
71
|
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.
|
|
72
72
|
|
|
73
|
+
### API-only suites — `startHttpTestServer({ web: false })`
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
await startHttpTestServer({ web: false });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
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.
|
|
80
|
+
|
|
73
81
|
### The port is preflighted
|
|
74
82
|
|
|
75
83
|
Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
|
|
@@ -125,8 +133,8 @@ import { defineConfig } from "vitest/config";
|
|
|
125
133
|
export default defineConfig({
|
|
126
134
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
127
135
|
test: {
|
|
128
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
129
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
136
|
+
globalSetup: "./src/test-global-setup.ts", // ← starts the HTTP server
|
|
137
|
+
setupFiles: ["./src/test-setup.ts"], // ← setupTest + afterAll(teardownTest), per test file
|
|
130
138
|
environment: "node",
|
|
131
139
|
globals: false,
|
|
132
140
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -142,6 +150,17 @@ Both files (and this config, with `lowerStage3Decorators()` first so decorated m
|
|
|
142
150
|
|
|
143
151
|
**HTTP is the exception and stays in `globalSetup`**, which genuinely does run once in the main vitest process and owns a real port. **That split is the point:** one server for the whole run, one framework per test file.
|
|
144
152
|
|
|
153
|
+
### Keep Vitest setup out of development
|
|
154
|
+
|
|
155
|
+
`warlock dev` (and `warlock build`) load your app's `vite.config.ts`. Anything you put there for tests runs in development too. Guard vitest-only setup:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
// vite.config.ts
|
|
159
|
+
if (process.env.VITEST) {
|
|
160
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
145
164
|
## HTTP request helpers
|
|
146
165
|
|
|
147
166
|
Everything is built on native `fetch` — no extra dependency, no special wire format.
|
|
@@ -151,7 +170,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
|
|
|
151
170
|
```ts
|
|
152
171
|
import { getTestServerUrl } from "@warlock.js/core/tests";
|
|
153
172
|
|
|
154
|
-
const url = getTestServerUrl();
|
|
173
|
+
const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
|
|
155
174
|
```
|
|
156
175
|
|
|
157
176
|
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.
|
|
@@ -189,9 +208,9 @@ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
|
|
|
189
208
|
const body = await parseJsonResponse<MyShape>(response);
|
|
190
209
|
|
|
191
210
|
// Assert status + parse in one call
|
|
192
|
-
const body = await expectJson<MyShape>(response);
|
|
193
|
-
const body = await expectJson<MyShape>(response, 201);
|
|
194
|
-
const body = await expectJson<MyShape>(response, 404);
|
|
211
|
+
const body = await expectJson<MyShape>(response); // expects 200
|
|
212
|
+
const body = await expectJson<MyShape>(response, 201); // expects 201
|
|
213
|
+
const body = await expectJson<MyShape>(response, 404); // expects 404 (testing error paths)
|
|
195
214
|
```
|
|
196
215
|
|
|
197
216
|
`expectJson` throws with the actual status + response body when the assertion fails — no chasing "got 500" messages without context.
|
|
@@ -227,7 +246,7 @@ describe("Products API — happy path", () => {
|
|
|
227
246
|
|
|
228
247
|
```ts
|
|
229
248
|
it("rejects missing required fields with 400", async () => {
|
|
230
|
-
const response = await testPost("/products", { name: "" });
|
|
249
|
+
const response = await testPost("/products", { name: "" }); // price missing
|
|
231
250
|
const body = await expectJson<{ errors: Array<{ key: string }> }>(response, 400);
|
|
232
251
|
|
|
233
252
|
expect(body.errors.some((e) => e.key === "price")).toBe(true);
|
|
@@ -11,13 +11,13 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
|
|
|
11
11
|
|
|
12
12
|
⚠ **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.
|
|
13
13
|
|
|
14
|
-
⛔ **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
|
|
14
|
+
⛔ **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.**
|
|
15
15
|
|
|
16
16
|
**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.**
|
|
17
17
|
|
|
18
|
-
⚠ **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
|
|
18
|
+
⚠ **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.
|
|
19
19
|
|
|
20
|
-
⚠ **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
|
|
20
|
+
⚠ **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.**
|
|
21
21
|
|
|
22
22
|
## The shape
|
|
23
23
|
|
|
@@ -34,7 +34,7 @@ describe("registerUserService", () => {
|
|
|
34
34
|
});
|
|
35
35
|
|
|
36
36
|
expect(user.get("email")).toBe("test@example.com");
|
|
37
|
-
expect(user.get("password")).not.toBe("secret");
|
|
37
|
+
expect(user.get("password")).not.toBe("secret"); // hashed by useHashedPassword()
|
|
38
38
|
|
|
39
39
|
const found = await usersRepository.first({ email: "test@example.com" });
|
|
40
40
|
expect(found).toBeDefined();
|
|
@@ -59,18 +59,18 @@ What it does (in order):
|
|
|
59
59
|
3. Runs `bootstrap()` — env, app, prestart hooks.
|
|
60
60
|
4. Initializes the `filesOrchestrator` (module/route/config discovery, no file watching).
|
|
61
61
|
5. Loads all `src/config/*.ts` files.
|
|
62
|
-
6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See
|
|
62
|
+
6. Resolves the connector selection — **an explicit parameter wins, then `tests.connectors` from config, then the `true` default.** See _Selecting connectors_ below.
|
|
63
63
|
7. Starts the chosen connectors — but **never `http`** when you pass a boolean. HTTP is the global-setup's job.
|
|
64
64
|
|
|
65
65
|
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.
|
|
66
66
|
|
|
67
67
|
### The `connectors` parameter
|
|
68
68
|
|
|
69
|
-
| Value
|
|
70
|
-
|
|
|
71
|
-
| `true`
|
|
72
|
-
| `false`
|
|
73
|
-
| `["database", "cache"]` | Just those, in that order
|
|
69
|
+
| Value | Boots | Use when |
|
|
70
|
+
| ----------------------- | ------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
|
|
71
|
+
| `true` _(default)_ | Every connector except `http` — via `startWithout(["http"])` (db, cache, logger, storage, and socket if configured) | Most service / repository / model tests. |
|
|
72
|
+
| `false` | None | Pure logic tests with no DB / cache touches (parsers, validators, util functions). |
|
|
73
|
+
| `["database", "cache"]` | Just those, in that order | A test that only needs DB but not, say, the storage driver. |
|
|
74
74
|
|
|
75
75
|
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.
|
|
76
76
|
|
|
@@ -86,25 +86,25 @@ export default testsConfigurations;
|
|
|
86
86
|
|
|
87
87
|
⚠ **BREAKING in 4.14.0 — the precedence flipped.**
|
|
88
88
|
|
|
89
|
-
|
|
|
90
|
-
|
|
91
|
-
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true`
|
|
92
|
-
| **4.14.0 onward**
|
|
89
|
+
| | Order |
|
|
90
|
+
| ---------------------- | ----------------------------------------------------------------------------------------------- |
|
|
91
|
+
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true` |
|
|
92
|
+
| **4.14.0 onward** | **explicit `setupTest({ connectors })` parameter** **>** `tests.connectors` config **>** `true` |
|
|
93
93
|
|
|
94
|
-
**An explicit call-site value now beats project config.** If your project sets `tests.connectors`
|
|
94
|
+
**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.
|
|
95
95
|
|
|
96
96
|
**"Explicit" means you supplied a non-`undefined` value.** Both of these fall through to config:
|
|
97
97
|
|
|
98
98
|
```ts
|
|
99
|
-
await setupTest();
|
|
100
|
-
await setupTest({});
|
|
99
|
+
await setupTest(); // → tests.connectors, else true
|
|
100
|
+
await setupTest({}); // → tests.connectors, else true
|
|
101
101
|
await setupTest({ connectors: undefined }); // → tests.connectors, else true — NOT "start none"
|
|
102
102
|
```
|
|
103
103
|
|
|
104
104
|
The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase your project config.
|
|
105
105
|
|
|
106
106
|
```ts
|
|
107
|
-
await setupTest({ connectors: false });
|
|
107
|
+
await setupTest({ connectors: false }); // → none, even if config says otherwise
|
|
108
108
|
await setupTest({ connectors: ["database"] }); // → exactly that, even if config differs
|
|
109
109
|
```
|
|
110
110
|
|
|
@@ -131,7 +131,7 @@ afterAll(teardownTest);
|
|
|
131
131
|
⛔ **Three things changed here in 4.14.0. If you generated this file earlier, replace all three — it is not a comment fix.**
|
|
132
132
|
|
|
133
133
|
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.
|
|
134
|
-
2. **The call is now bare `setupTest()`, not `setupTest({ connectors: true })`.** Under the flipped precedence, passing `true` is an
|
|
134
|
+
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".
|
|
135
135
|
3. **The comment used to say `Per-Worker Test Setup` / "Runs in EACH Vitest worker thread".** False — see the top of this skill.
|
|
136
136
|
|
|
137
137
|
```ts title="vite.config.ts"
|
|
@@ -142,8 +142,8 @@ import { defineConfig } from "vitest/config";
|
|
|
142
142
|
export default defineConfig({
|
|
143
143
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
144
144
|
test: {
|
|
145
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
146
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
145
|
+
globalSetup: "./src/test-global-setup.ts", // ← HTTP server (see test-http skill)
|
|
146
|
+
setupFiles: ["./src/test-setup.ts"], // ← runs setupTest before EACH test file
|
|
147
147
|
environment: "node",
|
|
148
148
|
globals: false,
|
|
149
149
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -153,6 +153,17 @@ export default defineConfig({
|
|
|
153
153
|
|
|
154
154
|
The `mongezVite()` plugin handles TypeScript path resolution and the framework's module shape. Without it, your imports break the moment vitest tries to load a Warlock module. `lowerStage3Decorators()` goes **first** — it lets decorated Cascade models (`@RegisterModel`, …) load under Vitest 4 / Vite 8; see [`@warlock.js/core/lower-stage3-decorators/SKILL.md`](@warlock.js/core/lower-stage3-decorators/SKILL.md).
|
|
155
155
|
|
|
156
|
+
### Keep Vitest setup out of development
|
|
157
|
+
|
|
158
|
+
`warlock dev` (and `warlock build`) load your app's `vite.config.ts`. Anything you put there for tests runs in development too. Guard vitest-only setup:
|
|
159
|
+
|
|
160
|
+
```ts
|
|
161
|
+
// vite.config.ts
|
|
162
|
+
if (process.env.VITEST) {
|
|
163
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
156
167
|
Tests live colocated with the module: `src/app/<module>/tests/*.test.ts`. The `include` pattern picks them up.
|
|
157
168
|
|
|
158
169
|
## Patterns
|
|
@@ -178,9 +189,9 @@ describe("createProductService", () => {
|
|
|
178
189
|
it("rejects duplicate names", async () => {
|
|
179
190
|
await Product.create({ name: "Existing", price: 10 });
|
|
180
191
|
|
|
181
|
-
await expect(
|
|
182
|
-
|
|
183
|
-
)
|
|
192
|
+
await expect(createProductService({ name: "Existing", price: 20 })).rejects.toThrow(
|
|
193
|
+
/already exists/i,
|
|
194
|
+
);
|
|
184
195
|
});
|
|
185
196
|
});
|
|
186
197
|
```
|
|
@@ -250,7 +261,7 @@ afterEach(async () => {
|
|
|
250
261
|
|
|
251
262
|
Vitest runs the tests within one file sequentially, so an `afterEach` truncate gives each test a clean slate.
|
|
252
263
|
|
|
253
|
-
⚠ **Cross
|
|
264
|
+
⚠ **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.
|
|
254
265
|
|
|
255
266
|
### Skipping connectors for pure logic tests
|
|
256
267
|
|
|
@@ -260,7 +271,7 @@ import { setupTest } from "@warlock.js/core/tests";
|
|
|
260
271
|
import { slugify } from "../utils/slugify";
|
|
261
272
|
|
|
262
273
|
beforeAll(async () => {
|
|
263
|
-
await setupTest({ connectors: false });
|
|
274
|
+
await setupTest({ connectors: false }); // starts no connectors at all
|
|
264
275
|
});
|
|
265
276
|
|
|
266
277
|
describe("slugify", () => {
|
|
@@ -287,7 +298,7 @@ beforeAll(async () => {
|
|
|
287
298
|
});
|
|
288
299
|
|
|
289
300
|
afterAll(async () => {
|
|
290
|
-
await teardownTest();
|
|
301
|
+
await teardownTest(); // ← REQUIRED, see below
|
|
291
302
|
});
|
|
292
303
|
```
|
|
293
304
|
|
|
@@ -308,16 +319,16 @@ afterAll(async () => {
|
|
|
308
319
|
|
|
309
320
|
`setupTest` / `teardownTest` are a pair. **The harness that calls one owns calling the other in the same context.**
|
|
310
321
|
|
|
311
|
-
| Call
|
|
312
|
-
|
|
313
|
-
| `setupTest(x)` while idle
|
|
314
|
-
| `setupTest(x)` while already ready with the **same** effective options
|
|
315
|
-
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested
|
|
316
|
-
| two concurrent `setupTest(x)` calls
|
|
317
|
-
| `setupTest` after a failed setup
|
|
318
|
-
| `teardownTest()` while idle
|
|
319
|
-
| two concurrent `teardownTest()` calls
|
|
320
|
-
| `setupTest(y)` after a **successful** teardown
|
|
322
|
+
| Call | Behaviour |
|
|
323
|
+
| --------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
324
|
+
| `setupTest(x)` while idle | bootstraps |
|
|
325
|
+
| `setupTest(x)` while already ready with the **same** effective options | no-op |
|
|
326
|
+
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested |
|
|
327
|
+
| two concurrent `setupTest(x)` calls | share one startup |
|
|
328
|
+
| `setupTest` after a failed setup | allowed — a failed setup unwinds and returns to idle |
|
|
329
|
+
| `teardownTest()` while idle | no-op |
|
|
330
|
+
| two concurrent `teardownTest()` calls | share one shutdown |
|
|
331
|
+
| `setupTest(y)` after a **successful** teardown | allowed, different options fine |
|
|
321
332
|
|
|
322
333
|
**"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.
|
|
323
334
|
|
|
@@ -332,7 +343,7 @@ afterAll(async () => {
|
|
|
332
343
|
```ts title="src/config/tests.ts"
|
|
333
344
|
const testsConfigurations = {
|
|
334
345
|
connectors: ["database", "logger"],
|
|
335
|
-
setupTimeout: 120000,
|
|
346
|
+
setupTimeout: 120000, // milliseconds — this is the default
|
|
336
347
|
};
|
|
337
348
|
|
|
338
349
|
export default testsConfigurations;
|
|
@@ -351,9 +362,9 @@ in `src/config/tests.ts` — milliseconds, default 120000.
|
|
|
351
362
|
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.
|
|
352
363
|
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.
|
|
353
364
|
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.
|
|
354
|
-
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
|
|
365
|
+
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.
|
|
355
366
|
|
|
356
|
-
⚠ **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
|
|
367
|
+
⚠ **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.
|
|
357
368
|
|
|
358
369
|
### State is per worker runtime, not per module
|
|
359
370
|
|
|
@@ -383,3 +394,13 @@ In both cases the guard's scope matches the resource's scope, which is the point
|
|
|
383
394
|
- [`test-http/SKILL.md`](../test-http/SKILL.md) — integration tests via the real HTTP server (`startHttpTestServer` + `testGet` / `testPost` / `expectJson`).
|
|
384
395
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — where tests live in a module (`tests/*.test.ts`).
|
|
385
396
|
- [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — `warlock add test` for the initial scaffold.
|
|
397
|
+
|
|
398
|
+
## App modules and `importModule`
|
|
399
|
+
|
|
400
|
+
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:
|
|
401
|
+
|
|
402
|
+
```ts
|
|
403
|
+
setupTest({ importModule: (file) => import(file) });
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
The generated `src/test-setup.ts` already does this. Without it, `setupTest()` under Vitest warns once.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: use-localization
|
|
3
|
-
description: 'Multi-locale translations via `groupedTranslations` (declare keys), `t()`
|
|
3
|
+
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`.'
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Warlock — translate keys + pick localized values
|
|
@@ -15,7 +15,7 @@ Both pivot on **the request's locale** — auto-detected from headers / query st
|
|
|
15
15
|
## The shape
|
|
16
16
|
|
|
17
17
|
```ts
|
|
18
|
-
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.
|
|
18
|
+
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.json)
|
|
19
19
|
import { groupedTranslations } from "@mongez/localization";
|
|
20
20
|
|
|
21
21
|
groupedTranslations("products", {
|
|
@@ -27,8 +27,7 @@ groupedTranslations("products", {
|
|
|
27
27
|
import { t } from "@warlock.js/core";
|
|
28
28
|
|
|
29
29
|
throw new ResourceNotFoundError(t("products.notFound"));
|
|
30
|
-
|
|
31
|
-
return response.success({ message: request.t("products.created") });
|
|
30
|
+
return response.success({ message: t("products.created") });
|
|
32
31
|
|
|
33
32
|
// 3. Pick a value from a localized-array column
|
|
34
33
|
import { getLocalized } from "@warlock.js/core";
|
|
@@ -37,7 +36,26 @@ const name = getLocalized(product.get("name_translations"));
|
|
|
37
36
|
// → reads the current request's locale, returns the matching value
|
|
38
37
|
```
|
|
39
38
|
|
|
40
|
-
## Declaring translations — `
|
|
39
|
+
## Declaring translations — module `locales.json`
|
|
40
|
+
|
|
41
|
+
Prefer a data-only dictionary in `src/app/<module>/utils/locales.json`. The
|
|
42
|
+
module folder is its default group, so this example registers
|
|
43
|
+
`products.notFound`; a root `$group` string replaces that group when a module
|
|
44
|
+
needs a different namespace. The module loader validates the strict JSON
|
|
45
|
+
shape, flattens nested keys, and rejects duplicate ownership. Do not import the
|
|
46
|
+
file yourself.
|
|
47
|
+
|
|
48
|
+
```json title="src/app/products/utils/locales.json"
|
|
49
|
+
{
|
|
50
|
+
"notFound": { "en": "Product not found", "ar": "المنتج غير موجود" },
|
|
51
|
+
"errors": { "outOfStock": { "en": "Product out of stock", "ar": "المنتج غير متوفر" } }
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`utils/locales.ts` with `groupedTranslations` remains supported but is
|
|
56
|
+
deprecated and will be removed in v6. Migrate it with `warlock doctor --fix`.
|
|
57
|
+
|
|
58
|
+
### Legacy `groupedTranslations`
|
|
41
59
|
|
|
42
60
|
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.
|
|
43
61
|
|
|
@@ -74,31 +92,25 @@ t("products.outOfStock", { name: "Pen", count: 0 });
|
|
|
74
92
|
|
|
75
93
|
Placeholders use the `:name` syntax. The lookup helper substitutes them from the second argument.
|
|
76
94
|
|
|
77
|
-
## Looking up translations —
|
|
95
|
+
## Looking up translations — always `t()`
|
|
78
96
|
|
|
79
|
-
|
|
97
|
+
**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`.
|
|
80
98
|
|
|
81
99
|
```ts
|
|
82
|
-
// 1. Top-level helper — works inside or outside a request.
|
|
83
|
-
// Inside a request: uses request's locale.
|
|
84
|
-
// Outside: uses the global default locale.
|
|
85
100
|
import { t } from "@warlock.js/core";
|
|
86
101
|
|
|
87
102
|
t("products.notFound");
|
|
88
103
|
t("products.outOfStock", { name: "Pen", count: 0 });
|
|
89
|
-
|
|
90
|
-
// 2. On the request object — explicit, scoped to that request.
|
|
91
|
-
request.t("products.notFound");
|
|
92
|
-
|
|
93
|
-
// 3. Alias for request.t — same behavior.
|
|
94
|
-
request.trans("products.notFound");
|
|
95
104
|
```
|
|
96
105
|
|
|
97
|
-
|
|
106
|
+
- 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`).
|
|
107
|
+
- Outside a request (jobs, CLI) it uses the configured default locale.
|
|
108
|
+
- `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.
|
|
109
|
+
- Don't wrap it in `String(...)`; it is already typed `string`.
|
|
98
110
|
|
|
99
111
|
### Web `useTrans()` key checking
|
|
100
112
|
|
|
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.
|
|
113
|
+
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.
|
|
102
114
|
|
|
103
115
|
`useTrans()` now works correctly across hydration (5.15.0): the hydration
|
|
104
116
|
payload ships a `translations` key with the active locale's keywords, and
|
|
@@ -144,7 +156,7 @@ export default {
|
|
|
144
156
|
request.setLocaleCode("ar");
|
|
145
157
|
```
|
|
146
158
|
|
|
147
|
-
After this, every `
|
|
159
|
+
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").
|
|
148
160
|
|
|
149
161
|
## Localized columns — `getLocalized`
|
|
150
162
|
|
|
@@ -241,6 +253,7 @@ The error's message is locale-aware — the `inject-request-context` middleware
|
|
|
241
253
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
242
254
|
import { type CreateProductSchema } from "../schema/create-product.schema";
|
|
243
255
|
import { createProductService } from "../services/create-product.service";
|
|
256
|
+
import { t } from "@warlock.js/core";
|
|
244
257
|
|
|
245
258
|
export const createProductController: GuardedRequestHandler<CreateProductSchema> = async ({
|
|
246
259
|
request,
|
|
@@ -248,7 +261,7 @@ export const createProductController: GuardedRequestHandler<CreateProductSchema>
|
|
|
248
261
|
}) => {
|
|
249
262
|
const product = await createProductService(request.validated());
|
|
250
263
|
return response.success({
|
|
251
|
-
message:
|
|
264
|
+
message: t("products.created"),
|
|
252
265
|
product,
|
|
253
266
|
});
|
|
254
267
|
};
|
|
@@ -288,11 +301,11 @@ await Product.create({
|
|
|
288
301
|
GET /products/42?locale=ar
|
|
289
302
|
```
|
|
290
303
|
|
|
291
|
-
`request.getLocaleCode()` returns `"ar"` — every `
|
|
304
|
+
`request.getLocaleCode()` returns `"ar"` — every `t(...)` and resource-output `getLocalized` returns Arabic. No app code changes required.
|
|
292
305
|
|
|
293
306
|
## Gotchas
|
|
294
307
|
|
|
295
|
-
- **Locales files are auto-loaded — don't import them.** `src/app/<module>/utils/locales.
|
|
308
|
+
- **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.
|
|
296
309
|
- **Group names should match module URL slugs.** `groupedTranslations("products", ...)` for the `/products` module. Mismatch breaks the convention but doesn't throw.
|
|
297
310
|
- **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.
|
|
298
311
|
- **`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.
|
|
@@ -305,8 +318,8 @@ GET /products/42?locale=ar
|
|
|
305
318
|
|
|
306
319
|
- [`send-response/SKILL.md`](../send-response/SKILL.md) — error helpers that pair with translated messages (`response.notFound({ error: t("...") })`).
|
|
307
320
|
- [`define-resource/SKILL.md`](../define-resource/SKILL.md) — using `getLocalized` inside resource output for clean per-locale responses.
|
|
308
|
-
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.
|
|
309
|
-
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.
|
|
321
|
+
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.json` file is part of the generated module scaffold.
|
|
322
|
+
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — `utils/locales.json` is auto-loaded; the suffix is mandatory.
|
|
310
323
|
|
|
311
324
|
## Route-owned web translations (5.17)
|
|
312
325
|
|
|
@@ -320,6 +333,6 @@ GET /products/42?locale=ar
|
|
|
320
333
|
}
|
|
321
334
|
```
|
|
322
335
|
|
|
323
|
-
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()
|
|
336
|
+
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.
|
|
324
337
|
|
|
325
|
-
When a route-locales snapshot applies, `
|
|
338
|
+
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.
|
|
@@ -13,6 +13,9 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
13
13
|
2. **Several files are auto-loaded — never `import` them.** The dev-server file watcher and the production builder categorize these as "special" and load them on boot.
|
|
14
14
|
- `src/app/<module>/routes.ts` — route declarations
|
|
15
15
|
- `src/app/<module>/main.ts` — per-module one-time setup
|
|
16
|
+
- `src/app/<module>/worker.ts` — worker-only setup (queue job definitions,
|
|
17
|
+
`scheduler.addJob(...)`, `scheduler.start()`); loaded only for the
|
|
18
|
+
`worker` role, and always loaded by `warlock dev`
|
|
16
19
|
- `src/app/main.ts` — **project-level** one-time application setup (global hooks and other app initialization; connector instances belong in `warlock.config.ts > connectors`)
|
|
17
20
|
- `src/app/<module>/events/*.ts(x)` — **any** `.ts(x)` file inside the `events/` folder (the `*.event.ts` suffix is convention, not a framework requirement)
|
|
18
21
|
- `src/app/<module>/utils/locales.ts` — module translations via `groupedTranslations(...)`
|
|
@@ -33,7 +36,7 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
33
36
|
```ts title="src/app/<module>/schema/create-<thing>.schema.ts"
|
|
34
37
|
import { v, type Infer } from "@warlock.js/seal";
|
|
35
38
|
|
|
36
|
-
export const createThingSchema = v.object({
|
|
39
|
+
export const createThingSchema = v.object({/* … */});
|
|
37
40
|
export type CreateThingSchema = Infer<typeof createThingSchema>;
|
|
38
41
|
```
|
|
39
42
|
|
|
@@ -42,13 +45,17 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
42
45
|
import { type CreateThingSchema, createThingSchema } from "../schema/create-thing.schema";
|
|
43
46
|
import { createThingService } from "../services/create-thing.service";
|
|
44
47
|
|
|
45
|
-
export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
|
|
48
|
+
export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
|
|
49
|
+
request,
|
|
50
|
+
response,
|
|
51
|
+
}) => {
|
|
46
52
|
const thing = await createThingService(request.validated());
|
|
47
53
|
return response.success({ thing });
|
|
48
54
|
};
|
|
49
55
|
|
|
50
56
|
createThingController.validation = { schema: createThingSchema };
|
|
51
57
|
```
|
|
58
|
+
|
|
52
59
|
8. **Model getters beat `.get<T>("field")`.** Add a typed getter on the model rather than scattering `.get<string>("name")` casts across call sites.
|
|
53
60
|
|
|
54
61
|
## Module layout (the standard subfolders)
|
|
@@ -67,26 +74,27 @@ src/app/<module>/
|
|
|
67
74
|
seeds/ seed data for `warlock seed`
|
|
68
75
|
routes.ts ← auto-loaded
|
|
69
76
|
main.ts ← auto-loaded once, one-time setup
|
|
77
|
+
worker.ts ← auto-loaded for the worker role only
|
|
70
78
|
```
|
|
71
79
|
|
|
72
80
|
Some older modules still have a `validation/` folder instead of `schema/` — historical drift. The framework treats them as plain folders either way; the generator emits to `schema/`.
|
|
73
81
|
|
|
74
82
|
## File naming (project-wide)
|
|
75
83
|
|
|
76
|
-
| Suffix
|
|
77
|
-
|
|
|
78
|
-
| `.controller.ts`
|
|
79
|
-
| `.service.ts`
|
|
80
|
-
| `.usecase.ts`
|
|
81
|
-
| `.model.ts`
|
|
82
|
-
| `.repository.ts`
|
|
83
|
-
| `.resource.ts`
|
|
84
|
-
| `.schema.ts`
|
|
85
|
-
| `.type.ts`
|
|
86
|
-
| `.contract.ts`
|
|
87
|
-
| `.event.ts`
|
|
88
|
-
| `.migration.ts`
|
|
89
|
-
| `.seed.ts`
|
|
84
|
+
| Suffix | Role | Example |
|
|
85
|
+
| ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
|
86
|
+
| `.controller.ts` | HTTP handler | `create-product.controller.ts` |
|
|
87
|
+
| `.service.ts` | Stateless business logic | `create-product.service.ts` |
|
|
88
|
+
| `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
|
|
89
|
+
| `.model.ts` | Cascade model class | `product.model.ts` |
|
|
90
|
+
| `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
|
|
91
|
+
| `.resource.ts` | `Resource` subclass | `product.resource.ts` |
|
|
92
|
+
| `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
|
|
93
|
+
| `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
|
|
94
|
+
| `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
|
|
95
|
+
| `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
|
|
96
|
+
| `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
|
|
97
|
+
| `.seed.ts` | seed data | `products.seed.ts` |
|
|
90
98
|
|
|
91
99
|
Filenames are kebab-case. Class names are PascalCase. Function names are camelCase. Table/collection names are snake_case (plural).
|
|
92
100
|
|
|
@@ -97,8 +105,8 @@ The scaffolded `tsconfig.json` defines:
|
|
|
97
105
|
```jsonc
|
|
98
106
|
{
|
|
99
107
|
"paths": {
|
|
100
|
-
"app/*": ["./src/app/*"]
|
|
101
|
-
}
|
|
108
|
+
"app/*": ["./src/app/*"],
|
|
109
|
+
},
|
|
102
110
|
}
|
|
103
111
|
```
|
|
104
112
|
|