@warlock.js/core 4.9.2 → 4.11.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.
Files changed (81) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/esm/application/application.d.mts +19 -0
  3. package/esm/application/application.d.mts.map +1 -1
  4. package/esm/application/application.mjs +25 -0
  5. package/esm/application/application.mjs.map +1 -1
  6. package/esm/application/boot-signal.d.mts +77 -0
  7. package/esm/application/boot-signal.d.mts.map +1 -0
  8. package/esm/application/boot-signal.mjs +64 -0
  9. package/esm/application/boot-signal.mjs.map +1 -0
  10. package/esm/application/index.d.mts +1 -0
  11. package/esm/application/index.mjs +2 -1
  12. package/esm/bootstrap.mjs +2 -2
  13. package/esm/bootstrap.mjs.map +1 -1
  14. package/esm/cli/cli-commands.manager.mjs +3 -3
  15. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  16. package/esm/cli/cli-commands.utils.mjs +57 -1
  17. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
  19. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  20. package/esm/cli/commands/start-production.command.mjs +4 -25
  21. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  22. package/esm/cli/types.d.mts +6 -1
  23. package/esm/cli/types.d.mts.map +1 -1
  24. package/esm/config/config-setter.d.mts +27 -0
  25. package/esm/config/config-setter.d.mts.map +1 -0
  26. package/esm/config/config-setter.mjs +10 -0
  27. package/esm/config/config-setter.mjs.map +1 -0
  28. package/esm/config/index.mjs +1 -0
  29. package/esm/connectors/http-connector.d.mts.map +1 -1
  30. package/esm/connectors/http-connector.mjs +2 -1
  31. package/esm/connectors/http-connector.mjs.map +1 -1
  32. package/esm/connectors/storage.connector.d.mts +11 -5
  33. package/esm/connectors/storage.connector.d.mts.map +1 -1
  34. package/esm/connectors/storage.connector.mjs +11 -5
  35. package/esm/connectors/storage.connector.mjs.map +1 -1
  36. package/esm/dev-server/loader/load-hook.mjs +1 -1
  37. package/esm/generations/stubs.mjs +1 -1
  38. package/esm/generations/stubs.mjs.map +1 -1
  39. package/esm/http/index.d.mts +1 -0
  40. package/esm/http/index.mjs +1 -0
  41. package/esm/http/port-preflight.d.mts +34 -0
  42. package/esm/http/port-preflight.d.mts.map +1 -0
  43. package/esm/http/port-preflight.mjs +66 -0
  44. package/esm/http/port-preflight.mjs.map +1 -0
  45. package/esm/http/response.d.mts +17 -2
  46. package/esm/http/response.d.mts.map +1 -1
  47. package/esm/http/response.mjs +36 -2
  48. package/esm/http/response.mjs.map +1 -1
  49. package/esm/index.d.mts +6 -2
  50. package/esm/index.mjs +6 -2
  51. package/esm/production/assert-generated-imports.mjs +94 -0
  52. package/esm/production/assert-generated-imports.mjs.map +1 -0
  53. package/esm/production/production-builder.mjs +29 -4
  54. package/esm/production/production-builder.mjs.map +1 -1
  55. package/esm/production/production-supervisor.mjs +108 -0
  56. package/esm/production/production-supervisor.mjs.map +1 -0
  57. package/esm/storage/storage.d.mts +16 -0
  58. package/esm/storage/storage.d.mts.map +1 -1
  59. package/esm/storage/storage.mjs +24 -1
  60. package/esm/storage/storage.mjs.map +1 -1
  61. package/esm/tests/index.d.mts +2 -1
  62. package/esm/tests/index.mjs +1 -0
  63. package/esm/tests/start-http-development-server.d.mts +15 -2
  64. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  65. package/esm/tests/start-http-development-server.mjs +43 -16
  66. package/esm/tests/start-http-development-server.mjs.map +1 -1
  67. package/esm/tests/test-helpers.d.mts.map +1 -1
  68. package/esm/tests/test-helpers.mjs +2 -1
  69. package/esm/tests/test-helpers.mjs.map +1 -1
  70. package/esm/tests/test-server-port-channel.d.mts +27 -0
  71. package/esm/tests/test-server-port-channel.d.mts.map +1 -0
  72. package/esm/tests/test-server-port-channel.mjs +32 -0
  73. package/esm/tests/test-server-port-channel.mjs.map +1 -0
  74. package/esm/utils/load-environment.mjs +59 -0
  75. package/esm/utils/load-environment.mjs.map +1 -0
  76. package/llms-full.txt +113 -7
  77. package/llms.txt +1 -1
  78. package/package.json +13 -13
  79. package/skills/run-app/SKILL.md +63 -0
  80. package/skills/send-response/SKILL.md +23 -4
  81. package/skills/test-http/SKILL.md +27 -3
@@ -219,6 +219,57 @@ Just `warlockConfig: true` — same as `build`. The actual app bootstrap happens
219
219
 
220
220
  Means `docker stop` / `kubectl delete pod` works as expected: SIGTERM reaches the bundle, your graceful-shutdown hooks fire, then the parent exits.
221
221
 
222
+ ### pnpm needs esbuild's install script allowed
223
+
224
+ pnpm 10+ will not run a dependency's install script unless the app names it. esbuild's script links its platform-native binary, and `warlock build` shells out to that binary — so the app installs cleanly and then cannot build:
225
+
226
+ ```
227
+ [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.27.7
228
+ ```
229
+
230
+ Fix it once in the app's `pnpm-workspace.yaml`:
231
+
232
+ ```yaml
233
+ allowBuilds:
234
+ esbuild: true
235
+ ```
236
+
237
+ Note pnpm reads this from `pnpm-workspace.yaml`, **not** from `package.json`'s `pnpm` field — pnpm 11 warns that the field is ignored and then carries on, so settings left there fail silently.
238
+
239
+ Nothing else is needed for pnpm. Warlock never requires an app to declare a package it does not import: generated code is checked at build time against the app's own `dependencies`, so `warlock build` failing over an unfamiliar package name is a framework bug, not a missing dependency.
240
+
241
+ ### Output streams — what a supervisor may trust
242
+
243
+ **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.
244
+
245
+ | Stream | Carries |
246
+ | ---------- | -------------------------------------------------------------- |
247
+ | **stdout** | the started banner, and start failures. Nothing else. |
248
+ | **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
249
+
250
+ 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`**.
251
+
252
+ ```bash
253
+ # a CI gate can be this blunt, and it is now correct
254
+ yarn warlock start | grep -q "production server started"
255
+ ```
256
+
257
+ ### How readiness is reported
258
+
259
+ `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:
260
+
261
+ ```ts
262
+ { type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }
263
+ ```
264
+
265
+ Three consequences worth knowing:
266
+
267
+ 1. **A queue worker with no http connector still reports.** Readiness hangs on a completed boot, not on a bound port. `port` is simply absent.
268
+ 2. **Running the bundle any other way changes nothing.** `node dist/app.js`, a Docker `CMD`, or pm2 — the signal is a strict no-op without both the IPC channel and the handshake flag, so Warlock never writes into a channel that belongs to another supervisor.
269
+ 3. **A bundle built before 4.11.0 has no signal.** It starts and runs normally, and after ten seconds prints a note on **stderr only** telling you to re-run `warlock build`. An absent signal is never an error, never fails the run, and never kills a slow boot.
270
+
271
+ If you need the same fact inside the app, use `Application.onceBooted()` / `Application.whenBooted()` — the signal and your listeners fire from the same latch.
272
+
222
273
  ## Picking which mode you're in
223
274
 
224
275
  `Application.environment` and `Application.runtimeStrategy` are separate axes:
@@ -286,6 +337,18 @@ export default defineConfig({
286
337
 
287
338
  CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
288
339
 
340
+ **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.
341
+
342
+ **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:
343
+
344
+ ```bash
345
+ warlock build # NODE_ENV unset → reads .env
346
+ NODE_ENV=production warlock build # → reads .env.production
347
+ NODE_ENV=staging warlock build # → reads .env.staging
348
+ ```
349
+
350
+ That is deliberate: `build` and `start` do **not** force `production`. Forcing it would silently change which file an existing `NODE_ENV=staging` pipeline reads, and would have the framework overriding an operator's explicit choice at the moment of deployment. Set `NODE_ENV` in your Dockerfile, CI job, or process manager — the same place you already set it for `Application.environment`.
351
+
289
352
  ### Skip type-gen on machines without write access
290
353
 
291
354
  ```bash
@@ -160,16 +160,35 @@ Browsers consume SSE via `new EventSource(url)`. Cheaper than websockets when yo
160
160
  response.header("X-Total-Count", "42");
161
161
 
162
162
  // JSON-wrapped (default) — round-trips with request.cookie("prefs")
163
- response.cookie("prefs", { theme: "dark" }, { httpOnly: true });
163
+ response.cookie("prefs", { theme: "dark" });
164
164
 
165
165
  // Plain string — use raw: true for session tokens / opaque IDs that
166
166
  // shouldn't be JSON-quoted on the wire
167
- response.cookie("session_id", "abc.def.ghi", { raw: true, httpOnly: true });
167
+ response.cookie("session_id", "abc.def.ghi", { raw: true });
168
+
169
+ // Deliberately readable by client-side JS — opt out explicitly
170
+ response.cookie("theme", "dark", { httpOnly: false });
168
171
 
169
172
  response.clearCookie("session_id");
170
173
  ```
171
174
 
172
- `response.cookie()` JSON-stringifies the value by default so structured cookies round-trip cleanly with `request.cookie(name)`. Pass `{ raw: true }` to skip the wrapping for plain-string cookies (session tokens, opaque IDs, simple flags). The new `CookieOptions` type extends Fastify's `CookieSerializeOptions` with the `raw` flag.
175
+ `response.cookie()` JSON-stringifies the value by default so structured cookies round-trip cleanly with `request.cookie(name)`. Pass `{ raw: true }` to skip the wrapping for plain-string cookies (session tokens, opaque IDs, simple flags). The `CookieOptions` type extends Fastify's `CookieSerializeOptions` with the `raw` flag.
176
+
177
+ ### Secure by default (4.10.0)
178
+
179
+ Every response cookie gets `httpOnly: true`, `sameSite: "lax"`, and `secure: true` outside development, unless you override them:
180
+
181
+ | Flag | Default | Why it's the default |
182
+ |---|---|---|
183
+ | `httpOnly` | `true` | without it, any injected script can read the cookie |
184
+ | `sameSite` | `"lax"` | without it, the cookie rides along on cross-site requests |
185
+ | `secure` | `true`, except in development | without it, the cookie travels in cleartext |
186
+
187
+ `secure` is relaxed in development only — browsers drop a `Secure` cookie over plain http, which would silently break every local login. It stays on in test and staging.
188
+
189
+ Precedence, lowest to highest: **framework defaults → `http.cookies.options` → the per-call `options` argument.** Set an app-wide policy in config, override per cookie when a specific one genuinely needs different treatment.
190
+
191
+ These are the flags whose absence never fails a test and is fatal in production: the app works perfectly and is simply insecure. Opting out is now explicit and visible in review.
173
192
 
174
193
  These mutate the response in place; chain or call before the final `return response.<helper>()`.
175
194
 
@@ -235,7 +254,7 @@ return response.header("X-Cache", "HIT").replay({
235
254
 
236
255
  - **Don't manually set the status code.** Use the matching helper. `response.send(...)` with a hand-rolled status loses error-handler integration.
237
256
  - **Don't call multiple helpers.** First helper wins; subsequent calls log a "response already sent" warning.
238
- - **Cookie options vary by environment.** Set `secure: Application.isProduction` so cookies work in dev (HTTP) and prod (HTTPS) without changing code.
257
+ - **Don't hand-set `secure: Application.isProduction` any more.** Since 4.10.0 `secure` already tracks the environment (on everywhere except development). Writing it by hand narrows the default — it would also switch `secure` off in test and staging.
239
258
  - **Streaming and SSE consume the response.** No `response.success(...)` afterward — call `stream.end()` / `sse.end()` to finish.
240
259
 
241
260
  ## See also
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: test-http
3
- description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
3
+ description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
4
4
  ---
5
5
 
6
6
  # Warlock — HTTP integration tests
@@ -56,6 +56,30 @@ Unlike the dev server, it doesn't watch files, doesn't do HMR, doesn't run healt
56
56
 
57
57
  Both are idempotent. A second `start` returns early; `stop` on a non-running server logs and returns.
58
58
 
59
+ ### Choosing the port — `startHttpTestServer({ port })`
60
+
61
+ ```ts
62
+ await startHttpTestServer({ port: 3999 });
63
+ ```
64
+
65
+ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`. Use it to run a suite while the dev server holds the configured port, or to run two suites side by side.
66
+
67
+ **Nothing else can move the server.** `startHttpTestServer()` bootstraps the app itself, and that bootstrap re-reads `.env` (dotenv overrides by default), so any value you set before calling it is gone before the port is read. Assigning `process.env.HTTP_PORT` does not work either: `env()` reads dotenv's own store and never falls back to `process.env`. The option is applied after the bootstrap, immediately before the connector binds — that is why it is the only channel that survives.
68
+
69
+ The request helpers follow with no extra wiring: the bound port is published as `WARLOCK_TEST_SERVER_PORT`, which the workers inherit, and `getTestServerUrl()` prefers it over their own `.env`-resolved `http.port`. `stopHttpTestServer()` withdraws it.
70
+
71
+ ### The port is preflighted
72
+
73
+ Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
74
+
75
+ ```
76
+ Port 2031 is already in use on localhost. Stop the dev server (or whatever else is
77
+ listening on port 2031) and run again, or start on a free port — e.g.
78
+ startHttpTestServer({ port: 2032 }).
79
+ ```
80
+
81
+ This runs whether or not you passed a port, so a collision never reaches you as a bare `EADDRINUSE` from inside Fastify. The failure is a `PortInUseError` carrying `port` and `host`. The same check is available on its own — `assertPortIsAvailable(port, host)` throws it, `isPortAvailable(port, host)` returns a boolean.
82
+
59
83
  ## Project wiring — `src/test-global-setup.ts` + `vite.config.ts`
60
84
 
61
85
  ```ts title="src/test-global-setup.ts"
@@ -105,7 +129,7 @@ import { getTestServerUrl } from "@warlock.js/core";
105
129
  const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
106
130
  ```
107
131
 
108
- Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config. If you change the HTTP config, helpers follow automatically.
132
+ 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.
109
133
 
110
134
  ### Verb helpers
111
135
 
@@ -274,7 +298,7 @@ This is fine for normal test flow. It bites when you're inside a transaction the
274
298
  ## Gotchas
275
299
 
276
300
  - **`globalSetup` must export `setup` and `teardown`.** Vitest reads them by name. A typo in the export gets you a confusing "server not running" error on the first `testGet` call.
277
- - **Port conflicts.** If `http.port` matches your running dev server, `startHttpTestServer()` fails to bind. Either stop the dev server or set a test-only port: `http: { port: 3999 }` in a test-config branch.
301
+ - **Port conflicts.** If `http.port` matches your running dev server, the preflight fails with a `PortInUseError` naming the port. Either stop the dev server or pass a free one: `startHttpTestServer({ port: 3999 })`.
278
302
  - **Auth tokens need a real user.** Generating a JWT with a non-existent `user_id` works — but the auth middleware's user-loading step will reject the request with 401 because it can't find the user in the DB.
279
303
  - **`expectJson` parses the body once.** If you call it twice on the same response, the second call gets an already-consumed stream error. Capture the result.
280
304
  - **The HTTP server's connection is NOT torn down between test files.** Data persists across files within a single `vitest` run. Either truncate in `afterEach` / `afterAll`, or design your tests to be order-independent.