@warlock.js/core 4.12.0 → 4.13.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 (181) hide show
  1. package/CHANGELOG.md +116 -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 +3 -3
  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 +2 -2
  123. package/esm/tests/index.mjs +1 -2
  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-helpers.d.mts.map +1 -1
  128. package/esm/tests/test-helpers.mjs +34 -13
  129. package/esm/tests/test-helpers.mjs.map +1 -1
  130. package/esm/tests/vitest-setup.d.mts +1 -1
  131. package/esm/tests/vitest-setup.d.mts.map +1 -1
  132. package/esm/tests/vitest-setup.mjs +3 -3
  133. package/esm/tests/vitest-setup.mjs.map +1 -1
  134. package/esm/utils/framework-vesion.mjs +1 -1
  135. package/esm/utils/index.d.mts +1 -0
  136. package/esm/utils/index.mjs +1 -0
  137. package/esm/utils/normalized-path.d.mts +55 -0
  138. package/esm/utils/normalized-path.d.mts.map +1 -0
  139. package/esm/{dev-server/path.mjs → utils/normalized-path.mjs} +15 -2
  140. package/esm/utils/normalized-path.mjs.map +1 -0
  141. package/esm/vite/index.d.mts +2 -0
  142. package/esm/vite/index.mjs +1 -1
  143. package/esm/vite/lower-stage3-decorators.d.mts +1 -1
  144. package/esm/vite/lower-stage3-decorators.mjs +1 -1
  145. package/esm/vite/lower-stage3-decorators.mjs.map +1 -1
  146. package/esm/warlock-config/index.mjs +1 -1
  147. package/esm/warlock-config/types.d.mts +34 -1
  148. package/esm/warlock-config/types.d.mts.map +1 -1
  149. package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
  150. package/esm/warlock-config/warlock-config.manager.mjs +1 -2
  151. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  152. package/llms-full.txt +78 -23
  153. package/llms.txt +3 -3
  154. package/package.json +24 -12
  155. package/skills/lower-stage3-decorators/SKILL.md +4 -2
  156. package/skills/run-app/SKILL.md +6 -0
  157. package/skills/test-http/SKILL.md +29 -10
  158. package/skills/test-service/SKILL.md +12 -8
  159. package/skills/use-middleware/SKILL.md +5 -1
  160. package/esm/cli/cli-command.d.mts.map +0 -1
  161. package/esm/cli/cli-command.mjs.map +0 -1
  162. package/esm/cli/types.d.mts.map +0 -1
  163. package/esm/dev-server/files-orchestrator.d.mts +0 -89
  164. package/esm/dev-server/files-orchestrator.d.mts.map +0 -1
  165. package/esm/dev-server/files-watcher.d.mts +0 -69
  166. package/esm/dev-server/files-watcher.d.mts.map +0 -1
  167. package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts +0 -55
  168. package/esm/dev-server/health-checker/checkers/base-health-checker.d.mts.map +0 -1
  169. package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts +0 -44
  170. package/esm/dev-server/health-checker/checkers/eslint-health-checker.d.mts.map +0 -1
  171. package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts +0 -60
  172. package/esm/dev-server/health-checker/checkers/typescript-health-checker.d.mts.map +0 -1
  173. package/esm/dev-server/health-checker/files-healthcare.manager.d.mts +0 -119
  174. package/esm/dev-server/health-checker/files-healthcare.manager.d.mts.map +0 -1
  175. package/esm/dev-server/health-checker/index.mjs +0 -7
  176. package/esm/dev-server/module-loader.d.mts +0 -63
  177. package/esm/dev-server/module-loader.d.mts.map +0 -1
  178. package/esm/dev-server/path.mjs.map +0 -1
  179. package/esm/tests/test-server-port-channel.d.mts +0 -27
  180. package/esm/tests/test-server-port-channel.d.mts.map +0 -1
  181. /package/esm/{cli → commands}/index.d.mts +0 -0
@@ -0,0 +1,55 @@
1
+ //#region ../core/src/utils/normalized-path.d.ts
2
+ /**
3
+ * Path helpers that always hand back forward slashes.
4
+ *
5
+ * Node's `path` returns `\` on Windows, and these values are compared against
6
+ * module specifiers, glob results and map keys — all of which use `/`. A raw
7
+ * `path.relative()` therefore produces a string that looks right in a log and
8
+ * fails every lookup it is used for.
9
+ *
10
+ * This lived under `dev-server/` and was imported from there by production
11
+ * modules (`connectors`, `database`, `config`), which put the whole dev-server
12
+ * directory into the production module graph. The code was never dev-only —
13
+ * only its location was.
14
+ */
15
+ declare class Path {
16
+ /**
17
+ * Convert the given absolute path to a relative path
18
+ */
19
+ static toRelative(absolutePath: string): string;
20
+ /**
21
+ * Get relative path of the given path
22
+ */
23
+ static relative(relativePath: string): string;
24
+ /**
25
+ * Get normalized absolute path of the given path
26
+ */
27
+ static toNormalizedAbsolute(relativePath: string): string;
28
+ /**
29
+ * Get absolute path of the given path
30
+ */
31
+ static toAbsolute(relativePath: string): string;
32
+ /**
33
+ * Normalize the given path (convert backslashes to forward slashes)
34
+ */
35
+ static normalize(filePath: string): string;
36
+ /**
37
+ * Join paths and normalize
38
+ */
39
+ static join(...paths: string[]): string;
40
+ /**
41
+ * Get directory name of a path
42
+ */
43
+ static dirname(filePath: string): string;
44
+ /**
45
+ * Get base name of a path
46
+ */
47
+ static basename(filePath: string, ext?: string): string;
48
+ /**
49
+ * Get extension of a path
50
+ */
51
+ static extname(filePath: string): string;
52
+ }
53
+ //#endregion
54
+ export { Path };
55
+ //# sourceMappingURL=normalized-path.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalized-path.d.mts","names":[],"sources":["../../../../../../../core/src/utils/normalized-path.ts"],"mappings":";;AAeA;;;;;;;;;;;;cAAa,IAAA;EAgCa;;;EAAA,OA5BV,UAAA,CAAW,YAAA;EA0CH;;;EAAA,OAnCR,QAAA,CAAS,YAAA;EAiDT;;;EAAA,OA1CA,oBAAA,CAAqB,YAAA;;;;SAOrB,UAAA,CAAW,YAAA;;;;SAOX,SAAA,CAAU,QAAA;;;;SAOV,IAAA,IAAQ,KAAA;;;;SAOR,OAAA,CAAQ,QAAA;;;;SAOR,QAAA,CAAS,QAAA,UAAkB,GAAA;;;;SAO3B,OAAA,CAAQ,QAAA;AAAA"}
@@ -1,6 +1,19 @@
1
1
  import path from "node:path";
2
2
 
3
- //#region ../core/src/dev-server/path.ts
3
+ //#region ../core/src/utils/normalized-path.ts
4
+ /**
5
+ * Path helpers that always hand back forward slashes.
6
+ *
7
+ * Node's `path` returns `\` on Windows, and these values are compared against
8
+ * module specifiers, glob results and map keys — all of which use `/`. A raw
9
+ * `path.relative()` therefore produces a string that looks right in a log and
10
+ * fails every lookup it is used for.
11
+ *
12
+ * This lived under `dev-server/` and was imported from there by production
13
+ * modules (`connectors`, `database`, `config`), which put the whole dev-server
14
+ * directory into the production module graph. The code was never dev-only —
15
+ * only its location was.
16
+ */
4
17
  var Path = class {
5
18
  /**
6
19
  * Convert the given absolute path to a relative path
@@ -60,4 +73,4 @@ var Path = class {
60
73
 
61
74
  //#endregion
62
75
  export { Path };
63
- //# sourceMappingURL=path.mjs.map
76
+ //# sourceMappingURL=normalized-path.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalized-path.mjs","names":[],"sources":["../../../../../../../core/src/utils/normalized-path.ts"],"sourcesContent":["import path from \"node:path\";\n\n/**\n * Path helpers that always hand back forward slashes.\n *\n * Node's `path` returns `\\` on Windows, and these values are compared against\n * module specifiers, glob results and map keys — all of which use `/`. A raw\n * `path.relative()` therefore produces a string that looks right in a log and\n * fails every lookup it is used for.\n *\n * This lived under `dev-server/` and was imported from there by production\n * modules (`connectors`, `database`, `config`), which put the whole dev-server\n * directory into the production module graph. The code was never dev-only —\n * only its location was.\n */\nexport class Path {\n /**\n * Convert the given absolute path to a relative path\n */\n public static toRelative(absolutePath: string) {\n return this.normalize(path.relative(process.cwd(), absolutePath));\n }\n\n /**\n * Get relative path of the given path\n */\n public static relative(relativePath: string) {\n return this.normalize(path.relative(process.cwd(), relativePath));\n }\n\n /**\n * Get normalized absolute path of the given path\n */\n public static toNormalizedAbsolute(relativePath: string) {\n return this.normalize(path.resolve(process.cwd(), relativePath));\n }\n\n /**\n * Get absolute path of the given path\n */\n public static toAbsolute(relativePath: string) {\n return this.normalize(path.resolve(process.cwd(), relativePath));\n }\n\n /**\n * Normalize the given path (convert backslashes to forward slashes)\n */\n public static normalize(filePath: string) {\n return filePath.replace(/\\\\/g, \"/\");\n }\n\n /**\n * Join paths and normalize\n */\n public static join(...paths: string[]) {\n return this.normalize(path.join(...paths));\n }\n\n /**\n * Get directory name of a path\n */\n public static dirname(filePath: string) {\n return this.normalize(path.dirname(filePath));\n }\n\n /**\n * Get base name of a path\n */\n public static basename(filePath: string, ext?: string) {\n return path.basename(filePath, ext);\n }\n\n /**\n * Get extension of a path\n */\n public static extname(filePath: string) {\n return path.extname(filePath);\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,IAAa,OAAb,MAAkB;;;;CAIhB,OAAc,WAAW,cAAsB;EAC7C,OAAO,KAAK,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,YAAY,CAAC;CAClE;;;;CAKA,OAAc,SAAS,cAAsB;EAC3C,OAAO,KAAK,UAAU,KAAK,SAAS,QAAQ,IAAI,GAAG,YAAY,CAAC;CAClE;;;;CAKA,OAAc,qBAAqB,cAAsB;EACvD,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,CAAC;CACjE;;;;CAKA,OAAc,WAAW,cAAsB;EAC7C,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,IAAI,GAAG,YAAY,CAAC;CACjE;;;;CAKA,OAAc,UAAU,UAAkB;EACxC,OAAO,SAAS,QAAQ,OAAO,GAAG;CACpC;;;;CAKA,OAAc,KAAK,GAAG,OAAiB;EACrC,OAAO,KAAK,UAAU,KAAK,KAAK,GAAG,KAAK,CAAC;CAC3C;;;;CAKA,OAAc,QAAQ,UAAkB;EACtC,OAAO,KAAK,UAAU,KAAK,QAAQ,QAAQ,CAAC;CAC9C;;;;CAKA,OAAc,SAAS,UAAkB,KAAc;EACrD,OAAO,KAAK,SAAS,UAAU,GAAG;CACpC;;;;CAKA,OAAc,QAAQ,UAAkB;EACtC,OAAO,KAAK,QAAQ,QAAQ;CAC9B;AACF"}
@@ -0,0 +1,2 @@
1
+ import { DecoratorLoweringPlugin, lowerStage3Decorators } from "./lower-stage3-decorators.mjs";
2
+ export { DecoratorLoweringPlugin, lowerStage3Decorators };
@@ -1,3 +1,3 @@
1
1
  import { lowerStage3Decorators } from "./lower-stage3-decorators.mjs";
2
2
 
3
- export { };
3
+ export { lowerStage3Decorators };
@@ -39,7 +39,7 @@ interface DecoratorLoweringPlugin {
39
39
  * @example
40
40
  * ```ts
41
41
  * // vite.config.ts / vitest.config.ts
42
- * import { lowerStage3Decorators } from "@warlock.js/core";
42
+ * import { lowerStage3Decorators } from "@warlock.js/core/vite";
43
43
  * import { defineConfig } from "vitest/config";
44
44
  *
45
45
  * export default defineConfig({
@@ -26,7 +26,7 @@ import { transform } from "esbuild";
26
26
  * @example
27
27
  * ```ts
28
28
  * // vite.config.ts / vitest.config.ts
29
- * import { lowerStage3Decorators } from "@warlock.js/core";
29
+ * import { lowerStage3Decorators } from "@warlock.js/core/vite";
30
30
  * import { defineConfig } from "vitest/config";
31
31
  *
32
32
  * export default defineConfig({
@@ -1 +1 @@
1
- {"version":3,"file":"lower-stage3-decorators.mjs","names":[],"sources":["../../../../../../../core/src/vite/lower-stage3-decorators.ts"],"sourcesContent":["import { transform } from \"esbuild\";\n\n/**\n * The Vite/Vitest plugin object returned by {@link lowerStage3Decorators}.\n *\n * Declared structurally so `@warlock.js/core` needn't take a `vite` dependency\n * just for one type — the shape is assignable to Vite's `PluginOption` wherever\n * it's placed in a config's `plugins` array.\n */\nexport interface DecoratorLoweringPlugin {\n name: string;\n enforce: \"pre\";\n transform(code: string, id: string): Promise<{ code: string; map: string } | null>;\n}\n\n/**\n * Vite/Vitest plugin that lowers TC39 Stage-3 (native) decorators to plain JS\n * **before** the rest of the transform pipeline runs.\n *\n * Cascade models declare relations with native decorators (`@RegisterModel`,\n * `@BelongsTo`, `@HasMany`, …). Vitest's bundled rolldown-vite transforms `.ts`\n * with **oxc**, and Vite's SSR `moduleRunnerTransform` rewrites the decorator's\n * import reference into `@(0, __vite_ssr_import__.RegisterModel)()` — invalid\n * decorator syntax that throws \"SyntaxError: Invalid or unexpected token\" on\n * every model load. oxc can only lower *legacy* decorators, and legacy lowering\n * would mangle the Stage-3 `(value, context)` signature these decorators rely on\n * (they read `context.kind` / `context.metadata`).\n *\n * esbuild lowers Stage-3 decorators correctly while preserving their runtime\n * semantics — the same transform the production `tsx`/build path uses, so a file\n * lowered here behaves identically in dev, test, and prod. Running `enforce:\n * \"pre\"` hands oxc / the SSR rewrite already-lowered, decorator-free code. Files\n * with no decorator skip esbuild entirely, so the fast path is untouched.\n *\n * Keep it **first** in the `plugins` array so it runs ahead of `@mongez/vite`\n * and Vite's core transform.\n *\n * @example\n * ```ts\n * // vite.config.ts / vitest.config.ts\n * import { lowerStage3Decorators } from \"@warlock.js/core\";\n * import { defineConfig } from \"vitest/config\";\n *\n * export default defineConfig({\n * plugins: [lowerStage3Decorators(), mongezVite()],\n * });\n * ```\n */\nexport function lowerStage3Decorators(): DecoratorLoweringPlugin {\n return {\n name: \"warlock:lower-stage3-decorators\",\n enforce: \"pre\",\n async transform(code, id) {\n const [filepath] = id.split(\"?\");\n\n if (!/\\.tsx?$/.test(filepath) || filepath.includes(\"/node_modules/\")) {\n return null;\n }\n\n // Cheap gate: only files that actually carry a decorator pay esbuild's cost.\n if (!/(^|\\n)\\s*@[A-Za-z_$]/.test(code)) {\n return null;\n }\n\n const result = await transform(code, {\n loader: filepath.endsWith(\".tsx\") ? \"tsx\" : \"ts\",\n format: \"esm\",\n target: \"es2022\",\n sourcemap: true,\n sourcefile: filepath,\n // Force native (Stage-3) semantics regardless of any ambient tsconfig\n // esbuild might otherwise honor.\n tsconfigRaw: { compilerOptions: { experimentalDecorators: false } },\n });\n\n return { code: result.code, map: result.map };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,wBAAiD;CAC/D,OAAO;EACL,MAAM;EACN,SAAS;EACT,MAAM,UAAU,MAAM,IAAI;GACxB,MAAM,CAAC,YAAY,GAAG,MAAM,GAAG;GAE/B,IAAI,CAAC,UAAU,KAAK,QAAQ,KAAK,SAAS,SAAS,gBAAgB,GACjE,OAAO;GAIT,IAAI,CAAC,uBAAuB,KAAK,IAAI,GACnC,OAAO;GAGT,MAAM,SAAS,MAAM,UAAU,MAAM;IACnC,QAAQ,SAAS,SAAS,MAAM,IAAI,QAAQ;IAC5C,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,YAAY;IAGZ,aAAa,EAAE,iBAAiB,EAAE,wBAAwB,MAAM,EAAE;GACpE,CAAC;GAED,OAAO;IAAE,MAAM,OAAO;IAAM,KAAK,OAAO;GAAI;EAC9C;CACF;AACF"}
1
+ {"version":3,"file":"lower-stage3-decorators.mjs","names":[],"sources":["../../../../../../../core/src/vite/lower-stage3-decorators.ts"],"sourcesContent":["import { transform } from \"esbuild\";\n\n/**\n * The Vite/Vitest plugin object returned by {@link lowerStage3Decorators}.\n *\n * Declared structurally so `@warlock.js/core` needn't take a `vite` dependency\n * just for one type — the shape is assignable to Vite's `PluginOption` wherever\n * it's placed in a config's `plugins` array.\n */\nexport interface DecoratorLoweringPlugin {\n name: string;\n enforce: \"pre\";\n transform(code: string, id: string): Promise<{ code: string; map: string } | null>;\n}\n\n/**\n * Vite/Vitest plugin that lowers TC39 Stage-3 (native) decorators to plain JS\n * **before** the rest of the transform pipeline runs.\n *\n * Cascade models declare relations with native decorators (`@RegisterModel`,\n * `@BelongsTo`, `@HasMany`, …). Vitest's bundled rolldown-vite transforms `.ts`\n * with **oxc**, and Vite's SSR `moduleRunnerTransform` rewrites the decorator's\n * import reference into `@(0, __vite_ssr_import__.RegisterModel)()` — invalid\n * decorator syntax that throws \"SyntaxError: Invalid or unexpected token\" on\n * every model load. oxc can only lower *legacy* decorators, and legacy lowering\n * would mangle the Stage-3 `(value, context)` signature these decorators rely on\n * (they read `context.kind` / `context.metadata`).\n *\n * esbuild lowers Stage-3 decorators correctly while preserving their runtime\n * semantics — the same transform the production `tsx`/build path uses, so a file\n * lowered here behaves identically in dev, test, and prod. Running `enforce:\n * \"pre\"` hands oxc / the SSR rewrite already-lowered, decorator-free code. Files\n * with no decorator skip esbuild entirely, so the fast path is untouched.\n *\n * Keep it **first** in the `plugins` array so it runs ahead of `@mongez/vite`\n * and Vite's core transform.\n *\n * @example\n * ```ts\n * // vite.config.ts / vitest.config.ts\n * import { lowerStage3Decorators } from \"@warlock.js/core/vite\";\n * import { defineConfig } from \"vitest/config\";\n *\n * export default defineConfig({\n * plugins: [lowerStage3Decorators(), mongezVite()],\n * });\n * ```\n */\nexport function lowerStage3Decorators(): DecoratorLoweringPlugin {\n return {\n name: \"warlock:lower-stage3-decorators\",\n enforce: \"pre\",\n async transform(code, id) {\n const [filepath] = id.split(\"?\");\n\n if (!/\\.tsx?$/.test(filepath) || filepath.includes(\"/node_modules/\")) {\n return null;\n }\n\n // Cheap gate: only files that actually carry a decorator pay esbuild's cost.\n if (!/(^|\\n)\\s*@[A-Za-z_$]/.test(code)) {\n return null;\n }\n\n const result = await transform(code, {\n loader: filepath.endsWith(\".tsx\") ? \"tsx\" : \"ts\",\n format: \"esm\",\n target: \"es2022\",\n sourcemap: true,\n sourcefile: filepath,\n // Force native (Stage-3) semantics regardless of any ambient tsconfig\n // esbuild might otherwise honor.\n tsconfigRaw: { compilerOptions: { experimentalDecorators: false } },\n });\n\n return { code: result.code, map: result.map };\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgDA,SAAgB,wBAAiD;CAC/D,OAAO;EACL,MAAM;EACN,SAAS;EACT,MAAM,UAAU,MAAM,IAAI;GACxB,MAAM,CAAC,YAAY,GAAG,MAAM,GAAG;GAE/B,IAAI,CAAC,UAAU,KAAK,QAAQ,KAAK,SAAS,SAAS,gBAAgB,GACjE,OAAO;GAIT,IAAI,CAAC,uBAAuB,KAAK,IAAI,GACnC,OAAO;GAGT,MAAM,SAAS,MAAM,UAAU,MAAM;IACnC,QAAQ,SAAS,SAAS,MAAM,IAAI,QAAQ;IAC5C,QAAQ;IACR,QAAQ;IACR,WAAW;IACX,YAAY;IAGZ,aAAa,EAAE,iBAAiB,EAAE,wBAAwB,MAAM,EAAE;GACpE,CAAC;GAED,OAAO;IAAE,MAAM,OAAO;IAAM,KAAK,OAAO;GAAI;EAC9C;CACF;AACF"}
@@ -1,4 +1,4 @@
1
- import { WarlockConfigManager, isUnknownTsExtensionError, warlockConfigManager } from "./warlock-config.manager.mjs";
2
1
  import { defineConfig } from "./define-config.mjs";
2
+ import { WarlockConfigManager, isUnknownTsExtensionError, warlockConfigManager } from "./warlock-config.manager.mjs";
3
3
 
4
4
  export { };
@@ -1,4 +1,4 @@
1
- import { CLICommand } from "../cli/cli-command.mjs";
1
+ import { CLICommand } from "../commands/cli-command.mjs";
2
2
  import { FileHealthCheckerContract } from "../dev-server/health-checker/file-health-checker.contract.mjs";
3
3
  import { MigrationConstructor } from "@warlock.js/cascade";
4
4
  import { BuildOptions } from "esbuild";
@@ -45,6 +45,39 @@ type WarlockConfig = {
45
45
  * @default app.js
46
46
  */
47
47
  outFile?: string;
48
+ /**
49
+ * Produce ONE self-contained file you can run with `node dist/app.js`.
50
+ *
51
+ * Sets `packages: "bundle"` and `splitting: false` as defaults — write
52
+ * either of them yourself and yours wins.
53
+ *
54
+ * ⚠ Not literally one file: native `.node` addons cannot be inlined by
55
+ * any bundler and are still emitted alongside it. "Single bundle" means
56
+ * one JavaScript file plus any native addons your dependencies carry.
57
+ *
58
+ * Without this flag the build stays the default: dependencies remain real
59
+ * `import` specifiers resolved from `node_modules` at runtime, which is
60
+ * what you want when you deploy the folder rather than the file.
61
+ *
62
+ * @default false
63
+ */
64
+ singleBundle?: boolean;
65
+ /**
66
+ * Inject the ESM interop shim — `require`, `__filename`, `__dirname`.
67
+ *
68
+ * Bundled CommonJS dependencies call `require(...)` and read `__dirname`
69
+ * to find their own assets. In an ES module none of those exist, so
70
+ * esbuild replaces them with a stub that **throws at runtime** — after a
71
+ * build that reported success. This recreates them via
72
+ * `createRequire(import.meta.url)`.
73
+ *
74
+ * Deliberately NOT owned by {@link singleBundle}: it applies to any ESM
75
+ * build, so setting `packages: "bundle"` by hand works too. Turn it off
76
+ * only if you are certain nothing in your graph is CommonJS.
77
+ *
78
+ * @default true
79
+ */
80
+ esmShim?: boolean;
48
81
  /**
49
82
  * Minify output
50
83
  *
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/types.ts"],"mappings":";;;;;;;;AAUA;;;KAAY,aAAA;EAaF;;;EATR,MAAA;IACE,IAAA;IACA,IAAA;IACA,cAAA;EAAA;EAFA;;;EAQF,KAAA,GAAQ,IAAA,CAAK,YAAA;IAAL;;;;;IAMN,MAAA;IA6BA;;;;;;;;;;IAlBA,YAAA;IAkEA;;;;;IA5DA,OAAA;IAuGmB;;;;;IAjGnB,MAAA;;;;;;IAMA,SAAA;EAAA;;;;EAMF,GAAA;IACE,QAAA,GAAW,UAAA;EAAA;;;;EAMb,SAAA;;;;IAIE,KAAA;;;;;;MAME,OAAA;;;;;;MAMA,OAAA;IAAA;;;;IAKF,cAAA,GAAiB,yBAAA;;;;;IAKjB,eAAA;;;;;;;;;IASA,eAAA;;;;;;;;;IASA,qBAAA;;;;;;;;;;IAUA,mBAAA;EAAA;;;;EAMF,QAAA;;;;;;;;;;;;;;;;;;IAkBE,UAAA,GAAa,KAAA,CAAM,oBAAA;EAAA;;;;;;;EASrB,OAAA;;;;;;;IAOE,OAAA;;;;;;IAOA,OAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"types.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/types.ts"],"mappings":";;;;;;;;AAUA;;;KAAY,aAAA;EAaF;;;EATR,MAAA;IACE,IAAA;IACA,IAAA;IACA,cAAA;EAAA;EAFA;;;EAQF,KAAA,GAAQ,IAAA,CAAK,YAAA;IAAL;;;;;IAMN,MAAA;IAkDA;;;;;;;;;;IAvCA,YAAA;IAqFiB;;;;;IA/EjB,OAAA;IAwIA;;;;;;;AAuBO;;;;;;;;;IA9IP,YAAA;;;;;;;;;;;;;;;;IAgBA,OAAA;;;;;;IAMA,MAAA;;;;;;IAMA,SAAA;EAAA;;;;EAMF,GAAA;IACE,QAAA,GAAW,UAAA;EAAA;;;;EAMb,SAAA;;;;IAIE,KAAA;;;;;;MAME,OAAA;;;;;;MAMA,OAAA;IAAA;;;;IAKF,cAAA,GAAiB,yBAAA;;;;;IAKjB,eAAA;;;;;;;;;IASA,eAAA;;;;;;;;;IASA,qBAAA;;;;;;;;;;IAUA,mBAAA;EAAA;;;;EAMF,QAAA;;;;;;;;;;;;;;;;;;IAkBE,UAAA,GAAa,KAAA,CAAM,oBAAA;EAAA;;;;;;;EASrB,OAAA;;;;;;;IAOE,OAAA;;;;;;IAOA,OAAA;EAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"warlock-config.manager.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"mappings":";;;;;AAgBA;;;;AAAwD;AAaxD;iBAbgB,yBAAA,CAA0B,KAAc;;;;;;;cAa3C,oBAAA;EAsHR;;;EAAA,QAlHK,MAAA;EAsIS;;;EAAA,QAjIT,OAAA;EAkIL;;;;;EA3HU,IAAA,IAAQ,OAAA,CAAQ,aAAA;EAPrB;;;;;;EAAA,QAgCM,MAAA;EA0EH;;;;;;;;EAAA,QAtCG,cAAA;EA2DR;;;;;;;;;;;EArBC,GAAA,mBAAsB,aAAA,EAC3B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,aAAA,CAAc,GAAA;EAsCV;;;;;AAWuB;AAoBhC;;;;EAnDQ,OAAA,mBAA0B,aAAA,EAC9B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,OAAA,CAAQ,aAAA,CAAc,GAAA;;;;MAQd,QAAA;;;;;;EASJ,MAAA,IAAU,aAAA;;;;EAWJ,MAAA,IAAU,OAAA;AAAA;;;;;;;;;;;;;;cAoBZ,oBAAA,EAAoB,oBAA6B"}
1
+ {"version":3,"file":"warlock-config.manager.d.mts","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"mappings":";;;;;AAeA;;;;AAAwD;AAaxD;iBAbgB,yBAAA,CAA0B,KAAc;;;;;;;cAa3C,oBAAA;EAyHR;;;EAAA,QArHK,MAAA;EAyIS;;;EAAA,QApIT,OAAA;EAqIL;;;;;EA9HU,IAAA,IAAQ,OAAA,CAAQ,aAAA;EAPrB;;;;;;EAAA,QAgCM,MAAA;EA6EH;;;;;;;;EAAA,QAtCG,cAAA;EA2DR;;;;;;;;;;;EArBC,GAAA,mBAAsB,aAAA,EAC3B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,aAAA,CAAc,GAAA;EAsCV;;;;;AAWuB;AAoBhC;;;;EAnDQ,OAAA,mBAA0B,aAAA,EAC9B,GAAA,EAAK,GAAA,EACL,YAAA,GAAe,aAAA,CAAc,GAAA,IAC5B,OAAA,CAAQ,aAAA,CAAc,GAAA;;;;MAQd,QAAA;;;;;;EASJ,MAAA,IAAU,aAAA;;;;EAWJ,MAAA,IAAU,OAAA;AAAA;;;;;;;;;;;;;;cAoBZ,oBAAA,EAAoB,oBAA6B"}
@@ -1,6 +1,5 @@
1
1
  import { rootPath } from "../utils/paths.mjs";
2
2
  import "../utils/index.mjs";
3
- import { devLogWarn } from "../dev-server/dev-logger.mjs";
4
3
  import { get } from "@mongez/reinforcements";
5
4
  import { fileExistsAsync } from "@warlock.js/fs";
6
5
  import { readFile, unlink, writeFile } from "node:fs/promises";
@@ -47,7 +46,7 @@ var WarlockConfigManager = class {
47
46
  async doLoad() {
48
47
  const configPath = rootPath("warlock.config.ts");
49
48
  if (!await fileExistsAsync(configPath)) {
50
- devLogWarn("warlock.config.ts is missing, it's highly recommended to create it, run warlock init to create it");
49
+ console.warn("warlock.config.ts is missing run `warlock init` to create it");
51
50
  return;
52
51
  }
53
52
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"warlock-config.manager.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { get } from \"@mongez/reinforcements\";\r\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"url\";\r\nimport { devLogWarn } from \"../dev-server/dev-logger\";\r\nimport { rootPath } from \"../utils\";\r\nimport { WarlockConfig } from \"./types\";\r\n\r\n/**\r\n * True when `error` is Node's \"this runtime can't import a `.ts` file\" error.\r\n *\r\n * Node only executes TypeScript natively from v22.18 / v23.6 onward (earlier\r\n * 22.x needs `--experimental-strip-types`). On older runtimes a bare\r\n * `import(\"warlock.config.ts\")` throws `ERR_UNKNOWN_FILE_EXTENSION` — which is\r\n * the signal to fall back to transpiling the config ourselves.\r\n */\r\nexport function isUnknownTsExtensionError(error: unknown): boolean {\r\n return (\r\n (error as { code?: string })?.code === \"ERR_UNKNOWN_FILE_EXTENSION\" ||\r\n /Unknown file extension \"\\.tsx?\"/.test(String((error as Error)?.message))\r\n );\r\n}\r\n\r\n/**\r\n * Warlock Config Manager\r\n *\r\n * Manages lazy loading of the pre-compiled warlock.config.js file\r\n * from the .warlock/cache directory.\r\n */\r\nexport class WarlockConfigManager {\r\n /**\r\n * Cached config instance\r\n */\r\n private config?: WarlockConfig;\r\n\r\n /**\r\n * Loading promise to prevent duplicate loads\r\n */\r\n private loading?: Promise<WarlockConfig | undefined>;\r\n\r\n /**\r\n * Load warlock.config.js (cached after first load)\r\n *\r\n * @returns The resolved Warlock configuration\r\n */\r\n public async load(): Promise<WarlockConfig | undefined> {\r\n // Already loaded\r\n if (this.config) {\r\n return this.config;\r\n }\r\n\r\n // Currently loading (prevent duplicate loads)\r\n if (this.loading) {\r\n return this.loading;\r\n }\r\n\r\n // Start loading\r\n this.loading = this.doLoad();\r\n this.config = await this.loading;\r\n this.loading = undefined;\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Internal load implementation\r\n *\r\n * The ESM loader hook transpiles `warlock.config.ts` on import — no\r\n * separate compile-to-disk step is needed.\r\n */\r\n private async doLoad(): Promise<WarlockConfig | undefined> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n devLogWarn(\r\n \"warlock.config.ts is missing, it's highly recommended to create it, run warlock init to create it\",\r\n );\r\n return;\r\n }\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(configPath).href);\r\n return configModule.default;\r\n } catch (error) {\r\n // `dev` registers a TS loader hook, and Node ≥ 22.18 / 23.6 strips types\r\n // natively — in both cases the direct import above works. But `build` /\r\n // `start` register no hook, so on an older Node the import throws\r\n // ERR_UNKNOWN_FILE_EXTENSION. Transpile the config with esbuild ourselves\r\n // so config loading is Node-version-independent rather than relying on an\r\n // experimental runtime feature.\r\n if (isUnknownTsExtensionError(error)) {\r\n return await this.loadViaEsbuild(configPath);\r\n }\r\n\r\n throw new Error(`Failed to load warlock.config.ts: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Fallback config loader for runtimes without native TypeScript support.\r\n *\r\n * Transpiles `warlock.config.ts` with esbuild (already a core dependency),\r\n * writes the result as a sibling `.mjs` — so the config's own imports (bare\r\n * `@warlock.js/*` and any relative paths) resolve exactly as they would from\r\n * the original location — imports it, then removes the temp file.\r\n */\r\n private async loadViaEsbuild(configPath: string): Promise<WarlockConfig | undefined> {\r\n const { transformSync } = await import(\"esbuild\");\r\n const source = await readFile(configPath, \"utf8\");\r\n\r\n const { code } = transformSync(source, {\r\n loader: \"ts\",\r\n format: \"esm\",\r\n target: `node${process.versions.node.split(\".\")[0]}`,\r\n sourcefile: configPath,\r\n });\r\n\r\n // Unique sibling name so concurrent processes never clobber each other.\r\n const compiledPath = configPath.replace(\r\n /warlock\\.config\\.ts$/,\r\n `.warlock.config.${process.pid}.mjs`,\r\n );\r\n\r\n await writeFile(compiledPath, code, \"utf8\");\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(compiledPath).href);\r\n return configModule.default;\r\n } finally {\r\n await unlink(compiledPath).catch(() => {});\r\n }\r\n }\r\n\r\n /**\r\n * Get config value by key (dot notation supported)\r\n *\r\n * @example\r\n * config.get(\"server.port\") // Returns 3000\r\n * config.get(\"cli.commands\") // Returns array of commands\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @returns The config value\r\n * @throws Error if config is not loaded\r\n */\r\n public get<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): WarlockConfig[Key] {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return get(this.config, key as string, defaultValue);\r\n }\r\n\r\n /**\r\n * Lazy get - loads config if not already loaded\r\n *\r\n * @example\r\n * const port = await config.lazyGet(\"server\");\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @param defaultValue - Default value if config key is undefined\r\n * @returns The config value\r\n */\r\n async lazyGet<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): Promise<WarlockConfig[Key]> {\r\n await this.load();\r\n return this.get(key, defaultValue);\r\n }\r\n\r\n /**\r\n * Check if config is loaded\r\n */\r\n public get isLoaded(): boolean {\r\n return this.config !== undefined;\r\n }\r\n\r\n /**\r\n * Get the entire config object\r\n *\r\n * @throws Error if config is not loaded\r\n */\r\n public getAll(): WarlockConfig {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Reload config (useful for HMR/development)\r\n */\r\n public async reload(): Promise<void> {\r\n this.config = undefined;\r\n this.loading = undefined;\r\n await this.load();\r\n }\r\n}\r\n\r\n/**\r\n * Exported singleton instance\r\n *\r\n * @example\r\n * import { warlockConfig } from \"@warlock.js/core\";\r\n *\r\n * // Lazy load and get value\r\n * const port = await warlockConfig.lazyGet(\"server.port\");\r\n *\r\n * // Or load first, then get\r\n * await warlockConfig.load();\r\n * const commands = warlockConfig.get(\"cli.commands\");\r\n */\r\nexport const warlockConfigManager = new WarlockConfigManager();\r\n"],"mappings":";;;;;;;;;;;;;;;;;AAgBA,SAAgB,0BAA0B,OAAyB;CACjE,OACG,OAA6B,SAAS,gCACvC,kCAAkC,KAAK,OAAQ,OAAiB,OAAO,CAAC;AAE5E;;;;;;;AAQA,IAAa,uBAAb,MAAkC;;;;;;CAgBhC,MAAa,OAA2C;EAEtD,IAAI,KAAK,QACP,OAAO,KAAK;EAId,IAAI,KAAK,SACP,OAAO,KAAK;EAId,KAAK,UAAU,KAAK,OAAO;EAC3B,KAAK,SAAS,MAAM,KAAK;EACzB,KAAK,UAAU;EAEf,OAAO,KAAK;CACd;;;;;;;CAQA,MAAc,SAA6C;EACzD,MAAM,aAAa,SAAS,mBAAmB;EAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;GACxC,WACE,mGACF;GACA;EACF;EAEA,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,UAAU,CAAC,CAAC,MACzC,CAAC;EACtB,SAAS,OAAO;GAOd,IAAI,0BAA0B,KAAK,GACjC,OAAO,MAAM,KAAK,eAAe,UAAU;GAG7C,MAAM,IAAI,MAAM,qCAAqC,OAAO;EAC9D;CACF;;;;;;;;;CAUA,MAAc,eAAe,YAAwD;EACnF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EAGvC,MAAM,EAAE,SAAS,cAAc,MAFV,SAAS,YAAY,MAAM,GAET;GACrC,QAAQ;GACR,QAAQ;GACR,QAAQ,OAAO,QAAQ,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;GAChD,YAAY;EACd,CAAC;EAGD,MAAM,eAAe,WAAW,QAC9B,wBACA,mBAAmB,QAAQ,IAAI,KACjC;EAEA,MAAM,UAAU,cAAc,MAAM,MAAM;EAE1C,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,YAAY,CAAC,CAAC,MAC3C,CAAC;EACtB,UAAU;GACR,MAAM,OAAO,YAAY,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C;CACF;;;;;;;;;;;;CAaA,AAAO,IACL,KACA,cACoB;EACpB,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,IAAI,KAAK,QAAQ,KAAe,YAAY;CACrD;;;;;;;;;;;CAYA,MAAM,QACJ,KACA,cAC6B;EAC7B,MAAM,KAAK,KAAK;EAChB,OAAO,KAAK,IAAI,KAAK,YAAY;CACnC;;;;CAKA,IAAW,WAAoB;EAC7B,OAAO,KAAK,WAAW;CACzB;;;;;;CAOA,AAAO,SAAwB;EAC7B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,KAAK;CACd;;;;CAKA,MAAa,SAAwB;EACnC,KAAK,SAAS;EACd,KAAK,UAAU;EACf,MAAM,KAAK,KAAK;CAClB;AACF;;;;;;;;;;;;;;AAeA,MAAa,uBAAuB,IAAI,qBAAqB"}
1
+ {"version":3,"file":"warlock-config.manager.mjs","names":[],"sources":["../../../../../../../core/src/warlock-config/warlock-config.manager.ts"],"sourcesContent":["import { fileExistsAsync } from \"@warlock.js/fs\";\r\nimport { get } from \"@mongez/reinforcements\";\r\nimport { readFile, unlink, writeFile } from \"node:fs/promises\";\r\nimport { pathToFileURL } from \"url\";\r\nimport { rootPath } from \"../utils\";\r\nimport { WarlockConfig } from \"./types\";\r\n\r\n/**\r\n * True when `error` is Node's \"this runtime can't import a `.ts` file\" error.\r\n *\r\n * Node only executes TypeScript natively from v22.18 / v23.6 onward (earlier\r\n * 22.x needs `--experimental-strip-types`). On older runtimes a bare\r\n * `import(\"warlock.config.ts\")` throws `ERR_UNKNOWN_FILE_EXTENSION` — which is\r\n * the signal to fall back to transpiling the config ourselves.\r\n */\r\nexport function isUnknownTsExtensionError(error: unknown): boolean {\r\n return (\r\n (error as { code?: string })?.code === \"ERR_UNKNOWN_FILE_EXTENSION\" ||\r\n /Unknown file extension \"\\.tsx?\"/.test(String((error as Error)?.message))\r\n );\r\n}\r\n\r\n/**\r\n * Warlock Config Manager\r\n *\r\n * Manages lazy loading of the pre-compiled warlock.config.js file\r\n * from the .warlock/cache directory.\r\n */\r\nexport class WarlockConfigManager {\r\n /**\r\n * Cached config instance\r\n */\r\n private config?: WarlockConfig;\r\n\r\n /**\r\n * Loading promise to prevent duplicate loads\r\n */\r\n private loading?: Promise<WarlockConfig | undefined>;\r\n\r\n /**\r\n * Load warlock.config.js (cached after first load)\r\n *\r\n * @returns The resolved Warlock configuration\r\n */\r\n public async load(): Promise<WarlockConfig | undefined> {\r\n // Already loaded\r\n if (this.config) {\r\n return this.config;\r\n }\r\n\r\n // Currently loading (prevent duplicate loads)\r\n if (this.loading) {\r\n return this.loading;\r\n }\r\n\r\n // Start loading\r\n this.loading = this.doLoad();\r\n this.config = await this.loading;\r\n this.loading = undefined;\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Internal load implementation\r\n *\r\n * The ESM loader hook transpiles `warlock.config.ts` on import — no\r\n * separate compile-to-disk step is needed.\r\n */\r\n private async doLoad(): Promise<WarlockConfig | undefined> {\r\n const configPath = rootPath(\"warlock.config.ts\");\r\n\r\n if (!(await fileExistsAsync(configPath))) {\r\n // Written straight to the console, NOT through `@warlock.js/logger`.\r\n // This runs during CLI bootstrap, before `LoggerConnector.start()` has\r\n // configured a single channel, so a logger call here reaches nobody in\r\n // any application — the warning would be silently dropped exactly when\r\n // the user most needs it.\r\n console.warn(\"warlock.config.ts is missing run `warlock init` to create it\");\r\n return;\r\n }\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(configPath).href);\r\n return configModule.default;\r\n } catch (error) {\r\n // `dev` registers a TS loader hook, and Node ≥ 22.18 / 23.6 strips types\r\n // natively — in both cases the direct import above works. But `build` /\r\n // `start` register no hook, so on an older Node the import throws\r\n // ERR_UNKNOWN_FILE_EXTENSION. Transpile the config with esbuild ourselves\r\n // so config loading is Node-version-independent rather than relying on an\r\n // experimental runtime feature.\r\n if (isUnknownTsExtensionError(error)) {\r\n return await this.loadViaEsbuild(configPath);\r\n }\r\n\r\n throw new Error(`Failed to load warlock.config.ts: ${error}`);\r\n }\r\n }\r\n\r\n /**\r\n * Fallback config loader for runtimes without native TypeScript support.\r\n *\r\n * Transpiles `warlock.config.ts` with esbuild (already a core dependency),\r\n * writes the result as a sibling `.mjs` — so the config's own imports (bare\r\n * `@warlock.js/*` and any relative paths) resolve exactly as they would from\r\n * the original location — imports it, then removes the temp file.\r\n */\r\n private async loadViaEsbuild(configPath: string): Promise<WarlockConfig | undefined> {\r\n const { transformSync } = await import(\"esbuild\");\r\n const source = await readFile(configPath, \"utf8\");\r\n\r\n const { code } = transformSync(source, {\r\n loader: \"ts\",\r\n format: \"esm\",\r\n target: `node${process.versions.node.split(\".\")[0]}`,\r\n sourcefile: configPath,\r\n });\r\n\r\n // Unique sibling name so concurrent processes never clobber each other.\r\n const compiledPath = configPath.replace(\r\n /warlock\\.config\\.ts$/,\r\n `.warlock.config.${process.pid}.mjs`,\r\n );\r\n\r\n await writeFile(compiledPath, code, \"utf8\");\r\n\r\n try {\r\n const configModule = await import(pathToFileURL(compiledPath).href);\r\n return configModule.default;\r\n } finally {\r\n await unlink(compiledPath).catch(() => {});\r\n }\r\n }\r\n\r\n /**\r\n * Get config value by key (dot notation supported)\r\n *\r\n * @example\r\n * config.get(\"server.port\") // Returns 3000\r\n * config.get(\"cli.commands\") // Returns array of commands\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @returns The config value\r\n * @throws Error if config is not loaded\r\n */\r\n public get<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): WarlockConfig[Key] {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return get(this.config, key as string, defaultValue);\r\n }\r\n\r\n /**\r\n * Lazy get - loads config if not already loaded\r\n *\r\n * @example\r\n * const port = await config.lazyGet(\"server\");\r\n *\r\n * @param key - Config key (supports dot notation), autocompletes for first level only\r\n * @param defaultValue - Default value if config key is undefined\r\n * @returns The config value\r\n */\r\n async lazyGet<Key extends keyof WarlockConfig>(\r\n key: Key,\r\n defaultValue?: WarlockConfig[Key],\r\n ): Promise<WarlockConfig[Key]> {\r\n await this.load();\r\n return this.get(key, defaultValue);\r\n }\r\n\r\n /**\r\n * Check if config is loaded\r\n */\r\n public get isLoaded(): boolean {\r\n return this.config !== undefined;\r\n }\r\n\r\n /**\r\n * Get the entire config object\r\n *\r\n * @throws Error if config is not loaded\r\n */\r\n public getAll(): WarlockConfig {\r\n if (!this.config) {\r\n throw new Error(\"WarlockConfig not loaded. Call load() first or use lazyGet().\");\r\n }\r\n\r\n return this.config;\r\n }\r\n\r\n /**\r\n * Reload config (useful for HMR/development)\r\n */\r\n public async reload(): Promise<void> {\r\n this.config = undefined;\r\n this.loading = undefined;\r\n await this.load();\r\n }\r\n}\r\n\r\n/**\r\n * Exported singleton instance\r\n *\r\n * @example\r\n * import { warlockConfig } from \"@warlock.js/core\";\r\n *\r\n * // Lazy load and get value\r\n * const port = await warlockConfig.lazyGet(\"server.port\");\r\n *\r\n * // Or load first, then get\r\n * await warlockConfig.load();\r\n * const commands = warlockConfig.get(\"cli.commands\");\r\n */\r\nexport const warlockConfigManager = new WarlockConfigManager();\r\n"],"mappings":";;;;;;;;;;;;;;;;AAeA,SAAgB,0BAA0B,OAAyB;CACjE,OACG,OAA6B,SAAS,gCACvC,kCAAkC,KAAK,OAAQ,OAAiB,OAAO,CAAC;AAE5E;;;;;;;AAQA,IAAa,uBAAb,MAAkC;;;;;;CAgBhC,MAAa,OAA2C;EAEtD,IAAI,KAAK,QACP,OAAO,KAAK;EAId,IAAI,KAAK,SACP,OAAO,KAAK;EAId,KAAK,UAAU,KAAK,OAAO;EAC3B,KAAK,SAAS,MAAM,KAAK;EACzB,KAAK,UAAU;EAEf,OAAO,KAAK;CACd;;;;;;;CAQA,MAAc,SAA6C;EACzD,MAAM,aAAa,SAAS,mBAAmB;EAE/C,IAAI,CAAE,MAAM,gBAAgB,UAAU,GAAI;GAMxC,QAAQ,KAAK,gEAAgE;GAC7E;EACF;EAEA,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,UAAU,CAAC,CAAC,MACzC,CAAC;EACtB,SAAS,OAAO;GAOd,IAAI,0BAA0B,KAAK,GACjC,OAAO,MAAM,KAAK,eAAe,UAAU;GAG7C,MAAM,IAAI,MAAM,qCAAqC,OAAO;EAC9D;CACF;;;;;;;;;CAUA,MAAc,eAAe,YAAwD;EACnF,MAAM,EAAE,kBAAkB,MAAM,OAAO;EAGvC,MAAM,EAAE,SAAS,cAAc,MAFV,SAAS,YAAY,MAAM,GAET;GACrC,QAAQ;GACR,QAAQ;GACR,QAAQ,OAAO,QAAQ,SAAS,KAAK,MAAM,GAAG,CAAC,CAAC;GAChD,YAAY;EACd,CAAC;EAGD,MAAM,eAAe,WAAW,QAC9B,wBACA,mBAAmB,QAAQ,IAAI,KACjC;EAEA,MAAM,UAAU,cAAc,MAAM,MAAM;EAE1C,IAAI;GAEF,QAAO,MADoB,OAAO,cAAc,YAAY,CAAC,CAAC,MAC3C,CAAC;EACtB,UAAU;GACR,MAAM,OAAO,YAAY,CAAC,CAAC,YAAY,CAAC,CAAC;EAC3C;CACF;;;;;;;;;;;;CAaA,AAAO,IACL,KACA,cACoB;EACpB,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,IAAI,KAAK,QAAQ,KAAe,YAAY;CACrD;;;;;;;;;;;CAYA,MAAM,QACJ,KACA,cAC6B;EAC7B,MAAM,KAAK,KAAK;EAChB,OAAO,KAAK,IAAI,KAAK,YAAY;CACnC;;;;CAKA,IAAW,WAAoB;EAC7B,OAAO,KAAK,WAAW;CACzB;;;;;;CAOA,AAAO,SAAwB;EAC7B,IAAI,CAAC,KAAK,QACR,MAAM,IAAI,MAAM,+DAA+D;EAGjF,OAAO,KAAK;CACd;;;;CAKA,MAAa,SAAwB;EACnC,KAAK,SAAS;EACd,KAAK,UAAU;EACf,MAAM,KAAK,KAAK;CAClB;AACF;;;;;;;;;;;;;;AAeA,MAAa,uBAAuB,IAAI,qBAAqB"}
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: