@warlock.js/core 4.10.0 → 4.12.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 (144) hide show
  1. package/CHANGELOG.md +129 -0
  2. package/esm/application/application.d.mts +19 -0
  3. package/esm/application/application.d.mts.map +1 -1
  4. package/esm/application/application.mjs +25 -0
  5. package/esm/application/application.mjs.map +1 -1
  6. package/esm/application/boot-signal.d.mts +77 -0
  7. package/esm/application/boot-signal.d.mts.map +1 -0
  8. package/esm/application/boot-signal.mjs +64 -0
  9. package/esm/application/boot-signal.mjs.map +1 -0
  10. package/esm/application/index.d.mts +1 -0
  11. package/esm/application/index.mjs +1 -0
  12. package/esm/bootstrap.mjs +2 -2
  13. package/esm/bootstrap.mjs.map +1 -1
  14. package/esm/cli/cli-commands.manager.mjs +31 -8
  15. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  16. package/esm/cli/cli-commands.utils.mjs +57 -1
  17. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  18. package/esm/cli/commands/add.command.mjs +4 -2
  19. package/esm/cli/commands/add.command.mjs.map +1 -1
  20. package/esm/cli/commands/generate/generate.command.mjs +42 -21
  21. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  22. package/esm/cli/commands/generate/generators/module.generator.mjs +1 -1
  23. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  24. package/esm/cli/commands/migrate.command.mjs +6 -2
  25. package/esm/cli/commands/migrate.command.mjs.map +1 -1
  26. package/esm/cli/commands/start-production.command.mjs +4 -25
  27. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  28. package/esm/cli/parse-cli-args.mjs +86 -9
  29. package/esm/cli/parse-cli-args.mjs.map +1 -1
  30. package/esm/cli/types.d.mts +6 -1
  31. package/esm/cli/types.d.mts.map +1 -1
  32. package/esm/config/config-getter.mjs +5 -5
  33. package/esm/config/config-getter.mjs.map +1 -1
  34. package/esm/config/config-loader.mjs +2 -2
  35. package/esm/config/config-loader.mjs.map +1 -1
  36. package/esm/config/config-setter.d.mts +27 -0
  37. package/esm/config/config-setter.d.mts.map +1 -0
  38. package/esm/config/config-setter.mjs +10 -0
  39. package/esm/config/config-setter.mjs.map +1 -0
  40. package/esm/config/index.mjs +1 -0
  41. package/esm/connectors/access-connector.mjs +2 -2
  42. package/esm/connectors/access-connector.mjs.map +1 -1
  43. package/esm/connectors/ai-connector.mjs +2 -2
  44. package/esm/connectors/ai-connector.mjs.map +1 -1
  45. package/esm/connectors/cache-connector.mjs +2 -2
  46. package/esm/connectors/cache-connector.mjs.map +1 -1
  47. package/esm/connectors/database-connector.mjs +2 -2
  48. package/esm/connectors/database-connector.mjs.map +1 -1
  49. package/esm/connectors/herald-connector.mjs +2 -2
  50. package/esm/connectors/herald-connector.mjs.map +1 -1
  51. package/esm/connectors/http-connector.d.mts.map +1 -1
  52. package/esm/connectors/http-connector.mjs +8 -7
  53. package/esm/connectors/http-connector.mjs.map +1 -1
  54. package/esm/connectors/logger-connector.mjs +2 -2
  55. package/esm/connectors/logger-connector.mjs.map +1 -1
  56. package/esm/connectors/mail-connector.mjs +2 -2
  57. package/esm/connectors/mail-connector.mjs.map +1 -1
  58. package/esm/connectors/notifications-connector.mjs +2 -2
  59. package/esm/connectors/notifications-connector.mjs.map +1 -1
  60. package/esm/connectors/socket-connector.mjs +3 -3
  61. package/esm/connectors/socket-connector.mjs.map +1 -1
  62. package/esm/connectors/storage.connector.d.mts +11 -5
  63. package/esm/connectors/storage.connector.d.mts.map +1 -1
  64. package/esm/connectors/storage.connector.mjs +11 -5
  65. package/esm/connectors/storage.connector.mjs.map +1 -1
  66. package/esm/database/migrate-action.mjs +53 -1
  67. package/esm/database/migrate-action.mjs.map +1 -1
  68. package/esm/database/pending-exit-code.mjs +39 -0
  69. package/esm/database/pending-exit-code.mjs.map +1 -0
  70. package/esm/database/resolve-pending-migrations.mjs +49 -0
  71. package/esm/database/resolve-pending-migrations.mjs.map +1 -0
  72. package/esm/dev-server/loader/load-hook.mjs +1 -1
  73. package/esm/generations/stubs.mjs +1 -1
  74. package/esm/generations/stubs.mjs.map +1 -1
  75. package/esm/http/config.mjs +2 -2
  76. package/esm/http/config.mjs.map +1 -1
  77. package/esm/http/createHttpApplication.mjs +2 -2
  78. package/esm/http/createHttpApplication.mjs.map +1 -1
  79. package/esm/http/index.d.mts +1 -0
  80. package/esm/http/index.mjs +1 -0
  81. package/esm/http/middleware/idempotency.middleware.mjs +5 -5
  82. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  83. package/esm/http/middleware/inject-request-context.mjs +2 -2
  84. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  85. package/esm/http/middleware/maintenance.middleware.mjs +4 -4
  86. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  87. package/esm/http/plugins.mjs +9 -9
  88. package/esm/http/plugins.mjs.map +1 -1
  89. package/esm/http/port-preflight.d.mts +34 -0
  90. package/esm/http/port-preflight.d.mts.map +1 -0
  91. package/esm/http/port-preflight.mjs +66 -0
  92. package/esm/http/port-preflight.mjs.map +1 -0
  93. package/esm/http/response.mjs +5 -5
  94. package/esm/http/response.mjs.map +1 -1
  95. package/esm/http/server.mjs +4 -4
  96. package/esm/http/server.mjs.map +1 -1
  97. package/esm/image/image.d.mts +3 -2
  98. package/esm/image/image.d.mts.map +1 -1
  99. package/esm/image/image.mjs +63 -17
  100. package/esm/image/image.mjs.map +1 -1
  101. package/esm/index.d.mts +6 -2
  102. package/esm/index.mjs +5 -1
  103. package/esm/mail/mailer-pool.d.mts.map +1 -1
  104. package/esm/mail/mailer-pool.mjs.map +1 -1
  105. package/esm/production/assert-generated-imports.mjs +94 -0
  106. package/esm/production/assert-generated-imports.mjs.map +1 -0
  107. package/esm/production/production-builder.mjs +29 -4
  108. package/esm/production/production-builder.mjs.map +1 -1
  109. package/esm/production/production-supervisor.mjs +108 -0
  110. package/esm/production/production-supervisor.mjs.map +1 -0
  111. package/esm/react/index.d.mts +4 -0
  112. package/esm/react/index.d.mts.map +1 -1
  113. package/esm/react/index.mjs +88 -14
  114. package/esm/react/index.mjs.map +1 -1
  115. package/esm/storage/storage.d.mts +16 -0
  116. package/esm/storage/storage.d.mts.map +1 -1
  117. package/esm/storage/storage.mjs +24 -1
  118. package/esm/storage/storage.mjs.map +1 -1
  119. package/esm/tests/index.d.mts +2 -1
  120. package/esm/tests/index.mjs +1 -0
  121. package/esm/tests/start-http-development-server.d.mts +15 -2
  122. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  123. package/esm/tests/start-http-development-server.mjs +43 -16
  124. package/esm/tests/start-http-development-server.mjs.map +1 -1
  125. package/esm/tests/test-helpers.d.mts.map +1 -1
  126. package/esm/tests/test-helpers.mjs +2 -1
  127. package/esm/tests/test-helpers.mjs.map +1 -1
  128. package/esm/tests/test-server-port-channel.d.mts +27 -0
  129. package/esm/tests/test-server-port-channel.d.mts.map +1 -0
  130. package/esm/tests/test-server-port-channel.mjs +32 -0
  131. package/esm/tests/test-server-port-channel.mjs.map +1 -0
  132. package/esm/utils/load-environment.mjs +59 -0
  133. package/esm/utils/load-environment.mjs.map +1 -0
  134. package/esm/utils/paths.mjs +2 -2
  135. package/esm/utils/paths.mjs.map +1 -1
  136. package/esm/validation/validateAll.mjs +2 -2
  137. package/esm/validation/validateAll.mjs.map +1 -1
  138. package/llms-full.txt +90 -3
  139. package/llms.txt +1 -1
  140. package/package.json +13 -13
  141. package/skills/process-image/SKILL.md +3 -1
  142. package/skills/run-app/SKILL.md +63 -0
  143. package/skills/test-http/SKILL.md +27 -3
  144. package/skills/write-cli-command/SKILL.md +19 -1
@@ -1 +1 @@
1
- {"version":3,"file":"paths.mjs","names":["config"],"sources":["../../../../../../../core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport path from \"path\";\r\n\r\n/**\r\n * Get root path or join the given paths to the root path\r\n */\r\nexport function rootPath(...paths: string[]) {\r\n return path.resolve(process.cwd(), ...paths);\r\n}\r\n\r\n/**\r\n * Get src directory path or join the given paths to the src directory path\r\n */\r\nexport function srcPath(...paths: string[]) {\r\n return rootPath(\"src\", ...paths);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the storage folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the storage folder\r\n */\r\nexport function storagePath(relativePath = \"\") {\r\n return rootPath(\"storage\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the uploads folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the uploads folder\r\n */\r\nexport function uploadsPath(relativePath = \"\") {\r\n const configPath = config.get(\"uploads.root\");\r\n if (!configPath) {\r\n return rootPath(\"storage\", \"uploads\", relativePath);\r\n }\r\n\r\n return typeof configPath === \"function\"\r\n ? configPath(relativePath)\r\n : path.resolve(configPath, relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the public folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the public folder\r\n */\r\nexport function publicPath(relativePath = \"\") {\r\n return rootPath(\"public\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the cache folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the cache folder\r\n */\r\nexport function cachePath(relativePath = \"\") {\r\n return rootPath(\"storage\", \"cache\", relativePath);\r\n}\r\n\r\n/**\r\n * App path\r\n */\r\nexport function appPath(relativePath = \"\") {\r\n return rootPath(\"src/app\", relativePath);\r\n}\r\n\r\n/**\r\n * Get logs directory path\r\n */\r\nexport function logsPath(relativePath = \"\") {\r\n return rootPath(\"storage/logs\", relativePath);\r\n}\r\n\r\n/**\r\n * Get a temp path\r\n */\r\nexport function tempPath(relativePath = \"\") {\r\n return rootPath(\"storage/tmp\", relativePath);\r\n}\r\n\r\n/**\r\n * Remove any invalid characters from the file path using regex\r\n * It should accept any language character, numbers, and the following characters: _ - .\r\n */\r\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\r\nexport function sanitizePath(filePath: string) {\r\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\r\n}\r\n\r\n/**\r\n * Warlock path\r\n * PLEASE DO NOT add any files in this directory as it may be deleted\r\n */\r\nexport function warlockPath(...path: string[]) {\r\n return rootPath(\".warlock\", ...path);\r\n}\r\n\r\n/**\r\n * Get config directory path\r\n */\r\nexport function configPath(...path: string[]) {\r\n return rootPath(\"src/config\", ...path);\r\n}\r\n\r\nexport const paths = {\r\n root: rootPath,\r\n src: srcPath,\r\n storage: storagePath,\r\n logs: logsPath,\r\n uploads: uploadsPath,\r\n public: publicPath,\r\n cache: cachePath,\r\n app: appPath,\r\n temp: tempPath,\r\n warlock: warlockPath,\r\n config: configPath,\r\n sanitize: sanitizePath,\r\n};\r\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,aAAaA,WAAO,IAAI,cAAc;CAC5C,IAAI,CAAC,YACH,OAAO,SAAS,WAAW,WAAW,YAAY;CAGpD,OAAO,OAAO,eAAe,aACzB,WAAW,YAAY,IACvB,KAAK,QAAQ,YAAY,YAAY;AAC3C;;;;;;AAOA,SAAgB,WAAW,eAAe,IAAI;CAC5C,OAAO,SAAS,UAAU,YAAY;AACxC;;;;;;AAOA,SAAgB,UAAU,eAAe,IAAI;CAC3C,OAAO,SAAS,WAAW,SAAS,YAAY;AAClD;;;;AAKA,SAAgB,QAAQ,eAAe,IAAI;CACzC,OAAO,SAAS,WAAW,YAAY;AACzC;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,gBAAgB,YAAY;AAC9C;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,eAAe,YAAY;AAC7C;;;;;AAMA,MAAM,oBAAoB;AAC1B,SAAgB,aAAa,UAAkB;CAC7C,OAAO,SAAS,QAAQ,mBAAmB,EAAE;AAC/C;;;;;AAMA,SAAgB,YAAY,GAAG,MAAgB;CAC7C,OAAO,SAAS,YAAY,GAAG,IAAI;AACrC;;;;AAKA,SAAgB,WAAW,GAAG,MAAgB;CAC5C,OAAO,SAAS,cAAc,GAAG,IAAI;AACvC;AAEA,MAAa,QAAQ;CACnB,MAAM;CACN,KAAK;CACL,SAAS;CACT,MAAM;CACN,SAAS;CACT,QAAQ;CACR,OAAO;CACP,KAAK;CACL,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;AACZ"}
1
+ {"version":3,"file":"paths.mjs","names":[],"sources":["../../../../../../../core/src/utils/paths.ts"],"sourcesContent":["import config from \"@mongez/config\";\r\nimport path from \"path\";\r\n\r\n/**\r\n * Get root path or join the given paths to the root path\r\n */\r\nexport function rootPath(...paths: string[]) {\r\n return path.resolve(process.cwd(), ...paths);\r\n}\r\n\r\n/**\r\n * Get src directory path or join the given paths to the src directory path\r\n */\r\nexport function srcPath(...paths: string[]) {\r\n return rootPath(\"src\", ...paths);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the storage folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the storage folder\r\n */\r\nexport function storagePath(relativePath = \"\") {\r\n return rootPath(\"storage\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the uploads folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the uploads folder\r\n */\r\nexport function uploadsPath(relativePath = \"\") {\r\n const configPath = config.get(\"uploads.root\");\r\n if (!configPath) {\r\n return rootPath(\"storage\", \"uploads\", relativePath);\r\n }\r\n\r\n return typeof configPath === \"function\"\r\n ? configPath(relativePath)\r\n : path.resolve(configPath, relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the public folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the public folder\r\n */\r\nexport function publicPath(relativePath = \"\") {\r\n return rootPath(\"public\", relativePath);\r\n}\r\n\r\n/**\r\n * Get the absolute path to the cache folder to the given path\r\n *\r\n * If no path is given, it will return the absolute path to the cache folder\r\n */\r\nexport function cachePath(relativePath = \"\") {\r\n return rootPath(\"storage\", \"cache\", relativePath);\r\n}\r\n\r\n/**\r\n * App path\r\n */\r\nexport function appPath(relativePath = \"\") {\r\n return rootPath(\"src/app\", relativePath);\r\n}\r\n\r\n/**\r\n * Get logs directory path\r\n */\r\nexport function logsPath(relativePath = \"\") {\r\n return rootPath(\"storage/logs\", relativePath);\r\n}\r\n\r\n/**\r\n * Get a temp path\r\n */\r\nexport function tempPath(relativePath = \"\") {\r\n return rootPath(\"storage/tmp\", relativePath);\r\n}\r\n\r\n/**\r\n * Remove any invalid characters from the file path using regex\r\n * It should accept any language character, numbers, and the following characters: _ - .\r\n */\r\nconst invalidCharsRegex = /[<>:\"/\\\\|?*]/g; // Regex to match invalid characters\r\nexport function sanitizePath(filePath: string) {\r\n return filePath.replace(invalidCharsRegex, \"\"); // Replace invalid characters with an empty string\r\n}\r\n\r\n/**\r\n * Warlock path\r\n * PLEASE DO NOT add any files in this directory as it may be deleted\r\n */\r\nexport function warlockPath(...path: string[]) {\r\n return rootPath(\".warlock\", ...path);\r\n}\r\n\r\n/**\r\n * Get config directory path\r\n */\r\nexport function configPath(...path: string[]) {\r\n return rootPath(\"src/config\", ...path);\r\n}\r\n\r\nexport const paths = {\r\n root: rootPath,\r\n src: srcPath,\r\n storage: storagePath,\r\n logs: logsPath,\r\n uploads: uploadsPath,\r\n public: publicPath,\r\n cache: cachePath,\r\n app: appPath,\r\n temp: tempPath,\r\n warlock: warlockPath,\r\n config: configPath,\r\n sanitize: sanitizePath,\r\n};\r\n"],"mappings":";;;;;;;AAMA,SAAgB,SAAS,GAAG,OAAiB;CAC3C,OAAO,KAAK,QAAQ,QAAQ,IAAI,GAAG,GAAG,KAAK;AAC7C;;;;AAKA,SAAgB,QAAQ,GAAG,OAAiB;CAC1C,OAAO,SAAS,OAAO,GAAG,KAAK;AACjC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,OAAO,SAAS,WAAW,YAAY;AACzC;;;;;;AAOA,SAAgB,YAAY,eAAe,IAAI;CAC7C,MAAM,aAAa,OAAO,IAAI,cAAc;CAC5C,IAAI,CAAC,YACH,OAAO,SAAS,WAAW,WAAW,YAAY;CAGpD,OAAO,OAAO,eAAe,aACzB,WAAW,YAAY,IACvB,KAAK,QAAQ,YAAY,YAAY;AAC3C;;;;;;AAOA,SAAgB,WAAW,eAAe,IAAI;CAC5C,OAAO,SAAS,UAAU,YAAY;AACxC;;;;;;AAOA,SAAgB,UAAU,eAAe,IAAI;CAC3C,OAAO,SAAS,WAAW,SAAS,YAAY;AAClD;;;;AAKA,SAAgB,QAAQ,eAAe,IAAI;CACzC,OAAO,SAAS,WAAW,YAAY;AACzC;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,gBAAgB,YAAY;AAC9C;;;;AAKA,SAAgB,SAAS,eAAe,IAAI;CAC1C,OAAO,SAAS,eAAe,YAAY;AAC7C;;;;;AAMA,MAAM,oBAAoB;AAC1B,SAAgB,aAAa,UAAkB;CAC7C,OAAO,SAAS,QAAQ,mBAAmB,EAAE;AAC/C;;;;;AAMA,SAAgB,YAAY,GAAG,MAAgB;CAC7C,OAAO,SAAS,YAAY,GAAG,IAAI;AACrC;;;;AAKA,SAAgB,WAAW,GAAG,MAAgB;CAC5C,OAAO,SAAS,cAAc,GAAG,IAAI;AACvC;AAEA,MAAa,QAAQ;CACnB,MAAM;CACN,KAAK;CACL,SAAS;CACT,MAAM;CACN,SAAS;CACT,QAAQ;CACR,OAAO;CACP,KAAK;CACL,MAAM;CACN,SAAS;CACT,QAAQ;CACR,UAAU;AACZ"}
@@ -1,5 +1,5 @@
1
1
  import { v } from "@warlock.js/seal";
2
- import baseConfig from "@mongez/config";
2
+ import config from "@mongez/config";
3
3
  import { log } from "@warlock.js/logger";
4
4
  import { merge } from "@mongez/reinforcements";
5
5
 
@@ -40,7 +40,7 @@ async function validateAll(validation, request, response) {
40
40
  if (validation.validate) {
41
41
  const result = await validation.validate(request, response);
42
42
  if (result) {
43
- if (!response.statusCode) response.setStatusCode(baseConfig.get("validation.responseStatus", 400));
43
+ if (!response.statusCode) response.setStatusCode(config.get("validation.responseStatus", 400));
44
44
  log.info("validation", "failed", "Validation failed");
45
45
  return result;
46
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"validateAll.mjs","names":["config"],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request, Response } from \"../http\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(validating: RequestHandlerValidation[\"validating\"], request: Request) {\n if (!validating || validating.length === 0) return request.allExceptParams();\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate(request, response);\n\n // if there is a result, it means it failed\n if (result) {\n // check if there is no response status code, then set it to config value or 400 as default\n if (!response.statusCode) {\n response.setStatusCode(config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n return result;\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;AAOA,SAAS,mBAAmB,YAAoD,SAAkB;CAChG,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO,QAAQ,gBAAgB;CAE3E,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,OAAO;GAC9D,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS,SAAS,QAAQ;EAG1D,IAAI,QAAQ;GAEV,IAAI,CAAC,SAAS,YACZ,SAAS,cAAcA,WAAO,IAAI,6BAA6B,GAAG,CAAC;GAGrE,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAEpD,OAAO;EACT;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
1
+ {"version":3,"file":"validateAll.mjs","names":[],"sources":["../../../../../../../core/src/validation/validateAll.ts"],"sourcesContent":["import config from \"@mongez/config\";\nimport { merge } from \"@mongez/reinforcements\";\nimport { log } from \"@warlock.js/logger\";\nimport { v } from \"@warlock.js/seal\";\nimport type { Request, Response } from \"../http\";\nimport type { RequestHandlerValidation, Route } from \"../router\";\n\nfunction resolveDataToParse(validating: RequestHandlerValidation[\"validating\"], request: Request) {\n if (!validating || validating.length === 0) return request.allExceptParams();\n\n let data: any = {};\n\n for (const validatingType of validating) {\n if (validatingType === \"body\") {\n data = merge(data, request.body);\n }\n\n if (validatingType === \"query\") {\n data = merge(data, request.query);\n }\n\n if (validatingType === \"params\") {\n data = merge(data, request.params);\n }\n\n if (validatingType === \"headers\") {\n data = merge(data, request.headers);\n }\n }\n\n return data;\n}\n\n/**\n * Validate the request route\n */\nexport async function validateAll(\n validation: Route[\"handler\"][\"validation\"],\n request: Request,\n response: Response,\n) {\n if (!validation) return;\n\n log.info(\"validation\", \"started\", \"Start validating the request\");\n\n if (validation.schema) {\n log.info(\"validation\", \"schema\", \"Validating request schema\");\n try {\n const data = resolveDataToParse(validation.validating, request);\n const result = await v.validate(validation.schema, data);\n\n if (result.data && result.isValid) {\n request.setValidatedData(result.data);\n }\n\n if (!result.isValid) {\n log.warn(\"validation\", \"schema\", \"Schema Validation failed\");\n return response.failedSchema(result);\n }\n\n log.success(\"validation\", \"schema\", \"Schema Validation passed\");\n } catch (error) {\n log.warn(\"app.validation\", \"error\", error);\n throw error;\n }\n }\n\n if (validation.validate) {\n const result = await validation.validate(request, response);\n\n // if there is a result, it means it failed\n if (result) {\n // check if there is no response status code, then set it to config value or 400 as default\n if (!response.statusCode) {\n response.setStatusCode(config.get(\"validation.responseStatus\", 400));\n }\n\n log.info(\"validation\", \"failed\", \"Validation failed\");\n\n return result;\n }\n\n log.info(\"validation\", \"passed\", \"Validation passed\");\n }\n}\n"],"mappings":";;;;;;AAOA,SAAS,mBAAmB,YAAoD,SAAkB;CAChG,IAAI,CAAC,cAAc,WAAW,WAAW,GAAG,OAAO,QAAQ,gBAAgB;CAE3E,IAAI,OAAY,CAAC;CAEjB,KAAK,MAAM,kBAAkB,YAAY;EACvC,IAAI,mBAAmB,QACrB,OAAO,MAAM,MAAM,QAAQ,IAAI;EAGjC,IAAI,mBAAmB,SACrB,OAAO,MAAM,MAAM,QAAQ,KAAK;EAGlC,IAAI,mBAAmB,UACrB,OAAO,MAAM,MAAM,QAAQ,MAAM;EAGnC,IAAI,mBAAmB,WACrB,OAAO,MAAM,MAAM,QAAQ,OAAO;CAEtC;CAEA,OAAO;AACT;;;;AAKA,eAAsB,YACpB,YACA,SACA,UACA;CACA,IAAI,CAAC,YAAY;CAEjB,IAAI,KAAK,cAAc,WAAW,8BAA8B;CAEhE,IAAI,WAAW,QAAQ;EACrB,IAAI,KAAK,cAAc,UAAU,2BAA2B;EAC5D,IAAI;GACF,MAAM,OAAO,mBAAmB,WAAW,YAAY,OAAO;GAC9D,MAAM,SAAS,MAAM,EAAE,SAAS,WAAW,QAAQ,IAAI;GAEvD,IAAI,OAAO,QAAQ,OAAO,SACxB,QAAQ,iBAAiB,OAAO,IAAI;GAGtC,IAAI,CAAC,OAAO,SAAS;IACnB,IAAI,KAAK,cAAc,UAAU,0BAA0B;IAC3D,OAAO,SAAS,aAAa,MAAM;GACrC;GAEA,IAAI,QAAQ,cAAc,UAAU,0BAA0B;EAChE,SAAS,OAAO;GACd,IAAI,KAAK,kBAAkB,SAAS,KAAK;GACzC,MAAM;EACR;CACF;CAEA,IAAI,WAAW,UAAU;EACvB,MAAM,SAAS,MAAM,WAAW,SAAS,SAAS,QAAQ;EAG1D,IAAI,QAAQ;GAEV,IAAI,CAAC,SAAS,YACZ,SAAS,cAAc,OAAO,IAAI,6BAA6B,GAAG,CAAC;GAGrE,IAAI,KAAK,cAAc,UAAU,mBAAmB;GAEpD,OAAO;EACT;EAEA,IAAI,KAAK,cAAc,UAAU,mBAAmB;CACtD;AACF"}
package/llms-full.txt CHANGED
@@ -3339,6 +3339,57 @@ Just `warlockConfig: true` — same as `build`. The actual app bootstrap happens
3339
3339
 
3340
3340
  Means `docker stop` / `kubectl delete pod` works as expected: SIGTERM reaches the bundle, your graceful-shutdown hooks fire, then the parent exits.
3341
3341
 
3342
+ ### pnpm needs esbuild's install script allowed
3343
+
3344
+ pnpm 10+ will not run a dependency's install script unless the app names it. esbuild's script links its platform-native binary, and `warlock build` shells out to that binary — so the app installs cleanly and then cannot build:
3345
+
3346
+ ```
3347
+ [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.27.7
3348
+ ```
3349
+
3350
+ Fix it once in the app's `pnpm-workspace.yaml`:
3351
+
3352
+ ```yaml
3353
+ allowBuilds:
3354
+ esbuild: true
3355
+ ```
3356
+
3357
+ Note pnpm reads this from `pnpm-workspace.yaml`, **not** from `package.json`'s `pnpm` field — pnpm 11 warns that the field is ignored and then carries on, so settings left there fail silently.
3358
+
3359
+ Nothing else is needed for pnpm. Warlock never requires an app to declare a package it does not import: generated code is checked at build time against the app's own `dependencies`, so `warlock build` failing over an unfamiliar package name is a framework bug, not a missing dependency.
3360
+
3361
+ ### Output streams — what a supervisor may trust
3362
+
3363
+ **A success line on stdout means the app is serving requests.** That is a contract, not a convention, and you can build a CI gate or a health probe on it.
3364
+
3365
+ | Stream | Carries |
3366
+ | ---------- | -------------------------------------------------------------- |
3367
+ | **stdout** | the started banner, and start failures. Nothing else. |
3368
+ | **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
3369
+
3370
+ The started banner prints **only** when the running application reports a completed boot — not when the command starts, not when the child is spawned. A child that dies before reporting is a failed start: the failure is written to **both** streams (stderr for humans and log collectors, stdout so a supervisor greping for the banner finds a failure rather than silence), and `warlock start` exits non-zero **even when the child itself exited `0`**.
3371
+
3372
+ ```bash
3373
+ # a CI gate can be this blunt, and it is now correct
3374
+ yarn warlock start | grep -q "production server started"
3375
+ ```
3376
+
3377
+ ### How readiness is reported
3378
+
3379
+ `warlock start` spawns the bundle with an IPC channel and sets `WARLOCK_BOOT_SIGNAL=1` on it. `Application.markBooted()` — which the production entry calls after the late-phase connectors (http, socket) are up — sends one versioned message and closes the channel:
3380
+
3381
+ ```ts
3382
+ { type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }
3383
+ ```
3384
+
3385
+ Three consequences worth knowing:
3386
+
3387
+ 1. **A queue worker with no http connector still reports.** Readiness hangs on a completed boot, not on a bound port. `port` is simply absent.
3388
+ 2. **Running the bundle any other way changes nothing.** `node dist/app.js`, a Docker `CMD`, or pm2 — the signal is a strict no-op without both the IPC channel and the handshake flag, so Warlock never writes into a channel that belongs to another supervisor.
3389
+ 3. **A bundle built before 4.11.0 has no signal.** It starts and runs normally, and after ten seconds prints a note on **stderr only** telling you to re-run `warlock build`. An absent signal is never an error, never fails the run, and never kills a slow boot.
3390
+
3391
+ If you need the same fact inside the app, use `Application.onceBooted()` / `Application.whenBooted()` — the signal and your listeners fire from the same latch.
3392
+
3342
3393
  ## Picking which mode you're in
3343
3394
 
3344
3395
  `Application.environment` and `Application.runtimeStrategy` are separate axes:
@@ -3406,6 +3457,18 @@ export default defineConfig({
3406
3457
 
3407
3458
  CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
3408
3459
 
3460
+ **This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated *before* any `.env` file was read, so every `env()` call in it returned its default — silently, under every command. If you copied this recipe earlier and concluded that `BUILD_OUT` was ignored, it was.
3461
+
3462
+ **Which file the value comes from is decided by `NODE_ENV`, and no Warlock command sets it.** `env()` reads `.env.<NODE_ENV>` when that file exists and falls back to plain `.env`. So:
3463
+
3464
+ ```bash
3465
+ warlock build # NODE_ENV unset → reads .env
3466
+ NODE_ENV=production warlock build # → reads .env.production
3467
+ NODE_ENV=staging warlock build # → reads .env.staging
3468
+ ```
3469
+
3470
+ That is deliberate: `build` and `start` do **not** force `production`. Forcing it would silently change which file an existing `NODE_ENV=staging` pipeline reads, and would have the framework overriding an operator's explicit choice at the moment of deployment. Set `NODE_ENV` in your Dockerfile, CI job, or process manager — the same place you already set it for `Application.environment`.
3471
+
3409
3472
  ### Skip type-gen on machines without write access
3410
3473
 
3411
3474
  ```bash
@@ -4493,7 +4556,7 @@ Client `PUT`s the bytes straight to R2 — your server never sees them.
4493
4556
 
4494
4557
  ---
4495
4558
  name: test-http
4496
- description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
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`.'
4497
4560
  ---
4498
4561
 
4499
4562
  # Warlock — HTTP integration tests
@@ -4549,6 +4612,30 @@ Unlike the dev server, it doesn't watch files, doesn't do HMR, doesn't run healt
4549
4612
 
4550
4613
  Both are idempotent. A second `start` returns early; `stop` on a non-running server logs and returns.
4551
4614
 
4615
+ ### Choosing the port — `startHttpTestServer({ port })`
4616
+
4617
+ ```ts
4618
+ await startHttpTestServer({ port: 3999 });
4619
+ ```
4620
+
4621
+ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`. Use it to run a suite while the dev server holds the configured port, or to run two suites side by side.
4622
+
4623
+ **Nothing else can move the server.** `startHttpTestServer()` bootstraps the app itself, and that bootstrap re-reads `.env` (dotenv overrides by default), so any value you set before calling it is gone before the port is read. Assigning `process.env.HTTP_PORT` does not work either: `env()` reads dotenv's own store and never falls back to `process.env`. The option is applied after the bootstrap, immediately before the connector binds — that is why it is the only channel that survives.
4624
+
4625
+ The request helpers follow with no extra wiring: the bound port is published as `WARLOCK_TEST_SERVER_PORT`, which the workers inherit, and `getTestServerUrl()` prefers it over their own `.env`-resolved `http.port`. `stopHttpTestServer()` withdraws it.
4626
+
4627
+ ### The port is preflighted
4628
+
4629
+ Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
4630
+
4631
+ ```
4632
+ Port 2031 is already in use on localhost. Stop the dev server (or whatever else is
4633
+ listening on port 2031) and run again, or start on a free port — e.g.
4634
+ startHttpTestServer({ port: 2032 }).
4635
+ ```
4636
+
4637
+ 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
+
4552
4639
  ## Project wiring — `src/test-global-setup.ts` + `vite.config.ts`
4553
4640
 
4554
4641
  ```ts title="src/test-global-setup.ts"
@@ -4598,7 +4685,7 @@ import { getTestServerUrl } from "@warlock.js/core";
4598
4685
  const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
4599
4686
  ```
4600
4687
 
4601
- Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config. If you change the HTTP config, helpers follow automatically.
4688
+ Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config — unless `startHttpTestServer` published a port, which wins, since a worker's own config never sees the `{ port }` option passed in `globalSetup`. If you change the HTTP config, helpers follow automatically.
4602
4689
 
4603
4690
  ### Verb helpers
4604
4691
 
@@ -4767,7 +4854,7 @@ This is fine for normal test flow. It bites when you're inside a transaction the
4767
4854
  ## Gotchas
4768
4855
 
4769
4856
  - **`globalSetup` must export `setup` and `teardown`.** Vitest reads them by name. A typo in the export gets you a confusing "server not running" error on the first `testGet` call.
4770
- - **Port conflicts.** If `http.port` matches your running dev server, `startHttpTestServer()` fails to bind. Either stop the dev server or set a test-only port: `http: { port: 3999 }` in a test-config branch.
4857
+ - **Port conflicts.** If `http.port` matches your running dev server, the preflight fails with a `PortInUseError` naming the port. Either stop the dev server or pass a free one: `startHttpTestServer({ port: 3999 })`.
4771
4858
  - **Auth tokens need a real user.** Generating a JWT with a non-existent `user_id` works — but the auth middleware's user-loading step will reject the request with 401 because it can't find the user in the DB.
4772
4859
  - **`expectJson` parses the body once.** If you call it twice on the same response, the second call gets an already-consumed stream error. Capture the result.
4773
4860
  - **The HTTP server's connection is NOT torn down between test files.** Data persists across files within a single `vitest` run. Either truncate in `afterEach` / `afterAll`, or design your tests to be order-independent.
package/llms.txt CHANGED
@@ -26,7 +26,7 @@
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`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.
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
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`.
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`.
package/package.json CHANGED
@@ -28,7 +28,7 @@
28
28
  "@mongez/concat-route": "^1.1.4",
29
29
  "@mongez/config": "^1.1.4",
30
30
  "@mongez/copper": "^2.1.2",
31
- "@mongez/dotenv": "^1.2.4",
31
+ "@mongez/dotenv": "^1.3.1",
32
32
  "@mongez/events": "^2.2.6",
33
33
  "@mongez/http": "^3.3.8",
34
34
  "@mongez/localization": "^3.4.6",
@@ -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.10.0",
40
- "@warlock.js/cache": "4.10.0",
41
- "@warlock.js/cascade": "4.10.0",
42
- "@warlock.js/context": "4.10.0",
43
- "@warlock.js/logger": "4.10.0",
44
- "@warlock.js/seal": "4.10.0",
45
- "@warlock.js/fs": "4.10.0",
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",
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.10.0",
72
- "@warlock.js/ai": "4.10.0",
73
- "@warlock.js/access": "4.10.0",
74
- "@warlock.js/notifications": "4.10.0"
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"
75
75
  },
76
76
  "bin": {
77
77
  "warlock": "bin/warlock.js"
78
78
  },
79
- "version": "4.10.0",
79
+ "version": "4.12.0",
80
80
  "type": "module",
81
81
  "main": "./esm/index.mjs",
82
82
  "module": "./esm/index.mjs",
@@ -22,7 +22,9 @@ await new Image("./photo.jpg")
22
22
  .save("./output.webp");
23
23
  ```
24
24
 
25
- That's the full contract. The chain doesn't touch sharp until the output method fires.
25
+ That's the full contract. The chain doesn't run sharp until the output method fires; the
26
+ constructor resolves the sharp module itself, so a missing sharp throws there rather than at the
27
+ final await.
26
28
 
27
29
  ## Installation
28
30
 
@@ -219,6 +219,57 @@ Just `warlockConfig: true` — same as `build`. The actual app bootstrap happens
219
219
 
220
220
  Means `docker stop` / `kubectl delete pod` works as expected: SIGTERM reaches the bundle, your graceful-shutdown hooks fire, then the parent exits.
221
221
 
222
+ ### pnpm needs esbuild's install script allowed
223
+
224
+ pnpm 10+ will not run a dependency's install script unless the app names it. esbuild's script links its platform-native binary, and `warlock build` shells out to that binary — so the app installs cleanly and then cannot build:
225
+
226
+ ```
227
+ [ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.27.7
228
+ ```
229
+
230
+ Fix it once in the app's `pnpm-workspace.yaml`:
231
+
232
+ ```yaml
233
+ allowBuilds:
234
+ esbuild: true
235
+ ```
236
+
237
+ Note pnpm reads this from `pnpm-workspace.yaml`, **not** from `package.json`'s `pnpm` field — pnpm 11 warns that the field is ignored and then carries on, so settings left there fail silently.
238
+
239
+ Nothing else is needed for pnpm. Warlock never requires an app to declare a package it does not import: generated code is checked at build time against the app's own `dependencies`, so `warlock build` failing over an unfamiliar package name is a framework bug, not a missing dependency.
240
+
241
+ ### Output streams — what a supervisor may trust
242
+
243
+ **A success line on stdout means the app is serving requests.** That is a contract, not a convention, and you can build a CI gate or a health probe on it.
244
+
245
+ | Stream | Carries |
246
+ | ---------- | -------------------------------------------------------------- |
247
+ | **stdout** | the started banner, and start failures. Nothing else. |
248
+ | **stderr** | progress (`🚀 Starting production server...`), diagnostics, the application's own logs |
249
+
250
+ The started banner prints **only** when the running application reports a completed boot — not when the command starts, not when the child is spawned. A child that dies before reporting is a failed start: the failure is written to **both** streams (stderr for humans and log collectors, stdout so a supervisor greping for the banner finds a failure rather than silence), and `warlock start` exits non-zero **even when the child itself exited `0`**.
251
+
252
+ ```bash
253
+ # a CI gate can be this blunt, and it is now correct
254
+ yarn warlock start | grep -q "production server started"
255
+ ```
256
+
257
+ ### How readiness is reported
258
+
259
+ `warlock start` spawns the bundle with an IPC channel and sets `WARLOCK_BOOT_SIGNAL=1` on it. `Application.markBooted()` — which the production entry calls after the late-phase connectors (http, socket) are up — sends one versioned message and closes the channel:
260
+
261
+ ```ts
262
+ { type: "warlock:ready", version: 1, pid, at, environment, runtimeStrategy, bootDurationMs?, port? }
263
+ ```
264
+
265
+ Three consequences worth knowing:
266
+
267
+ 1. **A queue worker with no http connector still reports.** Readiness hangs on a completed boot, not on a bound port. `port` is simply absent.
268
+ 2. **Running the bundle any other way changes nothing.** `node dist/app.js`, a Docker `CMD`, or pm2 — the signal is a strict no-op without both the IPC channel and the handshake flag, so Warlock never writes into a channel that belongs to another supervisor.
269
+ 3. **A bundle built before 4.11.0 has no signal.** It starts and runs normally, and after ten seconds prints a note on **stderr only** telling you to re-run `warlock build`. An absent signal is never an error, never fails the run, and never kills a slow boot.
270
+
271
+ If you need the same fact inside the app, use `Application.onceBooted()` / `Application.whenBooted()` — the signal and your listeners fire from the same latch.
272
+
222
273
  ## Picking which mode you're in
223
274
 
224
275
  `Application.environment` and `Application.runtimeStrategy` are separate axes:
@@ -286,6 +337,18 @@ export default defineConfig({
286
337
 
287
338
  CI sets `BUILD_OUT=build/<sha>` per pipeline. `warlock start` reads the same config and finds the bundle without any hardcoded paths.
288
339
 
340
+ **This recipe only started working in 4.11.0.** Before that, `warlock.config.ts` was evaluated *before* any `.env` file was read, so every `env()` call in it returned its default — silently, under every command. If you copied this recipe earlier and concluded that `BUILD_OUT` was ignored, it was.
341
+
342
+ **Which file the value comes from is decided by `NODE_ENV`, and no Warlock command sets it.** `env()` reads `.env.<NODE_ENV>` when that file exists and falls back to plain `.env`. So:
343
+
344
+ ```bash
345
+ warlock build # NODE_ENV unset → reads .env
346
+ NODE_ENV=production warlock build # → reads .env.production
347
+ NODE_ENV=staging warlock build # → reads .env.staging
348
+ ```
349
+
350
+ That is deliberate: `build` and `start` do **not** force `production`. Forcing it would silently change which file an existing `NODE_ENV=staging` pipeline reads, and would have the framework overriding an operator's explicit choice at the moment of deployment. Set `NODE_ENV` in your Dockerfile, CI job, or process manager — the same place you already set it for `Application.environment`.
351
+
289
352
  ### Skip type-gen on machines without write access
290
353
 
291
354
  ```bash
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: test-http
3
- description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
3
+ description: 'Integration tests against a real HTTP server — `startHttpTestServer()` boots one shared server in globalSetup, then `testGet` / `testPost` / `expectJson` make typed requests against it. Triggers: `startHttpTestServer`, `startHttpTestServer({ port })`, `stopHttpTestServer`, `testGet`, `testPost`, `testPut`, `testPatch`, `testDelete`, `expectJson`, `getTestServerUrl`, `testRequest`, `PortInUseError`, `assertPortIsAvailable`, `isPortAvailable`; "integration-test a controller", "end-to-end HTTP test", "globalSetup HTTP server", "assert status and body shape", "test server port already in use", "EADDRINUSE while running tests", "run tests while the dev server is up"; typical import `import { testGet, testPost, expectJson } from "@warlock.js/core"`. Skip: pure unit tests — `@warlock.js/core/test-service/SKILL.md`; controller shape — `@warlock.js/core/create-controller/SKILL.md`; competing libs `supertest`, `light-my-request`, `nock`.'
4
4
  ---
5
5
 
6
6
  # Warlock — HTTP integration tests
@@ -56,6 +56,30 @@ Unlike the dev server, it doesn't watch files, doesn't do HMR, doesn't run healt
56
56
 
57
57
  Both are idempotent. A second `start` returns early; `stop` on a non-running server logs and returns.
58
58
 
59
+ ### Choosing the port — `startHttpTestServer({ port })`
60
+
61
+ ```ts
62
+ await startHttpTestServer({ port: 3999 });
63
+ ```
64
+
65
+ The explicit port wins over `http.port` — including over `HTTP_PORT` in `.env`. Use it to run a suite while the dev server holds the configured port, or to run two suites side by side.
66
+
67
+ **Nothing else can move the server.** `startHttpTestServer()` bootstraps the app itself, and that bootstrap re-reads `.env` (dotenv overrides by default), so any value you set before calling it is gone before the port is read. Assigning `process.env.HTTP_PORT` does not work either: `env()` reads dotenv's own store and never falls back to `process.env`. The option is applied after the bootstrap, immediately before the connector binds — that is why it is the only channel that survives.
68
+
69
+ The request helpers follow with no extra wiring: the bound port is published as `WARLOCK_TEST_SERVER_PORT`, which the workers inherit, and `getTestServerUrl()` prefers it over their own `.env`-resolved `http.port`. `stopHttpTestServer()` withdraws it.
70
+
71
+ ### The port is preflighted
72
+
73
+ Before binding, the test server checks the resolved port is free and fails with an instruction if it isn't:
74
+
75
+ ```
76
+ Port 2031 is already in use on localhost. Stop the dev server (or whatever else is
77
+ listening on port 2031) and run again, or start on a free port — e.g.
78
+ startHttpTestServer({ port: 2032 }).
79
+ ```
80
+
81
+ This runs whether or not you passed a port, so a collision never reaches you as a bare `EADDRINUSE` from inside Fastify. The failure is a `PortInUseError` carrying `port` and `host`. The same check is available on its own — `assertPortIsAvailable(port, host)` throws it, `isPortAvailable(port, host)` returns a boolean.
82
+
59
83
  ## Project wiring — `src/test-global-setup.ts` + `vite.config.ts`
60
84
 
61
85
  ```ts title="src/test-global-setup.ts"
@@ -105,7 +129,7 @@ import { getTestServerUrl } from "@warlock.js/core";
105
129
  const url = getTestServerUrl(); // → "http://localhost:2031" (defaults)
106
130
  ```
107
131
 
108
- Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config. If you change the HTTP config, helpers follow automatically.
132
+ Reads `http.host` (default `"localhost"`) and `http.port` (default `2031`) from config — unless `startHttpTestServer` published a port, which wins, since a worker's own config never sees the `{ port }` option passed in `globalSetup`. If you change the HTTP config, helpers follow automatically.
109
133
 
110
134
  ### Verb helpers
111
135
 
@@ -274,7 +298,7 @@ This is fine for normal test flow. It bites when you're inside a transaction the
274
298
  ## Gotchas
275
299
 
276
300
  - **`globalSetup` must export `setup` and `teardown`.** Vitest reads them by name. A typo in the export gets you a confusing "server not running" error on the first `testGet` call.
277
- - **Port conflicts.** If `http.port` matches your running dev server, `startHttpTestServer()` fails to bind. Either stop the dev server or set a test-only port: `http: { port: 3999 }` in a test-config branch.
301
+ - **Port conflicts.** If `http.port` matches your running dev server, the preflight fails with a `PortInUseError` naming the port. Either stop the dev server or pass a free one: `startHttpTestServer({ port: 3999 })`.
278
302
  - **Auth tokens need a real user.** Generating a JWT with a non-existent `user_id` works — but the auth middleware's user-loading step will reject the request with 401 because it can't find the user in the DB.
279
303
  - **`expectJson` parses the body once.** If you call it twice on the same response, the second call gets an already-consumed stream error. Capture the result.
280
304
  - **The HTTP server's connection is NOT torn down between test files.** Data persists across files within a single `vitest` run. Either truncate in `afterEach` / `afterAll`, or design your tests to be order-independent.
@@ -156,12 +156,30 @@ The framework ships a fixed set of commands you call but don't author. Knowing t
156
156
 
157
157
  | Command | Flags / args | Preloads |
158
158
  | -------- | ----------------------------------------------- | ------------------------------ |
159
- | `warlock migrate` | `--list` (just list pending), `--fresh` / `-f` (drop tables first) | database, logger |
159
+ | `warlock migrate` | `--list` / `-l` (executed **and** pending), `--pending` (pending only, sets an exit code), `--fresh` / `-f` (drop tables first) | database, logger |
160
160
  | `warlock seed` | `--name <pattern>` (run seeds matching the pattern) | full bootstrap (env, configs, app modules) |
161
161
  | `warlock create-database <name>` | bare positional `<name>` | database |
162
162
  | `warlock drop.tables` | `--force, -f` (skip confirmation prompt) | database, logger |
163
163
  | `warlock db.indexes` | builds DB indexes for every registered model | database |
164
164
 
165
+ **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".
166
+
167
+ `--list` is a report and always exits `0`. `--pending` is a gate, and its exit code is its whole API:
168
+
169
+ | Exit | Meaning |
170
+ | ---- | ------- |
171
+ | `0` | computed, nothing pending |
172
+ | `1` | computed, N pending |
173
+ | `2` | **could not be computed** |
174
+
175
+ ```bash
176
+ warlock migrate --pending && ./deploy.sh
177
+ ```
178
+
179
+ `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`.
180
+
181
+ **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.
182
+
165
183
  ### Scaffolding
166
184
 
167
185
  The `generate.*` family covers every module piece: