@warlock.js/core 5.1.0 → 5.2.1

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 (356) hide show
  1. package/CHANGELOG.md +152 -0
  2. package/esm/application/application.d.mts.map +1 -1
  3. package/esm/application/application.mjs.map +1 -1
  4. package/esm/benchmark/benchmark-snapshots.d.mts.map +1 -1
  5. package/esm/benchmark/benchmark.d.mts.map +1 -1
  6. package/esm/benchmark/channels/console.channel.mjs.map +1 -1
  7. package/esm/benchmark/channels/noop.channel.d.mts.map +1 -1
  8. package/esm/benchmark/profiler.d.mts.map +1 -1
  9. package/esm/benchmark/profiler.mjs.map +1 -1
  10. package/esm/bootstrap/setup.d.mts.map +1 -1
  11. package/esm/bootstrap/setup.mjs.map +1 -1
  12. package/esm/bootstrap.d.mts.map +1 -1
  13. package/esm/cache/database-cache-driver.d.mts.map +1 -1
  14. package/esm/cache/database-cache-driver.mjs.map +1 -1
  15. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  16. package/esm/cli/cli-commands.utils.mjs +16 -10
  17. package/esm/cli/cli-commands.utils.mjs.map +1 -1
  18. package/esm/cli/commands/doctor/boot-for-diagnostics.mjs.map +1 -1
  19. package/esm/cli/commands/doctor/checks/connectors.check.mjs.map +1 -1
  20. package/esm/cli/commands/doctor/checks/handler-signature.check.mjs.map +1 -1
  21. package/esm/cli/commands/doctor/checks/health.check.mjs.map +1 -1
  22. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  23. package/esm/cli/commands/doctor/checks/routes.check.mjs.map +1 -1
  24. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  25. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
  26. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  27. package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
  28. package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
  29. package/esm/cli/commands/generate/utils/name-parser.mjs.map +1 -1
  30. package/esm/cli/commands/generate/utils/prompt.mjs.map +1 -1
  31. package/esm/cli/commands/routes/diff-page-routes.mjs +84 -0
  32. package/esm/cli/commands/routes/diff-page-routes.mjs.map +1 -0
  33. package/esm/cli/commands/routes/format-routes-table.mjs.map +1 -1
  34. package/esm/cli/commands/routes/route-row.mjs.map +1 -1
  35. package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
  36. package/esm/cli/commands/routes/routes-diff.action.mjs +84 -0
  37. package/esm/cli/commands/routes/routes-diff.action.mjs.map +1 -0
  38. package/esm/cli/commands/routes.command.mjs +15 -1
  39. package/esm/cli/commands/routes.command.mjs.map +1 -1
  40. package/esm/cli/commands/start-production.command.mjs +8 -1
  41. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  42. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  43. package/esm/cli/commands/typings-generator.command.mjs.map +1 -1
  44. package/esm/cli/commands-loader.mjs.map +1 -1
  45. package/esm/cli/framework-cli-commands.mjs +2 -1
  46. package/esm/cli/framework-cli-commands.mjs.map +1 -1
  47. package/esm/cli/start.mjs.map +1 -1
  48. package/esm/cli/string-similarity.mjs.map +1 -1
  49. package/esm/commands/cli-command.mjs.map +1 -1
  50. package/esm/config/config-loader.d.mts +1 -1
  51. package/esm/config/config-loader.d.mts.map +1 -1
  52. package/esm/config/config-loader.mjs.map +1 -1
  53. package/esm/config/config-setter.d.mts.map +1 -1
  54. package/esm/config/config-special-handlers.d.mts +1 -1
  55. package/esm/config/config-special-handlers.mjs.map +1 -1
  56. package/esm/config/load-config-files.mjs.map +1 -1
  57. package/esm/connectors/access-connector.d.mts.map +1 -1
  58. package/esm/connectors/ai-connector.d.mts.map +1 -1
  59. package/esm/connectors/assert-no-reserved-connector-names.mjs.map +1 -1
  60. package/esm/connectors/assert-unique-connector-names.mjs.map +1 -1
  61. package/esm/connectors/base-connector.d.mts.map +1 -1
  62. package/esm/connectors/base-connector.mjs.map +1 -1
  63. package/esm/connectors/cache-connector.d.mts.map +1 -1
  64. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  65. package/esm/connectors/connectors-manager.mjs.map +1 -1
  66. package/esm/connectors/database-connector.d.mts.map +1 -1
  67. package/esm/connectors/describe-server-address.mjs +20 -3
  68. package/esm/connectors/describe-server-address.mjs.map +1 -1
  69. package/esm/connectors/herald-connector.d.mts.map +1 -1
  70. package/esm/connectors/http-connector.d.mts.map +1 -1
  71. package/esm/connectors/http-connector.mjs +5 -1
  72. package/esm/connectors/http-connector.mjs.map +1 -1
  73. package/esm/connectors/logger-connector.d.mts.map +1 -1
  74. package/esm/connectors/mail-connector.d.mts.map +1 -1
  75. package/esm/connectors/notifications-connector.d.mts.map +1 -1
  76. package/esm/connectors/register-configured-connectors.mjs.map +1 -1
  77. package/esm/connectors/socket-connector.d.mts.map +1 -1
  78. package/esm/connectors/socket-connector.mjs.map +1 -1
  79. package/esm/connectors/storage.connector.d.mts.map +1 -1
  80. package/esm/container/index.d.mts.map +1 -1
  81. package/esm/database/create-database-action.mjs.map +1 -1
  82. package/esm/database/drop-tables-action.mjs.map +1 -1
  83. package/esm/database/migrate-action.mjs.map +1 -1
  84. package/esm/database/models/database-log/database-log.mjs.map +1 -1
  85. package/esm/database/seed-command-action.mjs.map +1 -1
  86. package/esm/database/seeds/seed-records-table-migration.mjs +1 -1
  87. package/esm/database/seeds/seed-records-table-migration.mjs.map +1 -1
  88. package/esm/database/seeds/seeder.mjs.map +1 -1
  89. package/esm/database/seeds/seeders.manager.mjs.map +1 -1
  90. package/esm/database/seeds/seeds-table-migration.mjs +1 -1
  91. package/esm/database/seeds/seeds-table-migration.mjs.map +1 -1
  92. package/esm/database/utils.mjs.map +1 -1
  93. package/esm/dev-server/check-for-updates.mjs.map +1 -1
  94. package/esm/dev-server/dependency-graph.d.mts.map +1 -1
  95. package/esm/dev-server/dependency-graph.mjs.map +1 -1
  96. package/esm/dev-server/dev-logger.mjs.map +1 -1
  97. package/esm/dev-server/development-server.mjs.map +1 -1
  98. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  99. package/esm/dev-server/file-manager.d.mts.map +1 -1
  100. package/esm/dev-server/file-manager.mjs.map +1 -1
  101. package/esm/dev-server/file-operations.d.mts.map +1 -1
  102. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  103. package/esm/dev-server/files-watcher.mjs.map +1 -1
  104. package/esm/dev-server/health-checker/checkers/eslint-health-checker.mjs.map +1 -1
  105. package/esm/dev-server/health-checker/checkers/typescript-health-checker.mjs.map +1 -1
  106. package/esm/dev-server/health-checker/file-health-result.d.mts.map +1 -1
  107. package/esm/dev-server/health-checker/file-health-result.mjs.map +1 -1
  108. package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
  109. package/esm/dev-server/layer-executor.mjs +10 -3
  110. package/esm/dev-server/layer-executor.mjs.map +1 -1
  111. package/esm/dev-server/loader/hook-thread.d.mts.map +1 -1
  112. package/esm/dev-server/loader/own-resolver.mjs.map +1 -1
  113. package/esm/dev-server/loader/register-loader.mjs.map +1 -1
  114. package/esm/dev-server/loader/resolve-hook.mjs.map +1 -1
  115. package/esm/dev-server/loader/source-slug.mjs.map +1 -1
  116. package/esm/dev-server/loader/transpile-cache.mjs +12 -3
  117. package/esm/dev-server/loader/transpile-cache.mjs.map +1 -1
  118. package/esm/dev-server/loader/version-registry.mjs.map +1 -1
  119. package/esm/dev-server/manifest-manager.d.mts.map +1 -1
  120. package/esm/dev-server/manifest-manager.mjs.map +1 -1
  121. package/esm/dev-server/module-loader.mjs.map +1 -1
  122. package/esm/dev-server/package-json-manager.mjs.map +1 -1
  123. package/esm/dev-server/parse-imports.mjs.map +1 -1
  124. package/esm/dev-server/ready-block.mjs.map +1 -1
  125. package/esm/dev-server/special-files-collector.d.mts.map +1 -1
  126. package/esm/dev-server/special-files-collector.mjs.map +1 -1
  127. package/esm/dev-server/supervisor.mjs +9 -6
  128. package/esm/dev-server/supervisor.mjs.map +1 -1
  129. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  130. package/esm/dev-server/type-generator.mjs.map +1 -1
  131. package/esm/dev-server/utils.mjs.map +1 -1
  132. package/esm/encryption/encrypt.mjs.map +1 -1
  133. package/esm/encryption/hash.mjs.map +1 -1
  134. package/esm/encryption/password.mjs.map +1 -1
  135. package/esm/generations/add-command.action.mjs +11 -4
  136. package/esm/generations/add-command.action.mjs.map +1 -1
  137. package/esm/generations/features/access.feature.mjs +2 -1
  138. package/esm/generations/features/access.feature.mjs.map +1 -1
  139. package/esm/generations/features/ai-anthropic.feature.mjs +3 -1
  140. package/esm/generations/features/ai-anthropic.feature.mjs.map +1 -1
  141. package/esm/generations/features/ai-bedrock.feature.mjs +3 -1
  142. package/esm/generations/features/ai-bedrock.feature.mjs.map +1 -1
  143. package/esm/generations/features/ai-google.feature.mjs +3 -1
  144. package/esm/generations/features/ai-google.feature.mjs.map +1 -1
  145. package/esm/generations/features/ai-ollama.feature.mjs +3 -1
  146. package/esm/generations/features/ai-ollama.feature.mjs.map +1 -1
  147. package/esm/generations/features/ai-openai.feature.mjs +3 -1
  148. package/esm/generations/features/ai-openai.feature.mjs.map +1 -1
  149. package/esm/generations/features/ai-panoptic.feature.mjs +2 -1
  150. package/esm/generations/features/ai-panoptic.feature.mjs.map +1 -1
  151. package/esm/generations/features/ai-tools.feature.mjs +2 -1
  152. package/esm/generations/features/ai-tools.feature.mjs.map +1 -1
  153. package/esm/generations/features/ai-workspace.feature.mjs +2 -1
  154. package/esm/generations/features/ai-workspace.feature.mjs.map +1 -1
  155. package/esm/generations/features/ai.feature.mjs +2 -1
  156. package/esm/generations/features/ai.feature.mjs.map +1 -1
  157. package/esm/generations/features/herald.feature.mjs +2 -1
  158. package/esm/generations/features/herald.feature.mjs.map +1 -1
  159. package/esm/generations/features/notifications.feature.mjs +3 -2
  160. package/esm/generations/features/notifications.feature.mjs.map +1 -1
  161. package/esm/generations/features/scheduler.feature.mjs +3 -1
  162. package/esm/generations/features/scheduler.feature.mjs.map +1 -1
  163. package/esm/generations/features/shadcn.feature.mjs.map +1 -1
  164. package/esm/generations/features/shared/migration-timestamp.mjs.map +1 -1
  165. package/esm/generations/features/tailwind.feature.mjs.map +1 -1
  166. package/esm/generations/features/types.mjs +14 -0
  167. package/esm/generations/features/types.mjs.map +1 -0
  168. package/esm/generations/features/web.feature.mjs +10 -2
  169. package/esm/generations/features/web.feature.mjs.map +1 -1
  170. package/esm/generations/stubs.mjs +169 -28
  171. package/esm/generations/stubs.mjs.map +1 -1
  172. package/esm/http/boot-port-preflight.d.mts +46 -0
  173. package/esm/http/boot-port-preflight.d.mts.map +1 -0
  174. package/esm/http/boot-port-preflight.mjs +87 -0
  175. package/esm/http/boot-port-preflight.mjs.map +1 -0
  176. package/esm/http/context/request-context.d.mts.map +1 -1
  177. package/esm/http/context/request-context.mjs.map +1 -1
  178. package/esm/http/context/request-memo.d.mts.map +1 -1
  179. package/esm/http/context/request-memo.mjs.map +1 -1
  180. package/esm/http/createHttpApplication.d.mts.map +1 -1
  181. package/esm/http/createHttpApplication.mjs.map +1 -1
  182. package/esm/http/health.d.mts.map +1 -1
  183. package/esm/http/index.d.mts +1 -0
  184. package/esm/http/index.mjs +1 -0
  185. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  186. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  187. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  188. package/esm/http/middleware/ip-filter.middleware.d.mts +1 -1
  189. package/esm/http/middleware/ip-filter.middleware.mjs +1 -1
  190. package/esm/http/middleware/ip-filter.middleware.mjs.map +1 -1
  191. package/esm/http/middleware/maintenance.middleware.mjs.map +1 -1
  192. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  193. package/esm/http/port-preflight.d.mts +8 -0
  194. package/esm/http/port-preflight.d.mts.map +1 -1
  195. package/esm/http/port-preflight.mjs +8 -1
  196. package/esm/http/port-preflight.mjs.map +1 -1
  197. package/esm/http/ready-report.d.mts.map +1 -1
  198. package/esm/http/request-controller.d.mts.map +1 -1
  199. package/esm/http/request.d.mts +14 -2
  200. package/esm/http/request.d.mts.map +1 -1
  201. package/esm/http/request.mjs +16 -2
  202. package/esm/http/request.mjs.map +1 -1
  203. package/esm/http/response.d.mts +3 -3
  204. package/esm/http/response.d.mts.map +1 -1
  205. package/esm/http/response.mjs.map +1 -1
  206. package/esm/http/server.d.mts.map +1 -1
  207. package/esm/http/server.mjs +26 -2
  208. package/esm/http/server.mjs.map +1 -1
  209. package/esm/http/types.d.mts +1 -3
  210. package/esm/http/types.d.mts.map +1 -1
  211. package/esm/http/uploaded-file.d.mts.map +1 -1
  212. package/esm/http/uploaded-file.mjs.map +1 -1
  213. package/esm/http/uploads-config.d.mts.map +1 -1
  214. package/esm/image/image.d.mts.map +1 -1
  215. package/esm/image/image.mjs.map +1 -1
  216. package/esm/index.d.mts +6 -1
  217. package/esm/index.mjs +6 -1
  218. package/esm/logger/fatal-visibility.d.mts +25 -0
  219. package/esm/logger/fatal-visibility.d.mts.map +1 -0
  220. package/esm/logger/fatal-visibility.mjs +31 -0
  221. package/esm/logger/fatal-visibility.mjs.map +1 -0
  222. package/esm/logger/index.d.mts +1 -0
  223. package/esm/logger/index.mjs +1 -0
  224. package/esm/logger/logger.mjs.map +1 -1
  225. package/esm/mail/config.d.mts.map +1 -1
  226. package/esm/mail/events.d.mts.map +1 -1
  227. package/esm/mail/mail.d.mts.map +1 -1
  228. package/esm/mail/mail.mjs.map +1 -1
  229. package/esm/mail/mailer-pool.d.mts.map +1 -1
  230. package/esm/mail/mailer-pool.mjs +2 -0
  231. package/esm/mail/mailer-pool.mjs.map +1 -1
  232. package/esm/mail/send-mail.mjs.map +1 -1
  233. package/esm/mail/test-mailbox.d.mts.map +1 -1
  234. package/esm/mail/test-mailbox.mjs.map +1 -1
  235. package/esm/production/assert-dist-ready-to-start.mjs +24 -0
  236. package/esm/production/assert-dist-ready-to-start.mjs.map +1 -0
  237. package/esm/production/assert-generated-imports.mjs.map +1 -1
  238. package/esm/production/build-app-production.mjs.map +1 -1
  239. package/esm/production/dist-build-manifest.mjs +47 -0
  240. package/esm/production/dist-build-manifest.mjs.map +1 -0
  241. package/esm/production/production-builder.mjs +68 -13
  242. package/esm/production/production-builder.mjs.map +1 -1
  243. package/esm/production/production-supervisor.mjs +21 -9
  244. package/esm/production/production-supervisor.mjs.map +1 -1
  245. package/esm/production/promote-dist.mjs +115 -0
  246. package/esm/production/promote-dist.mjs.map +1 -0
  247. package/esm/production/resolve-build-config.mjs.map +1 -1
  248. package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
  249. package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
  250. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts.map +1 -1
  251. package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
  252. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  253. package/esm/repositories/repository.manager.d.mts.map +1 -1
  254. package/esm/repositories/repository.manager.mjs.map +1 -1
  255. package/esm/resource/register-resource.d.mts.map +1 -1
  256. package/esm/resource/resource-field-builder.d.mts.map +1 -1
  257. package/esm/resource/resource-field-builder.mjs.map +1 -1
  258. package/esm/resource/resource.d.mts.map +1 -1
  259. package/esm/resource/resource.mjs.map +1 -1
  260. package/esm/restful/restful.d.mts.map +1 -1
  261. package/esm/restful/restful.mjs.map +1 -1
  262. package/esm/router/index.d.mts +3 -0
  263. package/esm/router/index.mjs +3 -0
  264. package/esm/router/log-request-lifecycle.mjs.map +1 -1
  265. package/esm/router/normalize-request-path.d.mts +14 -0
  266. package/esm/router/normalize-request-path.d.mts.map +1 -0
  267. package/esm/router/normalize-request-path.mjs +18 -0
  268. package/esm/router/normalize-request-path.mjs.map +1 -0
  269. package/esm/router/normalize-route-path.d.mts +28 -0
  270. package/esm/router/normalize-route-path.d.mts.map +1 -0
  271. package/esm/router/normalize-route-path.mjs +33 -0
  272. package/esm/router/normalize-route-path.mjs.map +1 -0
  273. package/esm/router/positional-handler-diagnostics.d.mts.map +1 -1
  274. package/esm/router/positional-handler-diagnostics.mjs.map +1 -1
  275. package/esm/router/route-name-method-suffix.d.mts +33 -0
  276. package/esm/router/route-name-method-suffix.d.mts.map +1 -0
  277. package/esm/router/route-name-method-suffix.mjs +39 -0
  278. package/esm/router/route-name-method-suffix.mjs.map +1 -0
  279. package/esm/router/route-registry.d.mts.map +1 -1
  280. package/esm/router/route-registry.mjs +3 -2
  281. package/esm/router/route-registry.mjs.map +1 -1
  282. package/esm/router/router.d.mts +14 -0
  283. package/esm/router/router.d.mts.map +1 -1
  284. package/esm/router/router.mjs +50 -11
  285. package/esm/router/router.mjs.map +1 -1
  286. package/esm/socket/utils.d.mts.map +1 -1
  287. package/esm/storage/config.d.mts.map +1 -1
  288. package/esm/storage/context/storage-driver-context.d.mts.map +1 -1
  289. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  290. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  291. package/esm/storage/drivers/do-spaces-driver.d.mts.map +1 -1
  292. package/esm/storage/drivers/local-driver.d.mts.map +1 -1
  293. package/esm/storage/drivers/local-driver.mjs.map +1 -1
  294. package/esm/storage/drivers/r2-driver.d.mts.map +1 -1
  295. package/esm/storage/scoped-storage.d.mts.map +1 -1
  296. package/esm/storage/scoped-storage.mjs.map +1 -1
  297. package/esm/storage/storage-file.d.mts.map +1 -1
  298. package/esm/storage/storage-file.mjs.map +1 -1
  299. package/esm/storage/storage.d.mts.map +1 -1
  300. package/esm/storage/types.d.mts +14 -0
  301. package/esm/storage/types.d.mts.map +1 -1
  302. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  303. package/esm/tests/start-http-development-server.d.mts.map +1 -1
  304. package/esm/tests/start-http-development-server.mjs +2 -1
  305. package/esm/tests/start-http-development-server.mjs.map +1 -1
  306. package/esm/tests/test-connectors-selection.mjs.map +1 -1
  307. package/esm/tests/test-helpers.d.mts.map +1 -1
  308. package/esm/tests/test-setup-timeout.mjs.map +1 -1
  309. package/esm/tests/vitest-setup.d.mts.map +1 -1
  310. package/esm/tests/vitest-setup.mjs.map +1 -1
  311. package/esm/updater/update-warlock-packages.mjs.map +1 -1
  312. package/esm/use-cases/use-case-broadcast.d.mts.map +1 -1
  313. package/esm/use-cases/use-case-events.d.mts.map +1 -1
  314. package/esm/use-cases/use-case-pipeline.d.mts.map +1 -1
  315. package/esm/use-cases/use-case.d.mts.map +1 -1
  316. package/esm/use-cases/use-cases-registry.d.mts.map +1 -1
  317. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  318. package/esm/utils/database-log.d.mts.map +1 -1
  319. package/esm/utils/database-log.mjs.map +1 -1
  320. package/esm/utils/environment.d.mts.map +1 -1
  321. package/esm/utils/framework-vesion.mjs.map +1 -1
  322. package/esm/utils/get-localized.mjs.map +1 -1
  323. package/esm/utils/load-environment.mjs.map +1 -1
  324. package/esm/utils/normalized-path.d.mts.map +1 -1
  325. package/esm/utils/paths.d.mts.map +1 -1
  326. package/esm/utils/promise-all-object.d.mts.map +1 -1
  327. package/esm/utils/promise-all-object.mjs.map +1 -1
  328. package/esm/utils/sluggable.mjs.map +1 -1
  329. package/esm/utils/version-compare.mjs.map +1 -1
  330. package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
  331. package/esm/validation/database/exists-except-current-user.mjs.map +1 -1
  332. package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
  333. package/esm/validation/plugins/localized-plugin.mjs.map +1 -1
  334. package/esm/validation/validators/file-validator.mjs.map +1 -1
  335. package/esm/vite/lower-stage3-decorators.d.mts.map +1 -1
  336. package/esm/warlock-config/default-configurations.mjs.map +1 -1
  337. package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
  338. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  339. package/llms-full.txt +205 -90
  340. package/llms.txt +5 -5
  341. package/package.json +12 -12
  342. package/skills/add-connector/SKILL.md +35 -28
  343. package/skills/create-controller/SKILL.md +1 -1
  344. package/skills/create-module/SKILL.md +12 -12
  345. package/skills/hash-password/SKILL.md +4 -4
  346. package/skills/run-app/SKILL.md +77 -16
  347. package/skills/send-mail/SKILL.md +2 -2
  348. package/skills/use-middleware/SKILL.md +13 -4
  349. package/skills/use-model-transformers/SKILL.md +2 -2
  350. package/skills/use-repository/SKILL.md +1 -1
  351. package/skills/validate-input/SKILL.md +1 -1
  352. package/skills/warlock-conventions/SKILL.md +1 -1
  353. package/skills/warlock-doctor/SKILL.md +3 -3
  354. package/skills/warlock-routes/SKILL.md +47 -9
  355. package/skills/write-cli-command/SKILL.md +1 -1
  356. package/skills/write-seeder/SKILL.md +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"restful.mjs","names":[],"sources":["../../../../../../../core/src/restful/restful.ts"],"sourcesContent":["import type { GenericObject } from \"@mongez/reinforcements\";\nimport type { Model } from \"@warlock.js/cascade\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Request, Response } from \"../http\";\nimport type { QueryBuilderContract, RepositoryManager } from \"../repositories\";\nimport type { HttpContext, RestfulMiddleware, RouteResource } from \"../router\";\n\nexport abstract class Restful<T extends Model> implements RouteResource {\n /**\n * Middleware for each method\n */\n protected middleware: RestfulMiddleware = {};\n\n /**\n * Record name\n */\n protected recordName = \"record\";\n\n /**\n * Records list name\n */\n protected recordsListName = \"records\";\n\n /**\n * Repository\n */\n protected abstract repository: RepositoryManager<T>;\n\n /**\n * Define what to be returned when a record is created|updated|deleted|patched\n */\n protected returnOn: Record<string, \"record\" | \"records\"> = {\n create: \"record\",\n update: \"record\",\n delete: \"record\",\n patch: \"record\",\n };\n\n /**\n * Enable fetching cache\n *\n * @default true\n */\n public cache = true;\n\n /**\n * Find record instance by id\n */\n public async find(id: number) {\n const findMethod = this.cache ? \"getCached\" : \"find\";\n return this.repository[findMethod](id);\n }\n\n /**\n * List records\n */\n public async list({ request, response }: HttpContext) {\n try {\n if (await this.callMiddleware(\"list\", request, response)) return;\n\n const responseDocument: GenericObject = {};\n\n const data = request.heavy();\n\n if (data.paginate === \"false\") {\n data.paginate = false;\n }\n\n const listMethod = this.cache ? \"listCached\" : \"list\";\n\n const { data: documents, pagination } = await this.repository[listMethod](data);\n\n responseDocument[this.recordsListName] = documents;\n\n if (pagination) {\n responseDocument.pagination = pagination;\n }\n\n return response.success(responseDocument);\n } catch (error: any) {\n log.error(\"restful\", \"list\", error);\n return response.serverError(error);\n }\n }\n\n /**\n * Get single record\n */\n public async get({ request, response }: HttpContext) {\n try {\n if (await this.callMiddleware(\"get\", request, response)) return;\n\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound();\n }\n\n return response.success({\n [this.recordName]: record,\n });\n } catch (error) {\n log.error(\"restful\", \"get\", error);\n }\n }\n\n /**\n * Create a new record\n */\n public async create({ request, response }: HttpContext) {\n try {\n const model = this.repository.newModel();\n const beforeCreate = await this.beforeCreate(request, response, model);\n\n if (beforeCreate) {\n return beforeCreate;\n }\n\n const beforeSave = await this.beforeSave(request, response, model);\n\n if (beforeSave) {\n return beforeSave;\n }\n\n const record = await this.repository.create(request.all());\n\n const createOutput = await this.onCreate(request, response, record);\n\n if (createOutput) {\n return createOutput;\n }\n\n const saveOutput = await this.onSave(request, response, record);\n\n if (saveOutput) {\n return saveOutput;\n }\n\n if (this.returnOn.create === \"records\") {\n return this.list({ request, response });\n }\n\n return response.successCreate({\n [this.recordName]: record,\n });\n } catch (error: Error | any) {\n log.error(\"restful\", \"create\", error);\n\n return response.badRequest({\n error: error.message,\n });\n }\n }\n\n /**\n * Update record\n */\n public async update({ request, response }: HttpContext) {\n try {\n // Find record\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound({\n error: \"Record not found\",\n });\n }\n\n const beforeOutput = await this.beforeUpdate(request, response, record);\n if (beforeOutput) {\n return beforeOutput;\n }\n\n const beforeSafe = await this.beforeSave(request, response, record);\n\n if (beforeSafe) {\n return beforeSafe;\n }\n\n const oldRecord = record.clone();\n\n await record.save(request.allExceptParams());\n\n this.onUpdate(request, response, record, oldRecord);\n this.onSave(request, response, record, oldRecord);\n\n if (this.returnOn.update === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success({\n [this.recordName]: record,\n });\n } catch (error) {\n log.error(\"restful\", \"update\", error);\n }\n }\n\n /**\n * Patch record\n */\n public async patch({ request, response }: HttpContext) {\n try {\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound({\n error: \"Record not found\",\n });\n }\n\n const oldRecord = record.clone();\n\n await this.beforePatch(request, response, record, oldRecord);\n await this.beforeSave(request, response, record, oldRecord);\n\n await record.save(request.heavyExceptParams());\n\n this.onPatch(request, response, record, oldRecord);\n this.onSave(request, response, record, oldRecord);\n\n if (this.returnOn.patch === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success({\n [this.recordName]: record,\n });\n } catch (error) {\n log.error(\"restful\", \"patch\", error);\n }\n }\n\n /**\n * Delete record\n */\n public async delete({ request, response }: HttpContext) {\n try {\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound();\n }\n\n if (await this.callMiddleware(\"delete\", request, response, record)) return;\n\n await this.beforeDelete(request, response, record);\n\n await record.destroy();\n\n this.onDelete(request, response, record);\n\n if (this.returnOn.delete === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success();\n } catch (error: Error | any) {\n log.error(\"restful\", \"delete\", error);\n\n return response.badRequest({\n error: error.message,\n });\n }\n }\n\n /**\n * Bulk delete records\n */\n public async bulkDelete({ request, response }: HttpContext) {\n try {\n const ids = request.input(\"id\");\n\n if (!Array.isArray(ids)) {\n return response.badRequest({\n error: \"id must be an array\",\n });\n }\n\n const records = await this.repository.all({\n perform: (query: QueryBuilderContract<T>) =>\n query.whereIn(\n \"id\",\n ids.map((id) => parseInt(id)),\n ),\n });\n\n await Promise.all(\n records.map(async (record) => {\n if (await this.callMiddleware(\"delete\", request, response, record)) {\n return;\n }\n\n await this.beforeDelete(request, response, record);\n await record.destroy();\n this.onDelete(request, response, record);\n }),\n );\n\n if (this.returnOn.delete === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success({\n deleted: records.length,\n });\n } catch (error: Error | any) {\n log.error(\"restful\", \"bulkDelete\", error);\n return response.badRequest({\n error: error.message,\n });\n }\n }\n\n /**\n * Before create\n */\n protected async beforeCreate(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * On create\n */\n protected async onCreate(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * Before update\n */\n protected async beforeUpdate(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * On update\n */\n protected async onUpdate(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord: T,\n ): Promise<any> {\n //\n }\n\n /**\n * Before delete\n */\n protected async beforeDelete(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * On delete\n */\n protected async onDelete(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * Before patch\n */\n protected async beforePatch(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * On patch\n */\n protected async onPatch(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord: T,\n ): Promise<any> {\n //\n }\n\n /**\n * Before save\n */\n protected async beforeSave(\n _request: Request,\n _response: Response,\n _record?: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * On save\n */\n protected async onSave(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * Call middleware for the given method\n *\n */\n protected async callMiddleware(\n method: string,\n request: Request,\n response: Response,\n _record?: any,\n ) {\n if (!this.middleware[method]) return;\n\n for (const middleware of this.middleware[method]) {\n const output = await middleware({ request, response });\n\n if (output) {\n return output;\n }\n }\n\n return;\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"restful.mjs","names":[],"sources":["../../../../../../../core/src/restful/restful.ts"],"sourcesContent":["import type { GenericObject } from \"@mongez/reinforcements\";\nimport type { Model } from \"@warlock.js/cascade\";\nimport { log } from \"@warlock.js/logger\";\nimport type { Request, Response } from \"../http\";\nimport type { QueryBuilderContract, RepositoryManager } from \"../repositories\";\nimport type { HttpContext, RestfulMiddleware, RouteResource } from \"../router\";\n\nexport abstract class Restful<T extends Model> implements RouteResource {\n /**\n * Middleware for each method\n */\n protected middleware: RestfulMiddleware = {};\n\n /**\n * Record name\n */\n protected recordName = \"record\";\n\n /**\n * Records list name\n */\n protected recordsListName = \"records\";\n\n /**\n * Repository\n */\n protected abstract repository: RepositoryManager<T>;\n\n /**\n * Define what to be returned when a record is created|updated|deleted|patched\n */\n protected returnOn: Record<string, \"record\" | \"records\"> = {\n create: \"record\",\n update: \"record\",\n delete: \"record\",\n patch: \"record\",\n };\n\n /**\n * Enable fetching cache\n *\n * @default true\n */\n public cache = true;\n\n /**\n * Find record instance by id\n */\n public async find(id: number) {\n const findMethod = this.cache ? \"getCached\" : \"find\";\n return this.repository[findMethod](id);\n }\n\n /**\n * List records\n */\n public async list({ request, response }: HttpContext) {\n try {\n if (await this.callMiddleware(\"list\", request, response)) return;\n\n const responseDocument: GenericObject = {};\n\n const data = request.heavy();\n\n if (data.paginate === \"false\") {\n data.paginate = false;\n }\n\n const listMethod = this.cache ? \"listCached\" : \"list\";\n\n const { data: documents, pagination } = await this.repository[listMethod](data);\n\n responseDocument[this.recordsListName] = documents;\n\n if (pagination) {\n responseDocument.pagination = pagination;\n }\n\n return response.success(responseDocument);\n } catch (error: any) {\n log.error(\"restful\", \"list\", error);\n return response.serverError(error);\n }\n }\n\n /**\n * Get single record\n */\n public async get({ request, response }: HttpContext) {\n try {\n if (await this.callMiddleware(\"get\", request, response)) return;\n\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound();\n }\n\n return response.success({\n [this.recordName]: record,\n });\n } catch (error) {\n log.error(\"restful\", \"get\", error);\n }\n }\n\n /**\n * Create a new record\n */\n public async create({ request, response }: HttpContext) {\n try {\n const model = this.repository.newModel();\n const beforeCreate = await this.beforeCreate(request, response, model);\n\n if (beforeCreate) {\n return beforeCreate;\n }\n\n const beforeSave = await this.beforeSave(request, response, model);\n\n if (beforeSave) {\n return beforeSave;\n }\n\n const record = await this.repository.create(request.all());\n\n const createOutput = await this.onCreate(request, response, record);\n\n if (createOutput) {\n return createOutput;\n }\n\n const saveOutput = await this.onSave(request, response, record);\n\n if (saveOutput) {\n return saveOutput;\n }\n\n if (this.returnOn.create === \"records\") {\n return this.list({ request, response });\n }\n\n return response.successCreate({\n [this.recordName]: record,\n });\n } catch (error: Error | any) {\n log.error(\"restful\", \"create\", error);\n\n return response.badRequest({\n error: error.message,\n });\n }\n }\n\n /**\n * Update record\n */\n public async update({ request, response }: HttpContext) {\n try {\n // Find record\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound({\n error: \"Record not found\",\n });\n }\n\n const beforeOutput = await this.beforeUpdate(request, response, record);\n if (beforeOutput) {\n return beforeOutput;\n }\n\n const beforeSafe = await this.beforeSave(request, response, record);\n\n if (beforeSafe) {\n return beforeSafe;\n }\n\n const oldRecord = record.clone();\n\n await record.save(request.allExceptParams());\n\n this.onUpdate(request, response, record, oldRecord);\n this.onSave(request, response, record, oldRecord);\n\n if (this.returnOn.update === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success({\n [this.recordName]: record,\n });\n } catch (error) {\n log.error(\"restful\", \"update\", error);\n }\n }\n\n /**\n * Patch record\n */\n public async patch({ request, response }: HttpContext) {\n try {\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound({\n error: \"Record not found\",\n });\n }\n\n const oldRecord = record.clone();\n\n await this.beforePatch(request, response, record, oldRecord);\n await this.beforeSave(request, response, record, oldRecord);\n\n await record.save(request.heavyExceptParams());\n\n this.onPatch(request, response, record, oldRecord);\n this.onSave(request, response, record, oldRecord);\n\n if (this.returnOn.patch === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success({\n [this.recordName]: record,\n });\n } catch (error) {\n log.error(\"restful\", \"patch\", error);\n }\n }\n\n /**\n * Delete record\n */\n public async delete({ request, response }: HttpContext) {\n try {\n const record = await this.find(request.input(\"id\"));\n\n if (!record) {\n return response.notFound();\n }\n\n if (await this.callMiddleware(\"delete\", request, response, record)) return;\n\n await this.beforeDelete(request, response, record);\n\n await record.destroy();\n\n this.onDelete(request, response, record);\n\n if (this.returnOn.delete === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success();\n } catch (error: Error | any) {\n log.error(\"restful\", \"delete\", error);\n\n return response.badRequest({\n error: error.message,\n });\n }\n }\n\n /**\n * Bulk delete records\n */\n public async bulkDelete({ request, response }: HttpContext) {\n try {\n const ids = request.input(\"id\");\n\n if (!Array.isArray(ids)) {\n return response.badRequest({\n error: \"id must be an array\",\n });\n }\n\n const records = await this.repository.all({\n perform: (query: QueryBuilderContract<T>) =>\n query.whereIn(\n \"id\",\n ids.map((id) => parseInt(id)),\n ),\n });\n\n await Promise.all(\n records.map(async (record) => {\n if (await this.callMiddleware(\"delete\", request, response, record)) {\n return;\n }\n\n await this.beforeDelete(request, response, record);\n await record.destroy();\n this.onDelete(request, response, record);\n }),\n );\n\n if (this.returnOn.delete === \"records\") {\n return this.list({ request, response });\n }\n\n return response.success({\n deleted: records.length,\n });\n } catch (error: Error | any) {\n log.error(\"restful\", \"bulkDelete\", error);\n return response.badRequest({\n error: error.message,\n });\n }\n }\n\n /**\n * Before create\n */\n protected async beforeCreate(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * On create\n */\n protected async onCreate(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * Before update\n */\n protected async beforeUpdate(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * On update\n */\n protected async onUpdate(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord: T,\n ): Promise<any> {\n //\n }\n\n /**\n * Before delete\n */\n protected async beforeDelete(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * On delete\n */\n protected async onDelete(_request: Request, _response: Response, _record: T): Promise<any> {\n //\n }\n\n /**\n * Before patch\n */\n protected async beforePatch(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * On patch\n */\n protected async onPatch(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord: T,\n ): Promise<any> {\n //\n }\n\n /**\n * Before save\n */\n protected async beforeSave(\n _request: Request,\n _response: Response,\n _record?: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * On save\n */\n protected async onSave(\n _request: Request,\n _response: Response,\n _record: T,\n _oldRecord?: T,\n ): Promise<any> {\n //\n }\n\n /**\n * Call middleware for the given method\n *\n */\n protected async callMiddleware(\n method: string,\n request: Request,\n response: Response,\n _record?: any,\n ) {\n if (!this.middleware[method]) return;\n\n for (const middleware of this.middleware[method]) {\n const output = await middleware({ request, response });\n\n if (output) {\n return output;\n }\n }\n\n return;\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,7 @@
1
+ import { normalizeRoutePath } from "./normalize-route-path.mjs";
2
+ import { normalizeRequestPath } from "./normalize-request-path.mjs";
1
3
  import { GroupedRoutesOptions, HttpContext, Middleware, MiddlewareResponse, PartialPick, RequestControllerContract, RequestHandler, RequestHandlerType, RequestHandlerValidation, RequestMethod, ResourceMethod, RestfulMiddleware, Route, RouteOptions, RouteResource, RouterGroupCallback, RouterStacks } from "./types.mjs";
2
4
  import { PositionalHandlerSuspect, clearPositionalHandlerSuspects, describePositionalHandlerSuspect, forgetPositionalHandlerSuspects, inspectHandlerSignature, listPositionalHandlerSuspects, looksLikePositionalHandler, reportPositionalHandlerSuspects } from "./positional-handler-diagnostics.mjs";
5
+ import { matchesDerivedRouteName, routeNameMethodSuffix } from "./route-name-method-suffix.mjs";
3
6
  import { RouteRegistry } from "./route-registry.mjs";
4
7
  import { Router, router } from "./router.mjs";
@@ -1,4 +1,7 @@
1
+ import { normalizeRoutePath } from "./normalize-route-path.mjs";
2
+ import { normalizeRequestPath } from "./normalize-request-path.mjs";
1
3
  import { clearPositionalHandlerSuspects, describePositionalHandlerSuspect, forgetPositionalHandlerSuspects, inspectHandlerSignature, listPositionalHandlerSuspects, looksLikePositionalHandler, reportPositionalHandlerSuspects } from "./positional-handler-diagnostics.mjs";
4
+ import { matchesDerivedRouteName, routeNameMethodSuffix } from "./route-name-method-suffix.mjs";
2
5
  import { RouteRegistry } from "./route-registry.mjs";
3
6
  import { Router, router } from "./router.mjs";
4
7
 
@@ -1 +1 @@
1
- {"version":3,"file":"log-request-lifecycle.mjs","names":[],"sources":["../../../../../../../core/src/router/log-request-lifecycle.ts"],"sourcesContent":["/**\n * Both ends of a request's log entry.\n *\n * ## Why the completion line exists\n *\n * The router logged `Starting Request: <id>` and nothing else. A request that\n * hung for thirty seconds and one that returned in two milliseconds produced\n * **byte-identical output**, so the log could not answer the one question\n * anyone opens it to ask.\n *\n * Note what \"detecting a hang\" actually means. Nothing can log the completion\n * of a request that never completes — the mechanism is the **absence** of a\n * completion line beside a start line that has one. That only works if the\n * completion line is emitted on every settled path, **including a throw**,\n * which is why the error path here is not an afterthought.\n *\n * ## Why the level tracks the status\n *\n * A handled 500 does not throw; it returns a response. Logging it at `info`\n * would bury the exact entries someone scanning the log is hunting for, so the\n * level is derived from the outcome: informational below 400, a warning for\n * client errors, an error for server errors and for anything that threw.\n *\n * ## Ports rather than a direct `log` import\n *\n * The behaviour worth pinning down is *which lines come out, in which order,\n * at which level, when the run throws*. Injecting the sink and the clock makes\n * that assertable without a server, a socket or a fake timer.\n */\n\nexport type RequestLogEntry = {\n module: string;\n action: string;\n message: string;\n context?: unknown;\n};\n\nexport type RequestLogPorts = {\n info(entry: RequestLogEntry): void;\n warn(entry: RequestLogEntry): void;\n error(entry: RequestLogEntry): void;\n /** Monotonic milliseconds. Called exactly twice per request. */\n now(): number;\n};\n\nexport type RequestLogDescriptor = {\n module: string;\n action: string;\n requestId: string;\n context?: unknown;\n /**\n * Read AFTER the run settles — the status is decided during the request, so\n * capturing it up front would report the default on every entry.\n */\n statusCode(): number | undefined;\n};\n\ntype Level = \"info\" | \"warn\" | \"error\";\n\nfunction levelForStatus(statusCode: number | undefined): Level {\n // An unknown status is not evidence of a problem; saying \"error\" because we\n // failed to read a number would be inventing a defect.\n if (statusCode === undefined) return \"info\";\n\n if (statusCode >= 500) return \"error\";\n\n if (statusCode >= 400) return \"warn\";\n\n return \"info\";\n}\n\nexport async function logRequestLifecycle<TResult>(\n ports: RequestLogPorts,\n descriptor: RequestLogDescriptor,\n run: () => Promise<TResult>,\n): Promise<TResult> {\n const startedAt = ports.now();\n\n ports.info({\n module: descriptor.module,\n action: descriptor.action,\n message: `Starting Request: ${descriptor.requestId}`,\n context: descriptor.context,\n });\n\n const finish = (level: Level, outcome: string): void => {\n const duration = Math.round(ports.now() - startedAt);\n\n ports[level]({\n module: descriptor.module,\n action: descriptor.action,\n message: `Completed Request: ${descriptor.requestId} — ${outcome} in ${duration}ms`,\n context: descriptor.context,\n });\n };\n\n try {\n const result = await run();\n\n const statusCode = descriptor.statusCode();\n\n finish(levelForStatus(statusCode), statusCode === undefined ? \"no status\" : String(statusCode));\n\n return result;\n } catch (error) {\n // The throw is reported and then re-thrown untouched: this function\n // observes the request, it does not own its error handling.\n finish(\"error\", `threw ${error instanceof Error ? error.name : typeof error}`);\n\n throw error;\n }\n}\n"],"mappings":";AA2DA,SAAS,eAAe,YAAuC;CAG7D,IAAI,eAAe,QAAW,OAAO;CAErC,IAAI,cAAc,KAAK,OAAO;CAE9B,IAAI,cAAc,KAAK,OAAO;CAE9B,OAAO;AACT;AAEA,eAAsB,oBACpB,OACA,YACA,KACkB;CAClB,MAAM,YAAY,MAAM,IAAI;CAE5B,MAAM,KAAK;EACT,QAAQ,WAAW;EACnB,QAAQ,WAAW;EACnB,SAAS,qBAAqB,WAAW;EACzC,SAAS,WAAW;CACtB,CAAC;CAED,MAAM,UAAU,OAAc,YAA0B;EACtD,MAAM,WAAW,KAAK,MAAM,MAAM,IAAI,IAAI,SAAS;EAEnD,MAAM,MAAM,CAAC;GACX,QAAQ,WAAW;GACnB,QAAQ,WAAW;GACnB,SAAS,sBAAsB,WAAW,UAAU,KAAK,QAAQ,MAAM,SAAS;GAChF,SAAS,WAAW;EACtB,CAAC;CACH;CAEA,IAAI;EACF,MAAM,SAAS,MAAM,IAAI;EAEzB,MAAM,aAAa,WAAW,WAAW;EAEzC,OAAO,eAAe,UAAU,GAAG,eAAe,SAAY,cAAc,OAAO,UAAU,CAAC;EAE9F,OAAO;CACT,SAAS,OAAO;EAGd,OAAO,SAAS,SAAS,iBAAiB,QAAQ,MAAM,OAAO,OAAO,OAAO;EAE7E,MAAM;CACR;AACF"}
1
+ {"version":3,"file":"log-request-lifecycle.mjs","names":[],"sources":["../../../../../../../core/src/router/log-request-lifecycle.ts"],"sourcesContent":["/**\n * Both ends of a request's log entry.\n *\n * ## Why the completion line exists\n *\n * The router logged `Starting Request: <id>` and nothing else. A request that\n * hung for thirty seconds and one that returned in two milliseconds produced\n * **byte-identical output**, so the log could not answer the one question\n * anyone opens it to ask.\n *\n * Note what \"detecting a hang\" actually means. Nothing can log the completion\n * of a request that never completes — the mechanism is the **absence** of a\n * completion line beside a start line that has one. That only works if the\n * completion line is emitted on every settled path, **including a throw**,\n * which is why the error path here is not an afterthought.\n *\n * ## Why the level tracks the status\n *\n * A handled 500 does not throw; it returns a response. Logging it at `info`\n * would bury the exact entries someone scanning the log is hunting for, so the\n * level is derived from the outcome: informational below 400, a warning for\n * client errors, an error for server errors and for anything that threw.\n *\n * ## Ports rather than a direct `log` import\n *\n * The behaviour worth pinning down is *which lines come out, in which order,\n * at which level, when the run throws*. Injecting the sink and the clock makes\n * that assertable without a server, a socket or a fake timer.\n */\n\nexport type RequestLogEntry = {\n module: string;\n action: string;\n message: string;\n /**\n * Typed as the logger accepts it, not as `unknown`. These entries exist to be\n * handed to `log.info` / `warn` / `error`, whose parameter is\n * `Record<string, any>` — `unknown` was not assignable, so the adapter in\n * router.ts could not compile even though every real caller passes an object.\n */\n context?: Record<string, any>;\n};\n\nexport type RequestLogPorts = {\n info(entry: RequestLogEntry): void;\n warn(entry: RequestLogEntry): void;\n error(entry: RequestLogEntry): void;\n /** Monotonic milliseconds. Called exactly twice per request. */\n now(): number;\n};\n\nexport type RequestLogDescriptor = {\n module: string;\n action: string;\n requestId: string;\n /**\n * Typed as the logger accepts it, not as `unknown`. These entries exist to be\n * handed to `log.info` / `warn` / `error`, whose parameter is\n * `Record<string, any>` — `unknown` was not assignable, so the adapter in\n * router.ts could not compile even though every real caller passes an object.\n */\n context?: Record<string, any>;\n /**\n * Read AFTER the run settles — the status is decided during the request, so\n * capturing it up front would report the default on every entry.\n */\n statusCode(): number | undefined;\n};\n\ntype Level = \"info\" | \"warn\" | \"error\";\n\nfunction levelForStatus(statusCode: number | undefined): Level {\n // An unknown status is not evidence of a problem; saying \"error\" because we\n // failed to read a number would be inventing a defect.\n if (statusCode === undefined) return \"info\";\n\n if (statusCode >= 500) return \"error\";\n\n if (statusCode >= 400) return \"warn\";\n\n return \"info\";\n}\n\nexport async function logRequestLifecycle<TResult>(\n ports: RequestLogPorts,\n descriptor: RequestLogDescriptor,\n run: () => Promise<TResult>,\n): Promise<TResult> {\n const startedAt = ports.now();\n\n ports.info({\n module: descriptor.module,\n action: descriptor.action,\n message: `Starting Request: ${descriptor.requestId}`,\n context: descriptor.context,\n });\n\n const finish = (level: Level, outcome: string): void => {\n const duration = Math.round(ports.now() - startedAt);\n\n ports[level]({\n module: descriptor.module,\n action: descriptor.action,\n message: `Completed Request: ${descriptor.requestId} — ${outcome} in ${duration}ms`,\n context: descriptor.context,\n });\n };\n\n try {\n const result = await run();\n\n const statusCode = descriptor.statusCode();\n\n finish(levelForStatus(statusCode), statusCode === undefined ? \"no status\" : String(statusCode));\n\n return result;\n } catch (error) {\n // The throw is reported and then re-thrown untouched: this function\n // observes the request, it does not own its error handling.\n finish(\"error\", `threw ${error instanceof Error ? error.name : typeof error}`);\n\n throw error;\n }\n}\n"],"mappings":";AAuEA,SAAS,eAAe,YAAuC;CAG7D,IAAI,eAAe,QAAW,OAAO;CAErC,IAAI,cAAc,KAAK,OAAO;CAE9B,IAAI,cAAc,KAAK,OAAO;CAE9B,OAAO;AACT;AAEA,eAAsB,oBACpB,OACA,YACA,KACkB;CAClB,MAAM,YAAY,MAAM,IAAI;CAE5B,MAAM,KAAK;EACT,QAAQ,WAAW;EACnB,QAAQ,WAAW;EACnB,SAAS,qBAAqB,WAAW;EACzC,SAAS,WAAW;CACtB,CAAC;CAED,MAAM,UAAU,OAAc,YAA0B;EACtD,MAAM,WAAW,KAAK,MAAM,MAAM,IAAI,IAAI,SAAS;EAEnD,MAAM,OAAO;GACX,QAAQ,WAAW;GACnB,QAAQ,WAAW;GACnB,SAAS,sBAAsB,WAAW,UAAU,KAAK,QAAQ,MAAM,SAAS;GAChF,SAAS,WAAW;EACtB,CAAC;CACH;CAEA,IAAI;EACF,MAAM,SAAS,MAAM,IAAI;EAEzB,MAAM,aAAa,WAAW,WAAW;EAEzC,OAAO,eAAe,UAAU,GAAG,eAAe,SAAY,cAAc,OAAO,UAAU,CAAC;EAE9F,OAAO;CACT,SAAS,OAAO;EAGd,OAAO,SAAS,SAAS,iBAAiB,QAAQ,MAAM,OAAO,OAAO,OAAO;EAE7E,MAAM;CACR;AACF"}
@@ -0,0 +1,14 @@
1
+ //#region ../core/src/router/normalize-request-path.d.ts
2
+ /**
3
+ * Canonicalize an incoming request pathname before either router matches it.
4
+ *
5
+ * This is deliberately narrower than `normalizeRoutePath`, which joins route
6
+ * declarations and also resolves doubled slashes plus `.` / `..` segments.
7
+ * A request must keep those bytes unchanged; v5.2 only makes one terminal
8
+ * slash optional. The root path is already canonical and must never become an
9
+ * empty string.
10
+ */
11
+ declare function normalizeRequestPath(pathname: string): string;
12
+ //#endregion
13
+ export { normalizeRequestPath };
14
+ //# sourceMappingURL=normalize-request-path.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-request-path.d.mts","names":[],"sources":["../../../../../../../core/src/router/normalize-request-path.ts"],"mappings":";;AASA;;;;AAAqD;;;;iBAArC,oBAAA,CAAqB,QAAgB"}
@@ -0,0 +1,18 @@
1
+ //#region ../core/src/router/normalize-request-path.ts
2
+ /**
3
+ * Canonicalize an incoming request pathname before either router matches it.
4
+ *
5
+ * This is deliberately narrower than `normalizeRoutePath`, which joins route
6
+ * declarations and also resolves doubled slashes plus `.` / `..` segments.
7
+ * A request must keep those bytes unchanged; v5.2 only makes one terminal
8
+ * slash optional. The root path is already canonical and must never become an
9
+ * empty string.
10
+ */
11
+ function normalizeRequestPath(pathname) {
12
+ if (pathname === "" || pathname === "/") return "/";
13
+ return pathname.endsWith("/") ? pathname.slice(0, -1) : pathname;
14
+ }
15
+
16
+ //#endregion
17
+ export { normalizeRequestPath };
18
+ //# sourceMappingURL=normalize-request-path.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-request-path.mjs","names":[],"sources":["../../../../../../../core/src/router/normalize-request-path.ts"],"sourcesContent":["/**\n * Canonicalize an incoming request pathname before either router matches it.\n *\n * This is deliberately narrower than `normalizeRoutePath`, which joins route\n * declarations and also resolves doubled slashes plus `.` / `..` segments.\n * A request must keep those bytes unchanged; v5.2 only makes one terminal\n * slash optional. The root path is already canonical and must never become an\n * empty string.\n */\nexport function normalizeRequestPath(pathname: string): string {\n if (pathname === \"\" || pathname === \"/\") return \"/\";\n return pathname.endsWith(\"/\") ? pathname.slice(0, -1) : pathname;\n}\n"],"mappings":";;;;;;;;;;AASA,SAAgB,qBAAqB,UAA0B;CAC7D,IAAI,aAAa,MAAM,aAAa,KAAK,OAAO;CAChD,OAAO,SAAS,SAAS,GAAG,IAAI,SAAS,MAAM,GAAG,EAAE,IAAI;AAC1D"}
@@ -0,0 +1,28 @@
1
+ //#region ../core/src/router/normalize-route-path.d.ts
2
+ /**
3
+ * Normalize route path segments into the canonical path the router serves.
4
+ *
5
+ * This is the ONE definition of a route path's canonical form. `Router.add`
6
+ * calls it to join a group prefix onto a route path, and that call is the only
7
+ * reason a bare `"*"` (the not-found catch-all) is registered — and therefore
8
+ * matched, listed and diffed — as `"/*"`.
9
+ *
10
+ * Anything that PREDICTS or RECORDS a route path outside registration has to
11
+ * call this same function. The build-time page-route manifest is the case that
12
+ * forced it into existence: it is derived from a filesystem scan that never
13
+ * registers a route, so before it normalized here it persisted `"*"` while the
14
+ * running router held `"/*"`, and `warlock routes:diff` reported a route as
15
+ * changed immediately after a successful build.
16
+ *
17
+ * Keeping one implementation is what lets the diff stay a STRICT string
18
+ * comparison instead of teaching every consumer which spellings are secretly
19
+ * equivalent.
20
+ *
21
+ * @example normalizeRoutePath("*") // "/*"
22
+ * @example normalizeRoutePath("/users/", "/:id") // "/users/:id"
23
+ * @example normalizeRoutePath("") // "/"
24
+ */
25
+ declare function normalizeRoutePath(...segments: string[]): string;
26
+ //#endregion
27
+ export { normalizeRoutePath };
28
+ //# sourceMappingURL=normalize-route-path.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-route-path.d.mts","names":[],"sources":["../../../../../../../core/src/router/normalize-route-path.ts"],"mappings":";;AAyBA;;;;AAA8C;;;;;;;;;;;;;;;;;;iBAA9B,kBAAA,CAAA,GAAsB,QAAQ"}
@@ -0,0 +1,33 @@
1
+ import concatRoute from "@mongez/concat-route";
2
+
3
+ //#region ../core/src/router/normalize-route-path.ts
4
+ /**
5
+ * Normalize route path segments into the canonical path the router serves.
6
+ *
7
+ * This is the ONE definition of a route path's canonical form. `Router.add`
8
+ * calls it to join a group prefix onto a route path, and that call is the only
9
+ * reason a bare `"*"` (the not-found catch-all) is registered — and therefore
10
+ * matched, listed and diffed — as `"/*"`.
11
+ *
12
+ * Anything that PREDICTS or RECORDS a route path outside registration has to
13
+ * call this same function. The build-time page-route manifest is the case that
14
+ * forced it into existence: it is derived from a filesystem scan that never
15
+ * registers a route, so before it normalized here it persisted `"*"` while the
16
+ * running router held `"/*"`, and `warlock routes:diff` reported a route as
17
+ * changed immediately after a successful build.
18
+ *
19
+ * Keeping one implementation is what lets the diff stay a STRICT string
20
+ * comparison instead of teaching every consumer which spellings are secretly
21
+ * equivalent.
22
+ *
23
+ * @example normalizeRoutePath("*") // "/*"
24
+ * @example normalizeRoutePath("/users/", "/:id") // "/users/:id"
25
+ * @example normalizeRoutePath("") // "/"
26
+ */
27
+ function normalizeRoutePath(...segments) {
28
+ return concatRoute(...segments);
29
+ }
30
+
31
+ //#endregion
32
+ export { normalizeRoutePath };
33
+ //# sourceMappingURL=normalize-route-path.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"normalize-route-path.mjs","names":[],"sources":["../../../../../../../core/src/router/normalize-route-path.ts"],"sourcesContent":["import concatRoute from \"@mongez/concat-route\";\n\n/**\n * Normalize route path segments into the canonical path the router serves.\n *\n * This is the ONE definition of a route path's canonical form. `Router.add`\n * calls it to join a group prefix onto a route path, and that call is the only\n * reason a bare `\"*\"` (the not-found catch-all) is registered — and therefore\n * matched, listed and diffed — as `\"/*\"`.\n *\n * Anything that PREDICTS or RECORDS a route path outside registration has to\n * call this same function. The build-time page-route manifest is the case that\n * forced it into existence: it is derived from a filesystem scan that never\n * registers a route, so before it normalized here it persisted `\"*\"` while the\n * running router held `\"/*\"`, and `warlock routes:diff` reported a route as\n * changed immediately after a successful build.\n *\n * Keeping one implementation is what lets the diff stay a STRICT string\n * comparison instead of teaching every consumer which spellings are secretly\n * equivalent.\n *\n * @example normalizeRoutePath(\"*\") // \"/*\"\n * @example normalizeRoutePath(\"/users/\", \"/:id\") // \"/users/:id\"\n * @example normalizeRoutePath(\"\") // \"/\"\n */\nexport function normalizeRoutePath(...segments: string[]): string {\n return concatRoute(...segments);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,mBAAmB,GAAG,UAA4B;CAChE,OAAO,YAAY,GAAG,QAAQ;AAChC"}
@@ -1 +1 @@
1
- {"version":3,"file":"positional-handler-diagnostics.d.mts","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"mappings":";;;;AAyPsE;AAmBtE;;AAnBsE,KArN1D,wBAAA;EAwOsE,kDAtOhF,MAAA,EAAQ,KAAK,YA+OC;EA5Od,IAAA;EAGA,WAAA,UAyOgE;EAtOhE,UAAA;AAAA;;;AAkP4C;AAS9C;;;;AAAkF;AAclF;;;;;;;;;;;;;AAAsF;;;;;;;;;;;iBAhGtE,0BAAA,CAA2B,OAAgB;;;;;;;;;iBA+B3C,uBAAA,CACd,OAAA,WACA,KAAA;EAAS,MAAA,EAAQ,KAAK;EAAY,IAAA;EAAc,UAAA;AAAA;;;;iBAmBlC,6BAAA,aAA0C,wBAAwB;;;;;;iBASlE,+BAAA,CAAgC,UAAkB;;;;;iBAYlD,8BAAA;;;;;;iBASA,gCAAA,CAAiC,OAAiC,EAAxB,wBAAwB;;;;;;;iBAclE,+BAAA,CAAgC,MAAA,GAAQ,IAAA,QAAY,GAAA,YAAkB,wBAAA"}
1
+ {"version":3,"file":"positional-handler-diagnostics.d.mts","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"mappings":";;;;AAyPsE;AAmBtE;;AAnBsE,KArN1D,wBAAA;EAwOsE,kDAtOhF,MAAA,EAAQ,KAAK,YA+OC;EA5Od,IAAA;EAGA,WAAA,UAyOgE;EAtOhE,UAAA;AAAA;;;AAkP4C;AAS9C;;;;AAAkF;AAclF;;;;;;;;;;;;;AAAsF;;;;;;;;;;;iBAhGtE,0BAAA,CAA2B,OAAgB;;;;;;;;;iBA+B3C,uBAAA,CACd,OAAA,WACA,KAAA;EAAS,MAAA,EAAQ,KAAK;EAAY,IAAA;EAAc,UAAA;AAAA;;;;iBAmBlC,6BAAA,CAAA,YAA0C,wBAAwB;;;;;;iBASlE,+BAAA,CAAgC,UAAkB;;;;;iBAYlD,8BAAA,CAAA;;;;;;iBASA,gCAAA,CAAiC,OAAiC,EAAxB,wBAAwB;;;;;;;iBAclE,+BAAA,CAAgC,MAAA,GAAQ,IAAA,QAAY,GAAA,YAAkB,wBAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"positional-handler-diagnostics.mjs","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"sourcesContent":["/**\n * Early detection of route handlers still written against the **v4 positional\n * signature** — `(request, response)` — after v5 moved to a single context\n * object, `({ request, response })`.\n *\n * ## Why this exists\n *\n * A v4 handler is not rejected anywhere. It registers fine, and the router\n * calls it with one argument, so `response` is simply `undefined`. The app only\n * breaks on the first request that reaches it, with:\n *\n * TypeError: Cannot read properties of undefined (reading 'success')\n *\n * That message names nothing the reader can act on — not the route, not the\n * handler, not the change to make. This module turns that late, opaque failure\n * into one explicit list printed at boot, and into a `warlock doctor` check.\n *\n * The break itself is intentional; nothing here restores the positional form.\n * Detection and diagnostics only.\n *\n * ## Why it warns and never throws\n *\n * The signal is a heuristic (see {@link looksLikePositionalHandler}), so a\n * false positive is possible in principle. Turning one into a boot failure\n * would take a working application down over a guess, which is strictly worse\n * than the problem being diagnosed. Everything on this path is therefore a\n * warning: nothing here throws, and nothing here changes how a handler is\n * called. A miss simply degrades to the old runtime `TypeError`.\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport type { Route } from \"./types\";\n\n/**\n * One route whose handler looks like it was written for v4.\n */\nexport type PositionalHandlerSuspect = {\n /** HTTP method the route was registered under. */\n method: Route[\"method\"];\n\n /** Full route path, prefixes already applied. */\n path: string;\n\n /** Handler function name, or `\"(anonymous)\"` when it has none. */\n handlerName: string;\n\n /** Route file the route came from, when the router knows it. */\n sourceFile: string;\n};\n\n/**\n * Collected across the whole registration pass so the warning can be emitted\n * ONCE, as a single list, rather than one line per route (or — as before — one\n * mystery 500 per request).\n */\nconst suspects: PositionalHandlerSuspect[] = [];\n\n/**\n * Split the parameter list of a function's source into top-level parameters.\n *\n * Depth-aware and quote-aware, because a default value may itself contain\n * commas, parentheses, braces or strings — `(request, response = f(a, b))` is\n * two parameters, not three. Anything unbalanced returns `undefined`: refusing\n * to guess is what keeps a parse quirk from becoming a false warning.\n *\n * @param openIndex index of the `(` that opens the parameter list.\n */\nfunction splitTopLevelParameters(source: string, openIndex: number): string[] | undefined {\n const parameters: string[] = [];\n\n let depth = 0;\n let current = \"\";\n let quote = \"\";\n\n for (let index = openIndex; index < source.length; index++) {\n const char = source[index];\n\n if (quote) {\n if (char === \"\\\\\") {\n current += char + (source[index + 1] ?? \"\");\n index++;\n continue;\n }\n\n if (char === quote) {\n quote = \"\";\n }\n\n current += char;\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === \"`\") {\n quote = char;\n current += char;\n continue;\n }\n\n if (char === \"(\" || char === \"[\" || char === \"{\") {\n depth++;\n\n // The paren that opens the list is punctuation, not part of a parameter.\n if (depth === 1) continue;\n\n current += char;\n continue;\n }\n\n if (char === \")\" || char === \"]\" || char === \"}\") {\n depth--;\n\n if (depth === 0) {\n if (current.trim()) {\n parameters.push(current.trim());\n }\n\n return parameters;\n }\n\n current += char;\n continue;\n }\n\n if (char === \",\" && depth === 1) {\n parameters.push(current.trim());\n current = \"\";\n continue;\n }\n\n current += char;\n }\n\n // Never found the closing paren — treat the source as unreadable.\n return undefined;\n}\n\n/**\n * Read a function's declared parameters from its own source.\n *\n * Returns `undefined` whenever the source cannot be trusted, which callers must\n * treat as \"no information\" rather than \"no parameters\":\n *\n * - `Function.prototype.bind` replaces the body with `[native code]`, so every\n * `[Controller, \"method\"]` tuple handler lands here (the router binds them);\n * - native functions likewise;\n * - a class, whose first `(` belongs to a constructor or a method, not to a\n * parameter list;\n * - anything that fails to parse.\n */\nfunction readParameterList(handler: Function): string[] | undefined {\n let source: string;\n\n try {\n source = Function.prototype.toString.call(handler);\n } catch {\n return undefined;\n }\n\n if (source.includes(\"[native code]\")) return undefined;\n\n if (/^\\s*class[\\s{]/.test(source)) return undefined;\n\n // A single parameter needs no parentheses: `ctx => …`, `async ctx => …`.\n const bareArrow = source.match(/^\\s*(?:async\\s+)?([A-Za-z_$][\\w$]*)\\s*=>/);\n\n if (bareArrow) return [bareArrow[1]];\n\n const openIndex = source.indexOf(\"(\");\n\n if (openIndex === -1) return undefined;\n\n return splitTopLevelParameters(source, openIndex);\n}\n\n/**\n * Whether a parameter is written as a destructuring pattern — `{ request }` or\n * `[a, b]` — as opposed to a plain name.\n */\nfunction isDestructured(parameter: string) {\n const start = parameter.trimStart();\n\n return start.startsWith(\"{\") || start.startsWith(\"[\");\n}\n\n/**\n * Does this handler look like it was written for the v4 positional signature?\n *\n * ## The signal, and what it misses\n *\n * `Function.length` alone is not enough, because it stops counting at the first\n * defaulted parameter:\n *\n * | handler | `.length` | actually |\n * | ---------------------------------- | --------- | -------- |\n * | `({ request, response }) => …` | 1 | v5 |\n * | `(ctx) => …` | 1 | v5 |\n * | `(request, response) => …` | 2 | v4 |\n * | `(request, response = x) => …` | 1 | v4 |\n *\n * So the primary signal is the function's own source: a handler is suspect when\n * it declares **two or more** parameters and the first is **not** destructured.\n * Reading the source catches the defaulted row that arity cannot, and rules out\n * the `({ … }, extra)` shape that arity would wrongly flag.\n *\n * When the source is unreadable — a bound or native function — this falls back\n * to `handler.length >= 2`, which `bind` preserves. That is a weaker signal but\n * never a wrong one for the common cases; it only loses the defaulted row.\n *\n * ## What is still missed, by design\n *\n * A one-parameter v4 handler (`(request) => …`) is indistinguishable from a v5\n * `(ctx) => …` and is deliberately not flagged — guessing there would produce\n * false positives on correct code. False positives against the canonical\n * `({ … })` form are essentially nil, and every miss simply leaves today's\n * behaviour in place.\n */\nexport function looksLikePositionalHandler(handler: unknown): boolean {\n if (typeof handler !== \"function\") return false;\n\n const parameters = readParameterList(handler);\n\n if (parameters) {\n return parameters.length >= 2 && !isDestructured(parameters[0]);\n }\n\n return handler.length >= 2;\n}\n\n/**\n * The name to print for a handler.\n *\n * `Function.bind` prefixes the name with `\"bound \"`, and the router binds every\n * `[Controller, \"method\"]` tuple handler. The bare method name is what the user\n * actually wrote, so that is what gets printed.\n */\nfunction handlerDisplayName(handler: Function): string {\n return handler.name?.replace(/^bound /, \"\").trim() || \"(anonymous)\";\n}\n\n/**\n * Inspect one route's handler as it registers, recording it when it looks like\n * the v4 positional form. Called from `Router.add`, so the cost is paid once per\n * route at registration and never per request.\n *\n * Deliberately total: any unexpected failure in the heuristic is swallowed, so a\n * diagnostic can never be the reason a route fails to register.\n */\nexport function inspectHandlerSignature(\n handler: unknown,\n route: { method: Route[\"method\"]; path: string; sourceFile: string },\n) {\n try {\n if (!looksLikePositionalHandler(handler)) return;\n\n suspects.push({\n method: route.method,\n path: route.path,\n handlerName: handlerDisplayName(handler as Function),\n sourceFile: route.sourceFile,\n });\n } catch {\n // A diagnostic must never break registration.\n }\n}\n\n/**\n * Every suspect recorded so far, in registration order.\n */\nexport function listPositionalHandlerSuspects(): readonly PositionalHandlerSuspect[] {\n return suspects;\n}\n\n/**\n * Drop the suspects belonging to a route file. Kept in step with\n * `Router.removeRoutesBySourceFile` so an HMR reload that fixes a handler does\n * not keep reporting the version it replaced.\n */\nexport function forgetPositionalHandlerSuspects(sourceFile: string) {\n for (let index = suspects.length - 1; index >= 0; index--) {\n if (suspects[index].sourceFile === sourceFile) {\n suspects.splice(index, 1);\n }\n }\n}\n\n/**\n * Drop every recorded suspect. Intended for tests, which share the router\n * singleton.\n */\nexport function clearPositionalHandlerSuspects() {\n suspects.length = 0;\n}\n\n/**\n * The one-line diagnostic for a single route. Written for someone who has never\n * read Warlock's internals: it names the handler, the route, what is wrong, and\n * the exact edit that fixes it.\n */\nexport function describePositionalHandlerSuspect(suspect: PositionalHandlerSuspect): string {\n return (\n `Handler \"${suspect.handlerName}\" (${suspect.method} ${suspect.path}) looks like ` +\n `the v4 positional signature (request, response). ` +\n `v5 passes a single context object — change it to ({ request, response }).`\n );\n}\n\n/**\n * The full boot-time warning: every suspect, once, in one entry.\n *\n * One boot, one list — not one warning per route, and emphatically not one 500\n * per request. Returns the reported suspects so callers can assert on them.\n */\nexport function reportPositionalHandlerSuspects(logger: Pick<typeof log, \"warn\"> = log) {\n const reported = [...suspects];\n\n if (reported.length === 0) return reported;\n\n const headline =\n reported.length === 1\n ? \"1 route handler looks like the v4 positional signature.\"\n : `${reported.length} route handlers look like the v4 positional signature.`;\n\n logger.warn({\n module: \"router\",\n action: \"handlerSignature\",\n message:\n `${headline} Warlock v5 calls every route handler with a single context object, ` +\n `so a second parameter is always undefined and the request fails as soon as it is used:\\n` +\n reported.map((suspect) => ` - ${describePositionalHandlerSuspect(suspect)}`).join(\"\\n\"),\n context: { handlers: reported },\n });\n\n return reported;\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"positional-handler-diagnostics.mjs","names":[],"sources":["../../../../../../../core/src/router/positional-handler-diagnostics.ts"],"sourcesContent":["/**\n * Early detection of route handlers still written against the **v4 positional\n * signature** — `(request, response)` — after v5 moved to a single context\n * object, `({ request, response })`.\n *\n * ## Why this exists\n *\n * A v4 handler is not rejected anywhere. It registers fine, and the router\n * calls it with one argument, so `response` is simply `undefined`. The app only\n * breaks on the first request that reaches it, with:\n *\n * TypeError: Cannot read properties of undefined (reading 'success')\n *\n * That message names nothing the reader can act on — not the route, not the\n * handler, not the change to make. This module turns that late, opaque failure\n * into one explicit list printed at boot, and into a `warlock doctor` check.\n *\n * The break itself is intentional; nothing here restores the positional form.\n * Detection and diagnostics only.\n *\n * ## Why it warns and never throws\n *\n * The signal is a heuristic (see {@link looksLikePositionalHandler}), so a\n * false positive is possible in principle. Turning one into a boot failure\n * would take a working application down over a guess, which is strictly worse\n * than the problem being diagnosed. Everything on this path is therefore a\n * warning: nothing here throws, and nothing here changes how a handler is\n * called. A miss simply degrades to the old runtime `TypeError`.\n */\n\nimport { log } from \"@warlock.js/logger\";\nimport type { Route } from \"./types\";\n\n/**\n * One route whose handler looks like it was written for v4.\n */\nexport type PositionalHandlerSuspect = {\n /** HTTP method the route was registered under. */\n method: Route[\"method\"];\n\n /** Full route path, prefixes already applied. */\n path: string;\n\n /** Handler function name, or `\"(anonymous)\"` when it has none. */\n handlerName: string;\n\n /** Route file the route came from, when the router knows it. */\n sourceFile: string;\n};\n\n/**\n * Collected across the whole registration pass so the warning can be emitted\n * ONCE, as a single list, rather than one line per route (or — as before — one\n * mystery 500 per request).\n */\nconst suspects: PositionalHandlerSuspect[] = [];\n\n/**\n * Split the parameter list of a function's source into top-level parameters.\n *\n * Depth-aware and quote-aware, because a default value may itself contain\n * commas, parentheses, braces or strings — `(request, response = f(a, b))` is\n * two parameters, not three. Anything unbalanced returns `undefined`: refusing\n * to guess is what keeps a parse quirk from becoming a false warning.\n *\n * @param openIndex index of the `(` that opens the parameter list.\n */\nfunction splitTopLevelParameters(source: string, openIndex: number): string[] | undefined {\n const parameters: string[] = [];\n\n let depth = 0;\n let current = \"\";\n let quote = \"\";\n\n for (let index = openIndex; index < source.length; index++) {\n const char = source[index];\n\n if (quote) {\n if (char === \"\\\\\") {\n current += char + (source[index + 1] ?? \"\");\n index++;\n continue;\n }\n\n if (char === quote) {\n quote = \"\";\n }\n\n current += char;\n continue;\n }\n\n if (char === '\"' || char === \"'\" || char === \"`\") {\n quote = char;\n current += char;\n continue;\n }\n\n if (char === \"(\" || char === \"[\" || char === \"{\") {\n depth++;\n\n // The paren that opens the list is punctuation, not part of a parameter.\n if (depth === 1) continue;\n\n current += char;\n continue;\n }\n\n if (char === \")\" || char === \"]\" || char === \"}\") {\n depth--;\n\n if (depth === 0) {\n if (current.trim()) {\n parameters.push(current.trim());\n }\n\n return parameters;\n }\n\n current += char;\n continue;\n }\n\n if (char === \",\" && depth === 1) {\n parameters.push(current.trim());\n current = \"\";\n continue;\n }\n\n current += char;\n }\n\n // Never found the closing paren — treat the source as unreadable.\n return undefined;\n}\n\n/**\n * Read a function's declared parameters from its own source.\n *\n * Returns `undefined` whenever the source cannot be trusted, which callers must\n * treat as \"no information\" rather than \"no parameters\":\n *\n * - `Function.prototype.bind` replaces the body with `[native code]`, so every\n * `[Controller, \"method\"]` tuple handler lands here (the router binds them);\n * - native functions likewise;\n * - a class, whose first `(` belongs to a constructor or a method, not to a\n * parameter list;\n * - anything that fails to parse.\n */\nfunction readParameterList(handler: Function): string[] | undefined {\n let source: string;\n\n try {\n source = Function.prototype.toString.call(handler);\n } catch {\n return undefined;\n }\n\n if (source.includes(\"[native code]\")) return undefined;\n\n if (/^\\s*class[\\s{]/.test(source)) return undefined;\n\n // A single parameter needs no parentheses: `ctx => …`, `async ctx => …`.\n const bareArrow = source.match(/^\\s*(?:async\\s+)?([A-Za-z_$][\\w$]*)\\s*=>/);\n\n if (bareArrow) return [bareArrow[1]];\n\n const openIndex = source.indexOf(\"(\");\n\n if (openIndex === -1) return undefined;\n\n return splitTopLevelParameters(source, openIndex);\n}\n\n/**\n * Whether a parameter is written as a destructuring pattern — `{ request }` or\n * `[a, b]` — as opposed to a plain name.\n */\nfunction isDestructured(parameter: string) {\n const start = parameter.trimStart();\n\n return start.startsWith(\"{\") || start.startsWith(\"[\");\n}\n\n/**\n * Does this handler look like it was written for the v4 positional signature?\n *\n * ## The signal, and what it misses\n *\n * `Function.length` alone is not enough, because it stops counting at the first\n * defaulted parameter:\n *\n * | handler | `.length` | actually |\n * | ---------------------------------- | --------- | -------- |\n * | `({ request, response }) => …` | 1 | v5 |\n * | `(ctx) => …` | 1 | v5 |\n * | `(request, response) => …` | 2 | v4 |\n * | `(request, response = x) => …` | 1 | v4 |\n *\n * So the primary signal is the function's own source: a handler is suspect when\n * it declares **two or more** parameters and the first is **not** destructured.\n * Reading the source catches the defaulted row that arity cannot, and rules out\n * the `({ … }, extra)` shape that arity would wrongly flag.\n *\n * When the source is unreadable — a bound or native function — this falls back\n * to `handler.length >= 2`, which `bind` preserves. That is a weaker signal but\n * never a wrong one for the common cases; it only loses the defaulted row.\n *\n * ## What is still missed, by design\n *\n * A one-parameter v4 handler (`(request) => …`) is indistinguishable from a v5\n * `(ctx) => …` and is deliberately not flagged — guessing there would produce\n * false positives on correct code. False positives against the canonical\n * `({ … })` form are essentially nil, and every miss simply leaves today's\n * behaviour in place.\n */\nexport function looksLikePositionalHandler(handler: unknown): boolean {\n if (typeof handler !== \"function\") return false;\n\n const parameters = readParameterList(handler);\n\n if (parameters) {\n return parameters.length >= 2 && !isDestructured(parameters[0]);\n }\n\n return handler.length >= 2;\n}\n\n/**\n * The name to print for a handler.\n *\n * `Function.bind` prefixes the name with `\"bound \"`, and the router binds every\n * `[Controller, \"method\"]` tuple handler. The bare method name is what the user\n * actually wrote, so that is what gets printed.\n */\nfunction handlerDisplayName(handler: Function): string {\n return handler.name?.replace(/^bound /, \"\").trim() || \"(anonymous)\";\n}\n\n/**\n * Inspect one route's handler as it registers, recording it when it looks like\n * the v4 positional form. Called from `Router.add`, so the cost is paid once per\n * route at registration and never per request.\n *\n * Deliberately total: any unexpected failure in the heuristic is swallowed, so a\n * diagnostic can never be the reason a route fails to register.\n */\nexport function inspectHandlerSignature(\n handler: unknown,\n route: { method: Route[\"method\"]; path: string; sourceFile: string },\n) {\n try {\n if (!looksLikePositionalHandler(handler)) return;\n\n suspects.push({\n method: route.method,\n path: route.path,\n handlerName: handlerDisplayName(handler as Function),\n sourceFile: route.sourceFile,\n });\n } catch {\n // A diagnostic must never break registration.\n }\n}\n\n/**\n * Every suspect recorded so far, in registration order.\n */\nexport function listPositionalHandlerSuspects(): readonly PositionalHandlerSuspect[] {\n return suspects;\n}\n\n/**\n * Drop the suspects belonging to a route file. Kept in step with\n * `Router.removeRoutesBySourceFile` so an HMR reload that fixes a handler does\n * not keep reporting the version it replaced.\n */\nexport function forgetPositionalHandlerSuspects(sourceFile: string) {\n for (let index = suspects.length - 1; index >= 0; index--) {\n if (suspects[index].sourceFile === sourceFile) {\n suspects.splice(index, 1);\n }\n }\n}\n\n/**\n * Drop every recorded suspect. Intended for tests, which share the router\n * singleton.\n */\nexport function clearPositionalHandlerSuspects() {\n suspects.length = 0;\n}\n\n/**\n * The one-line diagnostic for a single route. Written for someone who has never\n * read Warlock's internals: it names the handler, the route, what is wrong, and\n * the exact edit that fixes it.\n */\nexport function describePositionalHandlerSuspect(suspect: PositionalHandlerSuspect): string {\n return (\n `Handler \"${suspect.handlerName}\" (${suspect.method} ${suspect.path}) looks like ` +\n `the v4 positional signature (request, response). ` +\n `v5 passes a single context object — change it to ({ request, response }).`\n );\n}\n\n/**\n * The full boot-time warning: every suspect, once, in one entry.\n *\n * One boot, one list — not one warning per route, and emphatically not one 500\n * per request. Returns the reported suspects so callers can assert on them.\n */\nexport function reportPositionalHandlerSuspects(logger: Pick<typeof log, \"warn\"> = log) {\n const reported = [...suspects];\n\n if (reported.length === 0) return reported;\n\n const headline =\n reported.length === 1\n ? \"1 route handler looks like the v4 positional signature.\"\n : `${reported.length} route handlers look like the v4 positional signature.`;\n\n logger.warn({\n module: \"router\",\n action: \"handlerSignature\",\n message:\n `${headline} Warlock v5 calls every route handler with a single context object, ` +\n `so a second parameter is always undefined and the request fails as soon as it is used:\\n` +\n reported.map((suspect) => ` - ${describePositionalHandlerSuspect(suspect)}`).join(\"\\n\"),\n context: { handlers: reported },\n });\n\n return reported;\n}\n"],"mappings":"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"}
@@ -0,0 +1,33 @@
1
+ //#region ../core/src/router/route-name-method-suffix.d.ts
2
+ /**
3
+ * The route-name suffix the router appends at REGISTRATION time, and its
4
+ * inverse.
5
+ *
6
+ * `Router.add` gives a route the name it was asked for — unless another route
7
+ * already claimed that name under a DIFFERENT method, in which case it appends
8
+ * `.<method>` so both can coexist. That decision depends on the whole
9
+ * registration order across pages AND API routes, so it cannot be predicted by
10
+ * anything that only looks at one route, or at only one kind of route.
11
+ *
12
+ * Both halves live here so the rule is written down once: `Router.add` builds
13
+ * the suffix with {@link routeNameMethodSuffix}, and anything comparing a
14
+ * DERIVED name against a REGISTERED one uses {@link matchesDerivedRouteName}.
15
+ */
16
+ /**
17
+ * The suffix `Router.add` appends to a route name already claimed by another
18
+ * method.
19
+ */
20
+ declare function routeNameMethodSuffix(method: string): string;
21
+ /**
22
+ * Whether `registeredName` is what the router would have registered for a
23
+ * route whose name was DERIVED as `derivedName`.
24
+ *
25
+ * True for exactly two spellings: the derived name itself, and the derived
26
+ * name plus the registration collision suffix for `method`. It is not a fuzzy
27
+ * match — a `.get` suffix is accepted only where the router itself could have
28
+ * added one, and any other difference is still a difference.
29
+ */
30
+ declare function matchesDerivedRouteName(derivedName: string, registeredName: string, method: string): boolean;
31
+ //#endregion
32
+ export { matchesDerivedRouteName, routeNameMethodSuffix };
33
+ //# sourceMappingURL=route-name-method-suffix.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-name-method-suffix.d.mts","names":[],"sources":["../../../../../../../core/src/router/route-name-method-suffix.ts"],"mappings":";;AAmBA;;;;AAAoD;AAapD;;;;;;;;AAGgB;;;;AAAA,iBAhBA,qBAAA,CAAsB,MAAc;;;;;;;;;;iBAapC,uBAAA,CACd,WAAA,UACA,cAAA,UACA,MAAA"}
@@ -0,0 +1,39 @@
1
+ //#region ../core/src/router/route-name-method-suffix.ts
2
+ /**
3
+ * The route-name suffix the router appends at REGISTRATION time, and its
4
+ * inverse.
5
+ *
6
+ * `Router.add` gives a route the name it was asked for — unless another route
7
+ * already claimed that name under a DIFFERENT method, in which case it appends
8
+ * `.<method>` so both can coexist. That decision depends on the whole
9
+ * registration order across pages AND API routes, so it cannot be predicted by
10
+ * anything that only looks at one route, or at only one kind of route.
11
+ *
12
+ * Both halves live here so the rule is written down once: `Router.add` builds
13
+ * the suffix with {@link routeNameMethodSuffix}, and anything comparing a
14
+ * DERIVED name against a REGISTERED one uses {@link matchesDerivedRouteName}.
15
+ */
16
+ /**
17
+ * The suffix `Router.add` appends to a route name already claimed by another
18
+ * method.
19
+ */
20
+ function routeNameMethodSuffix(method) {
21
+ return `.${method.toLowerCase()}`;
22
+ }
23
+ /**
24
+ * Whether `registeredName` is what the router would have registered for a
25
+ * route whose name was DERIVED as `derivedName`.
26
+ *
27
+ * True for exactly two spellings: the derived name itself, and the derived
28
+ * name plus the registration collision suffix for `method`. It is not a fuzzy
29
+ * match — a `.get` suffix is accepted only where the router itself could have
30
+ * added one, and any other difference is still a difference.
31
+ */
32
+ function matchesDerivedRouteName(derivedName, registeredName, method) {
33
+ if (registeredName === derivedName) return true;
34
+ return registeredName === derivedName + routeNameMethodSuffix(method);
35
+ }
36
+
37
+ //#endregion
38
+ export { matchesDerivedRouteName, routeNameMethodSuffix };
39
+ //# sourceMappingURL=route-name-method-suffix.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-name-method-suffix.mjs","names":[],"sources":["../../../../../../../core/src/router/route-name-method-suffix.ts"],"sourcesContent":["/**\n * The route-name suffix the router appends at REGISTRATION time, and its\n * inverse.\n *\n * `Router.add` gives a route the name it was asked for — unless another route\n * already claimed that name under a DIFFERENT method, in which case it appends\n * `.<method>` so both can coexist. That decision depends on the whole\n * registration order across pages AND API routes, so it cannot be predicted by\n * anything that only looks at one route, or at only one kind of route.\n *\n * Both halves live here so the rule is written down once: `Router.add` builds\n * the suffix with {@link routeNameMethodSuffix}, and anything comparing a\n * DERIVED name against a REGISTERED one uses {@link matchesDerivedRouteName}.\n */\n\n/**\n * The suffix `Router.add` appends to a route name already claimed by another\n * method.\n */\nexport function routeNameMethodSuffix(method: string): string {\n return `.${method.toLowerCase()}`;\n}\n\n/**\n * Whether `registeredName` is what the router would have registered for a\n * route whose name was DERIVED as `derivedName`.\n *\n * True for exactly two spellings: the derived name itself, and the derived\n * name plus the registration collision suffix for `method`. It is not a fuzzy\n * match — a `.get` suffix is accepted only where the router itself could have\n * added one, and any other difference is still a difference.\n */\nexport function matchesDerivedRouteName(\n derivedName: string,\n registeredName: string,\n method: string,\n): boolean {\n if (registeredName === derivedName) {\n return true;\n }\n\n return registeredName === derivedName + routeNameMethodSuffix(method);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,sBAAsB,QAAwB;CAC5D,OAAO,IAAI,OAAO,YAAY;AAChC;;;;;;;;;;AAWA,SAAgB,wBACd,aACA,gBACA,QACS;CACT,IAAI,mBAAmB,aACrB,OAAO;CAGT,OAAO,mBAAmB,cAAc,sBAAsB,MAAM;AACtE"}
@@ -1 +1 @@
1
- {"version":3,"file":"route-registry.d.mts","names":[],"sources":["../../../../../../../core/src/router/route-registry.ts"],"mappings":";;;;;AAsBA;;cAAa,aAAA;EAAA,QACH,MAAA;;EA6CI;;;EAjCL,QAAA,CAAS,MAAA,EAAQ,KAAA;EAZhB;;;EA+BD,aAAA,CAAc,KAAA,EAAO,KAAA;EAnBZ;;;;EA8BT,IAAA,CACL,MAAA,UACA,GAAA;IACG,KAAA,EAAO,KAAA;IAAO,MAAA,EAAQ,MAAA;EAAA;EAAf;;;EAkBL,aAAA;AAAA"}
1
+ {"version":3,"file":"route-registry.d.mts","names":[],"sources":["../../../../../../../core/src/router/route-registry.ts"],"mappings":";;;;;AAuBA;;cAAa,aAAA;EAAA,QACH,MAAA;;EAgDI;;;EAjCL,QAAA,CAAS,MAAA,EAAQ,KAAA;EAfhB;;;EAkCD,aAAA,CAAc,KAAA,EAAO,KAAA;EAnBZ;;;;EA8BT,IAAA,CACL,MAAA,UACA,GAAA;IACG,KAAA,EAAO,KAAA;IAAO,MAAA,EAAQ,MAAA;EAAA;EAAf;;;EAkBL,aAAA,CAAA;AAAA"}
@@ -1,3 +1,4 @@
1
+ import { normalizeRequestPath } from "./normalize-request-path.mjs";
1
2
  import FindMyWay from "find-my-way";
2
3
 
3
4
  //#region ../core/src/router/route-registry.ts
@@ -22,7 +23,7 @@ const ALL_METHODS = [
22
23
  var RouteRegistry = class {
23
24
  constructor() {
24
25
  this.router = FindMyWay({
25
- ignoreTrailingSlash: true,
26
+ ignoreTrailingSlash: false,
26
27
  caseSensitive: false
27
28
  });
28
29
  }
@@ -52,7 +53,7 @@ var RouteRegistry = class {
52
53
  * @returns Matched route with extracted params, or null if no match
53
54
  */
54
55
  find(method, url) {
55
- const path = url.split("?")[0];
56
+ const path = normalizeRequestPath(url.split("?")[0]);
56
57
  const match = this.router.find(method, path);
57
58
  if (!match) return null;
58
59
  return match.handler(null, null, match.params, match.store, {});
@@ -1 +1 @@
1
- {"version":3,"file":"route-registry.mjs","names":[],"sources":["../../../../../../../core/src/router/route-registry.ts"],"sourcesContent":["import FindMyWay, { type HTTPMethod, type Instance } from \"find-my-way\";\nimport type { RequestMethod, Route } from \"./types\";\n\n/**\n * The concrete HTTP verbs an `all` route expands into.\n * Mirrors the set Fastify's `.all()` answers in production, so `router.any()`\n * routes behave identically under the dev server and prod Fastify.\n */\nconst ALL_METHODS: Exclude<RequestMethod, \"all\">[] = [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\",\n \"OPTIONS\",\n \"HEAD\",\n];\n\n/**\n * Route Registry\n * Manages dynamic route matching using find-my-way for HMR support\n */\nexport class RouteRegistry {\n private router: Instance<any>;\n\n public constructor() {\n this.router = FindMyWay({\n ignoreTrailingSlash: true,\n caseSensitive: false,\n });\n }\n\n /**\n * Register all routes from the router's internal list\n */\n public register(routes: Route[]): void {\n // Register each route\n for (const route of routes) {\n if (route.method === \"all\") {\n for (const method of ALL_METHODS) {\n this.registerRoute({\n ...route,\n method,\n });\n }\n } else {\n this.registerRoute(route);\n }\n }\n }\n\n /**\n * Register a single route\n */\n public registerRoute(route: Route): void {\n this.router.on(route.method as HTTPMethod, route.path, (req, res, params) => {\n // Store the route and params for later use\n return { route, params };\n });\n }\n\n /**\n * Find a matching route for the given method and URL\n * @returns Matched route with extracted params, or null if no match\n */\n public find(\n method: string,\n url: string,\n ): { route: Route; params: Record<string, string> } | null {\n // Strip query string from URL (find-my-way expects just the path)\n const path = url.split(\"?\")[0];\n\n const match = this.router.find(method as HTTPMethod, path);\n\n if (!match) {\n return null;\n }\n\n // find-my-way handler expects (req, res, params, store, searchParams)\n // We only care about the return value which contains { route, params }\n return match.handler(null as any, null as any, match.params, match.store, {});\n }\n\n /**\n * Get all registered routes count (for debugging)\n */\n public getRouteCount(): number {\n return this.router\n .prettyPrint()\n .split(\"\\n\")\n .filter((line) => line.trim()).length;\n }\n}\n"],"mappings":";;;;;;;;AAQA,MAAM,cAA+C;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,IAAa,gBAAb,MAA2B;CAGzB,AAAO,cAAc;EACnB,KAAK,SAAS,UAAU;GACtB,qBAAqB;GACrB,eAAe;EACjB,CAAC;CACH;;;;CAKA,AAAO,SAAS,QAAuB;EAErC,KAAK,MAAM,SAAS,QAClB,IAAI,MAAM,WAAW,OACnB,KAAK,MAAM,UAAU,aACnB,KAAK,cAAc;GACjB,GAAG;GACH;EACF,CAAC;OAGH,KAAK,cAAc,KAAK;CAG9B;;;;CAKA,AAAO,cAAc,OAAoB;EACvC,KAAK,OAAO,GAAG,MAAM,QAAsB,MAAM,OAAO,KAAK,KAAK,WAAW;GAE3E,OAAO;IAAE;IAAO;GAAO;EACzB,CAAC;CACH;;;;;CAMA,AAAO,KACL,QACA,KACyD;EAEzD,MAAM,OAAO,IAAI,MAAM,GAAG,CAAC,CAAC;EAE5B,MAAM,QAAQ,KAAK,OAAO,KAAK,QAAsB,IAAI;EAEzD,IAAI,CAAC,OACH,OAAO;EAKT,OAAO,MAAM,QAAQ,MAAa,MAAa,MAAM,QAAQ,MAAM,OAAO,CAAC,CAAC;CAC9E;;;;CAKA,AAAO,gBAAwB;EAC7B,OAAO,KAAK,OACT,YAAY,CAAC,CACb,MAAM,IAAI,CAAC,CACX,QAAQ,SAAS,KAAK,KAAK,CAAC,CAAC,CAAC;CACnC;AACF"}
1
+ {"version":3,"file":"route-registry.mjs","names":[],"sources":["../../../../../../../core/src/router/route-registry.ts"],"sourcesContent":["import FindMyWay, { type HTTPMethod, type Instance } from \"find-my-way\";\nimport { normalizeRequestPath } from \"./normalize-request-path\";\nimport type { RequestMethod, Route } from \"./types\";\n\n/**\n * The concrete HTTP verbs an `all` route expands into.\n * Mirrors the set Fastify's `.all()` answers in production, so `router.any()`\n * routes behave identically under the dev server and prod Fastify.\n */\nconst ALL_METHODS: Exclude<RequestMethod, \"all\">[] = [\n \"GET\",\n \"POST\",\n \"PUT\",\n \"PATCH\",\n \"DELETE\",\n \"OPTIONS\",\n \"HEAD\",\n];\n\n/**\n * Route Registry\n * Manages dynamic route matching using find-my-way for HMR support\n */\nexport class RouteRegistry {\n private router: Instance<any>;\n\n public constructor() {\n this.router = FindMyWay({\n // Request paths are normalized explicitly in `find`; leaving this off\n // makes that shared normalizer, rather than a second router option, the\n // reason development accepts a terminal slash.\n ignoreTrailingSlash: false,\n caseSensitive: false,\n });\n }\n\n /**\n * Register all routes from the router's internal list\n */\n public register(routes: Route[]): void {\n // Register each route\n for (const route of routes) {\n if (route.method === \"all\") {\n for (const method of ALL_METHODS) {\n this.registerRoute({\n ...route,\n method,\n });\n }\n } else {\n this.registerRoute(route);\n }\n }\n }\n\n /**\n * Register a single route\n */\n public registerRoute(route: Route): void {\n this.router.on(route.method as HTTPMethod, route.path, (req, res, params) => {\n // Store the route and params for later use\n return { route, params };\n });\n }\n\n /**\n * Find a matching route for the given method and URL\n * @returns Matched route with extracted params, or null if no match\n */\n public find(\n method: string,\n url: string,\n ): { route: Route; params: Record<string, string> } | null {\n // Strip query string from URL (find-my-way expects just the path)\n const path = normalizeRequestPath(url.split(\"?\")[0]);\n\n const match = this.router.find(method as HTTPMethod, path);\n\n if (!match) {\n return null;\n }\n\n // find-my-way handler expects (req, res, params, store, searchParams)\n // We only care about the return value which contains { route, params }\n return match.handler(null as any, null as any, match.params, match.store, {});\n }\n\n /**\n * Get all registered routes count (for debugging)\n */\n public getRouteCount(): number {\n return this.router\n .prettyPrint()\n .split(\"\\n\")\n .filter((line) => line.trim()).length;\n }\n}\n"],"mappings":";;;;;;;;;AASA,MAAM,cAA+C;CACnD;CACA;CACA;CACA;CACA;CACA;CACA;AACF;;;;;AAMA,IAAa,gBAAb,MAA2B;CAGzB,AAAO,cAAc;EACnB,KAAK,SAAS,UAAU;GAItB,qBAAqB;GACrB,eAAe;EACjB,CAAC;CACH;;;;CAKA,AAAO,SAAS,QAAuB;EAErC,KAAK,MAAM,SAAS,QAClB,IAAI,MAAM,WAAW,OACnB,KAAK,MAAM,UAAU,aACnB,KAAK,cAAc;GACjB,GAAG;GACH;EACF,CAAC;OAGH,KAAK,cAAc,KAAK;CAG9B;;;;CAKA,AAAO,cAAc,OAAoB;EACvC,KAAK,OAAO,GAAG,MAAM,QAAsB,MAAM,OAAO,KAAK,KAAK,WAAW;GAE3E,OAAO;IAAE;IAAO;GAAO;EACzB,CAAC;CACH;;;;;CAMA,AAAO,KACL,QACA,KACyD;EAEzD,MAAM,OAAO,qBAAqB,IAAI,MAAM,GAAG,EAAE,EAAE;EAEnD,MAAM,QAAQ,KAAK,OAAO,KAAK,QAAsB,IAAI;EAEzD,IAAI,CAAC,OACH,OAAO;EAKT,OAAO,MAAM,QAAQ,MAAa,MAAa,MAAM,QAAQ,MAAM,OAAO,CAAC,CAAC;CAC9E;;;;CAKA,AAAO,gBAAwB;EAC7B,OAAO,KAAK,OACT,YAAY,EACZ,MAAM,IAAI,EACV,QAAQ,SAAS,KAAK,KAAK,CAAC,EAAE;CACnC;AACF"}
@@ -19,6 +19,11 @@ declare class Router {
19
19
  * paths that no longer exist. A version counter cannot miss that.
20
20
  */
21
21
  private routesVersion;
22
+ /**
23
+ * In-progress atomic replacement, when one exists. Reads continue to use
24
+ * {@link routes}; mutations and collision checks use this isolated draft.
25
+ */
26
+ private routeReplacementTransaction?;
22
27
  /**
23
28
  * Router Instance
24
29
  */
@@ -169,6 +174,15 @@ declare class Router {
169
174
  * @param sourceFile Relative path to the source file
170
175
  */
171
176
  removeRoutesBySourceFile(sourceFile: string): void;
177
+ /**
178
+ * Atomically replace every route owned by the given exact source-file keys.
179
+ *
180
+ * The installer mutates an isolated draft. Public reads keep seeing the old
181
+ * live table across awaits, and a failed installer discards every draft
182
+ * mutation. A successful installer swaps the whole draft in one synchronous
183
+ * commit and invalidates the dev route registry exactly once.
184
+ */
185
+ replaceRoutesBySourceFiles<T>(sourceFiles: Iterable<string>, install: () => T | Promise<T>): Promise<T>;
172
186
  /**
173
187
  * Manage validation system for the given resource
174
188
  */
@@ -1 +1 @@
1
- {"version":3,"file":"router.d.mts","names":[],"sources":["../../../../../../../core/src/router/router.ts"],"mappings":";;;;;;;cA4Ha,MAAA;;AAAb;;UAIU,MAAA;EAoBqB;;;;;;;;EAAA,QAVrB,aAAA;EAuDgD;;;EAAA,eAlDzC,QAAA;EAwHE;;;EAAA,UAnHP,iBAAA,EAAmB,oBAAA;EA4IlB;;;EAAA,UAvID,cAAA,EAAgB,MAAA,WAAiB,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAQ,eAAA;EAqPjC;;;;EAAA,UA9OxB,MAAA,EAAQ,YAAA;EA4P6C;;;EAAA,OAnPjD,WAAA,IAAW,MAAA;EAAA,QAQlB,WAAA;EAgQ4B;;;EAzP5B,cAAA,CAAe,QAAA,GAAW,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAQ,eAAA;EAuQrB;;;EA9P7B,aAAA,CAAc,QAAA,GAAW,MAAA,EAAQ,MAAA,EAAQ,MAAA,EAAQ,eAAA;EAgR7C;;;EAvQJ,QAAA,CAAS,IAAA,UAAc,EAAA,UAAY,YAAA;EAyQpB;;;EAhQf,SAAA,CAAU,OAAA,EAAS,oBAAA;EAuW4B;;;EA9V/C,IAAA,CAAK,IAAA,UAAc,QAAA,UAAkB,SAAA;EA+ajC;;;EAtaJ,UAAA,CAAW,IAAA,UAAc,QAAA,UAAkB,SAAA;EAgmBrB;;;EAvlBtB,KAAA,CAAM,KAAA,EAAO,MAAA,kBAAwB,SAAA;EApGpC;;;EA+GD,WAAA,CAAY,KAAA,EAAO,MAAA,kBAAwB,SAAA;EAhGxC;;;EA2GH,KAAA,CACL,IAAA,UACA,OAAA,UACA,OAAA,GAAU,IAAA,CAAK,uBAAA;EAEV,KAAA,CAAM,OAAA,EAAS,uBAAA;EAhHqC;;;EAoIpD,GAAA,CACL,MAAA,EAAQ,KAAA,YACR,IAAA,qBACA,OAAA,EAAS,kBAAA,EACT,OAAA,GAAS,YAAA;EAxHc;;;EA8NlB,GAAA,CAAI,IAAA,UAAc,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EA/M9B;;;EAsN1B,GAAA,CAAI,IAAA,UAAc,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EA7MxD;;;EAoNA,IAAA,CAAK,IAAA,qBAAyB,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EApN3B;;;EA2NzC,GAAA,CAAI,IAAA,UAAc,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EAlNjC;;;EAyNvB,MAAA,CAAO,IAAA,qBAAyB,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EAhN5D;;;EAuNV,KAAA,CAAM,IAAA,UAAc,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EA9MrB;;;EAqNrC,IAAA,CAAK,IAAA,UAAc,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EA5Md;;;EAmN3C,OAAA,CAAQ,IAAA,UAAc,OAAA,EAAS,kBAAA,EAAoB,OAAA,GAAS,YAAA;EA1MvB;;;EAiNrC,KAAA,CAAM,IAAA,UAAc,OAAA,GAAS,YAAA,GAAiB,YAAA;EAtMH;;;;;;;;;;EAoN3C,eAAA,CACL,IAAA,UACA,QAAA,EAAU,aAAA,EACV,OAAA,GAAS,YAAA;IACP,IAAA,GAAO,cAAA;IACP,MAAA,GAAS,cAAA;IACT,OAAA,GAAU,OAAA,CAAQ,MAAA,CAAO,cAAA,EAAgB,cAAA;MACvC,UAAA,GAAa,cAAA;IAAA;EAAA;EAnLjB;;;EAyRK,KAAA,CAAM,OAAA,EAAS,oBAAA,EAAsB,QAAA,EAAU,mBAAA;EAnL7B;;;EAuOlB,MAAA,CAAO,MAAA,UAAgB,QAAA;EAhOnB;;;;;;;;;;;;EAgPJ,OAAA,CAAQ,OAAA,mBAA0B,QAAA;EAlOP;;;;;;EA4OrB,cAAA,UACX,UAAA,UACA,QAAA,QAAgB,CAAA,GAAI,OAAA,CAAQ,CAAA,IAC3B,OAAA,CAAQ,CAAA;EAxO4B;;;;;EAyQhC,wBAAA,CAAyB,UAAA;EAlQL;;;EAAA,QA+QnB,gBAAA;EAxQI;;;EAoVL,IAAA,IAAI,KAAA;EApV4C;;;;;;;;EAgWhD,UAAA;EAlV6B;;;EAyV7B,IAAA,CAAK,MAAA,EAAQ,eAAA;EA1UlB;;;;EAuXK,aAAA,CAAc,MAAA,EAAQ,eAAA;EApXlB;;;EA2eJ,QAAA,CAAS,IAAA,UAAc,MAAA;EAzehB;;;EAAA,QA8fN,WAAA;AAAA;AAAA,cA4CG,MAAA,EAAM,MAAuB"}
1
+ {"version":3,"file":"router.d.mts","names":[],"sources":["../../../../../../../core/src/router/router.ts"],"mappings":"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"}
@@ -1,4 +1,6 @@
1
+ import { normalizeRoutePath } from "./normalize-route-path.mjs";
1
2
  import { forgetPositionalHandlerSuspects, inspectHandlerSignature, reportPositionalHandlerSuspects } from "./positional-handler-diagnostics.mjs";
3
+ import { routeNameMethodSuffix } from "./route-name-method-suffix.mjs";
2
4
  import { RouteRegistry } from "./route-registry.mjs";
3
5
  import { container } from "../container/index.mjs";
4
6
  import { Response } from "../http/response.mjs";
@@ -9,9 +11,9 @@ import { RouteBuilder } from "./route-builder.mjs";
9
11
  import { log } from "@warlock.js/logger";
10
12
  import { isEmpty } from "@mongez/supportive-is";
11
13
  import { ltrim, merge, toCamelCase, trim } from "@mongez/reinforcements";
14
+ import concatRoute from "@mongez/concat-route";
12
15
  import proxy from "@fastify/http-proxy";
13
16
  import fastifyStatic from "@fastify/static";
14
- import concatRoute from "@mongez/concat-route";
15
17
 
16
18
  //#region ../core/src/router/router.ts
17
19
  /**
@@ -51,10 +53,9 @@ async function runRouteHooks(route, phase, request, reply) {
51
53
  * error is only actionable if it says which claimant is which, so `isPage` —
52
54
  * the router's only route-kind discriminator — is what separates them here.
53
55
  *
54
- * `sourceFile` is stamped only on routes registered through `withSourceFile`,
55
- * which page routes are not (the stack is a single slot, so page installation
56
- * cannot nest inside a route file's scope without clobbering it). Say the
57
- * source is unknown rather than printing an empty string.
56
+ * `sourceFile` is stamped only on routes registered through `withSourceFile`.
57
+ * Routes registered without that scope have no owner, so say the source is
58
+ * unknown rather than printing an empty string.
58
59
  */
59
60
  function describeRouteClaimant(route) {
60
61
  const type = route.isPage ? "page route" : "API route";
@@ -166,7 +167,7 @@ var Router = class Router {
166
167
  const name = this.stacks.name.reduceRight((name, prefixName) => {
167
168
  return trim(prefixName + "." + name, ".");
168
169
  }, options.name || trim(path.replace(/\//g, "."), "."));
169
- path = concatRoute(prefix, path);
170
+ path = normalizeRoutePath(prefix, path);
170
171
  if ((options.middlewarePrecedence || "after") === "before") options.middleware = [...options.middleware || [], ...this.stacks.middleware];
171
172
  else options.middleware = [...this.stacks.middleware, ...options.middleware || []];
172
173
  if (Array.isArray(handler)) {
@@ -190,14 +191,16 @@ var Router = class Router {
190
191
  $prefixStack: [...this.stacks.prefix],
191
192
  sourceFile: this.stacks.sourceFile || ""
192
193
  };
194
+ const routes = this.routeReplacementTransaction?.routes ?? this.routes;
193
195
  if (routeData.name) {
194
- const route = this.routes.find((route) => route.name === routeData.name);
196
+ const route = routes.find((route) => route.name === routeData.name);
195
197
  if (route) if (route.method === routeData.method) throw new Error(`Route name "${routeData.name}" is already taken.\n already registered: ${describeRouteClaimant(route)}\n now being added: ${describeRouteClaimant(routeData)}\nPages and API routes share one route-name namespace, so a name can only be claimed once. Rename one of them via the "name" route option.`);
196
- else routeData.name += `.${routeData.method.toLowerCase()}`;
198
+ else routeData.name += routeNameMethodSuffix(routeData.method);
197
199
  }
198
- inspectHandlerSignature(routeData.handler, routeData);
199
- this.routes.push(routeData);
200
- this.routesVersion++;
200
+ if (this.routeReplacementTransaction) this.routeReplacementTransaction.addedRoutes.push(routeData);
201
+ else inspectHandlerSignature(routeData.handler, routeData);
202
+ routes.push(routeData);
203
+ if (!this.routeReplacementTransaction) this.routesVersion++;
201
204
  return this;
202
205
  }
203
206
  /**
@@ -391,11 +394,47 @@ var Router = class Router {
391
394
  * @param sourceFile Relative path to the source file
392
395
  */
393
396
  removeRoutesBySourceFile(sourceFile) {
397
+ if (this.routeReplacementTransaction) {
398
+ this.routeReplacementTransaction.routes = this.routeReplacementTransaction.routes.filter((route) => route.sourceFile !== sourceFile);
399
+ this.routeReplacementTransaction.removedSourceFiles.add(sourceFile);
400
+ return;
401
+ }
394
402
  this.routes = this.routes.filter((route) => route.sourceFile !== sourceFile);
395
403
  forgetPositionalHandlerSuspects(sourceFile);
396
404
  this.routesVersion++;
397
405
  }
398
406
  /**
407
+ * Atomically replace every route owned by the given exact source-file keys.
408
+ *
409
+ * The installer mutates an isolated draft. Public reads keep seeing the old
410
+ * live table across awaits, and a failed installer discards every draft
411
+ * mutation. A successful installer swaps the whole draft in one synchronous
412
+ * commit and invalidates the dev route registry exactly once.
413
+ */
414
+ async replaceRoutesBySourceFiles(sourceFiles, install) {
415
+ if (this.routeReplacementTransaction) throw new Error("Cannot nest route replacement transactions");
416
+ const removedSourceFiles = new Set(sourceFiles);
417
+ this.routeReplacementTransaction = {
418
+ routes: this.routes.filter((route) => !removedSourceFiles.has(route.sourceFile)),
419
+ removedSourceFiles,
420
+ addedRoutes: []
421
+ };
422
+ try {
423
+ const result = await install();
424
+ const transaction = this.routeReplacementTransaction;
425
+ this.routes = transaction.routes;
426
+ this.routeReplacementTransaction = void 0;
427
+ for (const sourceFile of transaction.removedSourceFiles) forgetPositionalHandlerSuspects(sourceFile);
428
+ const committedRoutes = new Set(transaction.routes);
429
+ for (const route of transaction.addedRoutes) if (committedRoutes.has(route)) inspectHandlerSignature(route.handler, route);
430
+ this.routesVersion++;
431
+ return result;
432
+ } catch (error) {
433
+ this.routeReplacementTransaction = void 0;
434
+ throw error;
435
+ }
436
+ }
437
+ /**
399
438
  * Manage validation system for the given resource
400
439
  */
401
440
  manageValidation(resource, method) {