@warlock.js/core 5.23.2 → 5.25.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 +36 -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/drop-tables.command.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 +17 -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 +2 -1
- 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/manifest-manager.mjs.map +1 -1
- package/esm/dev-server/module-loader.mjs +10 -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 +3 -1
- 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/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.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/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/database/RequestLog.mjs.map +1 -1
- package/esm/http/index.d.mts +2 -1
- package/esm/http/index.mjs +2 -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/request.d.mts +7 -0
- package/esm/http/request.d.mts.map +1 -1
- package/esm/http/request.mjs +8 -0
- 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.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 +4 -2
- package/esm/index.mjs +4 -2
- 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/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 +28 -8
- 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 +2 -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/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 +9 -3
- 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 +264 -139
- package/package.json +15 -11
- package/skills/configure-app/SKILL.md +2 -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 +19 -8
- package/skills/test-service/SKILL.md +61 -40
- 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: {
|
|
@@ -3148,6 +3148,7 @@ type TracingContext = {
|
|
|
3148
3148
|
type TracingPhaseInfo = {
|
|
3149
3149
|
name: string;
|
|
3150
3150
|
durationMs: number;
|
|
3151
|
+
startedAt?: number; // epoch milliseconds; supplied when the phase started
|
|
3151
3152
|
attrs?: Record<string, unknown>;
|
|
3152
3153
|
};
|
|
3153
3154
|
|
|
@@ -3166,29 +3167,52 @@ type TracingHooks = {
|
|
|
3166
3167
|
|
|
3167
3168
|
Every verb is optional — a hook that only wants phase spans need not implement
|
|
3168
3169
|
`onRequestStart`/`onRequestEnd`. Register as many hooks as you like via
|
|
3169
|
-
`hooks: TracingHooks[]`; each fires independently.
|
|
3170
|
+
`hooks: TracingHooks[]`; each fires independently. Use `registerTracingHooks()`
|
|
3171
|
+
when the observer is installed by application code rather than static config:
|
|
3172
|
+
|
|
3173
|
+
```ts
|
|
3174
|
+
import { registerTracingHooks, type TracingHooks } from "@warlock.js/core";
|
|
3175
|
+
|
|
3176
|
+
const hooks: TracingHooks = {
|
|
3177
|
+
onPhase(ctx, phase) {
|
|
3178
|
+
console.log(ctx.traceId, phase.name, phase.startedAt);
|
|
3179
|
+
},
|
|
3180
|
+
};
|
|
3181
|
+
|
|
3182
|
+
const unregister = registerTracingHooks(hooks);
|
|
3183
|
+
// Later, for example during plugin teardown:
|
|
3184
|
+
unregister();
|
|
3185
|
+
```
|
|
3186
|
+
|
|
3187
|
+
`registerTracingHooks(hooks): () => void` enables tracing immediately. Runtime
|
|
3188
|
+
hooks run after configured hooks; calling the returned unregister function more
|
|
3189
|
+
than once is safe. When a phase did not provide `startedAt`, the dispatcher
|
|
3190
|
+
derives it from its duration before delivering the callback.
|
|
3170
3191
|
|
|
3171
3192
|
## Phase names
|
|
3172
3193
|
|
|
3173
3194
|
`core` wires five phases into the request lifecycle, in this order, for every
|
|
3174
3195
|
HTTP request:
|
|
3175
3196
|
|
|
3176
|
-
| Phase
|
|
3177
|
-
|
|
|
3178
|
-
| `route.match`
|
|
3179
|
-
| `middleware`
|
|
3180
|
-
| `validation`
|
|
3181
|
-
| `handler`
|
|
3197
|
+
| Phase | Fires around |
|
|
3198
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
3199
|
+
| `route.match` | Resolving the incoming path/method to a registered route |
|
|
3200
|
+
| `middleware` | Each middleware in the route's chain — one `onPhase` call per middleware, with `attrs: { name, index }` |
|
|
3201
|
+
| `validation` | The route's input validation (`v.object(...)` / RESTful resource validation) |
|
|
3202
|
+
| `handler` | The route handler itself |
|
|
3182
3203
|
| `response.write` | The overall request span, closed once the response has settled (success or thrown error) — this is also where `onRequestEnd` fires |
|
|
3183
3204
|
|
|
3184
3205
|
`@warlock.js/web` page requests report through this same `onPhase` surface
|
|
3185
3206
|
instead of adding a separate hook API. They add three phases:
|
|
3186
3207
|
|
|
3187
|
-
| Phase
|
|
3188
|
-
|
|
|
3189
|
-
| `loader`
|
|
3190
|
-
| `render.shell`
|
|
3191
|
-
| `stream.end`
|
|
3208
|
+
| Phase | Fires around |
|
|
3209
|
+
| ----------------- | --------------------------------------------------------------------------------------------------- |
|
|
3210
|
+
| `loader` | Each loader level, once per app/layout/page, with `attrs: { level, layoutPath? }` |
|
|
3211
|
+
| `render.shell` | Time from render start until React's shell is ready to stream |
|
|
3212
|
+
| `stream.end` | The whole streamed response, including every `defer()` value settling |
|
|
3213
|
+
| `page.middleware` | Page middleware chain; `attrs: { count, outcome }`, where outcome is `next`, `response`, or `error` |
|
|
3214
|
+
| `page.cache` | Page-cache lookup; `attrs: { status }`, where status is `hit`, `miss`, or `bypass` |
|
|
3215
|
+
| `defer.settle` | A deferred key settling; `attrs: { key, status }`, where status is `fulfilled` or `rejected` |
|
|
3192
3216
|
|
|
3193
3217
|
## Trace id derivation
|
|
3194
3218
|
|
|
@@ -3282,9 +3306,8 @@ export default {
|
|
|
3282
3306
|
- **No OTel dependency, and none planned for `core`.** An OTel (or other
|
|
3283
3307
|
vendor) bridge is a separate, optional package that subscribes to these
|
|
3284
3308
|
hooks — never add `@opentelemetry/api` to `core` itself.
|
|
3285
|
-
-
|
|
3286
|
-
|
|
3287
|
-
phases fire in this release.
|
|
3309
|
+
- **Web phases share the same hook.** Do not create a second page-tracing
|
|
3310
|
+
integration: subscribe with `TracingHooks.onPhase`.
|
|
3288
3311
|
- **`route` is `undefined` until routing has matched.** There's no path where
|
|
3289
3312
|
a hook fires before that, but code branching on `ctx.route` for an
|
|
3290
3313
|
early-failing request (e.g. a 404 with no match) must handle `undefined`.
|
|
@@ -3620,11 +3643,11 @@ Boots the framework in dev mode: file watcher, HMR-style module reload, on-disk
|
|
|
3620
3643
|
|
|
3621
3644
|
### Flags
|
|
3622
3645
|
|
|
3623
|
-
| Flag | Default | Purpose
|
|
3624
|
-
| --------------------- | ------- |
|
|
3646
|
+
| Flag | Default | Purpose |
|
|
3647
|
+
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
3625
3648
|
| `--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.
|
|
3649
|
+
| `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config. |
|
|
3650
|
+
| `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config. |
|
|
3628
3651
|
|
|
3629
3652
|
When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.*` value applies. When passed, the flag wins.
|
|
3630
3653
|
|
|
@@ -3632,14 +3655,14 @@ When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.
|
|
|
3632
3655
|
|
|
3633
3656
|
Once the server is ready, `warlock dev` listens for single keypresses:
|
|
3634
3657
|
|
|
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.
|
|
3658
|
+
| Key | Does |
|
|
3659
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
3660
|
+
| `r` | Restart the server on a fresh process. |
|
|
3661
|
+
| `c` | Clear the console. |
|
|
3662
|
+
| `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`. |
|
|
3663
|
+
| `h` | Print the shortcuts that are armed right now. |
|
|
3641
3664
|
| `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.
|
|
3665
|
+
| `Ctrl+C` | Graceful shutdown, unchanged. |
|
|
3643
3666
|
|
|
3644
3667
|
A "restart" is a fresh process (`r` and `u` both use it) — see [The supervisor](#the-supervisor) below for how that works.
|
|
3645
3668
|
|
|
@@ -3659,7 +3682,7 @@ shell
|
|
|
3659
3682
|
|
|
3660
3683
|
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
3684
|
|
|
3662
|
-
The supervisor is chosen in the `dev` command's `preAction`, which runs
|
|
3685
|
+
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
3686
|
|
|
3664
3687
|
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
3688
|
|
|
@@ -3673,13 +3696,13 @@ A worker that dies **after running healthily for at least 5s** — OOM, a native
|
|
|
3673
3696
|
14:31:02 Development server was killed by SIGSEGV — restarting.
|
|
3674
3697
|
```
|
|
3675
3698
|
|
|
3676
|
-
A worker that dies
|
|
3699
|
+
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
3700
|
|
|
3678
|
-
A busy port is now caught
|
|
3701
|
+
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
3702
|
|
|
3680
3703
|
Flapping is capped: more than 3 crashes inside 60s and the supervisor gives up rather than restarting behind your back.
|
|
3681
3704
|
|
|
3682
|
-
An explicit restart (`r`, `u`, a config change) is a
|
|
3705
|
+
An explicit restart (`r`, `u`, a config change) is a _request_, not a crash, so the uptime rule never swallows it.
|
|
3683
3706
|
|
|
3684
3707
|
### Restart on config change
|
|
3685
3708
|
|
|
@@ -3695,7 +3718,7 @@ Set `devServer.restartOnConfigChange: false` for the previous behaviour (a warni
|
|
|
3695
3718
|
|
|
3696
3719
|
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
3720
|
|
|
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
|
|
3721
|
+
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
3722
|
|
|
3700
3723
|
:::note[Fixed in 4.9.2 — pnpm users]
|
|
3701
3724
|
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 +3780,12 @@ esbuild bundle of the app down to a single JS file in `dist/`. No flags — ever
|
|
|
3757
3780
|
```ts title="warlock.config.ts"
|
|
3758
3781
|
export default defineConfig({
|
|
3759
3782
|
build: {
|
|
3760
|
-
outdir: "dist",
|
|
3761
|
-
outFile: "app.js",
|
|
3762
|
-
minify: true,
|
|
3763
|
-
sourcemap: true,
|
|
3764
|
-
singleBundle: false,
|
|
3765
|
-
esmShim: true,
|
|
3783
|
+
outdir: "dist", // default — relative or absolute
|
|
3784
|
+
outFile: "app.js", // default — bundle filename
|
|
3785
|
+
minify: true, // default — esbuild minify
|
|
3786
|
+
sourcemap: true, // default — true | false | "inline" | "linked"
|
|
3787
|
+
singleBundle: false, // default — one runnable file, deps inlined
|
|
3788
|
+
esmShim: true, // default — require/__filename/__dirname for bundled CJS
|
|
3766
3789
|
},
|
|
3767
3790
|
});
|
|
3768
3791
|
```
|
|
@@ -3774,7 +3797,7 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
|
|
|
3774
3797
|
- **`minify: false`** — flip to debug a production-only bug. Larger bundle, readable stack traces.
|
|
3775
3798
|
- **`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
3799
|
- **`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
|
|
3800
|
+
- **`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
3801
|
- **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
|
|
3779
3802
|
|
|
3780
3803
|
⚠ **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.
|
|
@@ -3833,6 +3856,41 @@ npx warlock start --inspect # → spawns node --enable-source-maps
|
|
|
3833
3856
|
npx warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
|
|
3834
3857
|
```
|
|
3835
3858
|
|
|
3859
|
+
### Deploy one build in parts
|
|
3860
|
+
|
|
3861
|
+
One production build can run as API, page, and background-worker processes.
|
|
3862
|
+
Omit `--role` to run every role (the existing default); combine roles with a
|
|
3863
|
+
comma-separated list:
|
|
3864
|
+
|
|
3865
|
+
```bash
|
|
3866
|
+
npx warlock start --role=api
|
|
3867
|
+
npx warlock start --role=web --sites=admin,storefront
|
|
3868
|
+
npx warlock start --role=worker
|
|
3869
|
+
npx warlock start --role=web,api
|
|
3870
|
+
```
|
|
3871
|
+
|
|
3872
|
+
`--sites=a,b` is valid only with the `web` role and installs the listed site
|
|
3873
|
+
keys' pages (shared non-site pages remain installed). Unknown roles, an empty
|
|
3874
|
+
list, `--sites` without `web`, and unknown site keys fail boot rather than
|
|
3875
|
+
silently serving the wrong traffic.
|
|
3876
|
+
|
|
3877
|
+
An API or web role binds HTTP. A worker-only process binds no HTTP port; it
|
|
3878
|
+
loads worker work, starts queue workers and the scheduler there, and its
|
|
3879
|
+
readiness banner reports `worker ready`. `warlock dev` always runs every role.
|
|
3880
|
+
|
|
3881
|
+
Inside application code, inspect the roles resolved at bootstrap:
|
|
3882
|
+
|
|
3883
|
+
```ts
|
|
3884
|
+
import { Application } from "@warlock.js/core";
|
|
3885
|
+
|
|
3886
|
+
if (Application.hasRole("worker")) {
|
|
3887
|
+
// worker-only integration
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
Application.roles; // ReadonlySet<"api" | "web" | "worker">
|
|
3891
|
+
Application.sites; // ReadonlySet<string> | undefined (undefined = all sites)
|
|
3892
|
+
```
|
|
3893
|
+
|
|
3836
3894
|
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
3895
|
|
|
3838
3896
|
### Source maps
|
|
@@ -3875,9 +3933,9 @@ Nothing else is needed for pnpm. Warlock never requires an app to declare a pack
|
|
|
3875
3933
|
|
|
3876
3934
|
**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
3935
|
|
|
3878
|
-
| Stream | Carries
|
|
3879
|
-
| ---------- |
|
|
3880
|
-
| **stdout** | the started banner, and start failures. Nothing else.
|
|
3936
|
+
| Stream | Carries |
|
|
3937
|
+
| ---------- | -------------------------------------------------------------------------------------- |
|
|
3938
|
+
| **stdout** | the started banner, and start failures. Nothing else. |
|
|
3881
3939
|
| **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
|
|
3882
3940
|
|
|
3883
3941
|
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 +3971,7 @@ else is listening on port 3000) and run again, or start on a free port — e.g.
|
|
|
3913
3971
|
startHttpTestServer({ port: 3001 }).
|
|
3914
3972
|
```
|
|
3915
3973
|
|
|
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
|
|
3974
|
+
**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
3975
|
|
|
3918
3976
|
`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
3977
|
|
|
@@ -3928,7 +3986,7 @@ Configured http.port "03999" normalised to 3999.
|
|
|
3928
3986
|
`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
3987
|
|
|
3930
3988
|
```ts
|
|
3931
|
-
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port
|
|
3989
|
+
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port?, roles }
|
|
3932
3990
|
```
|
|
3933
3991
|
|
|
3934
3992
|
Three consequences worth knowing:
|
|
@@ -3943,11 +4001,11 @@ If you need the same fact inside the app, use `Application.onceBooted()` / `Appl
|
|
|
3943
4001
|
|
|
3944
4002
|
`Application.environment` and `Application.runtimeStrategy` are separate axes:
|
|
3945
4003
|
|
|
3946
|
-
| Mode
|
|
3947
|
-
|
|
|
3948
|
-
| `warlock dev`
|
|
3949
|
-
| `warlock build`
|
|
3950
|
-
| `warlock start`
|
|
4004
|
+
| Mode | `environment` | `runtimeStrategy` | How |
|
|
4005
|
+
| --------------- | ------------------ | ----------------- | ------------------------------------- |
|
|
4006
|
+
| `warlock dev` | `development`* | `development` | preload force-sets `runtimeStrategy` |
|
|
4007
|
+
| `warlock build` | n/a (no app boots) | n/a | only loads warlock.config.ts |
|
|
4008
|
+
| `warlock start` | `production`* | `production`* | usually set via `NODE_ENV` in the env |
|
|
3951
4009
|
|
|
3952
4010
|
`*` — `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
4011
|
|
|
@@ -4006,7 +4064,7 @@ export default defineConfig({
|
|
|
4006
4064
|
|
|
4007
4065
|
CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
|
|
4008
4066
|
|
|
4009
|
-
**This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated
|
|
4067
|
+
**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
4068
|
|
|
4011
4069
|
**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
4070
|
|
|
@@ -4086,10 +4144,7 @@ Two APIs over the same engine. The fluent `Mail` builder reads top-to-bottom in
|
|
|
4086
4144
|
```ts
|
|
4087
4145
|
import { Mail } from "@warlock.js/core";
|
|
4088
4146
|
|
|
4089
|
-
await Mail.to("user@example.com")
|
|
4090
|
-
.subject("Welcome!")
|
|
4091
|
-
.text("Thanks for joining.")
|
|
4092
|
-
.send();
|
|
4147
|
+
await Mail.to("user@example.com").subject("Welcome!").text("Thanks for joining.").send();
|
|
4093
4148
|
```
|
|
4094
4149
|
|
|
4095
4150
|
Or:
|
|
@@ -4133,10 +4188,25 @@ For named mailers (multi-provider apps), export a `MailersConfig` instead:
|
|
|
4133
4188
|
|
|
4134
4189
|
```ts
|
|
4135
4190
|
const config: MailersConfig = {
|
|
4136
|
-
default: {
|
|
4191
|
+
default: {
|
|
4192
|
+
host: "smtp.sendgrid.net",
|
|
4193
|
+
port: 587,
|
|
4194
|
+
username: "apikey",
|
|
4195
|
+
password: env("SENDGRID_KEY"),
|
|
4196
|
+
},
|
|
4137
4197
|
mailers: {
|
|
4138
|
-
marketing: {
|
|
4139
|
-
|
|
4198
|
+
marketing: {
|
|
4199
|
+
host: "smtp.mailchimp.com",
|
|
4200
|
+
port: 587,
|
|
4201
|
+
username: env("MC_USER"),
|
|
4202
|
+
password: env("MC_PASS"),
|
|
4203
|
+
},
|
|
4204
|
+
transactional: {
|
|
4205
|
+
host: "smtp.postmark.com",
|
|
4206
|
+
port: 587,
|
|
4207
|
+
username: env("PM_USER"),
|
|
4208
|
+
password: env("PM_PASS"),
|
|
4209
|
+
},
|
|
4140
4210
|
},
|
|
4141
4211
|
};
|
|
4142
4212
|
```
|
|
@@ -4168,9 +4238,9 @@ The mail pipeline switches behavior on a global mode:
|
|
|
4168
4238
|
```ts
|
|
4169
4239
|
import { setMailMode } from "@warlock.js/core";
|
|
4170
4240
|
|
|
4171
|
-
setMailMode("production");
|
|
4172
|
-
setMailMode("development");
|
|
4173
|
-
setMailMode("test");
|
|
4241
|
+
setMailMode("production"); // actually send (default)
|
|
4242
|
+
setMailMode("development"); // log subject + recipient, no send
|
|
4243
|
+
setMailMode("test"); // capture to test mailbox, no send
|
|
4174
4244
|
```
|
|
4175
4245
|
|
|
4176
4246
|
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 +4253,7 @@ Production sends. Development logs and returns a fake-success `MailResult`. Test
|
|
|
4183
4253
|
|
|
4184
4254
|
```ts
|
|
4185
4255
|
await Mail.to("a@example.com")
|
|
4186
|
-
.to(["a@example.com", "b@example.com"])
|
|
4256
|
+
.to(["a@example.com", "b@example.com"]) // override
|
|
4187
4257
|
.cc("manager@example.com")
|
|
4188
4258
|
.bcc("audit@example.com")
|
|
4189
4259
|
.replyTo("support@example.com")
|
|
@@ -4224,15 +4294,15 @@ await Mail.to("u@e.com")
|
|
|
4224
4294
|
|
|
4225
4295
|
### Other knobs
|
|
4226
4296
|
|
|
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")`
|
|
4297
|
+
| Method | Purpose |
|
|
4298
|
+
| ---------------------------------------------- | ------------------------------------- |
|
|
4299
|
+
| `.priority("high" \| "normal" \| "low")` | priority header |
|
|
4300
|
+
| `.headers({ "X-Foo": "bar" })` | replace custom headers |
|
|
4301
|
+
| `.header("X-Foo", "bar")` | add one header |
|
|
4302
|
+
| `.tags(["welcome"])` / `.tag("transactional")` | categorization tags |
|
|
4303
|
+
| `.correlationId("req-123")` | tracking id (logged with sends) |
|
|
4304
|
+
| `.config(MailConfigurations)` | one-off override of the global config |
|
|
4305
|
+
| `.mailer("marketing")` | route via a named mailer from config |
|
|
4236
4306
|
|
|
4237
4307
|
### Per-mail event handlers
|
|
4238
4308
|
|
|
@@ -4245,9 +4315,9 @@ await Mail.to("u@e.com")
|
|
|
4245
4315
|
.beforeSending((mail) => {
|
|
4246
4316
|
// mutate `mail` or return false to cancel
|
|
4247
4317
|
})
|
|
4248
|
-
.onSent((mail, result, error) => {})
|
|
4249
|
-
.onSuccess((mail, result) => {})
|
|
4250
|
-
.onError((mail, error) => {})
|
|
4318
|
+
.onSent((mail, result, error) => {}) // always fires after attempt
|
|
4319
|
+
.onSuccess((mail, result) => {}) // only on success
|
|
4320
|
+
.onError((mail, error) => {}) // only on failure
|
|
4251
4321
|
.send();
|
|
4252
4322
|
```
|
|
4253
4323
|
|
|
@@ -4317,6 +4387,23 @@ mailEvents.onBeforeSending((mail) => {
|
|
|
4317
4387
|
|
|
4318
4388
|
Global hooks fire for **every** mail. Per-mail handlers from the builder / `sendMail` fire only for that one send.
|
|
4319
4389
|
|
|
4390
|
+
### Observe captured development and test mail
|
|
4391
|
+
|
|
4392
|
+
Development and test sends create a `CapturedMail` and synchronously emit the
|
|
4393
|
+
global `mail.captured` event. Subscribe with `mailEvents.onCaptured`:
|
|
4394
|
+
|
|
4395
|
+
```ts
|
|
4396
|
+
import { mailEvents, type CapturedMail } from "@warlock.js/core";
|
|
4397
|
+
|
|
4398
|
+
mailEvents.onCaptured((mail: CapturedMail) => {
|
|
4399
|
+
console.log(mail.id, mail.options.subject);
|
|
4400
|
+
});
|
|
4401
|
+
```
|
|
4402
|
+
|
|
4403
|
+
`CapturedMail.id` is the mail's generated or supplied id. This event is for
|
|
4404
|
+
development/test tooling; production sends do not emit it. Listener failures
|
|
4405
|
+
are logged and never interrupt mail delivery.
|
|
4406
|
+
|
|
4320
4407
|
For correlated tracking of a specific mail, generate an id and subscribe by id:
|
|
4321
4408
|
|
|
4322
4409
|
```ts
|
|
@@ -4370,10 +4457,11 @@ Captured mail shape:
|
|
|
4370
4457
|
|
|
4371
4458
|
```ts
|
|
4372
4459
|
type CapturedMail = {
|
|
4373
|
-
|
|
4460
|
+
id: string;
|
|
4461
|
+
options: MailOptions; // original payload
|
|
4374
4462
|
normalized: NormalizedMail; // post-normalization (arrays, resolved from)
|
|
4375
4463
|
timestamp: Date;
|
|
4376
|
-
result?: MailResult;
|
|
4464
|
+
result?: MailResult; // in test mode, always success
|
|
4377
4465
|
error?: MailError;
|
|
4378
4466
|
};
|
|
4379
4467
|
```
|
|
@@ -5310,6 +5398,17 @@ Both files (and this config, with `lowerStage3Decorators()` first so decorated m
|
|
|
5310
5398
|
|
|
5311
5399
|
**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
5400
|
|
|
5401
|
+
### Keep Vitest setup out of development
|
|
5402
|
+
|
|
5403
|
+
`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:
|
|
5404
|
+
|
|
5405
|
+
```ts
|
|
5406
|
+
// vite.config.ts
|
|
5407
|
+
if (process.env.VITEST) {
|
|
5408
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
5409
|
+
}
|
|
5410
|
+
```
|
|
5411
|
+
|
|
5313
5412
|
## HTTP request helpers
|
|
5314
5413
|
|
|
5315
5414
|
Everything is built on native `fetch` — no extra dependency, no special wire format.
|
|
@@ -5662,6 +5761,17 @@ export default defineConfig({
|
|
|
5662
5761
|
|
|
5663
5762
|
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
5763
|
|
|
5764
|
+
### Keep Vitest setup out of development
|
|
5765
|
+
|
|
5766
|
+
`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:
|
|
5767
|
+
|
|
5768
|
+
```ts
|
|
5769
|
+
// vite.config.ts
|
|
5770
|
+
if (process.env.VITEST) {
|
|
5771
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
5772
|
+
}
|
|
5773
|
+
```
|
|
5774
|
+
|
|
5665
5775
|
Tests live colocated with the module: `src/app/<module>/tests/*.test.ts`. The `include` pattern picks them up.
|
|
5666
5776
|
|
|
5667
5777
|
## Patterns
|
|
@@ -8417,6 +8527,9 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
8417
8527
|
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
8528
|
- `src/app/<module>/routes.ts` — route declarations
|
|
8419
8529
|
- `src/app/<module>/main.ts` — per-module one-time setup
|
|
8530
|
+
- `src/app/<module>/worker.ts` — worker-only setup (queue job definitions,
|
|
8531
|
+
`scheduler.addJob(...)`, `scheduler.start()`); loaded only for the
|
|
8532
|
+
`worker` role, and always loaded by `warlock dev`
|
|
8420
8533
|
- `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
8534
|
- `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
8535
|
- `src/app/<module>/utils/locales.ts` — module translations via `groupedTranslations(...)`
|
|
@@ -8437,7 +8550,7 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
8437
8550
|
```ts title="src/app/<module>/schema/create-<thing>.schema.ts"
|
|
8438
8551
|
import { v, type Infer } from "@warlock.js/seal";
|
|
8439
8552
|
|
|
8440
|
-
export const createThingSchema = v.object({
|
|
8553
|
+
export const createThingSchema = v.object({/* … */});
|
|
8441
8554
|
export type CreateThingSchema = Infer<typeof createThingSchema>;
|
|
8442
8555
|
```
|
|
8443
8556
|
|
|
@@ -8446,13 +8559,17 @@ This skill is the foundation. Every other warlock skill (`register-route`, `crea
|
|
|
8446
8559
|
import { type CreateThingSchema, createThingSchema } from "../schema/create-thing.schema";
|
|
8447
8560
|
import { createThingService } from "../services/create-thing.service";
|
|
8448
8561
|
|
|
8449
|
-
export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
|
|
8562
|
+
export const createThingController: GuardedRequestHandler<CreateThingSchema> = async ({
|
|
8563
|
+
request,
|
|
8564
|
+
response,
|
|
8565
|
+
}) => {
|
|
8450
8566
|
const thing = await createThingService(request.validated());
|
|
8451
8567
|
return response.success({ thing });
|
|
8452
8568
|
};
|
|
8453
8569
|
|
|
8454
8570
|
createThingController.validation = { schema: createThingSchema };
|
|
8455
8571
|
```
|
|
8572
|
+
|
|
8456
8573
|
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
8574
|
|
|
8458
8575
|
## Module layout (the standard subfolders)
|
|
@@ -8471,26 +8588,27 @@ src/app/<module>/
|
|
|
8471
8588
|
seeds/ seed data for `warlock seed`
|
|
8472
8589
|
routes.ts ← auto-loaded
|
|
8473
8590
|
main.ts ← auto-loaded once, one-time setup
|
|
8591
|
+
worker.ts ← auto-loaded for the worker role only
|
|
8474
8592
|
```
|
|
8475
8593
|
|
|
8476
8594
|
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
8595
|
|
|
8478
8596
|
## File naming (project-wide)
|
|
8479
8597
|
|
|
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`
|
|
8598
|
+
| Suffix | Role | Example |
|
|
8599
|
+
| ---------------- | -------------------------------------------------------------------------------------------- | ---------------------------------------- |
|
|
8600
|
+
| `.controller.ts` | HTTP handler | `create-product.controller.ts` |
|
|
8601
|
+
| `.service.ts` | Stateless business logic | `create-product.service.ts` |
|
|
8602
|
+
| `.usecase.ts` | `useCase()` pipeline | `login.usecase.ts` |
|
|
8603
|
+
| `.model.ts` | Cascade model class | `product.model.ts` |
|
|
8604
|
+
| `.repository.ts` | `RepositoryManager` subclass | `products.repository.ts` |
|
|
8605
|
+
| `.resource.ts` | `Resource` subclass | `product.resource.ts` |
|
|
8606
|
+
| `.schema.ts` | seal validation schema + its inferred `<Name>Schema` type from the same file | `create-product.schema.ts` |
|
|
8607
|
+
| `.type.ts` | data shape (TypeScript `type`) | `cart-state.type.ts` |
|
|
8608
|
+
| `.contract.ts` | interface (TypeScript `interface`) | `model.contract.ts` |
|
|
8609
|
+
| `.event.ts` | event listener registrations (any `.ts(x)` inside `events/` works; the suffix is convention) | `audit.event.ts` |
|
|
8610
|
+
| `.migration.ts` | Cascade migration | `2026_05_22_120000_product.migration.ts` |
|
|
8611
|
+
| `.seed.ts` | seed data | `products.seed.ts` |
|
|
8494
8612
|
|
|
8495
8613
|
Filenames are kebab-case. Class names are PascalCase. Function names are camelCase. Table/collection names are snake_case (plural).
|
|
8496
8614
|
|
|
@@ -8501,8 +8619,8 @@ The scaffolded `tsconfig.json` defines:
|
|
|
8501
8619
|
```jsonc
|
|
8502
8620
|
{
|
|
8503
8621
|
"paths": {
|
|
8504
|
-
"app/*": ["./src/app/*"]
|
|
8505
|
-
}
|
|
8622
|
+
"app/*": ["./src/app/*"],
|
|
8623
|
+
},
|
|
8506
8624
|
}
|
|
8507
8625
|
```
|
|
8508
8626
|
|
|
@@ -9147,16 +9265,16 @@ Run it: `npx warlock users.promote --email=hasan@example.com` (or `npx warlock u
|
|
|
9147
9265
|
|
|
9148
9266
|
## `CLICommandOptions` — the factory input
|
|
9149
9267
|
|
|
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[]`
|
|
9268
|
+
| Field | Type | Required | Notes |
|
|
9269
|
+
| ------------- | --------------------------- | -------- | ------------------------------------------------------------------------------------------------ |
|
|
9270
|
+
| `name` | `string` | yes | Dot notation OK (`db.seed`, `jwt.generate`). May include positional placeholders (`name <arg>`). |
|
|
9271
|
+
| `description` | `string` | | Shown in `warlock --help` and `warlock <cmd> --help`. |
|
|
9272
|
+
| `alias` | `string` | | Short name (`m` for `migrate`). |
|
|
9273
|
+
| `action` | `(data) => void \| Promise` | yes | Runs after preloaders. `data` is `{ args, options }`. |
|
|
9274
|
+
| `preAction` | `(data) => void \| Promise` | | Runs **before** preloaders — banner, input validation. |
|
|
9275
|
+
| `preload` | `CLICommandPreload` | | What to load before `action` runs. See below. |
|
|
9276
|
+
| `persistent` | `boolean` | | `true` for long-running commands (dev server). Skips the auto-exit. |
|
|
9277
|
+
| `options` | `CLICommandOption[]` | | Flag definitions. See below. |
|
|
9160
9278
|
|
|
9161
9279
|
## Options — flag shape
|
|
9162
9280
|
|
|
@@ -9258,29 +9376,29 @@ The framework ships a fixed set of commands you call but don't author. Knowing t
|
|
|
9258
9376
|
|
|
9259
9377
|
### Database + migrations
|
|
9260
9378
|
|
|
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`
|
|
9379
|
+
| Command | Flags / args | Preloads |
|
|
9380
|
+
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
|
|
9381
|
+
| `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
|
|
9382
|
+
| `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
|
|
9383
|
+
| `warlock create-database <name>` | bare positional `<name>` | database |
|
|
9384
|
+
| `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
|
|
9385
|
+
| `warlock db.indexes` | builds DB indexes for every registered model | database |
|
|
9268
9386
|
|
|
9269
9387
|
**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
9388
|
|
|
9271
9389
|
`--list` is a report and always exits `0`. `--pending` is a gate, and its exit code is its whole API:
|
|
9272
9390
|
|
|
9273
|
-
| Exit | Meaning
|
|
9274
|
-
| ---- |
|
|
9391
|
+
| Exit | Meaning |
|
|
9392
|
+
| ---- | ------------------------- |
|
|
9275
9393
|
| `0` | computed, nothing pending |
|
|
9276
|
-
| `1` | computed, N pending
|
|
9394
|
+
| `1` | computed, N pending |
|
|
9277
9395
|
| `2` | **could not be computed** |
|
|
9278
9396
|
|
|
9279
9397
|
```bash
|
|
9280
9398
|
warlock migrate --pending && ./deploy.sh
|
|
9281
9399
|
```
|
|
9282
9400
|
|
|
9283
|
-
`2` is separate from `1` on purpose — a script must be able to tell a backlog from an unknown, because one means
|
|
9401
|
+
`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
9402
|
|
|
9285
9403
|
**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
9404
|
|
|
@@ -9306,36 +9424,41 @@ warlock generate (alias: g) — interactive picker
|
|
|
9306
9424
|
|
|
9307
9425
|
`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
9426
|
|
|
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
|
|
9427
|
+
| Feature | Installs |
|
|
9428
|
+
| ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
9429
|
+
| `react-email` | `react-email` + `@react-email/components` + `@react-email/render` + `@react-email/tailwind`; drops a `welcome-email.tsx` sample; patches `tsconfig.json` |
|
|
9430
|
+
| `web` | Warlock Web + React stack; scaffolds the application page, localization, contact endpoint, and Web configuration |
|
|
9431
|
+
| `react` | `react` + `react-dom` + types |
|
|
9314
9432
|
| `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`
|
|
9433
|
+
| `image` | `sharp` (for the `Image` class) |
|
|
9434
|
+
| `mail` | `nodemailer` + types |
|
|
9435
|
+
| `ses` | `@aws-sdk/client-sesv2` |
|
|
9436
|
+
| `mongodb` | `mongodb` driver |
|
|
9437
|
+
| `postgres` | `pg` |
|
|
9438
|
+
| `mysql` | `mysql2` |
|
|
9439
|
+
| `redis` | `redis` |
|
|
9440
|
+
| `s3` | `@aws-sdk/client-s3` + `@aws-sdk/lib-storage` + `@aws-sdk/s3-request-presigner` |
|
|
9441
|
+
| `scheduler` | `@warlock.js/scheduler` |
|
|
9442
|
+
| `swagger` | `@warlock.js/swagger` |
|
|
9443
|
+
| `postman` | `@warlock.js/postman` |
|
|
9444
|
+
| `herald` | `@warlock.js/herald` + `amqplib` + types; ejects `src/config/communicator.ts` |
|
|
9445
|
+
| `test` | `vitest` + `@mongez/vite` + coverage; drops `test-global-setup.ts` / `test-setup.ts` / `vite.config.ts` |
|
|
9446
|
+
| `devtools` | `@warlock.js/devtools` as a dev dependency; `warlock dev` auto-loads its development dashboard at `/__warlock` |
|
|
9328
9447
|
|
|
9329
9448
|
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
9449
|
|
|
9450
|
+
`warlock add devtools` never adds anything to `warlock.config.ts`. The package
|
|
9451
|
+
is loaded automatically by `warlock dev` when installed, and is omitted from a
|
|
9452
|
+
production install with dev dependencies excluded.
|
|
9453
|
+
|
|
9331
9454
|
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
9455
|
|
|
9333
9456
|
### Misc
|
|
9334
9457
|
|
|
9335
|
-
| Command
|
|
9336
|
-
|
|
|
9337
|
-
| `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk
|
|
9338
|
-
| `warlock jwt.generate`
|
|
9458
|
+
| Command | Purpose |
|
|
9459
|
+
| ----------------------------------------------- | --------------------------------------------------------- |
|
|
9460
|
+
| `warlock storage.put <localPath> [destination]` | Upload a local file to the default storage disk |
|
|
9461
|
+
| `warlock jwt.generate` | Generate a JWT secret in `.env` (from `@warlock.js/auth`) |
|
|
9339
9462
|
|
|
9340
9463
|
## Common patterns
|
|
9341
9464
|
|
|
@@ -9397,7 +9520,7 @@ export default command({
|
|
|
9397
9520
|
});
|
|
9398
9521
|
```
|
|
9399
9522
|
|
|
9400
|
-
`preAction` runs
|
|
9523
|
+
`preAction` runs _before_ preloaders — cheap way to bail out without spinning up the database.
|
|
9401
9524
|
|
|
9402
9525
|
## Gotchas
|
|
9403
9526
|
|
|
@@ -9405,7 +9528,7 @@ export default command({
|
|
|
9405
9528
|
- **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
9529
|
- **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
9530
|
- **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
|
|
9531
|
+
- **`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
9532
|
- **Aliases must be unique across plugin + framework + project.** The first registration wins; later collisions silently overwrite the map entry.
|
|
9410
9533
|
|
|
9411
9534
|
## See also
|
|
@@ -10302,6 +10425,8 @@ Per-invocation callbacks fire first, then use-case-level, then globals. The retu
|
|
|
10302
10425
|
|
|
10303
10426
|
Don't reach for `useCase` for a 5-line service — the ceremony outweighs the benefit.
|
|
10304
10427
|
|
|
10428
|
+
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/).
|
|
10429
|
+
|
|
10305
10430
|
## Gotchas
|
|
10306
10431
|
|
|
10307
10432
|
- **`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"`.
|