@warlock.js/core 4.12.0 → 4.14.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 (189) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  3. package/esm/cli/commands/add.command.mjs +1 -1
  4. package/esm/cli/commands/add.command.mjs.map +1 -1
  5. package/esm/cli/commands/build.command.mjs +1 -1
  6. package/esm/cli/commands/build.command.mjs.map +1 -1
  7. package/esm/cli/commands/create-database.command.mjs +1 -1
  8. package/esm/cli/commands/create-database.command.mjs.map +1 -1
  9. package/esm/cli/commands/dev-server.command.mjs +1 -1
  10. package/esm/cli/commands/dev-server.command.mjs.map +1 -1
  11. package/esm/cli/commands/doctor/doctor-command.action.mjs.map +1 -1
  12. package/esm/cli/commands/doctor.command.mjs +1 -1
  13. package/esm/cli/commands/doctor.command.mjs.map +1 -1
  14. package/esm/cli/commands/drop-tables.command.mjs +1 -1
  15. package/esm/cli/commands/drop-tables.command.mjs.map +1 -1
  16. package/esm/cli/commands/generate/generate.command.mjs +1 -1
  17. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  18. package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
  19. package/esm/cli/commands/generate/generators/migration.generator.mjs +1 -1
  20. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  21. package/esm/cli/commands/generate/generators/model.generator.mjs +1 -1
  22. package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
  23. package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
  24. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  25. package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
  26. package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
  27. package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
  28. package/esm/cli/commands/generate/utils/path-resolver.mjs +1 -1
  29. package/esm/cli/commands/generate/utils/writer.mjs +1 -1
  30. package/esm/cli/commands/migrate.command.mjs +1 -1
  31. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  32. package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
  33. package/esm/cli/commands/routes.command.mjs +1 -1
  34. package/esm/cli/commands/routes.command.mjs.map +1 -1
  35. package/esm/cli/commands/seed.command.mjs +1 -1
  36. package/esm/cli/commands/seed.command.mjs.map +1 -1
  37. package/esm/cli/commands/start-production.command.mjs +1 -1
  38. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  39. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  40. package/esm/cli/commands/storage-put.command.mjs +1 -1
  41. package/esm/cli/commands/storage-put.command.mjs.map +1 -1
  42. package/esm/cli/commands/typings-generator.command.mjs +2 -2
  43. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  44. package/esm/cli/commands/update.command.mjs +1 -1
  45. package/esm/cli/commands/update.command.mjs.map +1 -1
  46. package/esm/cli/commands-loader.mjs +1 -1
  47. package/esm/cli/commands-loader.mjs.map +1 -1
  48. package/esm/{cli → commands}/cli-command.d.mts +1 -1
  49. package/esm/commands/cli-command.d.mts.map +1 -0
  50. package/esm/{cli → commands}/cli-command.mjs +1 -1
  51. package/esm/commands/cli-command.mjs.map +1 -0
  52. package/esm/{cli → commands}/types.d.mts +1 -1
  53. package/esm/commands/types.d.mts.map +1 -0
  54. package/esm/config/load-config-files.mjs +1 -1
  55. package/esm/config/load-config-files.mjs.map +1 -1
  56. package/esm/connectors/base-connector.mjs +1 -1
  57. package/esm/connectors/base-connector.mjs.map +1 -1
  58. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  59. package/esm/connectors/connectors-manager.mjs +5 -3
  60. package/esm/connectors/connectors-manager.mjs.map +1 -1
  61. package/esm/connectors/http-connector.d.mts.map +1 -1
  62. package/esm/connectors/http-connector.mjs +0 -2
  63. package/esm/connectors/http-connector.mjs.map +1 -1
  64. package/esm/database/create-database-action.mjs.map +1 -1
  65. package/esm/database/drop-tables-action.mjs.map +1 -1
  66. package/esm/database/migrate-action.mjs +1 -1
  67. package/esm/database/migrate-action.mjs.map +1 -1
  68. package/esm/database/seed-command-action.mjs +1 -1
  69. package/esm/database/seed-command-action.mjs.map +1 -1
  70. package/esm/dev-server/dev-logger.mjs +1 -1
  71. package/esm/dev-server/dev-logger.mjs.map +1 -1
  72. package/esm/dev-server/development-server.mjs +2 -2
  73. package/esm/dev-server/file-event-handler.mjs +1 -1
  74. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  75. package/esm/dev-server/file-manager.mjs +1 -1
  76. package/esm/dev-server/file-manager.mjs.map +1 -1
  77. package/esm/dev-server/file-operations.mjs +1 -1
  78. package/esm/dev-server/file-operations.mjs.map +1 -1
  79. package/esm/dev-server/files-orchestrator.mjs +3 -18
  80. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  81. package/esm/dev-server/files-watcher.mjs +1 -1
  82. package/esm/dev-server/files-watcher.mjs.map +1 -1
  83. package/esm/dev-server/health-checker/file-health-checker.contract.d.mts +1 -1
  84. package/esm/dev-server/layer-executor.mjs +1 -1
  85. package/esm/dev-server/loader/register-loader.mjs +1 -1
  86. package/esm/dev-server/package-json-manager.mjs +1 -1
  87. package/esm/dev-server/package-json-manager.mjs.map +1 -1
  88. package/esm/dev-server/parse-imports.mjs +2 -2
  89. package/esm/dev-server/parse-imports.mjs.map +1 -1
  90. package/esm/dev-server/tsconfig-manager.mjs +1 -1
  91. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  92. package/esm/dev-server/type-generator.mjs +1 -1
  93. package/esm/dev-server/type-generator.mjs.map +1 -1
  94. package/esm/dev-server/utils.mjs +1 -1
  95. package/esm/dev-server/utils.mjs.map +1 -1
  96. package/esm/generations/add-command.action.mjs +20 -8
  97. package/esm/generations/add-command.action.mjs.map +1 -1
  98. package/esm/http/build-cors-options.mjs +32 -0
  99. package/esm/http/build-cors-options.mjs.map +1 -0
  100. package/esm/http/middleware/max-body-size.middleware.d.mts +13 -5
  101. package/esm/http/middleware/max-body-size.middleware.d.mts.map +1 -1
  102. package/esm/http/middleware/max-body-size.middleware.mjs +13 -5
  103. package/esm/http/middleware/max-body-size.middleware.mjs.map +1 -1
  104. package/esm/http/plugins.d.mts.map +1 -1
  105. package/esm/http/plugins.mjs +2 -9
  106. package/esm/http/plugins.mjs.map +1 -1
  107. package/esm/http/server.d.mts.map +1 -1
  108. package/esm/http/server.mjs +3 -8
  109. package/esm/http/server.mjs.map +1 -1
  110. package/esm/index.d.mts +9 -17
  111. package/esm/index.mjs +4 -15
  112. package/esm/production/production-builder.mjs +51 -3
  113. package/esm/production/production-builder.mjs.map +1 -1
  114. package/esm/production/resolve-build-config.mjs +1 -1
  115. package/esm/production/resolve-build-config.mjs.map +1 -1
  116. package/esm/router/router.d.mts +9 -0
  117. package/esm/router/router.d.mts.map +1 -1
  118. package/esm/router/router.mjs +58 -5
  119. package/esm/router/router.mjs.map +1 -1
  120. package/esm/router/types.d.mts +24 -1
  121. package/esm/router/types.d.mts.map +1 -1
  122. package/esm/tests/index.d.mts +3 -2
  123. package/esm/tests/index.mjs +2 -3
  124. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  125. package/esm/tests/start-http-development-server.mjs +38 -15
  126. package/esm/tests/start-http-development-server.mjs.map +1 -1
  127. package/esm/tests/test-connectors-selection.d.mts +14 -0
  128. package/esm/tests/test-connectors-selection.d.mts.map +1 -0
  129. package/esm/tests/test-connectors-selection.mjs +72 -0
  130. package/esm/tests/test-connectors-selection.mjs.map +1 -0
  131. package/esm/tests/test-helpers.d.mts.map +1 -1
  132. package/esm/tests/test-helpers.mjs +34 -13
  133. package/esm/tests/test-helpers.mjs.map +1 -1
  134. package/esm/tests/test-lifecycle-state.mjs +27 -0
  135. package/esm/tests/test-lifecycle-state.mjs.map +1 -0
  136. package/esm/tests/test-setup-timeout.mjs +53 -0
  137. package/esm/tests/test-setup-timeout.mjs.map +1 -0
  138. package/esm/tests/vitest-setup.d.mts +64 -8
  139. package/esm/tests/vitest-setup.d.mts.map +1 -1
  140. package/esm/tests/vitest-setup.mjs +317 -11
  141. package/esm/tests/vitest-setup.mjs.map +1 -1
  142. package/esm/utils/framework-vesion.mjs +1 -1
  143. package/esm/utils/index.d.mts +1 -0
  144. package/esm/utils/index.mjs +1 -0
  145. package/esm/utils/normalized-path.d.mts +55 -0
  146. package/esm/utils/normalized-path.d.mts.map +1 -0
  147. package/esm/{dev-server/path.mjs → utils/normalized-path.mjs} +15 -2
  148. package/esm/utils/normalized-path.mjs.map +1 -0
  149. package/esm/vite/index.d.mts +2 -0
  150. package/esm/vite/index.mjs +1 -1
  151. package/esm/vite/lower-stage3-decorators.d.mts +1 -1
  152. package/esm/vite/lower-stage3-decorators.mjs +1 -1
  153. package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
  154. package/esm/warlock-config/index.mjs +1 -1
  155. package/esm/warlock-config/types.d.mts +34 -1
  156. package/esm/warlock-config/types.d.mts.map +1 -1
  157. package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
  158. package/esm/warlock-config/warlock-config.manager.mjs +1 -2
  159. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  160. package/llms-full.txt +78 -23
  161. package/llms.txt +3 -3
  162. package/package.json +24 -12
  163. package/skills/lower-stage3-decorators/SKILL.md +4 -2
  164. package/skills/run-app/SKILL.md +6 -0
  165. package/skills/test-http/SKILL.md +37 -12
  166. package/skills/test-service/SKILL.md +167 -24
  167. package/skills/use-middleware/SKILL.md +5 -1
  168. package/esm/cli/cli-command.d.mts.map +0 -1
  169. package/esm/cli/cli-command.mjs.map +0 -1
  170. package/esm/cli/types.d.mts.map +0 -1
  171. package/esm/dev-server/files-orchestrator.d.mts +0 -89
  172. package/esm/dev-server/files-orchestrator.d.mts.map +0 -1
  173. package/esm/dev-server/files-watcher.d.mts +0 -69
  174. package/esm/dev-server/files-watcher.d.mts.map +0 -1
  175. package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts +0 -55
  176. package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts.map +0 -1
  177. package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts +0 -44
  178. package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts.map +0 -1
  179. package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts +0 -60
  180. package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts.map +0 -1
  181. package/esm/dev-server/health-checker/files-healthcare.manager.d.mts +0 -119
  182. package/esm/dev-server/health-checker/files-healthcare.manager.d.mts.map +0 -1
  183. package/esm/dev-server/health-checker/index.mjs +0 -7
  184. package/esm/dev-server/module-loader.d.mts +0 -63
  185. package/esm/dev-server/module-loader.d.mts.map +0 -1
  186. package/esm/dev-server/path.mjs.map +0 -1
  187. package/esm/tests/test-server-port-channel.d.mts +0 -27
  188. package/esm/tests/test-server-port-channel.d.mts.map +0 -1
  189. /package/esm/{cli → commands}/index.d.mts +0 -0
package/llms-full.txt CHANGED
@@ -2439,7 +2439,7 @@ For an even smoother handoff, give the load balancer time to observe the 503 bef
2439
2439
 
2440
2440
  ---
2441
2441
  name: lower-stage3-decorators
2442
- description: 'Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won''t load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.'
2442
+ description: 'Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won''t load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.'
2443
2443
  ---
2444
2444
 
2445
2445
  # Lower Stage-3 decorators for Vite/Vitest
@@ -2454,12 +2454,14 @@ The cause is two transforms that don't understand each other. Vite 8 transpiles
2454
2454
 
2455
2455
  `lowerStage3Decorators()` fixes it by getting **esbuild** to lower the decorators *first*.
2456
2456
 
2457
+ ⚠ **Changed in 4.13.0 — the import is a subpath now.** The plugin used to be re-exported from the package root; it is not any more, because that put the Vite integration into every application's production module graph. `import { lowerStage3Decorators } from "@warlock.js/core"` now fails with *"has no exported member"* — **add `/vite` to the specifier and nothing else changes.**
2458
+
2457
2459
  ## Use it
2458
2460
 
2459
2461
  Drop it **first** in your `plugins` array:
2460
2462
 
2461
2463
  ```ts title="vite.config.ts"
2462
- import { lowerStage3Decorators } from "@warlock.js/core";
2464
+ import { lowerStage3Decorators } from "@warlock.js/core/vite";
2463
2465
  import mongezVite from "@mongez/vite";
2464
2466
  import { defineConfig } from "vitest/config";
2465
2467
 
@@ -2519,7 +2521,9 @@ await new Image("./photo.jpg")
2519
2521
  .save("./output.webp");
2520
2522
  ```
2521
2523
 
2522
- That's the full contract. The chain doesn't touch sharp until the output method fires.
2524
+ That's the full contract. The chain doesn't run sharp until the output method fires; the
2525
+ constructor resolves the sharp module itself, so a missing sharp throws there rather than at the
2526
+ final await.
2523
2527
 
2524
2528
  ## Installation
2525
2529
 
@@ -3285,6 +3289,8 @@ export default defineConfig({
3285
3289
  outFile: "app.js", // default — bundle filename
3286
3290
  minify: true, // default — esbuild minify
3287
3291
  sourcemap: true, // default — true | false | "inline" | "linked"
3292
+ singleBundle: false, // default — one runnable file, deps inlined
3293
+ esmShim: true, // default — require/__filename/__dirname for bundled CJS
3288
3294
  },
3289
3295
  });
3290
3296
  ```
@@ -3296,6 +3302,10 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
3296
3302
  - **`minify: false`** — flip to debug a production-only bug. Larger bundle, readable stack traces.
3297
3303
  - **`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.
3298
3304
  - **`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).
3305
+ - **`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.
3306
+ - **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
3307
+
3308
+ ⚠ **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.
3299
3309
 
3300
3310
  ### What it preloads
3301
3311
 
@@ -4556,20 +4566,22 @@ Client `PUT`s the bytes straight to R2 — your server never sees them.
4556
4566
 
4557
4567
  ---
4558
4568
  name: test-http
4559
- 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`.'
4569
+ 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/tests"`. 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`.'
4560
4570
  ---
4561
4571
 
4562
4572
  # Warlock — HTTP integration tests
4563
4573
 
4564
4574
  Some tests need the full stack: route matching, middleware chain, validation, controller, response serialization. For those, you boot the real HTTP server once per test run and make real `fetch` calls against it.
4565
4575
 
4566
- `startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core`.
4576
+ `startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
4577
+
4578
+ ⚠ **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.**
4567
4579
 
4568
4580
  ## The shape
4569
4581
 
4570
4582
  ```ts title="src/app/users/tests/users.controller.test.ts"
4571
4583
  import { describe, expect, it } from "vitest";
4572
- import { expectJson, testGet, testPost } from "@warlock.js/core";
4584
+ import { expectJson, testGet, testPost } from "@warlock.js/core/tests";
4573
4585
 
4574
4586
  describe("Users API", () => {
4575
4587
  it("GET /users returns the list", async () => {
@@ -4595,7 +4607,7 @@ No `beforeAll`, no manual server start — the project's `src/test-global-setup.
4595
4607
  ## The bootstrap — `startHttpTestServer` / `stopHttpTestServer`
4596
4608
 
4597
4609
  ```ts
4598
- import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core";
4610
+ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests";
4599
4611
  ```
4600
4612
 
4601
4613
  `startHttpTestServer()` boots a **minimal but real** HTTP server:
@@ -4636,6 +4648,23 @@ startHttpTestServer({ port: 2032 }).
4636
4648
 
4637
4649
  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.
4638
4650
 
4651
+ ### Port `0` is refused
4652
+
4653
+ `port: 0` — "let the OS pick one" — is not supported, and says so rather than half-working:
4654
+
4655
+ ```
4656
+ startHttpTestServer() cannot run on port 0. Pass an explicit port — e.g.
4657
+ startHttpTestServer({ port: 3999 }) — or set `http.port` in your config. Test workers
4658
+ are separate processes that resolve the server's URL from the port published at
4659
+ startup, and an OS-assigned port is not knowable to them.
4660
+ ```
4661
+
4662
+ The reason is the worker handoff described above: the port the OS assigns is never recorded anywhere the workers can read, so `getTestServerUrl()` would resolve `0` and send every request to `http://host:0`.
4663
+
4664
+ ### If startup fails, nothing is left behind
4665
+
4666
+ `startHttpTestServer()` owns the connectors it starts. A failure part-way through — a bad config, a connector that won't boot — tears down whatever came up, withdraws the published port, and **rethrows the original error**; a failure during that cleanup is logged but never replaces the cause. `stopHttpTestServer()` withdraws the port and resets its state even when the shutdown itself throws, and still surfaces that failure.
4667
+
4639
4668
  ## Project wiring — `src/test-global-setup.ts` + `vite.config.ts`
4640
4669
 
4641
4670
  ```ts title="src/test-global-setup.ts"
@@ -4643,7 +4672,7 @@ This runs whether or not you passed a port, so a collision never reaches you as
4643
4672
  * Global Test Setup
4644
4673
  * Runs ONCE in the main process before all test workers start.
4645
4674
  */
4646
- import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core";
4675
+ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests";
4647
4676
 
4648
4677
  export async function setup() {
4649
4678
  await startHttpTestServer();
@@ -4655,7 +4684,7 @@ export async function teardown() {
4655
4684
  ```
4656
4685
 
4657
4686
  ```ts title="vite.config.ts"
4658
- import { lowerStage3Decorators } from "@warlock.js/core";
4687
+ import { lowerStage3Decorators } from "@warlock.js/core/vite";
4659
4688
  import mongezVite from "@mongez/vite";
4660
4689
  import { defineConfig } from "vitest/config";
4661
4690
 
@@ -4680,7 +4709,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
4680
4709
  ### URL resolution
4681
4710
 
4682
4711
  ```ts
4683
- import { getTestServerUrl } from "@warlock.js/core";
4712
+ import { getTestServerUrl } from "@warlock.js/core/tests";
4684
4713
 
4685
4714
  const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
4686
4715
  ```
@@ -4697,7 +4726,7 @@ import {
4697
4726
  testPut,
4698
4727
  testPatch,
4699
4728
  testDelete,
4700
- } from "@warlock.js/core";
4729
+ } from "@warlock.js/core/tests";
4701
4730
 
4702
4731
  await testGet("/products");
4703
4732
  await testGet("/products?published=true", { headers: { "X-Tenant": "abc" } });
@@ -4714,7 +4743,7 @@ All accept a relative path (leading `/` optional) and a standard `RequestInit`.
4714
4743
  ### Parsing + asserting — `expectJson<T>`
4715
4744
 
4716
4745
  ```ts
4717
- import { expectJson, parseJsonResponse } from "@warlock.js/core";
4746
+ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
4718
4747
 
4719
4748
  // Parse-only
4720
4749
  const body = await parseJsonResponse<MyShape>(response);
@@ -4733,7 +4762,7 @@ const body = await expectJson<MyShape>(response, 404); // expects 404 (testi
4733
4762
 
4734
4763
  ```ts
4735
4764
  import { describe, expect, it } from "vitest";
4736
- import { expectJson, testGet, testPost } from "@warlock.js/core";
4765
+ import { expectJson, testGet, testPost } from "@warlock.js/core/tests";
4737
4766
 
4738
4767
  describe("Products API — happy path", () => {
4739
4768
  it("creates and reads back a product", async () => {
@@ -4872,7 +4901,7 @@ This is fine for normal test flow. It bites when you're inside a transaction the
4872
4901
 
4873
4902
  ---
4874
4903
  name: test-service
4875
- description: 'Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } from "@warlock.js/core"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.'
4904
+ description: 'Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } from "@warlock.js/core/tests"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.'
4876
4905
  ---
4877
4906
 
4878
4907
  # Warlock — test a service
@@ -4881,6 +4910,8 @@ For unit tests, you import the thing under test and call it directly. No HTTP, n
4881
4910
 
4882
4911
  `setupTest()` is the one-call bootstrap that gives each worker that environment.
4883
4912
 
4913
+ ⚠ **Changed in 4.13.0 — the import is a subpath now.** `setupTest` used to be re-exported from the package root; it is not any more, because that put the test helpers into every application's production module graph. `import { setupTest } from "@warlock.js/core"` now fails with *"has no exported member"* — **add `/tests` to the specifier and nothing else changes.**
4914
+
4884
4915
  ## The shape
4885
4916
 
4886
4917
  ```ts title="src/app/users/tests/register-user.service.test.ts"
@@ -4909,7 +4940,7 @@ No `beforeAll(setupTest)` in this file — the project's `src/test-setup.ts` (re
4909
4940
  ## `setupTest({ connectors })` — the worker bootstrap
4910
4941
 
4911
4942
  ```ts
4912
- import { setupTest } from "@warlock.js/core";
4943
+ import { setupTest } from "@warlock.js/core/tests";
4913
4944
 
4914
4945
  await setupTest({ connectors: true });
4915
4946
  ```
@@ -4957,13 +4988,13 @@ The `warlock add test` feature creates both files. The standard wiring:
4957
4988
  * Per-Worker Test Setup
4958
4989
  * Runs in EACH Vitest worker thread before tests execute.
4959
4990
  */
4960
- import { setupTest } from "@warlock.js/core";
4991
+ import { setupTest } from "@warlock.js/core/tests";
4961
4992
 
4962
4993
  await setupTest({ connectors: true });
4963
4994
  ```
4964
4995
 
4965
4996
  ```ts title="vite.config.ts"
4966
- import { lowerStage3Decorators } from "@warlock.js/core";
4997
+ import { lowerStage3Decorators } from "@warlock.js/core/vite";
4967
4998
  import mongezVite from "@mongez/vite";
4968
4999
  import { defineConfig } from "vitest/config";
4969
5000
 
@@ -5082,11 +5113,11 @@ Vitest runs tests in a single worker file sequentially, so an `afterEach` trunca
5082
5113
 
5083
5114
  ```ts title="src/app/utils/tests/slugify.test.ts"
5084
5115
  import { beforeAll, describe, expect, it } from "vitest";
5085
- import { setupTest } from "@warlock.js/core";
5116
+ import { setupTest } from "@warlock.js/core/tests";
5086
5117
  import { slugify } from "../utils/slugify";
5087
5118
 
5088
5119
  beforeAll(async () => {
5089
- await setupTest({ connectors: false }); // override the project default
5120
+ await setupTest({ connectors: false }); // starts no connectors at all
5090
5121
  });
5091
5122
 
5092
5123
  describe("slugify", () => {
@@ -5096,7 +5127,9 @@ describe("slugify", () => {
5096
5127
  });
5097
5128
  ```
5098
5129
 
5099
- `setupTest` is idempotent per worker (`isSetupComplete` flag) — calling it again with different options after `src/test-setup.ts` already ran is a no-op. To genuinely skip connectors, either set `tests.connectors: false` in config (project-wide) or rely on the default in `src/test-setup.ts` being what you want most of the time.
5130
+ `setupTest` is idempotent per worker (`isSetupComplete` flag) — calling it again with different options after `src/test-setup.ts` already ran is a no-op. **That includes a `connectors: false` call: if `src/test-setup.ts` already ran `setupTest()` in this worker, the example above changes nothing.** To genuinely skip connectors, either set `tests.connectors: false` in config (project-wide) or rely on the default in `src/test-setup.ts` being what you want most of the time.
5131
+
5132
+ ⚠ **Config beats the parameter.** If `tests.connectors` is set at all, `setupTest({ connectors })` cannot override it — the config value wins. That is the current contract, not an accident; a per-call override is under discussion for a later release.
5100
5133
 
5101
5134
  ## Gotchas
5102
5135
 
@@ -5104,7 +5137,7 @@ describe("slugify", () => {
5104
5137
  - **Per-worker connections are separate from the HTTP server's connections.** A row inserted by a service-level test is on the worker's connection; the HTTP test server has its own. They don't see each other unless they're both pointing at the same physical DB and the inserting test has already committed.
5105
5138
  - **`NODE_ENV` is set to `"test"`** by `setupTest`. Code that branches on `Application.isProduction` / `Application.isDevelopment` sees `false` for both. If your tests need production-like config (cookies, CORS), set those values in `src/config/*.ts` explicitly under the test branch — don't rely on the env flag.
5106
5139
  - **No HTTP from this layer.** `setupTest({ connectors: true })` never starts the HTTP connector by design. Don't try to `request.app.http` your way to a fetch test — use the `test-http` skill instead.
5107
- - **Don't import `vitest-setup` from `@warlock.js/core/src/...`.** The public surface is `import { setupTest } from "@warlock.js/core"`. Reaching into source paths breaks when the package layout shifts.
5140
+ - **Don't import `vitest-setup` from `@warlock.js/core/src/...`.** The public surface is `import { setupTest } from "@warlock.js/core/tests"`. Reaching into source paths breaks when the package layout shifts.
5108
5141
  - **Test files need the `.test.ts` suffix.** `include: ["src/app/**/*.test.ts"]` is what vitest scans. A file named `service.tests.ts` (plural) silently doesn't run.
5109
5142
 
5110
5143
  ## See also
@@ -6241,6 +6274,8 @@ Server errors (5xx) are not cached — clients can retry past a 5xx. 4xx respons
6241
6274
 
6242
6275
  `http.bodyLimit` in config is read by Fastify at server-start and applies to every body. `middleware.maxBodySize()` is a per-route middleware on top — it checks `Content-Length` after route match and rejects with 413 before body parsing runs. Use both: global as a safety net, per-route for tight caps on small-payload endpoints.
6243
6276
 
6277
+ ⚠ **Since 4.13.0, configuring nothing gives you Fastify's own 1 MB limit.** Earlier versions defaulted to 200 GB, which **replaced** Fastify's protection rather than adding to it — so an app that set nothing had effectively no cap. **Set `bodyLimit` explicitly if you need more than 1 MB.**
6278
+
6244
6279
  ```ts
6245
6280
  // src/config/http.ts
6246
6281
  export default { bodyLimit: 10 * 1024 * 1024 }; // 10MB globally
@@ -6271,7 +6306,9 @@ export default {
6271
6306
 
6272
6307
  ## `ipFilter` — fail-closed
6273
6308
 
6274
- `deny` wins over `allow`. If the IP can't be read (empty / unparseable), the request is rejected with 403. Reads via `request.detectIp()` which honors `X-Real-IP` and `X-Forwarded-For` (Fastify starts with `trustProxy: true`).
6309
+ `deny` wins over `allow`. If the IP can't be read (empty / unparseable), the request is rejected with 403. Reads via `request.detectIp()`.
6310
+
6311
+ ⚠ **Since 4.13.0 `http.trustProxy` defaults to `false`**, so `request.detectIp()` returns the socket address and **`X-Real-IP` / `X-Forwarded-For` are ignored unless you opt in.** Set `trustProxy: true` **only when you are genuinely behind a proxy that overwrites those headers** — before 4.13.0 the default was `true`, which meant any client could set its own forwarding header and be believed.
6275
6312
 
6276
6313
  ```ts
6277
6314
  import { middleware } from "@warlock.js/core";
@@ -8151,12 +8188,30 @@ The framework ships a fixed set of commands you call but don't author. Knowing t
8151
8188
 
8152
8189
  | Command | Flags / args | Preloads |
8153
8190
  | -------- | ----------------------------------------------- | ------------------------------ |
8154
- | `warlock migrate` | `--list` (just list pending), `--fresh` / `-f` (drop tables first) | database, logger |
8191
+ | `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
8155
8192
  | `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
8156
8193
  | `warlock create-database <name>` | bare positional `<name>` | database |
8157
8194
  | `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
8158
8195
  | `warlock db.indexes` | builds DB indexes for every registered model | database |
8159
8196
 
8197
+ **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".
8198
+
8199
+ `--list` is a report and always exits `0`. `--pending` is a gate, and its exit code is its whole API:
8200
+
8201
+ | Exit | Meaning |
8202
+ | ---- | ------- |
8203
+ | `0` | computed, nothing pending |
8204
+ | `1` | computed, N pending |
8205
+ | `2` | **could not be computed** |
8206
+
8207
+ ```bash
8208
+ warlock migrate --pending && ./deploy.sh
8209
+ ```
8210
+
8211
+ `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`.
8212
+
8213
+ **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.
8214
+
8160
8215
  ### Scaffolding
8161
8216
 
8162
8217
  The `generate.*` family covers every module piece:
package/llms.txt CHANGED
@@ -17,7 +17,7 @@
17
17
  - [encrypt-data](@warlock.js/core/encrypt-data/SKILL.md): Reversible AES-256-GCM `encrypt` / `decrypt` for secrets you need to read back; one-way HMAC-SHA256 `hmacHash` for deterministic fingerprints (lookup/dedup of encrypted columns). Keys come from `src/config/encryption.ts`. Triggers: `encrypt`, `decrypt`, `hmacHash`, `EncryptionConfigurations`, `APP_ENCRYPTION_KEY`, `APP_HMAC_KEY`; "store an API key reversibly", "fingerprint an encrypted column for lookup", "AES-256-GCM secret", "HMAC-SHA256 dedup key"; typical import `import { encrypt, decrypt, hmacHash } from "@warlock.js/core"`. Skip: password hashing — `@warlock.js/core/hash-password/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs Node `crypto` direct, `crypto-js`, `libsodium-wrappers`.
18
18
  - [hash-password](@warlock.js/core/hash-password/SKILL.md): One-way bcrypt password hashing — `hashPassword` / `verifyPassword`, plus the declarative `useHashedPassword()` schema transformer that auto-hashes a model's password field on save. Salt rounds come from `src/config/encryption.ts`. Triggers: `hashPassword`, `verifyPassword`, `useHashedPassword`, `password.salt`, `bcryptjs`; "hash a user password", "verify login credentials", "auto-hash on save", "rotate a password"; typical import `import { hashPassword, verifyPassword } from "@warlock.js/core"`. Skip: reversible secrets — `@warlock.js/core/encrypt-data/SKILL.md`; the other transformers — `@warlock.js/core/use-model-transformers/SKILL.md`; config wiring — `@warlock.js/core/configure-app/SKILL.md`; competing libs `bcrypt` native, `argon2`, `scrypt`.
19
19
  - [health-checks](@warlock.js/core/health-checks/SKILL.md): Built-in liveness (`/health`) and readiness (`/ready`) endpoints plus graceful HTTP request draining for zero-downtime deploys — the `health` registry (`health.addCheck`/`removeCheck`), the `http.health.*` and `http.gracefulShutdown.*` config, and how readiness ties into `Application.isShuttingDown`. Triggers: `health`, `health.addCheck`, `health.removeCheck`, `HealthCheck`, `/health`, `/ready`, `http.health`, `http.gracefulShutdown`, `forceCloseConnections`, "liveness probe", "readiness probe", "graceful shutdown", "drain in-flight requests", "zero-downtime deploy", "kubernetes health check", "503 until ready"; typical import `import { health } from "@warlock.js/core"`. Skip: the `Application.onShutdown` / `onceBooted` lifecycle hooks — `@warlock.js/core/use-app-context/SKILL.md`; maintenance-mode 503s — `@warlock.js/core/use-middleware/SKILL.md`; connector lifecycle — `@warlock.js/core/add-connector/SKILL.md`; competing libs `@fastify/under-pressure`, `terminus`, hand-rolled `/health` controllers.
20
- - [lower-stage3-decorators](@warlock.js/core/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.
20
+ - [lower-stage3-decorators](@warlock.js/core/lower-stage3-decorators/SKILL.md): Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won't load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.
21
21
  - [process-image](@warlock.js/core/process-image/SKILL.md): Transform images with the `Image` class — resize, crop, rotate, format, quality, watermark, blur, etc. — using a deferred pipeline that runs only at `save()` / `toBuffer()` / `toBase64()` / `toDataUrl()` time. Requires sharp via `warlock add image`. Triggers: `Image`, `Image.fromFile`, `Image.fromBuffer`, `Image.fromUrl`, `.resize`, `.crop`, `.watermark`, `.toBuffer`, `.toDataUrl`, `.apply`; "resize an image", "generate a thumbnail", "watermark a product photo", "build an image pipeline"; typical import `import { Image } from "@warlock.js/core"`. Skip: multipart upload entry — `@warlock.js/core/upload-file/SKILL.md`; storage persistence — `@warlock.js/core/store-file/SKILL.md`; competing libs `sharp` direct, `jimp`, `imagemagick`, `gm`.
22
22
  - [register-route](@warlock.js/core/register-route/SKILL.md): Register HTTP routes via @warlock.js/core's router — single routes, prefix groups, middleware-guarded blocks, and RESTful resource chains. Routes always live in `src/app/<module>/routes.ts`. Triggers: `router.get`, `router.post`, `router.prefix`, `router.group`, `router.route`, `guarded`; "add a route", "wire a controller to a URL", "group routes by prefix", "register a RESTful resource"; typical import `import { router } from "@warlock.js/core"`. Skip: handler shape — `@warlock.js/core/create-controller/SKILL.md`; CRUD chain details — `@warlock.js/core/build-restful/SKILL.md`; middleware authoring — `@warlock.js/core/write-middleware/SKILL.md`; competing libs `express`, `fastify`, `koa`, `@nestjs/common`.
23
23
  - [resolve-path](@warlock.js/core/resolve-path/SKILL.md): Path helpers anchored at `process.cwd()` — `rootPath`, `srcPath`, `appPath`, `configPath`, `publicPath`, `storagePath`, `uploadsPath`, `cachePath`, `logsPath`, `tempPath`, `warlockPath`, `sanitizePath`. Optional `uploads.root` config overrides the uploads anchor. Triggers: `appPath`, `configPath`, `uploadsPath`, `storagePath`, `publicPath`, `cachePath`, `logsPath`, `tempPath`, `sanitizePath`, `paths`; "resolve a path inside src/app", "absolute upload destination", "sanitize a user filename", "ship uploads to a mounted volume"; typical import `import { appPath, uploadsPath } from "@warlock.js/core"`. Skip: HTTP URL helpers — `@warlock.js/core/build-url/SKILL.md`; app metadata — `@warlock.js/core/use-app-context/SKILL.md`; storage abstraction — `@warlock.js/core/store-file/SKILL.md`; competing patterns: `path.join(process.cwd(), ...)`, hand-rolled directory constants.
@@ -26,8 +26,8 @@
26
26
  - [send-mail](@warlock.js/core/send-mail/SKILL.md): Send transactional email — `Mail` fluent builder, `sendMail()` direct call, React Email components. Test mode auto-captures into an in-memory mailbox; dev mode logs. Triggers: `Mail.to`, `sendMail`, `setMailMode`, `mailEvents`, `assertMailSent`, `getTestMailbox`, `wasMailSentTo`, `closeAllMailers`; "send a transactional email", "build a React Email template", "configure SMTP or SES", "assert an email was sent in tests"; typical import `import { Mail, sendMail } from "@warlock.js/core"`. Skip: per-config wiring — `@warlock.js/core/configure-app/SKILL.md`; layered service patterns — `@warlock.js/core/warlock-conventions/SKILL.md`; competing libs `nodemailer` direct, `@sendgrid/mail`, `resend`, `mailgun.js`.
27
27
  - [send-response](@warlock.js/core/send-response/SKILL.md): Send HTTP responses via @warlock.js/core's Response helpers — success/error variants, status helpers, redirects, files, streams, and SSE. Picking the right helper carries the HTTP semantic without manual status codes. Triggers: `response.success`, `response.successCreate`, `response.notFound`, `response.forbidden`, `response.badRequest`, `response.sendFile`, `response.stream`, `response.sse`, `response.replay`, `ResourceNotFoundError`, `ForbiddenError`; "return a 201 from a controller", "send a file", "stream Server-Sent Events", "throw HTTP-shaped errors from services"; typical import `import type { RequestHandler, Response } from "@warlock.js/core"`. Skip: controller shape — `@warlock.js/core/create-controller/SKILL.md`; route registration — `@warlock.js/core/register-route/SKILL.md`; competing patterns: hand-rolled status codes via `reply.code(404).send(...)`, raw Fastify reply.
28
28
  - [store-file](@warlock.js/core/store-file/SKILL.md): Read/write/delete files via the `storage` singleton — disks, drivers (local/S3/R2/DO Spaces), `storage.use(name)`, `StorageFile` handles, presigned URLs. Triggers: `storage.put`, `storage.get`, `storage.use`, `StorageFile`, `storageConfigurations`, `getPresignedUrl`, `getPresignedUploadUrl`; "save an uploaded file", "switch between local and S3", "generate a presigned URL", "read file metadata"; typical import `import { storage } from "@warlock.js/core"`. Skip: multipart parsing + image chain — `@warlock.js/core/upload-file/SKILL.md`; image transforms — `@warlock.js/core/process-image/SKILL.md`; storage config shape — `@warlock.js/core/configure-app/SKILL.md`; competing libs `@aws-sdk/client-s3`, `multer`, `formidable`.
29
- - [test-http](@warlock.js/core/test-http/SKILL.md): 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`.
30
- - [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } from "@warlock.js/core"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.
29
+ - [test-http](@warlock.js/core/test-http/SKILL.md): 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/tests"`. 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`.
30
+ - [test-service](@warlock.js/core/test-service/SKILL.md): Pure unit tests against services, repositories, models, and use-cases — `setupTest({ connectors })` bootstraps each Vitest worker with its own DB/cache connections so you can call your code directly. Triggers: `setupTest`, `src/test-setup.ts`, `tests.connectors`, `Application.setEnvironment`; "unit-test a service", "test a repository query", "vitest setupFiles", "skip connectors for pure-logic tests"; typical import `import { setupTest } from "@warlock.js/core/tests"`. Skip: HTTP integration — `@warlock.js/core/test-http/SKILL.md`; warlock add test scaffold — `@warlock.js/core/write-cli-command/SKILL.md`; competing tooling: jest direct, `supertest`, `nock`.
31
31
  - [update-packages](@warlock.js/core/update-packages/SKILL.md): Keep a project current with `warlock update` — bump every `@warlock.js/*` dependency in package.json to its latest published version (range operator preserved), then run the lockfile-detected package manager install. Also covers the `warlock dev` update notice, its `u` update-and-restart keyboard shortcut, and the `devServer.checkForUpdates` toggle. Triggers: `warlock update`, `--no-install`, `--dry-run`, `--check`, `checkForUpdates`, `fetchLatestVersion`, `isNewerVersion`; "update warlock packages", "upgrade the framework", "is there a new warlock version", "update notice in the dev server", "press u to update", "dev server keyboard shortcut", "update check offline", "bump @warlock.js/* to latest"; typical CLI `warlock update`. Skip: dev/build/start runtime — `@warlock.js/core/run-app/SKILL.md`; writing a custom command — `@warlock.js/core/write-cli-command/SKILL.md`; installing a NEW feature package (auth, mail, storage) — that is `warlock add`; releasing/publishing the framework — workspace release tooling, not this command.
32
32
  - [upload-file](@warlock.js/core/upload-file/SKILL.md): Handle multipart file uploads — read via `request.file()` or `request.validated()`, validate with `v.file()`, save via `UploadedFile.save()` or the storage layer, transform images inline. Triggers: `UploadedFile`, `request.file`, `v.file`, `.save`, `.saveAs`, `.resize`, `.format`, `.quality`, `.image`, `.mimeType`, `.maxSize`; "accept a file upload", "validate file size and mime", "save to S3 or local disk", "resize an uploaded image on save"; typical import `import type { UploadedFile, RequestHandler } from "@warlock.js/core"`. Skip: storage drivers + presigned URLs — `@warlock.js/core/store-file/SKILL.md`; image-only transforms — `@warlock.js/core/process-image/SKILL.md`; schema rules — `@warlock.js/core/validate-input/SKILL.md`; competing libs `multer`, `formidable`, `busboy`.
33
33
  - [use-app-context](@warlock.js/core/use-app-context/SKILL.md): Read app-wide context — the `Application` static class (env, version, uptime, runtime strategy, boot lifecycle) plus the `app` runtime accessor (live Fastify, socket.io, router, database via the DI container). Triggers: `Application.isProduction`, `Application.environment`, `Application.runtimeStrategy`, `Application.uptime`, `Application.version`, `Application.onceBooted`, `Application.whenBooted`, `Application.isBooted`, `Application.onShutdown`, `Application.isShuttingDown`, `app.http`, `app.socket`, `app.database`, `app.router`; "branch on environment", "reach the live Fastify instance", "framework version in health endpoint", "dev vs production runtime check", "run code once the app is fully booted", "after all connectors started", "app booted hook", "run cleanup before shutdown", "graceful shutdown hook"; typical import `import { Application, app } from "@warlock.js/core"`. Skip: path helpers — `@warlock.js/core/resolve-path/SKILL.md`; connector start order — `@warlock.js/core/add-connector/SKILL.md`; competing patterns: bare `process.env.NODE_ENV`, ad-hoc Fastify imports.
package/package.json CHANGED
@@ -36,13 +36,13 @@
36
36
  "@mongez/slug": "^1.0.7",
37
37
  "@mongez/supportive-is": "^2.1.3",
38
38
  "@mongez/time-wizard": "^1.0.6",
39
- "@warlock.js/auth": "4.12.0",
40
- "@warlock.js/cache": "4.12.0",
41
- "@warlock.js/cascade": "4.12.0",
42
- "@warlock.js/context": "4.12.0",
43
- "@warlock.js/logger": "4.12.0",
44
- "@warlock.js/seal": "4.12.0",
45
- "@warlock.js/fs": "4.12.0",
39
+ "@warlock.js/auth": "4.14.0",
40
+ "@warlock.js/cache": "4.14.0",
41
+ "@warlock.js/cascade": "4.14.0",
42
+ "@warlock.js/context": "4.14.0",
43
+ "@warlock.js/logger": "4.14.0",
44
+ "@warlock.js/seal": "4.14.0",
45
+ "@warlock.js/fs": "4.14.0",
46
46
  "chokidar": "^5.0.0",
47
47
  "dayjs": "^1.11.19",
48
48
  "es-module-lexer": "^2.0.0",
@@ -68,15 +68,15 @@
68
68
  "react": "^19.2.3",
69
69
  "react-dom": "^19.2.3",
70
70
  "@react-email/render": "^2.0.5",
71
- "@warlock.js/herald": "4.12.0",
72
- "@warlock.js/ai": "4.12.0",
73
- "@warlock.js/access": "4.12.0",
74
- "@warlock.js/notifications": "4.12.0"
71
+ "@warlock.js/herald": "4.14.0",
72
+ "@warlock.js/ai": "4.14.0",
73
+ "@warlock.js/access": "4.14.0",
74
+ "@warlock.js/notifications": "4.14.0"
75
75
  },
76
76
  "bin": {
77
77
  "warlock": "bin/warlock.js"
78
78
  },
79
- "version": "4.12.0",
79
+ "version": "4.14.0",
80
80
  "type": "module",
81
81
  "main": "./esm/index.mjs",
82
82
  "module": "./esm/index.mjs",
@@ -94,6 +94,18 @@
94
94
  "default": "./esm/cli/start.mjs"
95
95
  }
96
96
  },
97
+ "./tests": {
98
+ "import": {
99
+ "types": "./esm/tests/index.d.mts",
100
+ "default": "./esm/tests/index.mjs"
101
+ }
102
+ },
103
+ "./vite": {
104
+ "import": {
105
+ "types": "./esm/vite/index.d.mts",
106
+ "default": "./esm/vite/index.mjs"
107
+ }
108
+ },
97
109
  "./dev-server/health-checker/workers/eslint-health.worker": {
98
110
  "import": {
99
111
  "types": "./esm/dev-server/health-checker/workers/eslint-health.worker.d.mts",
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: lower-stage3-decorators
3
- description: 'Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won''t load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.'
3
+ description: 'Vite/Vitest plugin `lowerStage3Decorators()` that lowers TC39 Stage-3 (native) decorators with esbuild before oxc / the SSR rewrite mangles them — so `@RegisterModel`-decorated Cascade models load under Vitest 4 / Vite 8. Triggers: `lowerStage3Decorators`, "SyntaxError: Invalid or unexpected token" on a decorated class, "@(0, __vite_ssr_import__.X)()", "decorator crashes vitest", "models won''t load in tests", "Vite 8 oxc decorators"; typical import `import { lowerStage3Decorators } from "@warlock.js/core/vite"` in `vite.config.ts` / `vitest.config.ts`. Skip: writing the tests themselves — `@warlock.js/core/test-service/SKILL.md` / `@warlock.js/core/test-http/SKILL.md`; runtime migrations — `@warlock.js/cascade/write-migration/SKILL.md`.'
4
4
  ---
5
5
 
6
6
  # Lower Stage-3 decorators for Vite/Vitest
@@ -15,12 +15,14 @@ The cause is two transforms that don't understand each other. Vite 8 transpiles
15
15
 
16
16
  `lowerStage3Decorators()` fixes it by getting **esbuild** to lower the decorators *first*.
17
17
 
18
+ ⚠ **Changed in 4.13.0 — the import is a subpath now.** The plugin used to be re-exported from the package root; it is not any more, because that put the Vite integration into every application's production module graph. `import { lowerStage3Decorators } from "@warlock.js/core"` now fails with *"has no exported member"* — **add `/vite` to the specifier and nothing else changes.**
19
+
18
20
  ## Use it
19
21
 
20
22
  Drop it **first** in your `plugins` array:
21
23
 
22
24
  ```ts title="vite.config.ts"
23
- import { lowerStage3Decorators } from "@warlock.js/core";
25
+ import { lowerStage3Decorators } from "@warlock.js/core/vite";
24
26
  import mongezVite from "@mongez/vite";
25
27
  import { defineConfig } from "vitest/config";
26
28
 
@@ -165,6 +165,8 @@ export default defineConfig({
165
165
  outFile: "app.js", // default — bundle filename
166
166
  minify: true, // default — esbuild minify
167
167
  sourcemap: true, // default — true | false | "inline" | "linked"
168
+ singleBundle: false, // default — one runnable file, deps inlined
169
+ esmShim: true, // default — require/__filename/__dirname for bundled CJS
168
170
  },
169
171
  });
170
172
  ```
@@ -176,6 +178,10 @@ Defaults are sensible for the typical "Node service" deployment. Knobs to actual
176
178
  - **`minify: false`** — flip to debug a production-only bug. Larger bundle, readable stack traces.
177
179
  - **`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.
178
180
  - **`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).
181
+ - **`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.
182
+ - **`esmShim: false`** — only if you are certain nothing in the graph is CommonJS. Leave it on.
183
+
184
+ ⚠ **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.
179
185
 
180
186
  ### What it preloads
181
187
 
@@ -1,19 +1,21 @@
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`, `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`.'
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/tests"`. 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
7
7
 
8
8
  Some tests need the full stack: route matching, middleware chain, validation, controller, response serialization. For those, you boot the real HTTP server once per test run and make real `fetch` calls against it.
9
9
 
10
- `startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core`.
10
+ `startHttpTestServer()` is the bootstrap. `testGet` / `testPost` / `expectJson` are the call helpers. Both ship in `@warlock.js/core/tests`.
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 *"has no exported member"* — **add `/tests` to the specifier and nothing else changes.**
11
13
 
12
14
  ## The shape
13
15
 
14
16
  ```ts title="src/app/users/tests/users.controller.test.ts"
15
17
  import { describe, expect, it } from "vitest";
16
- import { expectJson, testGet, testPost } from "@warlock.js/core";
18
+ import { expectJson, testGet, testPost } from "@warlock.js/core/tests";
17
19
 
18
20
  describe("Users API", () => {
19
21
  it("GET /users returns the list", async () => {
@@ -39,7 +41,7 @@ No `beforeAll`, no manual server start — the project's `src/test-global-setup.
39
41
  ## The bootstrap — `startHttpTestServer` / `stopHttpTestServer`
40
42
 
41
43
  ```ts
42
- import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core";
44
+ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests";
43
45
  ```
44
46
 
45
47
  `startHttpTestServer()` boots a **minimal but real** HTTP server:
@@ -80,6 +82,23 @@ startHttpTestServer({ port: 2032 }).
80
82
 
81
83
  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
84
 
85
+ ### Port `0` is refused
86
+
87
+ `port: 0` — "let the OS pick one" — is not supported, and says so rather than half-working:
88
+
89
+ ```
90
+ startHttpTestServer() cannot run on port 0. Pass an explicit port — e.g.
91
+ startHttpTestServer({ port: 3999 }) — or set `http.port` in your config. Test workers
92
+ are separate processes that resolve the server's URL from the port published at
93
+ startup, and an OS-assigned port is not knowable to them.
94
+ ```
95
+
96
+ The reason is the worker handoff described above: the port the OS assigns is never recorded anywhere the workers can read, so `getTestServerUrl()` would resolve `0` and send every request to `http://host:0`.
97
+
98
+ ### If startup fails, nothing is left behind
99
+
100
+ `startHttpTestServer()` owns the connectors it starts. A failure part-way through — a bad config, a connector that won't boot — tears down whatever came up, withdraws the published port, and **rethrows the original error**; a failure during that cleanup is logged but never replaces the cause. `stopHttpTestServer()` withdraws the port and resets its state even when the shutdown itself throws, and still surfaces that failure.
101
+
83
102
  ## Project wiring — `src/test-global-setup.ts` + `vite.config.ts`
84
103
 
85
104
  ```ts title="src/test-global-setup.ts"
@@ -87,7 +106,7 @@ This runs whether or not you passed a port, so a collision never reaches you as
87
106
  * Global Test Setup
88
107
  * Runs ONCE in the main process before all test workers start.
89
108
  */
90
- import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core";
109
+ import { startHttpTestServer, stopHttpTestServer } from "@warlock.js/core/tests";
91
110
 
92
111
  export async function setup() {
93
112
  await startHttpTestServer();
@@ -99,7 +118,7 @@ export async function teardown() {
99
118
  ```
100
119
 
101
120
  ```ts title="vite.config.ts"
102
- import { lowerStage3Decorators } from "@warlock.js/core";
121
+ import { lowerStage3Decorators } from "@warlock.js/core/vite";
103
122
  import mongezVite from "@mongez/vite";
104
123
  import { defineConfig } from "vitest/config";
105
124
 
@@ -107,7 +126,7 @@ export default defineConfig({
107
126
  plugins: [lowerStage3Decorators(), mongezVite()],
108
127
  test: {
109
128
  globalSetup: "./src/test-global-setup.ts", // ← starts the HTTP server
110
- setupFiles: ["./src/test-setup.ts"], // ← per-worker setupTest (see test-service skill)
129
+ setupFiles: ["./src/test-setup.ts"], // ← setupTest + afterAll(teardownTest), per test file
111
130
  environment: "node",
112
131
  globals: false,
113
132
  include: ["src/app/**/*.test.ts"],
@@ -115,7 +134,13 @@ export default defineConfig({
115
134
  });
116
135
  ```
117
136
 
118
- Both files (and this config, with `lowerStage3Decorators()` first so decorated models load) are created by `warlock add test`. The split is intentional: `globalSetup` runs ONCE in the main vitest process; `setupFiles` runs per worker thread.
137
+ Both files (and this config, with `lowerStage3Decorators()` first so decorated models load) are created by `warlock add test`. The split is intentional: `globalSetup` runs **ONCE** in the main vitest process; `setupFiles` runs **before every test file**.
138
+
139
+ ⚠ **Corrected in 4.14.0.** This line previously said `setupFiles` runs "per worker thread". **It does not** — Vitest runs it before each test file, and the setup module's registry is rebuilt every time. Measured across all four `pool` × `isolate` combinations.
140
+
141
+ **So the service-layer framework is file-scoped: bootstrapped by `setupTest` and closed by the `afterAll(teardownTest)` the setup file registers, once per test file.** ⚠ **`setupTest` alone is not the whole wiring — the paired teardown is mandatory from 4.14.0**; see the `test-service` skill.
142
+
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.
119
144
 
120
145
  ## HTTP request helpers
121
146
 
@@ -124,7 +149,7 @@ Everything is built on native `fetch` — no extra dependency, no special wire f
124
149
  ### URL resolution
125
150
 
126
151
  ```ts
127
- import { getTestServerUrl } from "@warlock.js/core";
152
+ import { getTestServerUrl } from "@warlock.js/core/tests";
128
153
 
129
154
  const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
130
155
  ```
@@ -141,7 +166,7 @@ import {
141
166
  testPut,
142
167
  testPatch,
143
168
  testDelete,
144
- } from "@warlock.js/core";
169
+ } from "@warlock.js/core/tests";
145
170
 
146
171
  await testGet("/products");
147
172
  await testGet("/products?published=true", { headers: { "X-Tenant": "abc" } });
@@ -158,7 +183,7 @@ All accept a relative path (leading `/` optional) and a standard `RequestInit`.
158
183
  ### Parsing + asserting — `expectJson<T>`
159
184
 
160
185
  ```ts
161
- import { expectJson, parseJsonResponse } from "@warlock.js/core";
186
+ import { expectJson, parseJsonResponse } from "@warlock.js/core/tests";
162
187
 
163
188
  // Parse-only
164
189
  const body = await parseJsonResponse<MyShape>(response);
@@ -177,7 +202,7 @@ const body = await expectJson<MyShape>(response, 404); // expects 404 (testi
177
202
 
178
203
  ```ts
179
204
  import { describe, expect, it } from "vitest";
180
- import { expectJson, testGet, testPost } from "@warlock.js/core";
205
+ import { expectJson, testGet, testPost } from "@warlock.js/core/tests";
181
206
 
182
207
  describe("Products API — happy path", () => {
183
208
  it("creates and reads back a product", async () => {