@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/package.json
CHANGED
|
@@ -25,12 +25,12 @@
|
|
|
25
25
|
"@mongez/slug": "^1.0.7",
|
|
26
26
|
"@mongez/supportive-is": "^2.1.5",
|
|
27
27
|
"@mongez/time-wizard": "^1.0.6",
|
|
28
|
-
"@warlock.js/cache": "5.
|
|
29
|
-
"@warlock.js/cascade": "5.
|
|
30
|
-
"@warlock.js/context": "5.
|
|
31
|
-
"@warlock.js/fs": "5.
|
|
32
|
-
"@warlock.js/logger": "5.
|
|
33
|
-
"@warlock.js/seal": "5.
|
|
28
|
+
"@warlock.js/cache": "5.25.0",
|
|
29
|
+
"@warlock.js/cascade": "5.25.0",
|
|
30
|
+
"@warlock.js/context": "5.25.0",
|
|
31
|
+
"@warlock.js/fs": "5.25.0",
|
|
32
|
+
"@warlock.js/logger": "5.25.0",
|
|
33
|
+
"@warlock.js/seal": "5.25.0",
|
|
34
34
|
"bcryptjs": "^3.0.3",
|
|
35
35
|
"chokidar": "^5.0.0",
|
|
36
36
|
"dayjs": "^1.11.19",
|
|
@@ -52,10 +52,11 @@
|
|
|
52
52
|
"@aws-sdk/lib-storage": "^3.955.0",
|
|
53
53
|
"@aws-sdk/s3-request-presigner": "^3.955.0",
|
|
54
54
|
"@react-email/render": "^2.0.5",
|
|
55
|
-
"@warlock.js/access": "5.
|
|
56
|
-
"@warlock.js/ai": "5.
|
|
57
|
-
"@warlock.js/herald": "5.
|
|
58
|
-
"@warlock.js/notifications": "5.
|
|
55
|
+
"@warlock.js/access": "5.25.0",
|
|
56
|
+
"@warlock.js/ai": "5.25.0",
|
|
57
|
+
"@warlock.js/herald": "5.25.0",
|
|
58
|
+
"@warlock.js/notifications": "5.25.0",
|
|
59
|
+
"@warlock.js/devtools": "5.25.0",
|
|
59
60
|
"nodemailer": "^8.0.5",
|
|
60
61
|
"react": "^19.2.3",
|
|
61
62
|
"react-dom": "^19.2.3",
|
|
@@ -104,6 +105,9 @@
|
|
|
104
105
|
},
|
|
105
106
|
"@warlock.js/notifications": {
|
|
106
107
|
"optional": true
|
|
108
|
+
},
|
|
109
|
+
"@warlock.js/devtools": {
|
|
110
|
+
"optional": true
|
|
107
111
|
}
|
|
108
112
|
},
|
|
109
113
|
"repository": {
|
|
@@ -123,7 +127,7 @@
|
|
|
123
127
|
],
|
|
124
128
|
"author": "hassanzohdy",
|
|
125
129
|
"license": "MIT",
|
|
126
|
-
"version": "5.
|
|
130
|
+
"version": "5.25.0",
|
|
127
131
|
"type": "module",
|
|
128
132
|
"main": "./esm/index.mjs",
|
|
129
133
|
"module": "./esm/index.mjs",
|
|
@@ -229,6 +229,8 @@ Same shape — TS validates the union; runtime picks the `default` key.
|
|
|
229
229
|
- `socket.adapter` — adapter factory for cross-server broadcast.
|
|
230
230
|
- `cache.silenceSingleServerWarning`, `storage.silenceSingleServerWarning`, `socket.silenceSingleServerWarning` — silence the production warnings for in-memory cache, local storage, and adapter-less sockets.
|
|
231
231
|
|
|
232
|
+
`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 }`.
|
|
233
|
+
|
|
232
234
|
## Per-env knobs
|
|
233
235
|
|
|
234
236
|
For one-off switches gated on environment, the canonical pattern is `Application.isProduction` (also `isDevelopment`, `isTest`):
|
|
@@ -221,6 +221,10 @@ export class Product extends Model {
|
|
|
221
221
|
|
|
222
222
|
This is convention, not a framework requirement. Controllers/services that import the resource explicitly work the same way.
|
|
223
223
|
|
|
224
|
+
### `id` and Mongo `_id`
|
|
225
|
+
|
|
226
|
+
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.
|
|
227
|
+
|
|
224
228
|
## Output-only rule
|
|
225
229
|
|
|
226
230
|
Resources map fields. They do not:
|
|
@@ -55,6 +55,7 @@ type TracingContext = {
|
|
|
55
55
|
type TracingPhaseInfo = {
|
|
56
56
|
name: string;
|
|
57
57
|
durationMs: number;
|
|
58
|
+
startedAt?: number; // epoch milliseconds; supplied when the phase started
|
|
58
59
|
attrs?: Record<string, unknown>;
|
|
59
60
|
};
|
|
60
61
|
|
|
@@ -73,29 +74,52 @@ type TracingHooks = {
|
|
|
73
74
|
|
|
74
75
|
Every verb is optional — a hook that only wants phase spans need not implement
|
|
75
76
|
`onRequestStart`/`onRequestEnd`. Register as many hooks as you like via
|
|
76
|
-
`hooks: TracingHooks[]`; each fires independently.
|
|
77
|
+
`hooks: TracingHooks[]`; each fires independently. Use `registerTracingHooks()`
|
|
78
|
+
when the observer is installed by application code rather than static config:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import { registerTracingHooks, type TracingHooks } from "@warlock.js/core";
|
|
82
|
+
|
|
83
|
+
const hooks: TracingHooks = {
|
|
84
|
+
onPhase(ctx, phase) {
|
|
85
|
+
console.log(ctx.traceId, phase.name, phase.startedAt);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const unregister = registerTracingHooks(hooks);
|
|
90
|
+
// Later, for example during plugin teardown:
|
|
91
|
+
unregister();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`registerTracingHooks(hooks): () => void` enables tracing immediately. Runtime
|
|
95
|
+
hooks run after configured hooks; calling the returned unregister function more
|
|
96
|
+
than once is safe. When a phase did not provide `startedAt`, the dispatcher
|
|
97
|
+
derives it from its duration before delivering the callback.
|
|
77
98
|
|
|
78
99
|
## Phase names
|
|
79
100
|
|
|
80
101
|
`core` wires five phases into the request lifecycle, in this order, for every
|
|
81
102
|
HTTP request:
|
|
82
103
|
|
|
83
|
-
| Phase
|
|
84
|
-
|
|
|
85
|
-
| `route.match`
|
|
86
|
-
| `middleware`
|
|
87
|
-
| `validation`
|
|
88
|
-
| `handler`
|
|
104
|
+
| Phase | Fires around |
|
|
105
|
+
| ---------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
|
|
106
|
+
| `route.match` | Resolving the incoming path/method to a registered route |
|
|
107
|
+
| `middleware` | Each middleware in the route's chain — one `onPhase` call per middleware, with `attrs: { name, index }` |
|
|
108
|
+
| `validation` | The route's input validation (`v.object(...)` / RESTful resource validation) |
|
|
109
|
+
| `handler` | The route handler itself |
|
|
89
110
|
| `response.write` | The overall request span, closed once the response has settled (success or thrown error) — this is also where `onRequestEnd` fires |
|
|
90
111
|
|
|
91
112
|
`@warlock.js/web` page requests report through this same `onPhase` surface
|
|
92
113
|
instead of adding a separate hook API. They add three phases:
|
|
93
114
|
|
|
94
|
-
| Phase
|
|
95
|
-
|
|
|
96
|
-
| `loader`
|
|
97
|
-
| `render.shell`
|
|
98
|
-
| `stream.end`
|
|
115
|
+
| Phase | Fires around |
|
|
116
|
+
| ----------------- | --------------------------------------------------------------------------------------------------- |
|
|
117
|
+
| `loader` | Each loader level, once per app/layout/page, with `attrs: { level, layoutPath? }` |
|
|
118
|
+
| `render.shell` | Time from render start until React's shell is ready to stream |
|
|
119
|
+
| `stream.end` | The whole streamed response, including every `defer()` value settling |
|
|
120
|
+
| `page.middleware` | Page middleware chain; `attrs: { count, outcome }`, where outcome is `next`, `response`, or `error` |
|
|
121
|
+
| `page.cache` | Page-cache lookup; `attrs: { status }`, where status is `hit`, `miss`, or `bypass` |
|
|
122
|
+
| `defer.settle` | A deferred key settling; `attrs: { key, status }`, where status is `fulfilled` or `rejected` |
|
|
99
123
|
|
|
100
124
|
## Trace id derivation
|
|
101
125
|
|
|
@@ -189,9 +213,8 @@ export default {
|
|
|
189
213
|
- **No OTel dependency, and none planned for `core`.** An OTel (or other
|
|
190
214
|
vendor) bridge is a separate, optional package that subscribes to these
|
|
191
215
|
hooks — never add `@opentelemetry/api` to `core` itself.
|
|
192
|
-
-
|
|
193
|
-
|
|
194
|
-
phases fire in this release.
|
|
216
|
+
- **Web phases share the same hook.** Do not create a second page-tracing
|
|
217
|
+
integration: subscribe with `TracingHooks.onPhase`.
|
|
195
218
|
- **`route` is `undefined` until routing has matched.** There's no path where
|
|
196
219
|
a hook fires before that, but code branching on `ctx.route` for an
|
|
197
220
|
early-failing request (e.g. a 404 with no match) must handle `undefined`.
|
package/skills/run-app/SKILL.md
CHANGED
|
@@ -28,11 +28,11 @@ Boots the framework in dev mode: file watcher, HMR-style module reload, on-disk
|
|
|
28
28
|
|
|
29
29
|
### Flags
|
|
30
30
|
|
|
31
|
-
| Flag | Default | Purpose
|
|
32
|
-
| --------------------- | ------- |
|
|
31
|
+
| Flag | Default | Purpose |
|
|
32
|
+
| --------------------- | ------- | ----------------------------------------------------------------------------------------------------------- |
|
|
33
33
|
| `--fresh, -f` | off | Delete `.warlock/manifest.json` before start — forces a full re-parse from disk. Use after odd boot states. |
|
|
34
|
-
| `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config.
|
|
35
|
-
| `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config.
|
|
34
|
+
| `--skip-typings, -st` | off | Skip background type generation **for this run**. Overrides `devServer.generateTypings` config. |
|
|
35
|
+
| `--skip-health, -sh` | off | Skip file health checkers **for this run**. Overrides `devServer.healthCheckers` config. |
|
|
36
36
|
|
|
37
37
|
When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.*` value applies. When passed, the flag wins.
|
|
38
38
|
|
|
@@ -40,14 +40,14 @@ When a flag is **not** passed, the corresponding `warlock.config.ts > devServer.
|
|
|
40
40
|
|
|
41
41
|
Once the server is ready, `warlock dev` listens for single keypresses:
|
|
42
42
|
|
|
43
|
-
| Key | Does
|
|
44
|
-
| -------- |
|
|
45
|
-
| `r` | Restart the server on a fresh process.
|
|
46
|
-
| `c` | Clear the console.
|
|
47
|
-
| `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`.
|
|
48
|
-
| `h` | Print the shortcuts that are armed right now.
|
|
43
|
+
| Key | Does |
|
|
44
|
+
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `r` | Restart the server on a fresh process. |
|
|
46
|
+
| `c` | Clear the console. |
|
|
47
|
+
| `q` | Graceful shutdown, exit `0` — the same path as `Ctrl+C`. |
|
|
48
|
+
| `h` | Print the shortcuts that are armed right now. |
|
|
49
49
|
| `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). |
|
|
50
|
-
| `Ctrl+C` | Graceful shutdown, unchanged.
|
|
50
|
+
| `Ctrl+C` | Graceful shutdown, unchanged. |
|
|
51
51
|
|
|
52
52
|
A "restart" is a fresh process (`r` and `u` both use it) — see [The supervisor](#the-supervisor) below for how that works.
|
|
53
53
|
|
|
@@ -67,7 +67,7 @@ shell
|
|
|
67
67
|
|
|
68
68
|
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.
|
|
69
69
|
|
|
70
|
-
The supervisor is chosen in the `dev` command's `preAction`, which runs
|
|
70
|
+
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.
|
|
71
71
|
|
|
72
72
|
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.
|
|
73
73
|
|
|
@@ -81,13 +81,13 @@ A worker that dies **after running healthily for at least 5s** — OOM, a native
|
|
|
81
81
|
14:31:02 Development server was killed by SIGSEGV — restarting.
|
|
82
82
|
```
|
|
83
83
|
|
|
84
|
-
A worker that dies
|
|
84
|
+
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.
|
|
85
85
|
|
|
86
|
-
A busy port is now caught
|
|
86
|
+
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`.
|
|
87
87
|
|
|
88
88
|
Flapping is capped: more than 3 crashes inside 60s and the supervisor gives up rather than restarting behind your back.
|
|
89
89
|
|
|
90
|
-
An explicit restart (`r`, `u`, a config change) is a
|
|
90
|
+
An explicit restart (`r`, `u`, a config change) is a _request_, not a crash, so the uptime rule never swallows it.
|
|
91
91
|
|
|
92
92
|
### Restart on config change
|
|
93
93
|
|
|
@@ -103,7 +103,7 @@ Set `devServer.restartOnConfigChange: false` for the previous behaviour (a warni
|
|
|
103
103
|
|
|
104
104
|
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.
|
|
105
105
|
|
|
106
|
-
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
|
|
106
|
+
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.
|
|
107
107
|
|
|
108
108
|
:::note[Fixed in 4.9.2 — pnpm users]
|
|
109
109
|
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.
|
|
@@ -165,12 +165,13 @@ esbuild bundle of the app down to a single JS file in `dist/`. No flags — ever
|
|
|
165
165
|
```ts title="warlock.config.ts"
|
|
166
166
|
export default defineConfig({
|
|
167
167
|
build: {
|
|
168
|
-
outdir: "dist",
|
|
169
|
-
outFile: "app.js",
|
|
170
|
-
minify: true,
|
|
171
|
-
sourcemap: true,
|
|
172
|
-
singleBundle: false,
|
|
173
|
-
esmShim: true,
|
|
168
|
+
outdir: "dist", // default — relative or absolute
|
|
169
|
+
outFile: "app.js", // default — bundle filename
|
|
170
|
+
minify: true, // default — esbuild minify
|
|
171
|
+
sourcemap: true, // default — true | false | "inline" | "linked"
|
|
172
|
+
singleBundle: false, // default — one runnable file, deps inlined
|
|
173
|
+
esmShim: true, // default — require/__filename/__dirname for bundled CJS
|
|
174
|
+
routeRegistrationTimeoutMs: 30_000, // default — route-registration child deadline
|
|
174
175
|
},
|
|
175
176
|
});
|
|
176
177
|
```
|
|
@@ -182,8 +183,9 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
|
|
|
182
183
|
- **`minify: false`** — flip to debug a production-only bug. Larger bundle, readable stack traces.
|
|
183
184
|
- **`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.
|
|
184
185
|
- **`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).
|
|
185
|
-
- **`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
|
|
186
|
+
- **`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.
|
|
186
187
|
- **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
|
|
188
|
+
- **`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.
|
|
187
189
|
|
|
188
190
|
⚠ **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.
|
|
189
191
|
|
|
@@ -241,6 +243,41 @@ npx warlock start --inspect # → spawns node --enable-source-maps
|
|
|
241
243
|
npx warlock start --max-old-space-size=4096 # → spawns node --enable-source-maps --max-old-space-size=4096 dist/app.js
|
|
242
244
|
```
|
|
243
245
|
|
|
246
|
+
### Deploy one build in parts
|
|
247
|
+
|
|
248
|
+
One production build can run as API, page, and background-worker processes.
|
|
249
|
+
Omit `--role` to run every role (the existing default); combine roles with a
|
|
250
|
+
comma-separated list:
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
npx warlock start --role=api
|
|
254
|
+
npx warlock start --role=web --sites=admin,storefront
|
|
255
|
+
npx warlock start --role=worker
|
|
256
|
+
npx warlock start --role=web,api
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
`--sites=a,b` is valid only with the `web` role and installs the listed site
|
|
260
|
+
keys' pages (shared non-site pages remain installed). Unknown roles, an empty
|
|
261
|
+
list, `--sites` without `web`, and unknown site keys fail boot rather than
|
|
262
|
+
silently serving the wrong traffic.
|
|
263
|
+
|
|
264
|
+
An API or web role binds HTTP. A worker-only process binds no HTTP port; it
|
|
265
|
+
loads worker work, starts queue workers and the scheduler there, and its
|
|
266
|
+
readiness banner reports `worker ready`. `warlock dev` always runs every role.
|
|
267
|
+
|
|
268
|
+
Inside application code, inspect the roles resolved at bootstrap:
|
|
269
|
+
|
|
270
|
+
```ts
|
|
271
|
+
import { Application } from "@warlock.js/core";
|
|
272
|
+
|
|
273
|
+
if (Application.hasRole("worker")) {
|
|
274
|
+
// worker-only integration
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
Application.roles; // ReadonlySet<"api" | "web" | "worker">
|
|
278
|
+
Application.sites; // ReadonlySet<string> | undefined (undefined = all sites)
|
|
279
|
+
```
|
|
280
|
+
|
|
244
281
|
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.
|
|
245
282
|
|
|
246
283
|
### Source maps
|
|
@@ -283,9 +320,9 @@ Nothing else is needed for pnpm. Warlock never requires an app to declare a pack
|
|
|
283
320
|
|
|
284
321
|
**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.
|
|
285
322
|
|
|
286
|
-
| Stream | Carries
|
|
287
|
-
| ---------- |
|
|
288
|
-
| **stdout** | the started banner, and start failures. Nothing else.
|
|
323
|
+
| Stream | Carries |
|
|
324
|
+
| ---------- | -------------------------------------------------------------------------------------- |
|
|
325
|
+
| **stdout** | the started banner, and start failures. Nothing else. |
|
|
289
326
|
| **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
|
|
290
327
|
|
|
291
328
|
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`**.
|
|
@@ -321,7 +358,7 @@ else is listening on port 3000) and run again, or start on a free port — e.g.
|
|
|
321
358
|
startHttpTestServer({ port: 3001 }).
|
|
322
359
|
```
|
|
323
360
|
|
|
324
|
-
**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
|
|
361
|
+
**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.
|
|
325
362
|
|
|
326
363
|
`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.
|
|
327
364
|
|
|
@@ -336,7 +373,7 @@ Configured http.port "03999" normalised to 3999.
|
|
|
336
373
|
`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:
|
|
337
374
|
|
|
338
375
|
```ts
|
|
339
|
-
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port
|
|
376
|
+
{ type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port?, roles }
|
|
340
377
|
```
|
|
341
378
|
|
|
342
379
|
Three consequences worth knowing:
|
|
@@ -351,11 +388,11 @@ If you need the same fact inside the app, use `Application.onceBooted()` / `Appl
|
|
|
351
388
|
|
|
352
389
|
`Application.environment` and `Application.runtimeStrategy` are separate axes:
|
|
353
390
|
|
|
354
|
-
| Mode
|
|
355
|
-
|
|
|
356
|
-
| `warlock dev`
|
|
357
|
-
| `warlock build`
|
|
358
|
-
| `warlock start`
|
|
391
|
+
| Mode | `environment` | `runtimeStrategy` | How |
|
|
392
|
+
| --------------- | ------------------ | ----------------- | ------------------------------------- |
|
|
393
|
+
| `warlock dev` | `development`* | `development` | preload force-sets `runtimeStrategy` |
|
|
394
|
+
| `warlock build` | n/a (no app boots) | n/a | only loads warlock.config.ts |
|
|
395
|
+
| `warlock start` | `production`* | `production`* | usually set via `NODE_ENV` in the env |
|
|
359
396
|
|
|
360
397
|
`*` — `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.
|
|
361
398
|
|
|
@@ -414,7 +451,7 @@ export default defineConfig({
|
|
|
414
451
|
|
|
415
452
|
CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
|
|
416
453
|
|
|
417
|
-
**This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated
|
|
454
|
+
**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.
|
|
418
455
|
|
|
419
456
|
**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:
|
|
420
457
|
|
|
@@ -12,10 +12,7 @@ Two APIs over the same engine. The fluent `Mail` builder reads top-to-bottom in
|
|
|
12
12
|
```ts
|
|
13
13
|
import { Mail } from "@warlock.js/core";
|
|
14
14
|
|
|
15
|
-
await Mail.to("user@example.com")
|
|
16
|
-
.subject("Welcome!")
|
|
17
|
-
.text("Thanks for joining.")
|
|
18
|
-
.send();
|
|
15
|
+
await Mail.to("user@example.com").subject("Welcome!").text("Thanks for joining.").send();
|
|
19
16
|
```
|
|
20
17
|
|
|
21
18
|
Or:
|
|
@@ -59,10 +56,25 @@ For named mailers (multi-provider apps), export a `MailersConfig` instead:
|
|
|
59
56
|
|
|
60
57
|
```ts
|
|
61
58
|
const config: MailersConfig = {
|
|
62
|
-
default: {
|
|
59
|
+
default: {
|
|
60
|
+
host: "smtp.sendgrid.net",
|
|
61
|
+
port: 587,
|
|
62
|
+
username: "apikey",
|
|
63
|
+
password: env("SENDGRID_KEY"),
|
|
64
|
+
},
|
|
63
65
|
mailers: {
|
|
64
|
-
marketing: {
|
|
65
|
-
|
|
66
|
+
marketing: {
|
|
67
|
+
host: "smtp.mailchimp.com",
|
|
68
|
+
port: 587,
|
|
69
|
+
username: env("MC_USER"),
|
|
70
|
+
password: env("MC_PASS"),
|
|
71
|
+
},
|
|
72
|
+
transactional: {
|
|
73
|
+
host: "smtp.postmark.com",
|
|
74
|
+
port: 587,
|
|
75
|
+
username: env("PM_USER"),
|
|
76
|
+
password: env("PM_PASS"),
|
|
77
|
+
},
|
|
66
78
|
},
|
|
67
79
|
};
|
|
68
80
|
```
|
|
@@ -94,9 +106,9 @@ The mail pipeline switches behavior on a global mode:
|
|
|
94
106
|
```ts
|
|
95
107
|
import { setMailMode } from "@warlock.js/core";
|
|
96
108
|
|
|
97
|
-
setMailMode("production");
|
|
98
|
-
setMailMode("development");
|
|
99
|
-
setMailMode("test");
|
|
109
|
+
setMailMode("production"); // actually send (default)
|
|
110
|
+
setMailMode("development"); // log subject + recipient, no send
|
|
111
|
+
setMailMode("test"); // capture to test mailbox, no send
|
|
100
112
|
```
|
|
101
113
|
|
|
102
114
|
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.
|
|
@@ -109,7 +121,7 @@ Production sends. Development logs and returns a fake-success `MailResult`. Test
|
|
|
109
121
|
|
|
110
122
|
```ts
|
|
111
123
|
await Mail.to("a@example.com")
|
|
112
|
-
.to(["a@example.com", "b@example.com"])
|
|
124
|
+
.to(["a@example.com", "b@example.com"]) // override
|
|
113
125
|
.cc("manager@example.com")
|
|
114
126
|
.bcc("audit@example.com")
|
|
115
127
|
.replyTo("support@example.com")
|
|
@@ -150,15 +162,15 @@ await Mail.to("u@e.com")
|
|
|
150
162
|
|
|
151
163
|
### Other knobs
|
|
152
164
|
|
|
153
|
-
| Method
|
|
154
|
-
|
|
|
155
|
-
| `.priority("high" \| "normal" \| "low")`
|
|
156
|
-
| `.headers({ "X-Foo": "bar" })`
|
|
157
|
-
| `.header("X-Foo", "bar")`
|
|
158
|
-
| `.tags(["welcome"])` / `.tag("transactional")` | categorization tags
|
|
159
|
-
| `.correlationId("req-123")`
|
|
160
|
-
| `.config(MailConfigurations)`
|
|
161
|
-
| `.mailer("marketing")`
|
|
165
|
+
| Method | Purpose |
|
|
166
|
+
| ---------------------------------------------- | ------------------------------------- |
|
|
167
|
+
| `.priority("high" \| "normal" \| "low")` | priority header |
|
|
168
|
+
| `.headers({ "X-Foo": "bar" })` | replace custom headers |
|
|
169
|
+
| `.header("X-Foo", "bar")` | add one header |
|
|
170
|
+
| `.tags(["welcome"])` / `.tag("transactional")` | categorization tags |
|
|
171
|
+
| `.correlationId("req-123")` | tracking id (logged with sends) |
|
|
172
|
+
| `.config(MailConfigurations)` | one-off override of the global config |
|
|
173
|
+
| `.mailer("marketing")` | route via a named mailer from config |
|
|
162
174
|
|
|
163
175
|
### Per-mail event handlers
|
|
164
176
|
|
|
@@ -171,9 +183,9 @@ await Mail.to("u@e.com")
|
|
|
171
183
|
.beforeSending((mail) => {
|
|
172
184
|
// mutate `mail` or return false to cancel
|
|
173
185
|
})
|
|
174
|
-
.onSent((mail, result, error) => {})
|
|
175
|
-
.onSuccess((mail, result) => {})
|
|
176
|
-
.onError((mail, error) => {})
|
|
186
|
+
.onSent((mail, result, error) => {}) // always fires after attempt
|
|
187
|
+
.onSuccess((mail, result) => {}) // only on success
|
|
188
|
+
.onError((mail, error) => {}) // only on failure
|
|
177
189
|
.send();
|
|
178
190
|
```
|
|
179
191
|
|
|
@@ -243,6 +255,23 @@ mailEvents.onBeforeSending((mail) => {
|
|
|
243
255
|
|
|
244
256
|
Global hooks fire for **every** mail. Per-mail handlers from the builder / `sendMail` fire only for that one send.
|
|
245
257
|
|
|
258
|
+
### Observe captured development and test mail
|
|
259
|
+
|
|
260
|
+
Development and test sends create a `CapturedMail` and synchronously emit the
|
|
261
|
+
global `mail.captured` event. Subscribe with `mailEvents.onCaptured`:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
import { mailEvents, type CapturedMail } from "@warlock.js/core";
|
|
265
|
+
|
|
266
|
+
mailEvents.onCaptured((mail: CapturedMail) => {
|
|
267
|
+
console.log(mail.id, mail.options.subject);
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`CapturedMail.id` is the mail's generated or supplied id. This event is for
|
|
272
|
+
development/test tooling; production sends do not emit it. Listener failures
|
|
273
|
+
are logged and never interrupt mail delivery.
|
|
274
|
+
|
|
246
275
|
For correlated tracking of a specific mail, generate an id and subscribe by id:
|
|
247
276
|
|
|
248
277
|
```ts
|
|
@@ -296,10 +325,11 @@ Captured mail shape:
|
|
|
296
325
|
|
|
297
326
|
```ts
|
|
298
327
|
type CapturedMail = {
|
|
299
|
-
|
|
328
|
+
id: string;
|
|
329
|
+
options: MailOptions; // original payload
|
|
300
330
|
normalized: NormalizedMail; // post-normalization (arrays, resolved from)
|
|
301
331
|
timestamp: Date;
|
|
302
|
-
result?: MailResult;
|
|
332
|
+
result?: MailResult; // in test mode, always success
|
|
303
333
|
error?: MailError;
|
|
304
334
|
};
|
|
305
335
|
```
|
|
@@ -9,7 +9,7 @@ Some tests need the full stack: route matching, middleware chain, validation, co
|
|
|
9
9
|
|
|
10
10
|
`startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
|
|
11
11
|
|
|
12
|
-
⚠ **Changed in 4.13.0 — the import is a subpath now.** These helpers used to be re-exported from the package root; they are not any more, because that put the test helpers into every application's production module graph. `import { testGet } from "@warlock.js/core"` now fails with
|
|
12
|
+
⚠ **Changed in 4.13.0 — the import is a subpath now.** These helpers used to be re-exported from the package root; they are not any more, because that put the test helpers into every application's production module graph. `import { testGet } from "@warlock.js/core"` now fails with _"has no exported member"_ — **add `/tests` to the specifier and nothing else changes.**
|
|
13
13
|
|
|
14
14
|
## The shape
|
|
15
15
|
|
|
@@ -125,8 +125,8 @@ import { defineConfig } from "vitest/config";
|
|
|
125
125
|
export default defineConfig({
|
|
126
126
|
plugins: [lowerStage3Decorators(), mongezVite()],
|
|
127
127
|
test: {
|
|
128
|
-
globalSetup: "./src/test-global-setup.ts",
|
|
129
|
-
setupFiles: ["./src/test-setup.ts"],
|
|
128
|
+
globalSetup: "./src/test-global-setup.ts", // ← starts the HTTP server
|
|
129
|
+
setupFiles: ["./src/test-setup.ts"], // ← setupTest + afterAll(teardownTest), per test file
|
|
130
130
|
environment: "node",
|
|
131
131
|
globals: false,
|
|
132
132
|
include: ["src/app/**/*.test.ts"],
|
|
@@ -142,6 +142,17 @@ Both files (and this config, with `lowerStage3Decorators()` first so decorated m
|
|
|
142
142
|
|
|
143
143
|
**HTTP is the exception and stays in `globalSetup`**, which genuinely does run once in the main vitest process and owns a real port. **That split is the point:** one server for the whole run, one framework per test file.
|
|
144
144
|
|
|
145
|
+
### Keep Vitest setup out of development
|
|
146
|
+
|
|
147
|
+
`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:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
// vite.config.ts
|
|
151
|
+
if (process.env.VITEST) {
|
|
152
|
+
Object.assign(process.env, testEnv); // test database, test keys...
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
145
156
|
## HTTP request helpers
|
|
146
157
|
|
|
147
158
|
Everything is built on native `fetch` — no extra dependency, no special wire format.
|
|
@@ -151,7 +162,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
|
|
|
151
162
|
```ts
|
|
152
163
|
import { getTestServerUrl } from "@warlock.js/core/tests";
|
|
153
164
|
|
|
154
|
-
const url = getTestServerUrl();
|
|
165
|
+
const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
|
|
155
166
|
```
|
|
156
167
|
|
|
157
168
|
Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config — unless `startHttpTestServer` published a port, which wins, since a worker's own config never sees the `{ port }` option passed in `globalSetup`. If you change the HTTP config, helpers follow automatically.
|
|
@@ -189,9 +200,9 @@ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
|
|
|
189
200
|
const body = await parseJsonResponse<MyShape>(response);
|
|
190
201
|
|
|
191
202
|
// Assert status + parse in one call
|
|
192
|
-
const body = await expectJson<MyShape>(response);
|
|
193
|
-
const body = await expectJson<MyShape>(response, 201);
|
|
194
|
-
const body = await expectJson<MyShape>(response, 404);
|
|
203
|
+
const body = await expectJson<MyShape>(response); // expects 200
|
|
204
|
+
const body = await expectJson<MyShape>(response, 201); // expects 201
|
|
205
|
+
const body = await expectJson<MyShape>(response, 404); // expects 404 (testing error paths)
|
|
195
206
|
```
|
|
196
207
|
|
|
197
208
|
`expectJson` throws with the actual status + response body when the assertion fails — no chasing "got 500" messages without context.
|
|
@@ -227,7 +238,7 @@ describe("Products API — happy path", () => {
|
|
|
227
238
|
|
|
228
239
|
```ts
|
|
229
240
|
it("rejects missing required fields with 400", async () => {
|
|
230
|
-
const response = await testPost("/products", { name: "" });
|
|
241
|
+
const response = await testPost("/products", { name: "" }); // price missing
|
|
231
242
|
const body = await expectJson<{ errors: Array<{ key: string }> }>(response, 400);
|
|
232
243
|
|
|
233
244
|
expect(body.errors.some((e) => e.key === "price")).toBe(true);
|