@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
package/llms-full.txt
CHANGED
|
@@ -618,8 +618,8 @@ export class UsersRestful extends Restful<User> implements RouteResource {
|
|
|
618
618
|
public validation = {
|
|
619
619
|
create: {
|
|
620
620
|
schema: v.object({
|
|
621
|
-
name: v.string().
|
|
622
|
-
email: v.email()
|
|
621
|
+
name: v.string().min(2),
|
|
622
|
+
email: v.email(),
|
|
623
623
|
}),
|
|
624
624
|
},
|
|
625
625
|
update: {
|
|
@@ -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:
|
|
@@ -3148,6 +3158,7 @@ type TracingContext = {
|
|
|
3148
3158
|
type TracingPhaseInfo = {
|
|
3149
3159
|
name: string;
|
|
3150
3160
|
durationMs: number;
|
|
3161
|
+
startedAt?: number; // epoch milliseconds; supplied when the phase started
|
|
3151
3162
|
attrs?: Record<string, unknown>;
|
|
3152
3163
|
};
|
|
3153
3164
|
|
|
@@ -3166,29 +3177,52 @@ type TracingHooks = {
|
|
|
3166
3177
|
|
|
3167
3178
|
Every verb is optional — a hook that only wants phase spans need not implement
|
|
3168
3179
|
`onRequestStart`/`onRequestEnd`. Register as many hooks as you like via
|
|
3169
|
-
`hooks: TracingHooks[]`; each fires independently.
|
|
3180
|
+
`hooks: TracingHooks[]`; each fires independently. Use `registerTracingHooks()`
|
|
3181
|
+
when the observer is installed by application code rather than static config:
|
|
3182
|
+
|
|
3183
|
+
```ts
|
|
3184
|
+
import { registerTracingHooks, type TracingHooks } from "@warlock.js/core";
|
|
3185
|
+
|
|
3186
|
+
const hooks: TracingHooks = {
|
|
3187
|
+
onPhase(ctx, phase) {
|
|
3188
|
+
console.log(ctx.traceId, phase.name, phase.startedAt);
|
|
3189
|
+
},
|
|
3190
|
+
};
|
|
3191
|
+
|
|
3192
|
+
const unregister = registerTracingHooks(hooks);
|
|
3193
|
+
// Later, for example during plugin teardown:
|
|
3194
|
+
unregister();
|
|
3195
|
+
```
|
|
3196
|
+
|
|
3197
|
+
`registerTracingHooks(hooks): () => void` enables tracing immediately. Runtime
|
|
3198
|
+
hooks run after configured hooks; calling the returned unregister function more
|
|
3199
|
+
than once is safe. When a phase did not provide `startedAt`, the dispatcher
|
|
3200
|
+
derives it from its duration before delivering the callback.
|
|
3170
3201
|
|
|
3171
3202
|
## Phase names
|
|
3172
3203
|
|
|
3173
3204
|
`core` wires five phases into the request lifecycle, in this order, for every
|
|
3174
3205
|
HTTP request:
|
|
3175
3206
|
|
|
3176
|
-
| Phase
|
|
3177
|
-
|
|
|
3178
|
-
| `route.match`
|
|
3179
|
-
| `middleware`
|
|
3180
|
-
| `validation`
|
|
3181
|
-
| `handler`
|
|
3207
|
+
| Phase | Fires around |
|
|
3208
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
3209
|
+
| `route.match` | Resolving the incoming path/method to a registered route |
|
|
3210
|
+
| `middleware` | Each middleware in the route's chain — one `onPhase` call per middleware, with `attrs: { name, index }` |
|
|
3211
|
+
| `validation` | The route's input validation (`v.object(...)` / RESTful resource validation) |
|
|
3212
|
+
| `handler` | The route handler itself |
|
|
3182
3213
|
| `response.write` | The overall request span, closed once the response has settled (success or thrown error) — this is also where `onRequestEnd` fires |
|
|
3183
3214
|
|
|
3184
3215
|
`@warlock.js/web` page requests report through this same `onPhase` surface
|
|
3185
3216
|
instead of adding a separate hook API. They add three phases:
|
|
3186
3217
|
|
|
3187
|
-
| Phase
|
|
3188
|
-
|
|
|
3189
|
-
| `loader`
|
|
3190
|
-
| `render.shell`
|
|
3191
|
-
| `stream.end`
|
|
3218
|
+
| Phase | Fires around |
|
|
3219
|
+
| ----------------- | --------------------------------------------------------------------------------------------------- |
|
|
3220
|
+
| `loader` | Each loader level, once per app/layout/page, with `attrs: { level, layoutPath? }` |
|
|
3221
|
+
| `render.shell` | Time from render start until React's shell is ready to stream |
|
|
3222
|
+
| `stream.end` | The whole streamed response, including every `defer()` value settling |
|
|
3223
|
+
| `page.middleware` | Page middleware chain; `attrs: { count, outcome }`, where outcome is `next`, `response`, or `error` |
|
|
3224
|
+
| `page.cache` | Page-cache lookup; `attrs: { status }`, where status is `hit`, `miss`, or `bypass` |
|
|
3225
|
+
| `defer.settle` | A deferred key settling; `attrs: { key, status }`, where status is `fulfilled` or `rejected` |
|
|
3192
3226
|
|
|
3193
3227
|
## Trace id derivation
|
|
3194
3228
|
|
|
@@ -3282,9 +3316,8 @@ export default {
|
|
|
3282
3316
|
- **No OTel dependency, and none planned for `core`.** An OTel (or other
|
|
3283
3317
|
vendor) bridge is a separate, optional package that subscribes to these
|
|
3284
3318
|
hooks — never add `@opentelemetry/api` to `core` itself.
|
|
3285
|
-
-
|
|
3286
|
-
|
|
3287
|
-
phases fire in this release.
|
|
3319
|
+
- **Web phases share the same hook.** Do not create a second page-tracing
|
|
3320
|
+
integration: subscribe with `TracingHooks.onPhase`.
|
|
3288
3321
|
- **`route` is `undefined` until routing has matched.** There's no path where
|
|
3289
3322
|
a hook fires before that, but code branching on `ctx.route` for an
|
|
3290
3323
|
early-failing request (e.g. a 404 with no match) must handle `undefined`.
|
|
@@ -3620,11 +3653,11 @@ Boots the framework in dev mode: file watcher, HMR-style module reload, on-disk
|
|
|
3620
3653
|
|
|
3621
3654
|
### Flags
|
|
3622
3655
|
|
|
3623
|
-
| Flag | Default | Purpose
|
|
3624
|
-
| --------------------- | ------- |
|
|
3656
|
+
| Flag | Default | Purpose |
|
|
3657
|
+
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
3625
3658
|
| `--fresh, -f` | off | Delete `.warlock/manifest.json` before start — forces a full re-parse from disk. Use after odd boot states. |
|
|
3626
|
-
| `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config.
|
|
3627
|
-
| `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config.
|
|
3659
|
+
| `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config. |
|
|
3660
|
+
| `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config. |
|
|
3628
3661
|
|
|
3629
3662
|
When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.*` value applies. When passed, the flag wins.
|
|
3630
3663
|
|
|
@@ -3632,14 +3665,14 @@ When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.
|
|
|
3632
3665
|
|
|
3633
3666
|
Once the server is ready, `warlock dev` listens for single keypresses:
|
|
3634
3667
|
|
|
3635
|
-
| Key | Does
|
|
3636
|
-
| -------- |
|
|
3637
|
-
| `r` | Restart the server on a fresh process.
|
|
3638
|
-
| `c` | Clear the console.
|
|
3639
|
-
| `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`.
|
|
3640
|
-
| `h` | Print the shortcuts that are armed right now.
|
|
3668
|
+
| Key | Does |
|
|
3669
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3670
|
+
| `r` | Restart the server on a fresh process. |
|
|
3671
|
+
| `c` | Clear the console. |
|
|
3672
|
+
| `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`. |
|
|
3673
|
+
| `h` | Print the shortcuts that are armed right now. |
|
|
3641
3674
|
| `u` | **Only while an update notice is showing** — update every `@warlock.js/*` package, install, and restart. See [`update-packages/SKILL.md`](../update-packages/SKILL.md). |
|
|
3642
|
-
| `Ctrl+C` | Graceful shutdown, unchanged.
|
|
3675
|
+
| `Ctrl+C` | Graceful shutdown, unchanged. |
|
|
3643
3676
|
|
|
3644
3677
|
A "restart" is a fresh process (`r` and `u` both use it) — see [The supervisor](#the-supervisor) below for how that works.
|
|
3645
3678
|
|
|
@@ -3659,7 +3692,7 @@ shell
|
|
|
3659
3692
|
|
|
3660
3693
|
The supervisor spawns the worker with `stdio: "inherit"` and mirrors its exit code. To restart, the worker shuts down cleanly — freeing the http port — and exits `75`; the supervisor spawns a replacement. **The tree never gets deeper**, however many restarts happen.
|
|
3661
3694
|
|
|
3662
|
-
The supervisor is chosen in the `dev` command's `preAction`, which runs
|
|
3695
|
+
The supervisor is chosen in the `dev` command's `preAction`, which runs _before_ the preloaders — deliberately, so the supervisor never loads config or starts connectors. A supervisor holding a second database connection open for the session would be a real bug, not a cosmetic one.
|
|
3663
3696
|
|
|
3664
3697
|
Signals: `SIGINT` reaches the worker directly (same process group), so the supervisor ignores it and only notes that the next exit is final — acting on it would exit the parent while the worker was still draining. `SIGTERM` / `SIGHUP` are forwarded explicitly, since they don't propagate to the group on Windows. After any worker exits, the supervisor puts stdin back out of raw mode in case the worker was killed before it could.
|
|
3665
3698
|
|
|
@@ -3673,13 +3706,13 @@ A worker that dies **after running healthily for at least 5s** — OOM, a native
|
|
|
3673
3706
|
14:31:02 Development server was killed by SIGSEGV — restarting.
|
|
3674
3707
|
```
|
|
3675
3708
|
|
|
3676
|
-
A worker that dies
|
|
3709
|
+
A worker that dies _sooner_ than that failed to **boot** — a broken config, a port already taken — and it has already printed why. Restarting there would just reprint the same error and bury it, so the supervisor mirrors the exit code and stops.
|
|
3677
3710
|
|
|
3678
|
-
A busy port is now caught
|
|
3711
|
+
A busy port is now caught _before_ the bind attempt: the HTTP connector calls `assertPortIsAvailable(port, host)` immediately before `listen()`, so the failure is `Port <port> is already in use on <host>. Stop the dev server (or whatever else is listening on port <port>) and run again, or start on a free port…` — not a raw `EADDRINUSE` surfacing from inside Fastify. Same connector, same preflight, for `warlock start`.
|
|
3679
3712
|
|
|
3680
3713
|
Flapping is capped: more than 3 crashes inside 60s and the supervisor gives up rather than restarting behind your back.
|
|
3681
3714
|
|
|
3682
|
-
An explicit restart (`r`, `u`, a config change) is a
|
|
3715
|
+
An explicit restart (`r`, `u`, a config change) is a _request_, not a crash, so the uptime rule never swallows it.
|
|
3683
3716
|
|
|
3684
3717
|
### Restart on config change
|
|
3685
3718
|
|
|
@@ -3695,7 +3728,7 @@ Set `devServer.restartOnConfigChange: false` for the previous behaviour (a warni
|
|
|
3695
3728
|
|
|
3696
3729
|
On first boot `warlock dev` bundles its ESM loader hook and writes it to **your project's** `.warlock/loader-hook.mjs` — the hook runs in a fresh Node worker thread with no TypeScript loader of its own, so it has to be plain, pre-bundled ESM.
|
|
3697
3730
|
|
|
3698
|
-
Because that file lives in your directory rather than core's, every npm import inside it is rewritten at generation time to an **absolute path resolved from core's own install**. A bare `import "esbuild"` there would resolve from
|
|
3731
|
+
Because that file lives in your directory rather than core's, every npm import inside it is rewritten at generation time to an **absolute path resolved from core's own install**. A bare `import "esbuild"` there would resolve from _your_ `node_modules`, and `esbuild` / `get-tsconfig` are core's dependencies, not yours.
|
|
3699
3732
|
|
|
3700
3733
|
:::note[Fixed in 4.9.2 — pnpm users]
|
|
3701
3734
|
Before 4.9.2 those imports were left bare. npm and yarn hoist every transitive dependency into one flat tree, so they resolved by accident; pnpm's strict layout does not, and the dev server failed with `ERR_MODULE_NOT_FOUND: Cannot find package 'esbuild'`. The workaround was declaring `esbuild` and `get-tsconfig` in your own `package.json` — no longer needed, and you can drop them.
|
|
@@ -3757,12 +3790,13 @@ esbuild bundle of the app down to a single JS file in `dist/`. No flags — ever
|
|
|
3757
3790
|
```ts title="warlock.config.ts"
|
|
3758
3791
|
export default defineConfig({
|
|
3759
3792
|
build: {
|
|
3760
|
-
outdir: "dist",
|
|
3761
|
-
outFile: "app.js",
|
|
3762
|
-
minify: true,
|
|
3763
|
-
sourcemap: true,
|
|
3764
|
-
singleBundle: false,
|
|
3765
|
-
esmShim: true,
|
|
3793
|
+
outdir: "dist", // default — relative or absolute
|
|
3794
|
+
outFile: "app.js", // default — bundle filename
|
|
3795
|
+
minify: true, // default — esbuild minify
|
|
3796
|
+
sourcemap: true, // default — true | false | "inline" | "linked"
|
|
3797
|
+
singleBundle: false, // default — one runnable file, deps inlined
|
|
3798
|
+
esmShim: true, // default — require/__filename/__dirname for bundled CJS
|
|
3799
|
+
routeRegistrationTimeoutMs: 30_000, // default — route-registration child deadline
|
|
3766
3800
|
},
|
|
3767
3801
|
});
|
|
3768
3802
|
```
|
|
@@ -3774,8 +3808,9 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
|
|
|
3774
3808
|
- **`minify: false`** — flip to debug a production-only bug. Larger bundle, readable stack traces.
|
|
3775
3809
|
- **`sourcemap: "inline"`** — embed the source map in the bundle. Useful when your error reporter only captures the bundle and can't fetch a `.map` sidecar.
|
|
3776
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).
|
|
3777
|
-
- **`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
|
|
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.
|
|
3778
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.
|
|
3779
3814
|
|
|
3780
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.
|
|
3781
3816
|
|
|
@@ -3833,6 +3868,41 @@ npx warlock start --inspect # → spawns node --enable-source-maps
|
|
|
3833
3868
|
npx warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
|
|
3834
3869
|
```
|
|
3835
3870
|
|
|
3871
|
+
### Deploy one build in parts
|
|
3872
|
+
|
|
3873
|
+
One production build can run as API, page, and background-worker processes.
|
|
3874
|
+
Omit `--role` to run every role (the existing default); combine roles with a
|
|
3875
|
+
comma-separated list:
|
|
3876
|
+
|
|
3877
|
+
```bash
|
|
3878
|
+
npx warlock start --role=api
|
|
3879
|
+
npx warlock start --role=web --sites=admin,storefront
|
|
3880
|
+
npx warlock start --role=worker
|
|
3881
|
+
npx warlock start --role=web,api
|
|
3882
|
+
```
|
|
3883
|
+
|
|
3884
|
+
`--sites=a,b` is valid only with the `web` role and installs the listed site
|
|
3885
|
+
keys' pages (shared non-site pages remain installed). Unknown roles, an empty
|
|
3886
|
+
list, `--sites` without `web`, and unknown site keys fail boot rather than
|
|
3887
|
+
silently serving the wrong traffic.
|
|
3888
|
+
|
|
3889
|
+
An API or web role binds HTTP. A worker-only process binds no HTTP port; it
|
|
3890
|
+
loads worker work, starts queue workers and the scheduler there, and its
|
|
3891
|
+
readiness banner reports `worker ready`. `warlock dev` always runs every role.
|
|
3892
|
+
|
|
3893
|
+
Inside application code, inspect the roles resolved at bootstrap:
|
|
3894
|
+
|
|
3895
|
+
```ts
|
|
3896
|
+
import { Application } from "@warlock.js/core";
|
|
3897
|
+
|
|
3898
|
+
if (Application.hasRole("worker")) {
|
|
3899
|
+
// worker-only integration
|
|
3900
|
+
}
|
|
3901
|
+
|
|
3902
|
+
Application.roles; // ReadonlySet<"api" | "web" | "worker">
|
|
3903
|
+
Application.sites; // ReadonlySet<string> | undefined (undefined = all sites)
|
|
3904
|
+
```
|
|
3905
|
+
|
|
3836
3906
|
Everything you pass after `start` is forwarded to the spawned Node process. Use this to attach a debugger (`--inspect`), tune memory (`--max-old-space-size`), or pass any other Node flag without editing the command.
|
|
3837
3907
|
|
|
3838
3908
|
### Source maps
|
|
@@ -3875,9 +3945,9 @@ Nothing else is needed for pnpm. Warlock never requires an app to declare a pack
|
|
|
3875
3945
|
|
|
3876
3946
|
**A success line on stdout means the app is serving requests.** That is a contract, not a convention, and you can build a CI gate or a health probe on it.
|
|
3877
3947
|
|
|
3878
|
-
| Stream | Carries
|
|
3879
|
-
| ---------- |
|
|
3880
|
-
| **stdout** | the started banner, and start failures. Nothing else.
|
|
3948
|
+
| Stream | Carries |
|
|
3949
|
+
| ---------- | -------------------------------------------------------------------------------------- |
|
|
3950
|
+
| **stdout** | the started banner, and start failures. Nothing else. |
|
|
3881
3951
|
| **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
|
|
3882
3952
|
|
|
3883
3953
|
The started banner prints **only** when the running application reports a completed boot — not when the command starts, not when the child is spawned. A child that dies before reporting is a failed start: the failure is written to **both** streams (stderr for humans and log collectors, stdout so a supervisor greping for the banner finds a failure rather than silence), and `warlock start` exits non-zero **even when the child itself exited `0`**.
|
|
@@ -3913,7 +3983,7 @@ else is listening on port 3000) and run again, or start on a free port — e.g.
|
|
|
3913
3983
|
startHttpTestServer({ port: 3001 }).
|
|
3914
3984
|
```
|
|
3915
3985
|
|
|
3916
|
-
**The process exits `78` (`EX_CONFIG`), and `warlock dev`'s supervisor treats that as terminal — it prints once and stops rather than restarting.** That code is the counterpart of `75`, which a worker uses to ask for a fresh process. The distinction is the point: a code error is worth restarting, and a
|
|
3986
|
+
**The process exits `78` (`EX_CONFIG`), and `warlock dev`'s supervisor treats that as terminal — it prints once and stops rather than restarting.** That code is the counterpart of `75`, which a worker uses to ask for a fresh process. The distinction is the point: a code error is worth restarting, and a _startup precondition_ — a port held by something else, a port value that is not a port, a boot validator that rejected — cannot change because we tried again six seconds later. Retrying one only reprints the diagnostic and then scrolls it away.
|
|
3917
3987
|
|
|
3918
3988
|
`EACCES` on the port is treated the same way as `EADDRINUSE` (a privileged port you may not bind is also "not available"); anything else the probe throws is rethrown untouched.
|
|
3919
3989
|
|
|
@@ -3928,7 +3998,7 @@ Configured http.port "03999" normalised to 3999.
|
|
|
3928
3998
|
`warlock start` spawns the bundle with an IPC channel and sets `WARLOCK_BOOT_SIGNAL=1` on it. `Application.markBooted()` — which the production entry calls after the late-phase connectors (http, socket) are up — sends one versioned message and closes the channel:
|
|
3929
3999
|
|
|
3930
4000
|
```ts
|
|
3931
|
-
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port
|
|
4001
|
+
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port?, roles }
|
|
3932
4002
|
```
|
|
3933
4003
|
|
|
3934
4004
|
Three consequences worth knowing:
|
|
@@ -3943,11 +4013,11 @@ If you need the same fact inside the app, use `Application.onceBooted()` / `Appl
|
|
|
3943
4013
|
|
|
3944
4014
|
`Application.environment` and `Application.runtimeStrategy` are separate axes:
|
|
3945
4015
|
|
|
3946
|
-
| Mode
|
|
3947
|
-
|
|
|
3948
|
-
| `warlock dev`
|
|
3949
|
-
| `warlock build`
|
|
3950
|
-
| `warlock start`
|
|
4016
|
+
| Mode | `environment` | `runtimeStrategy` | How |
|
|
4017
|
+
| --------------- | ------------------ | ----------------- | ------------------------------------- |
|
|
4018
|
+
| `warlock dev` | `development`* | `development` | preload force-sets `runtimeStrategy` |
|
|
4019
|
+
| `warlock build` | n/a (no app boots) | n/a | only loads warlock.config.ts |
|
|
4020
|
+
| `warlock start` | `production`* | `production`* | usually set via `NODE_ENV` in the env |
|
|
3951
4021
|
|
|
3952
4022
|
`*` — `environment` follows `NODE_ENV`. The dev command doesn't force it, but the default in most projects is `development`. `start` doesn't force it either; deployments set `NODE_ENV=production` themselves.
|
|
3953
4023
|
|
|
@@ -4006,7 +4076,7 @@ export default defineConfig({
|
|
|
4006
4076
|
|
|
4007
4077
|
CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
|
|
4008
4078
|
|
|
4009
|
-
**This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated
|
|
4079
|
+
**This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated _before_ any `.env` file was read, so every `env()` call in it returned its default — silently, under every command. If you copied this recipe earlier and concluded that `BUILD_OUT` was ignored, it was.
|
|
4010
4080
|
|
|
4011
4081
|
**Which file the value comes from is decided by `NODE_ENV`, and no Warlock command sets it.** `env()` reads `.env.<NODE_ENV>` when that file exists and falls back to plain `.env`. So:
|
|
4012
4082
|
|
|
@@ -4086,10 +4156,7 @@ Two APIs over the same engine. The fluent `Mail` builder reads top-to-bottom in
|
|
|
4086
4156
|
```ts
|
|
4087
4157
|
import { Mail } from "@warlock.js/core";
|
|
4088
4158
|
|
|
4089
|
-
await Mail.to("user@example.com")
|
|
4090
|
-
.subject("Welcome!")
|
|
4091
|
-
.text("Thanks for joining.")
|
|
4092
|
-
.send();
|
|
4159
|
+
await Mail.to("user@example.com").subject("Welcome!").text("Thanks for joining.").send();
|
|
4093
4160
|
```
|
|
4094
4161
|
|
|
4095
4162
|
Or:
|
|
@@ -4133,10 +4200,25 @@ For named mailers (multi-provider apps), export a `MailersConfig` instead:
|
|
|
4133
4200
|
|
|
4134
4201
|
```ts
|
|
4135
4202
|
const config: MailersConfig = {
|
|
4136
|
-
default: {
|
|
4203
|
+
default: {
|
|
4204
|
+
host: "smtp.sendgrid.net",
|
|
4205
|
+
port: 587,
|
|
4206
|
+
username: "apikey",
|
|
4207
|
+
password: env("SENDGRID_KEY"),
|
|
4208
|
+
},
|
|
4137
4209
|
mailers: {
|
|
4138
|
-
marketing: {
|
|
4139
|
-
|
|
4210
|
+
marketing: {
|
|
4211
|
+
host: "smtp.mailchimp.com",
|
|
4212
|
+
port: 587,
|
|
4213
|
+
username: env("MC_USER"),
|
|
4214
|
+
password: env("MC_PASS"),
|
|
4215
|
+
},
|
|
4216
|
+
transactional: {
|
|
4217
|
+
host: "smtp.postmark.com",
|
|
4218
|
+
port: 587,
|
|
4219
|
+
username: env("PM_USER"),
|
|
4220
|
+
password: env("PM_PASS"),
|
|
4221
|
+
},
|
|
4140
4222
|
},
|
|
4141
4223
|
};
|
|
4142
4224
|
```
|
|
@@ -4168,9 +4250,9 @@ The mail pipeline switches behavior on a global mode:
|
|
|
4168
4250
|
```ts
|
|
4169
4251
|
import { setMailMode } from "@warlock.js/core";
|
|
4170
4252
|
|
|
4171
|
-
setMailMode("production");
|
|
4172
|
-
setMailMode("development");
|
|
4173
|
-
setMailMode("test");
|
|
4253
|
+
setMailMode("production"); // actually send (default)
|
|
4254
|
+
setMailMode("development"); // log subject + recipient, no send
|
|
4255
|
+
setMailMode("test"); // capture to test mailbox, no send
|
|
4174
4256
|
```
|
|
4175
4257
|
|
|
4176
4258
|
Production sends. Development logs and returns a fake-success `MailResult`. Test captures every call into an in-memory mailbox (cleared between tests) and returns success — handy for asserting "the welcome email got sent" without hitting SMTP.
|
|
@@ -4183,7 +4265,7 @@ Production sends. Development logs and returns a fake-success `MailResult`. Test
|
|
|
4183
4265
|
|
|
4184
4266
|
```ts
|
|
4185
4267
|
await Mail.to("a@example.com")
|
|
4186
|
-
.to(["a@example.com", "b@example.com"])
|
|
4268
|
+
.to(["a@example.com", "b@example.com"]) // override
|
|
4187
4269
|
.cc("manager@example.com")
|
|
4188
4270
|
.bcc("audit@example.com")
|
|
4189
4271
|
.replyTo("support@example.com")
|
|
@@ -4224,15 +4306,15 @@ await Mail.to("u@e.com")
|
|
|
4224
4306
|
|
|
4225
4307
|
### Other knobs
|
|
4226
4308
|
|
|
4227
|
-
| Method
|
|
4228
|
-
|
|
|
4229
|
-
| `.priority("high" \| "normal" \| "low")`
|
|
4230
|
-
| `.headers({ "X-Foo": "bar" })`
|
|
4231
|
-
| `.header("X-Foo", "bar")`
|
|
4232
|
-
| `.tags(["welcome"])` / `.tag("transactional")` | categorization tags
|
|
4233
|
-
| `.correlationId("req-123")`
|
|
4234
|
-
| `.config(MailConfigurations)`
|
|
4235
|
-
| `.mailer("marketing")`
|
|
4309
|
+
| Method | Purpose |
|
|
4310
|
+
| ---------------------------------------------- | ------------------------------------- |
|
|
4311
|
+
| `.priority("high" \| "normal" \| "low")` | priority header |
|
|
4312
|
+
| `.headers({ "X-Foo": "bar" })` | replace custom headers |
|
|
4313
|
+
| `.header("X-Foo", "bar")` | add one header |
|
|
4314
|
+
| `.tags(["welcome"])` / `.tag("transactional")` | categorization tags |
|
|
4315
|
+
| `.correlationId("req-123")` | tracking id (logged with sends) |
|
|
4316
|
+
| `.config(MailConfigurations)` | one-off override of the global config |
|
|
4317
|
+
| `.mailer("marketing")` | route via a named mailer from config |
|
|
4236
4318
|
|
|
4237
4319
|
### Per-mail event handlers
|
|
4238
4320
|
|
|
@@ -4245,9 +4327,9 @@ await Mail.to("u@e.com")
|
|
|
4245
4327
|
.beforeSending((mail) => {
|
|
4246
4328
|
// mutate `mail` or return false to cancel
|
|
4247
4329
|
})
|
|
4248
|
-
.onSent((mail, result, error) => {})
|
|
4249
|
-
.onSuccess((mail, result) => {})
|
|
4250
|
-
.onError((mail, error) => {})
|
|
4330
|
+
.onSent((mail, result, error) => {}) // always fires after attempt
|
|
4331
|
+
.onSuccess((mail, result) => {}) // only on success
|
|
4332
|
+
.onError((mail, error) => {}) // only on failure
|
|
4251
4333
|
.send();
|
|
4252
4334
|
```
|
|
4253
4335
|
|
|
@@ -4317,6 +4399,23 @@ mailEvents.onBeforeSending((mail) => {
|
|
|
4317
4399
|
|
|
4318
4400
|
Global hooks fire for **every** mail. Per-mail handlers from the builder / `sendMail` fire only for that one send.
|
|
4319
4401
|
|
|
4402
|
+
### Observe captured development and test mail
|
|
4403
|
+
|
|
4404
|
+
Development and test sends create a `CapturedMail` and synchronously emit the
|
|
4405
|
+
global `mail.captured` event. Subscribe with `mailEvents.onCaptured`:
|
|
4406
|
+
|
|
4407
|
+
```ts
|
|
4408
|
+
import { mailEvents, type CapturedMail } from "@warlock.js/core";
|
|
4409
|
+
|
|
4410
|
+
mailEvents.onCaptured((mail: CapturedMail) => {
|
|
4411
|
+
console.log(mail.id, mail.options.subject);
|
|
4412
|
+
});
|
|
4413
|
+
```
|
|
4414
|
+
|
|
4415
|
+
`CapturedMail.id` is the mail's generated or supplied id. This event is for
|
|
4416
|
+
development/test tooling; production sends do not emit it. Listener failures
|
|
4417
|
+
are logged and never interrupt mail delivery.
|
|
4418
|
+
|
|
4320
4419
|
For correlated tracking of a specific mail, generate an id and subscribe by id:
|
|
4321
4420
|
|
|
4322
4421
|
```ts
|
|
@@ -4370,10 +4469,11 @@ Captured mail shape:
|
|
|
4370
4469
|
|
|
4371
4470
|
```ts
|
|
4372
4471
|
type CapturedMail = {
|
|
4373
|
-
|
|
4472
|
+
id: string;
|
|
4473
|
+
options: MailOptions; // original payload
|
|
4374
4474
|
normalized: NormalizedMail; // post-normalization (arrays, resolved from)
|
|
4375
4475
|
timestamp: Date;
|
|
4376
|
-
result?: MailResult;
|
|
4476
|
+
result?: MailResult; // in test mode, always success
|
|
4377
4477
|
error?: MailError;
|
|
4378
4478
|
};
|
|
4379
4479
|
```
|
|
@@ -5177,7 +5277,7 @@ Some tests need the full stack: route matching, middleware chain, validation, co
|
|
|
5177
5277
|
|
|
5178
5278
|
`startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
|
|
5179
5279
|
|
|
5180
|
-
⚠ **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.**
|
|
5181
5281
|
|
|
5182
5282
|
## The shape
|
|
5183
5283
|
|
|
@@ -5218,7 +5318,7 @@ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests"
|
|
|
5218
5318
|
- Loads `warlock.config.ts` + bootstrap + `filesOrchestrator` (without file watching).
|
|
5219
5319
|
- Loads `src/config/*.ts`.
|
|
5220
5320
|
- Loads every module (`routes.ts`, `main.ts`, `events/*.ts`).
|
|
5221
|
-
- Starts **ALL** connectors, including HTTP.
|
|
5321
|
+
- Starts **ALL** connectors, including HTTP, by default.
|
|
5222
5322
|
|
|
5223
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.
|
|
5224
5324
|
|
|
@@ -5238,6 +5338,14 @@ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`
|
|
|
5238
5338
|
|
|
5239
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.
|
|
5240
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
|
+
|
|
5241
5349
|
### The port is preflighted
|
|
5242
5350
|
|
|
5243
5351
|
Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
|
|
@@ -5293,8 +5401,8 @@ import { defineConfig } from "vitest/config";
|
|
|
5293
5401
|
export default defineConfig({
|
|
5294
5402
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
5295
5403
|
test: {
|
|
5296
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
5297
|
-
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
|
|
5298
5406
|
environment: "node",
|
|
5299
5407
|
globals: false,
|
|
5300
5408
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -5310,6 +5418,17 @@ Both files (and this config, with `lowerStage3Decorators()` first so decorated m
|
|
|
5310
5418
|
|
|
5311
5419
|
**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.
|
|
5312
5420
|
|
|
5421
|
+
### Keep Vitest setup out of development
|
|
5422
|
+
|
|
5423
|
+
`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:
|
|
5424
|
+
|
|
5425
|
+
```ts
|
|
5426
|
+
// vite.config.ts
|
|
5427
|
+
if (process.env.VITEST) {
|
|
5428
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
5429
|
+
}
|
|
5430
|
+
```
|
|
5431
|
+
|
|
5313
5432
|
## HTTP request helpers
|
|
5314
5433
|
|
|
5315
5434
|
Everything is built on native `fetch` — no extra dependency, no special wire format.
|
|
@@ -5319,7 +5438,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
|
|
|
5319
5438
|
```ts
|
|
5320
5439
|
import { getTestServerUrl } from "@warlock.js/core/tests";
|
|
5321
5440
|
|
|
5322
|
-
const url = getTestServerUrl();
|
|
5441
|
+
const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
|
|
5323
5442
|
```
|
|
5324
5443
|
|
|
5325
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.
|
|
@@ -5357,9 +5476,9 @@ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
|
|
|
5357
5476
|
const body = await parseJsonResponse<MyShape>(response);
|
|
5358
5477
|
|
|
5359
5478
|
// Assert status + parse in one call
|
|
5360
|
-
const body = await expectJson<MyShape>(response);
|
|
5361
|
-
const body = await expectJson<MyShape>(response, 201);
|
|
5362
|
-
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)
|
|
5363
5482
|
```
|
|
5364
5483
|
|
|
5365
5484
|
`expectJson` throws with the actual status + response body when the assertion fails — no chasing "got 500" messages without context.
|
|
@@ -5395,7 +5514,7 @@ describe("Products API — happy path", () => {
|
|
|
5395
5514
|
|
|
5396
5515
|
```ts
|
|
5397
5516
|
it("rejects missing required fields with 400", async () => {
|
|
5398
|
-
const response = await testPost("/products", { name: "" });
|
|
5517
|
+
const response = await testPost("/products", { name: "" }); // price missing
|
|
5399
5518
|
const body = await expectJson<{ errors: Array<{ key: string }> }>(response, 400);
|
|
5400
5519
|
|
|
5401
5520
|
expect(body.errors.some((e) => e.key === "price")).toBe(true);
|
|
@@ -5520,13 +5639,13 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
|
|
|
5520
5639
|
|
|
5521
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.
|
|
5522
5641
|
|
|
5523
|
-
⛔ **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.**
|
|
5524
5643
|
|
|
5525
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.**
|
|
5526
5645
|
|
|
5527
|
-
⚠ **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.
|
|
5528
5647
|
|
|
5529
|
-
⚠ **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.**
|
|
5530
5649
|
|
|
5531
5650
|
## The shape
|
|
5532
5651
|
|
|
@@ -5543,7 +5662,7 @@ describe("registerUserService", () => {
|
|
|
5543
5662
|
});
|
|
5544
5663
|
|
|
5545
5664
|
expect(user.get("email")).toBe("test@example.com");
|
|
5546
|
-
expect(user.get("password")).not.toBe("secret");
|
|
5665
|
+
expect(user.get("password")).not.toBe("secret"); // hashed by useHashedPassword()
|
|
5547
5666
|
|
|
5548
5667
|
const found = await usersRepository.first({ email: "test@example.com" });
|
|
5549
5668
|
expect(found).toBeDefined();
|
|
@@ -5568,18 +5687,18 @@ What it does (in order):
|
|
|
5568
5687
|
3. Runs `bootstrap()` — env, app, prestart hooks.
|
|
5569
5688
|
4. Initializes the `filesOrchestrator` (module/route/config discovery, no file watching).
|
|
5570
5689
|
5. Loads all `src/config/*.ts` files.
|
|
5571
|
-
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.
|
|
5572
5691
|
7. Starts the chosen connectors — but **never `http`** when you pass a boolean. HTTP is the global-setup's job.
|
|
5573
5692
|
|
|
5574
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.
|
|
5575
5694
|
|
|
5576
5695
|
### The `connectors` parameter
|
|
5577
5696
|
|
|
5578
|
-
| Value
|
|
5579
|
-
|
|
|
5580
|
-
| `true`
|
|
5581
|
-
| `false`
|
|
5582
|
-
| `["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. |
|
|
5583
5702
|
|
|
5584
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.
|
|
5585
5704
|
|
|
@@ -5595,25 +5714,25 @@ export default testsConfigurations;
|
|
|
5595
5714
|
|
|
5596
5715
|
⚠ **BREAKING in 4.14.0 — the precedence flipped.**
|
|
5597
5716
|
|
|
5598
|
-
|
|
|
5599
|
-
|
|
5600
|
-
| **4.13.0 and earlier** | `tests.connectors` config **>** `setupTest({ connectors })` parameter **>** `true`
|
|
5601
|
-
| **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` |
|
|
5602
5721
|
|
|
5603
|
-
**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.
|
|
5604
5723
|
|
|
5605
5724
|
**"Explicit" means you supplied a non-`undefined` value.** Both of these fall through to config:
|
|
5606
5725
|
|
|
5607
5726
|
```ts
|
|
5608
|
-
await setupTest();
|
|
5609
|
-
await setupTest({});
|
|
5727
|
+
await setupTest(); // → tests.connectors, else true
|
|
5728
|
+
await setupTest({}); // → tests.connectors, else true
|
|
5610
5729
|
await setupTest({ connectors: undefined }); // → tests.connectors, else true — NOT "start none"
|
|
5611
5730
|
```
|
|
5612
5731
|
|
|
5613
5732
|
The `undefined` rule is deliberate: an optional variable that happens to be `undefined` must not silently erase your project config.
|
|
5614
5733
|
|
|
5615
5734
|
```ts
|
|
5616
|
-
await setupTest({ connectors: false });
|
|
5735
|
+
await setupTest({ connectors: false }); // → none, even if config says otherwise
|
|
5617
5736
|
await setupTest({ connectors: ["database"] }); // → exactly that, even if config differs
|
|
5618
5737
|
```
|
|
5619
5738
|
|
|
@@ -5640,7 +5759,7 @@ afterAll(teardownTest);
|
|
|
5640
5759
|
⛔ **Three things changed here in 4.14.0. If you generated this file earlier, replace all three — it is not a comment fix.**
|
|
5641
5760
|
|
|
5642
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.
|
|
5643
|
-
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".
|
|
5644
5763
|
3. **The comment used to say `Per-Worker Test Setup` / "Runs in EACH Vitest worker thread".** False — see the top of this skill.
|
|
5645
5764
|
|
|
5646
5765
|
```ts title="vite.config.ts"
|
|
@@ -5651,8 +5770,8 @@ import { defineConfig } from "vitest/config";
|
|
|
5651
5770
|
export default defineConfig({
|
|
5652
5771
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
5653
5772
|
test: {
|
|
5654
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
5655
|
-
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
|
|
5656
5775
|
environment: "node",
|
|
5657
5776
|
globals: false,
|
|
5658
5777
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -5662,6 +5781,17 @@ export default defineConfig({
|
|
|
5662
5781
|
|
|
5663
5782
|
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).
|
|
5664
5783
|
|
|
5784
|
+
### Keep Vitest setup out of development
|
|
5785
|
+
|
|
5786
|
+
`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:
|
|
5787
|
+
|
|
5788
|
+
```ts
|
|
5789
|
+
// vite.config.ts
|
|
5790
|
+
if (process.env.VITEST) {
|
|
5791
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
5792
|
+
}
|
|
5793
|
+
```
|
|
5794
|
+
|
|
5665
5795
|
Tests live colocated with the module: `src/app/<module>/tests/*.test.ts`. The `include` pattern picks them up.
|
|
5666
5796
|
|
|
5667
5797
|
## Patterns
|
|
@@ -5687,9 +5817,9 @@ describe("createProductService", () => {
|
|
|
5687
5817
|
it("rejects duplicate names", async () => {
|
|
5688
5818
|
await Product.create({ name: "Existing", price: 10 });
|
|
5689
5819
|
|
|
5690
|
-
await expect(
|
|
5691
|
-
|
|
5692
|
-
)
|
|
5820
|
+
await expect(createProductService({ name: "Existing", price: 20 })).rejects.toThrow(
|
|
5821
|
+
/already exists/i,
|
|
5822
|
+
);
|
|
5693
5823
|
});
|
|
5694
5824
|
});
|
|
5695
5825
|
```
|
|
@@ -5759,7 +5889,7 @@ afterEach(async () => {
|
|
|
5759
5889
|
|
|
5760
5890
|
Vitest runs the tests within one file sequentially, so an `afterEach` truncate gives each test a clean slate.
|
|
5761
5891
|
|
|
5762
|
-
⚠ **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.
|
|
5763
5893
|
|
|
5764
5894
|
### Skipping connectors for pure logic tests
|
|
5765
5895
|
|
|
@@ -5769,7 +5899,7 @@ import { setupTest } from "@warlock.js/core/tests";
|
|
|
5769
5899
|
import { slugify } from "../utils/slugify";
|
|
5770
5900
|
|
|
5771
5901
|
beforeAll(async () => {
|
|
5772
|
-
await setupTest({ connectors: false });
|
|
5902
|
+
await setupTest({ connectors: false }); // starts no connectors at all
|
|
5773
5903
|
});
|
|
5774
5904
|
|
|
5775
5905
|
describe("slugify", () => {
|
|
@@ -5796,7 +5926,7 @@ beforeAll(async () => {
|
|
|
5796
5926
|
});
|
|
5797
5927
|
|
|
5798
5928
|
afterAll(async () => {
|
|
5799
|
-
await teardownTest();
|
|
5929
|
+
await teardownTest(); // ← REQUIRED, see below
|
|
5800
5930
|
});
|
|
5801
5931
|
```
|
|
5802
5932
|
|
|
@@ -5817,16 +5947,16 @@ afterAll(async () => {
|
|
|
5817
5947
|
|
|
5818
5948
|
`setupTest` / `teardownTest` are a pair. **The harness that calls one owns calling the other in the same context.**
|
|
5819
5949
|
|
|
5820
|
-
| Call
|
|
5821
|
-
|
|
5822
|
-
| `setupTest(x)` while idle
|
|
5823
|
-
| `setupTest(x)` while already ready with the **same** effective options
|
|
5824
|
-
| `setupTest(y)` while ready or starting with **different** effective options | ⛔ **rejects**, naming active vs requested
|
|
5825
|
-
| two concurrent `setupTest(x)` calls
|
|
5826
|
-
| `setupTest` after a failed setup
|
|
5827
|
-
| `teardownTest()` while idle
|
|
5828
|
-
| two concurrent `teardownTest()` calls
|
|
5829
|
-
| `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 |
|
|
5830
5960
|
|
|
5831
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.
|
|
5832
5962
|
|
|
@@ -5841,7 +5971,7 @@ afterAll(async () => {
|
|
|
5841
5971
|
```ts title="src/config/tests.ts"
|
|
5842
5972
|
const testsConfigurations = {
|
|
5843
5973
|
connectors: ["database", "logger"],
|
|
5844
|
-
setupTimeout: 120000,
|
|
5974
|
+
setupTimeout: 120000, // milliseconds — this is the default
|
|
5845
5975
|
};
|
|
5846
5976
|
|
|
5847
5977
|
export default testsConfigurations;
|
|
@@ -5860,9 +5990,9 @@ in `src/config/tests.ts` — milliseconds, default 120000.
|
|
|
5860
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.
|
|
5861
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.
|
|
5862
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.
|
|
5863
|
-
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.
|
|
5864
5994
|
|
|
5865
|
-
⚠ **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.
|
|
5866
5996
|
|
|
5867
5997
|
### State is per worker runtime, not per module
|
|
5868
5998
|
|
|
@@ -5893,6 +6023,16 @@ In both cases the guard's scope matches the resource's scope, which is the point
|
|
|
5893
6023
|
- [`warlock-conventions/SKILL.md`](../warlock-conventions/SKILL.md) — where tests live in a module (`tests/*.test.ts`).
|
|
5894
6024
|
- [`write-cli-command/SKILL.md`](../write-cli-command/SKILL.md) — `warlock add test` for the initial scaffold.
|
|
5895
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
|
+
|
|
5896
6036
|
|
|
5897
6037
|
## update-packages `@warlock.js/core/update-packages/SKILL.md`
|
|
5898
6038
|
|
|
@@ -6654,7 +6794,7 @@ cors: {
|
|
|
6654
6794
|
|
|
6655
6795
|
---
|
|
6656
6796
|
name: use-localization
|
|
6657
|
-
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`.'
|
|
6658
6798
|
---
|
|
6659
6799
|
|
|
6660
6800
|
# Warlock — translate keys + pick localized values
|
|
@@ -6669,7 +6809,7 @@ Both pivot on **the request's locale** — auto-detected from headers / query st
|
|
|
6669
6809
|
## The shape
|
|
6670
6810
|
|
|
6671
6811
|
```ts
|
|
6672
|
-
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.
|
|
6812
|
+
// 1. Declare keys (auto-loaded from src/app/<module>/utils/locales.json)
|
|
6673
6813
|
import { groupedTranslations } from "@mongez/localization";
|
|
6674
6814
|
|
|
6675
6815
|
groupedTranslations("products", {
|
|
@@ -6681,8 +6821,7 @@ groupedTranslations("products", {
|
|
|
6681
6821
|
import { t } from "@warlock.js/core";
|
|
6682
6822
|
|
|
6683
6823
|
throw new ResourceNotFoundError(t("products.notFound"));
|
|
6684
|
-
|
|
6685
|
-
return response.success({ message: request.t("products.created") });
|
|
6824
|
+
return response.success({ message: t("products.created") });
|
|
6686
6825
|
|
|
6687
6826
|
// 3. Pick a value from a localized-array column
|
|
6688
6827
|
import { getLocalized } from "@warlock.js/core";
|
|
@@ -6691,7 +6830,26 @@ const name = getLocalized(product.get("name_translations"));
|
|
|
6691
6830
|
// → reads the current request's locale, returns the matching value
|
|
6692
6831
|
```
|
|
6693
6832
|
|
|
6694
|
-
## 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`
|
|
6695
6853
|
|
|
6696
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.
|
|
6697
6855
|
|
|
@@ -6728,31 +6886,25 @@ t("products.outOfStock", { name: "Pen", count: 0 });
|
|
|
6728
6886
|
|
|
6729
6887
|
Placeholders use the `:name` syntax. The lookup helper substitutes them from the second argument.
|
|
6730
6888
|
|
|
6731
|
-
## Looking up translations —
|
|
6889
|
+
## Looking up translations — always `t()`
|
|
6732
6890
|
|
|
6733
|
-
|
|
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`.
|
|
6734
6892
|
|
|
6735
6893
|
```ts
|
|
6736
|
-
// 1. Top-level helper — works inside or outside a request.
|
|
6737
|
-
// Inside a request: uses request's locale.
|
|
6738
|
-
// Outside: uses the global default locale.
|
|
6739
6894
|
import { t } from "@warlock.js/core";
|
|
6740
6895
|
|
|
6741
6896
|
t("products.notFound");
|
|
6742
6897
|
t("products.outOfStock", { name: "Pen", count: 0 });
|
|
6743
|
-
|
|
6744
|
-
// 2. On the request object — explicit, scoped to that request.
|
|
6745
|
-
request.t("products.notFound");
|
|
6746
|
-
|
|
6747
|
-
// 3. Alias for request.t — same behavior.
|
|
6748
|
-
request.trans("products.notFound");
|
|
6749
6898
|
```
|
|
6750
6899
|
|
|
6751
|
-
|
|
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`.
|
|
6752
6904
|
|
|
6753
6905
|
### Web `useTrans()` key checking
|
|
6754
6906
|
|
|
6755
|
-
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.
|
|
6756
6908
|
|
|
6757
6909
|
`useTrans()` now works correctly across hydration (5.15.0): the hydration
|
|
6758
6910
|
payload ships a `translations` key with the active locale's keywords, and
|
|
@@ -6798,7 +6950,7 @@ export default {
|
|
|
6798
6950
|
request.setLocaleCode("ar");
|
|
6799
6951
|
```
|
|
6800
6952
|
|
|
6801
|
-
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").
|
|
6802
6954
|
|
|
6803
6955
|
## Localized columns — `getLocalized`
|
|
6804
6956
|
|
|
@@ -6895,6 +7047,7 @@ The error's message is locale-aware — the `inject-request-context` middleware
|
|
|
6895
7047
|
import { type GuardedRequestHandler } from "app/auth/requests/guarded.request";
|
|
6896
7048
|
import { type CreateProductSchema } from "../schema/create-product.schema";
|
|
6897
7049
|
import { createProductService } from "../services/create-product.service";
|
|
7050
|
+
import { t } from "@warlock.js/core";
|
|
6898
7051
|
|
|
6899
7052
|
export const createProductController: GuardedRequestHandler<CreateProductSchema> = async ({
|
|
6900
7053
|
request,
|
|
@@ -6902,7 +7055,7 @@ export const createProductController: GuardedRequestHandler<CreateProductSchema>
|
|
|
6902
7055
|
}) => {
|
|
6903
7056
|
const product = await createProductService(request.validated());
|
|
6904
7057
|
return response.success({
|
|
6905
|
-
message:
|
|
7058
|
+
message: t("products.created"),
|
|
6906
7059
|
product,
|
|
6907
7060
|
});
|
|
6908
7061
|
};
|
|
@@ -6942,11 +7095,11 @@ await Product.create({
|
|
|
6942
7095
|
GET /products/42?locale=ar
|
|
6943
7096
|
```
|
|
6944
7097
|
|
|
6945
|
-
`request.getLocaleCode()` returns `"ar"` — every `
|
|
7098
|
+
`request.getLocaleCode()` returns `"ar"` — every `t(...)` and resource-output `getLocalized` returns Arabic. No app code changes required.
|
|
6946
7099
|
|
|
6947
7100
|
## Gotchas
|
|
6948
7101
|
|
|
6949
|
-
- **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.
|
|
6950
7103
|
- **Group names should match module URL slugs.** `groupedTranslations("products", ...)` for the `/products` module. Mismatch breaks the convention but doesn't throw.
|
|
6951
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.
|
|
6952
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.
|
|
@@ -6959,8 +7112,8 @@ GET /products/42?locale=ar
|
|
|
6959
7112
|
|
|
6960
7113
|
- [`send-response/SKILL.md`](../send-response/SKILL.md) — error helpers that pair with translated messages (`response.notFound({ error: t("...") })`).
|
|
6961
7114
|
- [`define-resource/SKILL.md`](../define-resource/SKILL.md) — using `getLocalized` inside resource output for clean per-locale responses.
|
|
6962
|
-
- [`create-module/SKILL.md`](../create-module/SKILL.md) — the `utils/locales.
|
|
6963
|
-
- [`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.
|
|
6964
7117
|
|
|
6965
7118
|
## Route-owned web translations (5.17)
|
|
6966
7119
|
|
|
@@ -6974,9 +7127,9 @@ GET /products/42?locale=ar
|
|
|
6974
7127
|
}
|
|
6975
7128
|
```
|
|
6976
7129
|
|
|
6977
|
-
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.
|
|
6978
7131
|
|
|
6979
|
-
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.
|
|
6980
7133
|
|
|
6981
7134
|
|
|
6982
7135
|
## use-middleware `@warlock.js/core/use-middleware/SKILL.md`
|
|
@@ -8417,6 +8570,9 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
8417
8570
|
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.
|
|
8418
8571
|
- `src/app/<module>/routes.ts` — route declarations
|
|
8419
8572
|
- `src/app/<module>/main.ts` — per-module one-time setup
|
|
8573
|
+
- `src/app/<module>/worker.ts` — worker-only setup (queue job definitions,
|
|
8574
|
+
`scheduler.addJob(...)`, `scheduler.start()`); loaded only for the
|
|
8575
|
+
`worker` role, and always loaded by `warlock dev`
|
|
8420
8576
|
- `src/app/main.ts` — **project-level** one-time application setup (global hooks and other app initialization; connector instances belong in `warlock.config.ts > connectors`)
|
|
8421
8577
|
- `src/app/<module>/events/*.ts(x)` — **any** `.ts(x)` file inside the `events/` folder (the `*.event.ts` suffix is convention, not a framework requirement)
|
|
8422
8578
|
- `src/app/<module>/utils/locales.ts` — module translations via `groupedTranslations(...)`
|
|
@@ -8437,7 +8593,7 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
8437
8593
|
```ts title="src/app/<module>/schema/create-<thing>.schema.ts"
|
|
8438
8594
|
import { v, type Infer } from "@warlock.js/seal";
|
|
8439
8595
|
|
|
8440
|
-
export const createThingSchema = v.object({
|
|
8596
|
+
export const createThingSchema = v.object({/* … */});
|
|
8441
8597
|
export type CreateThingSchema = Infer<typeof createThingSchema>;
|
|
8442
8598
|
```
|
|
8443
8599
|
|
|
@@ -8446,13 +8602,17 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
8446
8602
|
import { type CreateThingSchema, createThingSchema } from "../schema/create-thing.schema";
|
|
8447
8603
|
import { createThingService } from "../services/create-thing.service";
|
|
8448
8604
|
|
|
8449
|
-
export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
|
|
8605
|
+
export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
|
|
8606
|
+
request,
|
|
8607
|
+
response,
|
|
8608
|
+
}) => {
|
|
8450
8609
|
const thing = await createThingService(request.validated());
|
|
8451
8610
|
return response.success({ thing });
|
|
8452
8611
|
};
|
|
8453
8612
|
|
|
8454
8613
|
createThingController.validation = { schema: createThingSchema };
|
|
8455
8614
|
```
|
|
8615
|
+
|
|
8456
8616
|
8. **Model getters beat `.get<T>("field")`.** Add a typed getter on the model rather than scattering `.get<string>("name")` casts across call sites.
|
|
8457
8617
|
|
|
8458
8618
|
## Module layout (the standard subfolders)
|
|
@@ -8471,26 +8631,27 @@ src/app/<module>/
|
|
|
8471
8631
|
seeds/ seed data for `warlock seed`
|
|
8472
8632
|
routes.ts ← auto-loaded
|
|
8473
8633
|
main.ts ← auto-loaded once, one-time setup
|
|
8634
|
+
worker.ts ← auto-loaded for the worker role only
|
|
8474
8635
|
```
|
|
8475
8636
|
|
|
8476
8637
|
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/`.
|
|
8477
8638
|
|
|
8478
8639
|
## File naming (project-wide)
|
|
8479
8640
|
|
|
8480
|
-
| Suffix
|
|
8481
|
-
|
|
|
8482
|
-
| `.controller.ts`
|
|
8483
|
-
| `.service.ts`
|
|
8484
|
-
| `.usecase.ts`
|
|
8485
|
-
| `.model.ts`
|
|
8486
|
-
| `.repository.ts`
|
|
8487
|
-
| `.resource.ts`
|
|
8488
|
-
| `.schema.ts`
|
|
8489
|
-
| `.type.ts`
|
|
8490
|
-
| `.contract.ts`
|
|
8491
|
-
| `.event.ts`
|
|
8492
|
-
| `.migration.ts`
|
|
8493
|
-
| `.seed.ts`
|
|
8641
|
+
| Suffix | Role | Example |
|
|
8642
|
+
| ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
|
8643
|
+
| `.controller.ts` | HTTP handler | `create-product.controller.ts` |
|
|
8644
|
+
| `.service.ts` | Stateless business logic | `create-product.service.ts` |
|
|
8645
|
+
| `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
|
|
8646
|
+
| `.model.ts` | Cascade model class | `product.model.ts` |
|
|
8647
|
+
| `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
|
|
8648
|
+
| `.resource.ts` | `Resource` subclass | `product.resource.ts` |
|
|
8649
|
+
| `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
|
|
8650
|
+
| `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
|
|
8651
|
+
| `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
|
|
8652
|
+
| `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
|
|
8653
|
+
| `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
|
|
8654
|
+
| `.seed.ts` | seed data | `products.seed.ts` |
|
|
8494
8655
|
|
|
8495
8656
|
Filenames are kebab-case. Class names are PascalCase. Function names are camelCase. Table/collection names are snake_case (plural).
|
|
8496
8657
|
|
|
@@ -8501,8 +8662,8 @@ The scaffolded `tsconfig.json` defines:
|
|
|
8501
8662
|
```jsonc
|
|
8502
8663
|
{
|
|
8503
8664
|
"paths": {
|
|
8504
|
-
"app/*": ["./src/app/*"]
|
|
8505
|
-
}
|
|
8665
|
+
"app/*": ["./src/app/*"],
|
|
8666
|
+
},
|
|
8506
8667
|
}
|
|
8507
8668
|
```
|
|
8508
8669
|
|
|
@@ -9147,16 +9308,16 @@ Run it: `npx warlock users.promote --email=hasan@example.com` (or `npx warlock u
|
|
|
9147
9308
|
|
|
9148
9309
|
## `CLICommandOptions` — the factory input
|
|
9149
9310
|
|
|
9150
|
-
| Field | Type
|
|
9151
|
-
| ------------- |
|
|
9152
|
-
| `name` | `string`
|
|
9153
|
-
| `description` | `string`
|
|
9154
|
-
| `alias` | `string`
|
|
9155
|
-
| `action` | `(data) => void \| Promise`
|
|
9156
|
-
| `preAction` | `(data) => void \| Promise`
|
|
9157
|
-
| `preload` | `CLICommandPreload`
|
|
9158
|
-
| `persistent` | `boolean`
|
|
9159
|
-
| `options` | `CLICommandOption[]`
|
|
9311
|
+
| Field | Type | Required | Notes |
|
|
9312
|
+
| ------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
|
|
9313
|
+
| `name` | `string` | yes | Dot notation OK (`db.seed`, `jwt.generate`). May include positional placeholders (`name <arg>`). |
|
|
9314
|
+
| `description` | `string` | | Shown in `warlock --help` and `warlock <cmd> --help`. |
|
|
9315
|
+
| `alias` | `string` | | Short name (`m` for `migrate`). |
|
|
9316
|
+
| `action` | `(data) => void \| Promise` | yes | Runs after preloaders. `data` is `{ args, options }`. |
|
|
9317
|
+
| `preAction` | `(data) => void \| Promise` | | Runs **before** preloaders — banner, input validation. |
|
|
9318
|
+
| `preload` | `CLICommandPreload` | | What to load before `action` runs. See below. |
|
|
9319
|
+
| `persistent` | `boolean` | | `true` for long-running commands (dev server). Skips the auto-exit. |
|
|
9320
|
+
| `options` | `CLICommandOption[]` | | Flag definitions. See below. |
|
|
9160
9321
|
|
|
9161
9322
|
## Options — flag shape
|
|
9162
9323
|
|
|
@@ -9258,29 +9419,29 @@ The framework ships a fixed set of commands you call but don't author. Knowing t
|
|
|
9258
9419
|
|
|
9259
9420
|
### Database + migrations
|
|
9260
9421
|
|
|
9261
|
-
| Command
|
|
9262
|
-
|
|
|
9263
|
-
| `warlock migrate`
|
|
9264
|
-
| `warlock seed`
|
|
9265
|
-
| `warlock create-database <name>` | bare positional `<name>`
|
|
9266
|
-
| `warlock drop.tables`
|
|
9267
|
-
| `warlock db.indexes`
|
|
9422
|
+
| Command | Flags / args | Preloads |
|
|
9423
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
|
9424
|
+
| `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
|
|
9425
|
+
| `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
|
|
9426
|
+
| `warlock create-database <name>` | bare positional `<name>` | database |
|
|
9427
|
+
| `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
|
|
9428
|
+
| `warlock db.indexes` | builds DB indexes for every registered model | database |
|
|
9268
9429
|
|
|
9269
9430
|
**Asking what will run next.** `warlock migrate --list` prints executed migrations and then the pending ones **in execution order**. Do not derive the pending set by differencing `--all` against `--list`: `--all` globs `src/app` only, while `--list` reads the migrations table, which also holds migrations that packages register through `database.migrations` (`@warlock.js/auth` contributes two). The difference under-counts pending, in the direction that says "safe to proceed".
|
|
9270
9431
|
|
|
9271
9432
|
`--list` is a report and always exits `0`. `--pending` is a gate, and its exit code is its whole API:
|
|
9272
9433
|
|
|
9273
|
-
| Exit | Meaning
|
|
9274
|
-
| ---- |
|
|
9434
|
+
| Exit | Meaning |
|
|
9435
|
+
| ---- | ------------------------- |
|
|
9275
9436
|
| `0` | computed, nothing pending |
|
|
9276
|
-
| `1` | computed, N pending
|
|
9437
|
+
| `1` | computed, N pending |
|
|
9277
9438
|
| `2` | **could not be computed** |
|
|
9278
9439
|
|
|
9279
9440
|
```bash
|
|
9280
9441
|
warlock migrate --pending && ./deploy.sh
|
|
9281
9442
|
```
|
|
9282
9443
|
|
|
9283
|
-
`2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means
|
|
9444
|
+
`2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means _run them_ and the other means _stop_. When the migration files cannot be loaded, both commands print `Pending: unavailable — <reason>` beneath a complete executed listing rather than reporting `0`.
|
|
9284
9445
|
|
|
9285
9446
|
**If you are writing a command that reports on pending migrations:** register migrations first. `listPendingMigrations()` filters the runner's registry, so a caller that has not loaded anything gets `[]` — which reads as "nothing pending" and is not the same claim.
|
|
9286
9447
|
|
|
@@ -9306,36 +9467,41 @@ warlock generate (alias: g) — interactive picker
|
|
|
9306
9467
|
|
|
9307
9468
|
`warlock add <features...>` installs framework-adjacent features through a curated registry — bundles npm dependencies + scaffold files + tsconfig patches + package.json scripts in one call. Featured names (from `add-command.action.ts`):
|
|
9308
9469
|
|
|
9309
|
-
| Feature | Installs
|
|
9310
|
-
| ------------- |
|
|
9311
|
-
| `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json`
|
|
9312
|
-
| `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration
|
|
9313
|
-
| `react` | `react` + `react-dom` + types
|
|
9470
|
+
| Feature | Installs |
|
|
9471
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9472
|
+
| `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
|
|
9473
|
+
| `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
|
|
9474
|
+
| `react` | `react` + `react-dom` + types |
|
|
9314
9475
|
| `bull-board` | `@bull-board/api` + `@bull-board/fastify`; writes a `dashboard` block to `src/config/queue.ts`. `requires: ["queue"]` — adds the `queue` feature first automatically when it's missing |
|
|
9315
|
-
| `image` | `sharp` (for the `Image` class)
|
|
9316
|
-
| `mail` | `nodemailer` + types
|
|
9317
|
-
| `ses` | `@aws-sdk/client-sesv2`
|
|
9318
|
-
| `mongodb` | `mongodb` driver
|
|
9319
|
-
| `postgres` | `pg`
|
|
9320
|
-
| `mysql` | `mysql2`
|
|
9321
|
-
| `redis` | `redis`
|
|
9322
|
-
| `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner`
|
|
9323
|
-
| `scheduler` | `@warlock.js/scheduler`
|
|
9324
|
-
| `swagger` | `@warlock.js/swagger`
|
|
9325
|
-
| `postman` | `@warlock.js/postman`
|
|
9326
|
-
| `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts`
|
|
9327
|
-
| `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts`
|
|
9476
|
+
| `image` | `sharp` (for the `Image` class) |
|
|
9477
|
+
| `mail` | `nodemailer` + types |
|
|
9478
|
+
| `ses` | `@aws-sdk/client-sesv2` |
|
|
9479
|
+
| `mongodb` | `mongodb` driver |
|
|
9480
|
+
| `postgres` | `pg` |
|
|
9481
|
+
| `mysql` | `mysql2` |
|
|
9482
|
+
| `redis` | `redis` |
|
|
9483
|
+
| `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner` |
|
|
9484
|
+
| `scheduler` | `@warlock.js/scheduler` |
|
|
9485
|
+
| `swagger` | `@warlock.js/swagger` |
|
|
9486
|
+
| `postman` | `@warlock.js/postman` |
|
|
9487
|
+
| `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts` |
|
|
9488
|
+
| `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts` |
|
|
9489
|
+
| `devtools` | `@warlock.js/devtools` as a dev dependency; `warlock dev` auto-loads its development dashboard at `/__warlock` |
|
|
9328
9490
|
|
|
9329
9491
|
Run `warlock add --list` to see what's currently registered. Pass `--packageManager <yarn\|pnpm\|npm>` to override auto-detection (defaults to whichever lockfile is present).
|
|
9330
9492
|
|
|
9493
|
+
`warlock add devtools` never adds anything to `warlock.config.ts`. The package
|
|
9494
|
+
is loaded automatically by `warlock dev` when installed, and is omitted from a
|
|
9495
|
+
production install with dev dependencies excluded.
|
|
9496
|
+
|
|
9331
9497
|
The Web starter keeps the page's universal non-component setup in `src/web/index.register.ts`, while `src/web/index.page.tsx` re-exports `register`. Warlock still discovers and runs the hook from the page namespace in both SSR and the browser, but editing the page component remains inside React's Fast Refresh boundary. Do not move the `register()` declaration inline beside the component: React Refresh treats that non-component export as incompatible and reloads the page, losing component state.
|
|
9332
9498
|
|
|
9333
9499
|
### Misc
|
|
9334
9500
|
|
|
9335
|
-
| Command
|
|
9336
|
-
|
|
|
9337
|
-
| `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk
|
|
9338
|
-
| `warlock jwt.generate`
|
|
9501
|
+
| Command | Purpose |
|
|
9502
|
+
| ----------------------------------------------- | --------------------------------------------------------- |
|
|
9503
|
+
| `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk |
|
|
9504
|
+
| `warlock jwt.generate` | Generate a JWT secret in `.env` (from `@warlock.js/auth`) |
|
|
9339
9505
|
|
|
9340
9506
|
## Common patterns
|
|
9341
9507
|
|
|
@@ -9397,7 +9563,7 @@ export default command({
|
|
|
9397
9563
|
});
|
|
9398
9564
|
```
|
|
9399
9565
|
|
|
9400
|
-
`preAction` runs
|
|
9566
|
+
`preAction` runs _before_ preloaders — cheap way to bail out without spinning up the database.
|
|
9401
9567
|
|
|
9402
9568
|
## Gotchas
|
|
9403
9569
|
|
|
@@ -9405,7 +9571,7 @@ export default command({
|
|
|
9405
9571
|
- **Don't put logic at module top level.** The file gets imported during the commands scan (`warlock --warm-cache`). Anything outside `action` runs at scan time, possibly before any config or env is loaded.
|
|
9406
9572
|
- **Required options block execution.** If `required: true` and the user omits the flag, the framework prints `Missing required options:` and exits `1` before `action` runs.
|
|
9407
9573
|
- **Connectors are not free.** `connectors: true` boots the database, cache, storage, etc. For a print-version-and-exit command, leave `preload` undefined.
|
|
9408
|
-
- **`name` field with positional slots** (`name: "storage.put <localPath>"`) — the
|
|
9574
|
+
- **`name` field with positional slots** (`name: "storage.put <localPath>"`) — the _registered_ name is still `storage.put` (the first whitespace-separated token), so look up + alias work normally. Slots are only documentation/help-output.
|
|
9409
9575
|
- **Aliases must be unique across plugin + framework + project.** The first registration wins; later collisions silently overwrite the map entry.
|
|
9410
9576
|
|
|
9411
9577
|
## See also
|
|
@@ -10302,6 +10468,8 @@ Per-invocation callbacks fire first, then use-case-level, then globals. The retu
|
|
|
10302
10468
|
|
|
10303
10469
|
Don't reach for `useCase` for a 5-line service — the ceremony outweighs the benefit.
|
|
10304
10470
|
|
|
10471
|
+
For the plain-function flavour (`warlock generate.use-case <module>/<verb-noun>`) shared by a page action and a controller, see the guide [One use-case, two adapters](https://warlock.js.org/v/latest/core/guides/one-use-case-two-adapters/).
|
|
10472
|
+
|
|
10305
10473
|
## Gotchas
|
|
10306
10474
|
|
|
10307
10475
|
- **`name` must be unique.** The registry de-dupes by name; a duplicate in dev logs a warning and one wins. Use namespaced names: `"orders.placeOrder"`.
|