@warlock.js/core 5.19.1 → 5.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (378) hide show
  1. package/CHANGELOG.md +1136 -1073
  2. package/esm/benchmark/benchmark.mjs +34 -25
  3. package/esm/benchmark/benchmark.mjs.map +1 -1
  4. package/esm/benchmark/profiler.d.mts.map +1 -1
  5. package/esm/benchmark/profiler.mjs +8 -3
  6. package/esm/benchmark/profiler.mjs.map +1 -1
  7. package/esm/bootstrap.d.mts.map +1 -1
  8. package/esm/bootstrap.mjs +3 -0
  9. package/esm/bootstrap.mjs.map +1 -1
  10. package/esm/cache/database-cache-driver.d.mts +2 -2
  11. package/esm/cache/database-cache-driver.d.mts.map +1 -1
  12. package/esm/cache/database-cache-driver.mjs +3 -2
  13. package/esm/cache/database-cache-driver.mjs.map +1 -1
  14. package/esm/cli/cli-commands.manager.mjs +16 -14
  15. package/esm/cli/cli-commands.manager.mjs.map +1 -1
  16. package/esm/cli/commands/add.command.mjs +1 -1
  17. package/esm/cli/commands/add.command.mjs.map +1 -1
  18. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs +3 -2
  19. package/esm/cli/commands/doctor/checks/optional-peers.check.mjs.map +1 -1
  20. package/esm/cli/commands/generate/generate.command.mjs +20 -0
  21. package/esm/cli/commands/generate/generate.command.mjs.map +1 -1
  22. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs +4 -2
  23. package/esm/cli/commands/generate/generators/column-dsl-parser.mjs.map +1 -1
  24. package/esm/cli/commands/generate/generators/controller.generator.mjs +1 -1
  25. package/esm/cli/commands/generate/generators/controller.generator.mjs.map +1 -1
  26. package/esm/cli/commands/generate/generators/migration.generator.mjs +22 -6
  27. package/esm/cli/commands/generate/generators/migration.generator.mjs.map +1 -1
  28. package/esm/cli/commands/generate/generators/model.generator.mjs +22 -7
  29. package/esm/cli/commands/generate/generators/model.generator.mjs.map +1 -1
  30. package/esm/cli/commands/generate/generators/module.generator.mjs +8 -6
  31. package/esm/cli/commands/generate/generators/module.generator.mjs.map +1 -1
  32. package/esm/cli/commands/generate/generators/repository.generator.mjs +1 -1
  33. package/esm/cli/commands/generate/generators/repository.generator.mjs.map +1 -1
  34. package/esm/cli/commands/generate/generators/resource.generator.mjs +1 -1
  35. package/esm/cli/commands/generate/generators/resource.generator.mjs.map +1 -1
  36. package/esm/cli/commands/generate/generators/service.generator.mjs +1 -1
  37. package/esm/cli/commands/generate/generators/service.generator.mjs.map +1 -1
  38. package/esm/cli/commands/generate/templates/stubs.mjs +7 -7
  39. package/esm/cli/commands/generate/templates/stubs.mjs.map +1 -1
  40. package/esm/cli/commands/generate/utils/writer.mjs +25 -1
  41. package/esm/cli/commands/generate/utils/writer.mjs.map +1 -1
  42. package/esm/cli/commands/routes/routes-command.action.mjs +5 -1
  43. package/esm/cli/commands/routes/routes-command.action.mjs.map +1 -1
  44. package/esm/cli/commands/routes/with-console-on-stderr.mjs +24 -0
  45. package/esm/cli/commands/routes/with-console-on-stderr.mjs.map +1 -0
  46. package/esm/cli/commands/routes.command.mjs +3 -4
  47. package/esm/cli/commands/routes.command.mjs.map +1 -1
  48. package/esm/cli/commands/start-production.command.mjs +5 -5
  49. package/esm/cli/commands/start-production.command.mjs.map +1 -1
  50. package/esm/cli/commands/storage-put.action.mjs +2 -1
  51. package/esm/cli/commands/storage-put.action.mjs.map +1 -1
  52. package/esm/cli/commands/update.command.mjs +1 -0
  53. package/esm/cli/commands/update.command.mjs.map +1 -1
  54. package/esm/cli/parse-cli-args.mjs +13 -1
  55. package/esm/cli/parse-cli-args.mjs.map +1 -1
  56. package/esm/commands/cli-command.d.mts.map +1 -1
  57. package/esm/commands/cli-command.mjs +4 -2
  58. package/esm/commands/cli-command.mjs.map +1 -1
  59. package/esm/commands/index.mjs +3 -0
  60. package/esm/config/config-key-from-path.mjs +20 -0
  61. package/esm/config/config-key-from-path.mjs.map +1 -0
  62. package/esm/config/config-loader.d.mts.map +1 -1
  63. package/esm/config/config-loader.mjs +2 -3
  64. package/esm/config/config-loader.mjs.map +1 -1
  65. package/esm/connectors/cache-connector.d.mts.map +1 -1
  66. package/esm/connectors/cache-connector.mjs +5 -0
  67. package/esm/connectors/cache-connector.mjs.map +1 -1
  68. package/esm/connectors/connectors-manager.d.mts +4 -0
  69. package/esm/connectors/connectors-manager.d.mts.map +1 -1
  70. package/esm/connectors/connectors-manager.mjs +30 -2
  71. package/esm/connectors/connectors-manager.mjs.map +1 -1
  72. package/esm/connectors/single-server-warnings.mjs +33 -0
  73. package/esm/connectors/single-server-warnings.mjs.map +1 -0
  74. package/esm/connectors/socket-connector.d.mts +29 -0
  75. package/esm/connectors/socket-connector.d.mts.map +1 -1
  76. package/esm/connectors/socket-connector.mjs +77 -9
  77. package/esm/connectors/socket-connector.mjs.map +1 -1
  78. package/esm/connectors/storage.connector.d.mts.map +1 -1
  79. package/esm/connectors/storage.connector.mjs +6 -0
  80. package/esm/connectors/storage.connector.mjs.map +1 -1
  81. package/esm/connectors/types.d.mts +4 -2
  82. package/esm/connectors/types.d.mts.map +1 -1
  83. package/esm/connectors/types.mjs.map +1 -1
  84. package/esm/database/models/database-log/database-log.d.mts +47 -24
  85. package/esm/database/models/database-log/database-log.d.mts.map +1 -1
  86. package/esm/database/models/database-log/database-log.mjs +36 -4
  87. package/esm/database/models/database-log/database-log.mjs.map +1 -1
  88. package/esm/database/seed-command-action.mjs +10 -2
  89. package/esm/database/seed-command-action.mjs.map +1 -1
  90. package/esm/dev-server/development-server.mjs +15 -4
  91. package/esm/dev-server/development-server.mjs.map +1 -1
  92. package/esm/dev-server/file-event-handler.mjs +26 -6
  93. package/esm/dev-server/file-event-handler.mjs.map +1 -1
  94. package/esm/dev-server/file-manager.d.mts.map +1 -1
  95. package/esm/dev-server/file-manager.mjs +2 -2
  96. package/esm/dev-server/file-manager.mjs.map +1 -1
  97. package/esm/dev-server/files-orchestrator.mjs +3 -1
  98. package/esm/dev-server/files-orchestrator.mjs.map +1 -1
  99. package/esm/dev-server/files-watcher.mjs +64 -5
  100. package/esm/dev-server/files-watcher.mjs.map +1 -1
  101. package/esm/dev-server/health-checker/files-healthcare.manager.mjs +31 -9
  102. package/esm/dev-server/health-checker/files-healthcare.manager.mjs.map +1 -1
  103. package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs +11 -7
  104. package/esm/dev-server/health-checker/workers/eslint-health.worker.mjs.map +1 -1
  105. package/esm/dev-server/loader/load-hook.mjs +1 -1
  106. package/esm/dev-server/loader/register-loader.mjs +1 -1
  107. package/esm/dev-server/manifest-manager.d.mts.map +1 -1
  108. package/esm/dev-server/manifest-manager.mjs +12 -6
  109. package/esm/dev-server/manifest-manager.mjs.map +1 -1
  110. package/esm/dev-server/parse-imports.mjs +9 -0
  111. package/esm/dev-server/parse-imports.mjs.map +1 -1
  112. package/esm/dev-server/register-dev-shortcuts.mjs +2 -1
  113. package/esm/dev-server/register-dev-shortcuts.mjs.map +1 -1
  114. package/esm/dev-server/route-types-publisher.mjs +60 -0
  115. package/esm/dev-server/route-types-publisher.mjs.map +1 -0
  116. package/esm/dev-server/shortcuts.mjs +2 -2
  117. package/esm/dev-server/shortcuts.mjs.map +1 -1
  118. package/esm/dev-server/special-file-patterns.mjs +18 -0
  119. package/esm/dev-server/special-file-patterns.mjs.map +1 -0
  120. package/esm/dev-server/special-files-collector.d.mts.map +1 -1
  121. package/esm/dev-server/special-files-collector.mjs +2 -5
  122. package/esm/dev-server/special-files-collector.mjs.map +1 -1
  123. package/esm/dev-server/start-development-server.mjs +1 -2
  124. package/esm/dev-server/start-development-server.mjs.map +1 -1
  125. package/esm/dev-server/supervisor.mjs +23 -3
  126. package/esm/dev-server/supervisor.mjs.map +1 -1
  127. package/esm/dev-server/tsconfig-manager.mjs +22 -12
  128. package/esm/dev-server/tsconfig-manager.mjs.map +1 -1
  129. package/esm/dev-server/type-generator.mjs +10 -9
  130. package/esm/dev-server/type-generator.mjs.map +1 -1
  131. package/esm/encryption/encrypt.d.mts.map +1 -1
  132. package/esm/encryption/encrypt.mjs +16 -5
  133. package/esm/encryption/encrypt.mjs.map +1 -1
  134. package/esm/encryption/hash.d.mts.map +1 -1
  135. package/esm/encryption/hash.mjs +2 -0
  136. package/esm/encryption/hash.mjs.map +1 -1
  137. package/esm/encryption/password.d.mts.map +1 -1
  138. package/esm/encryption/password.mjs +3 -1
  139. package/esm/encryption/password.mjs.map +1 -1
  140. package/esm/generations/features/index.mjs +1 -1
  141. package/esm/generations/features/mysql.feature.mjs +9 -2
  142. package/esm/generations/features/mysql.feature.mjs.map +1 -1
  143. package/esm/generations/features/test.feature.mjs +2 -1
  144. package/esm/generations/features/test.feature.mjs.map +1 -1
  145. package/esm/generations/features/web.feature.mjs +4 -2
  146. package/esm/generations/features/web.feature.mjs.map +1 -1
  147. package/esm/http/build-rate-limit-options.d.mts +37 -0
  148. package/esm/http/build-rate-limit-options.d.mts.map +1 -0
  149. package/esm/http/build-rate-limit-options.mjs +37 -0
  150. package/esm/http/build-rate-limit-options.mjs.map +1 -0
  151. package/esm/http/csrf-origin-policy.d.mts.map +1 -1
  152. package/esm/http/csrf-origin-policy.mjs +5 -2
  153. package/esm/http/csrf-origin-policy.mjs.map +1 -1
  154. package/esm/http/flush-pending-cookies.mjs +52 -0
  155. package/esm/http/flush-pending-cookies.mjs.map +1 -0
  156. package/esm/http/middleware/cache-response-middleware.d.mts.map +1 -1
  157. package/esm/http/middleware/cache-response-middleware.mjs +4 -2
  158. package/esm/http/middleware/cache-response-middleware.mjs.map +1 -1
  159. package/esm/http/middleware/idempotency.middleware.d.mts +5 -0
  160. package/esm/http/middleware/idempotency.middleware.d.mts.map +1 -1
  161. package/esm/http/middleware/idempotency.middleware.mjs +35 -3
  162. package/esm/http/middleware/idempotency.middleware.mjs.map +1 -1
  163. package/esm/http/middleware/inject-request-context.d.mts.map +1 -1
  164. package/esm/http/middleware/inject-request-context.mjs +14 -3
  165. package/esm/http/middleware/inject-request-context.mjs.map +1 -1
  166. package/esm/http/middleware/rate-limit.middleware.d.mts +4 -3
  167. package/esm/http/middleware/rate-limit.middleware.d.mts.map +1 -1
  168. package/esm/http/middleware/rate-limit.middleware.mjs +4 -3
  169. package/esm/http/middleware/rate-limit.middleware.mjs.map +1 -1
  170. package/esm/http/middleware/utils/cidr-match.d.mts +1 -1
  171. package/esm/http/middleware/utils/cidr-match.d.mts.map +1 -1
  172. package/esm/http/middleware/utils/cidr-match.mjs +11 -2
  173. package/esm/http/middleware/utils/cidr-match.mjs.map +1 -1
  174. package/esm/http/middleware/utils/idempotency-key.mjs +1 -1
  175. package/esm/http/middleware/utils/idempotency-key.mjs.map +1 -1
  176. package/esm/http/plugins.d.mts.map +1 -1
  177. package/esm/http/plugins.mjs +13 -5
  178. package/esm/http/plugins.mjs.map +1 -1
  179. package/esm/http/request.d.mts +12 -1
  180. package/esm/http/request.d.mts.map +1 -1
  181. package/esm/http/request.mjs +35 -7
  182. package/esm/http/request.mjs.map +1 -1
  183. package/esm/http/response.d.mts +16 -0
  184. package/esm/http/response.d.mts.map +1 -1
  185. package/esm/http/response.mjs +75 -37
  186. package/esm/http/response.mjs.map +1 -1
  187. package/esm/http/types.d.mts +26 -14
  188. package/esm/http/types.d.mts.map +1 -1
  189. package/esm/http/uploads/uploaded-file.controller.mjs +1 -1
  190. package/esm/http/uploads-config.mjs +2 -1
  191. package/esm/http/uploads-config.mjs.map +1 -1
  192. package/esm/http/uploads-types.d.mts +3 -3
  193. package/esm/image/image.d.mts +1 -1
  194. package/esm/image/image.d.mts.map +1 -1
  195. package/esm/image/image.mjs +25 -15
  196. package/esm/image/image.mjs.map +1 -1
  197. package/esm/index.d.mts +6 -4
  198. package/esm/index.mjs +7 -4
  199. package/esm/logger/logger.mjs +6 -1
  200. package/esm/logger/logger.mjs.map +1 -1
  201. package/esm/mail/config.d.mts.map +1 -1
  202. package/esm/mail/config.mjs +31 -21
  203. package/esm/mail/config.mjs.map +1 -1
  204. package/esm/mail/react-mail.mjs +6 -3
  205. package/esm/mail/react-mail.mjs.map +1 -1
  206. package/esm/mail/send-mail.d.mts.map +1 -1
  207. package/esm/mail/send-mail.mjs +40 -19
  208. package/esm/mail/send-mail.mjs.map +1 -1
  209. package/esm/production/assert-generated-imports.mjs +1 -0
  210. package/esm/production/assert-generated-imports.mjs.map +1 -1
  211. package/esm/production/build-contributions.mjs +3 -0
  212. package/esm/production/build-contributions.mjs.map +1 -1
  213. package/esm/production/esbuild-plugins.mjs +3 -2
  214. package/esm/production/esbuild-plugins.mjs.map +1 -1
  215. package/esm/production/production-builder.mjs +49 -37
  216. package/esm/production/production-builder.mjs.map +1 -1
  217. package/esm/production/production-supervisor.mjs +12 -5
  218. package/esm/production/production-supervisor.mjs.map +1 -1
  219. package/esm/production/route-registration-child.d.mts +1 -0
  220. package/esm/production/route-registration-child.mjs +65 -0
  221. package/esm/production/route-registration-child.mjs.map +1 -0
  222. package/esm/production/route-registration-snapshot.mjs +166 -0
  223. package/esm/production/route-registration-snapshot.mjs.map +1 -0
  224. package/esm/repositories/adapters/cascade/cascade-adapter.d.mts +3 -1
  225. package/esm/repositories/adapters/cascade/cascade-adapter.d.mts.map +1 -1
  226. package/esm/repositories/adapters/cascade/cascade-adapter.mjs +22 -12
  227. package/esm/repositories/adapters/cascade/cascade-adapter.mjs.map +1 -1
  228. package/esm/repositories/adapters/cascade/cascade-query-builder.d.mts +3 -3
  229. package/esm/repositories/adapters/cascade/cascade-query-builder.mjs.map +1 -1
  230. package/esm/repositories/adapters/cascade/filter-applicator.mjs +88 -61
  231. package/esm/repositories/adapters/cascade/filter-applicator.mjs.map +1 -1
  232. package/esm/repositories/adapters/cascade/index.d.mts +1 -1
  233. package/esm/repositories/adapters/cascade/index.mjs +1 -1
  234. package/esm/repositories/contracts/query-builder.contract.d.mts +3 -3
  235. package/esm/repositories/contracts/types.d.mts +5 -0
  236. package/esm/repositories/contracts/types.d.mts.map +1 -1
  237. package/esm/repositories/index.d.mts +1 -1
  238. package/esm/repositories/index.mjs +1 -1
  239. package/esm/repositories/repository.manager.d.mts +65 -3
  240. package/esm/repositories/repository.manager.d.mts.map +1 -1
  241. package/esm/repositories/repository.manager.mjs +146 -57
  242. package/esm/repositories/repository.manager.mjs.map +1 -1
  243. package/esm/resource/resource-field-builder.d.mts.map +1 -1
  244. package/esm/resource/resource-field-builder.mjs +1 -0
  245. package/esm/resource/resource-field-builder.mjs.map +1 -1
  246. package/esm/resource/resource.d.mts +10 -3
  247. package/esm/resource/resource.d.mts.map +1 -1
  248. package/esm/resource/resource.mjs +19 -5
  249. package/esm/resource/resource.mjs.map +1 -1
  250. package/esm/restful/restful.d.mts +12 -5
  251. package/esm/restful/restful.d.mts.map +1 -1
  252. package/esm/restful/restful.mjs +47 -16
  253. package/esm/restful/restful.mjs.map +1 -1
  254. package/esm/router/build-route-rate-limit.mjs +22 -0
  255. package/esm/router/build-route-rate-limit.mjs.map +1 -0
  256. package/esm/router/dev-dispatch.mjs +60 -0
  257. package/esm/router/dev-dispatch.mjs.map +1 -0
  258. package/esm/router/route-builder.mjs +4 -1
  259. package/esm/router/route-builder.mjs.map +1 -1
  260. package/esm/router/route-registry.d.mts.map +1 -1
  261. package/esm/router/route-registry.mjs +2 -2
  262. package/esm/router/route-registry.mjs.map +1 -1
  263. package/esm/router/router.d.mts +1 -1
  264. package/esm/router/router.d.mts.map +1 -1
  265. package/esm/router/router.mjs +57 -39
  266. package/esm/router/router.mjs.map +1 -1
  267. package/esm/socket/index.d.mts +1 -1
  268. package/esm/socket/types.d.mts +25 -3
  269. package/esm/socket/types.d.mts.map +1 -1
  270. package/esm/storage/drivers/cloud-driver.d.mts +26 -0
  271. package/esm/storage/drivers/cloud-driver.d.mts.map +1 -1
  272. package/esm/storage/drivers/cloud-driver.mjs +100 -16
  273. package/esm/storage/drivers/cloud-driver.mjs.map +1 -1
  274. package/esm/storage/drivers/do-spaces-driver.d.mts +7 -0
  275. package/esm/storage/drivers/do-spaces-driver.d.mts.map +1 -1
  276. package/esm/storage/drivers/do-spaces-driver.mjs +9 -2
  277. package/esm/storage/drivers/do-spaces-driver.mjs.map +1 -1
  278. package/esm/storage/drivers/local-driver.d.mts +16 -5
  279. package/esm/storage/drivers/local-driver.d.mts.map +1 -1
  280. package/esm/storage/drivers/local-driver.mjs +71 -28
  281. package/esm/storage/drivers/local-driver.mjs.map +1 -1
  282. package/esm/storage/index.d.mts +3 -1
  283. package/esm/storage/index.mjs +4 -2
  284. package/esm/storage/scoped-storage.d.mts +27 -1
  285. package/esm/storage/scoped-storage.d.mts.map +1 -1
  286. package/esm/storage/scoped-storage.mjs +40 -8
  287. package/esm/storage/scoped-storage.mjs.map +1 -1
  288. package/esm/storage/storage.d.mts +11 -5
  289. package/esm/storage/storage.d.mts.map +1 -1
  290. package/esm/storage/storage.mjs +39 -16
  291. package/esm/storage/storage.mjs.map +1 -1
  292. package/esm/storage/types.d.mts +43 -4
  293. package/esm/storage/types.d.mts.map +1 -1
  294. package/esm/storage/utils/classify-ip-address.d.mts +13 -0
  295. package/esm/storage/utils/classify-ip-address.d.mts.map +1 -0
  296. package/esm/storage/utils/classify-ip-address.mjs +98 -0
  297. package/esm/storage/utils/classify-ip-address.mjs.map +1 -0
  298. package/esm/storage/utils/safe-fetch.d.mts +3 -12
  299. package/esm/storage/utils/safe-fetch.d.mts.map +1 -1
  300. package/esm/storage/utils/safe-fetch.mjs +3 -48
  301. package/esm/storage/utils/safe-fetch.mjs.map +1 -1
  302. package/esm/storage/utils/storage-capability-error.d.mts +25 -0
  303. package/esm/storage/utils/storage-capability-error.d.mts.map +1 -0
  304. package/esm/storage/utils/storage-capability-error.mjs +26 -0
  305. package/esm/storage/utils/storage-capability-error.mjs.map +1 -0
  306. package/esm/updater/update-warlock-packages.mjs +11 -1
  307. package/esm/updater/update-warlock-packages.mjs.map +1 -1
  308. package/esm/use-cases/use-case-broadcast.mjs +7 -1
  309. package/esm/use-cases/use-case-broadcast.mjs.map +1 -1
  310. package/esm/use-cases/use-cases-registry.mjs +21 -2
  311. package/esm/use-cases/use-cases-registry.mjs.map +1 -1
  312. package/esm/utils/database-log.mjs +0 -3
  313. package/esm/utils/database-log.mjs.map +1 -1
  314. package/esm/utils/queue.d.mts +6 -4
  315. package/esm/utils/queue.d.mts.map +1 -1
  316. package/esm/utils/queue.mjs +20 -9
  317. package/esm/utils/queue.mjs.map +1 -1
  318. package/esm/validation/database/exists-except-current-id.mjs +6 -2
  319. package/esm/validation/database/exists-except-current-id.mjs.map +1 -1
  320. package/esm/validation/database/unique-except-current-id.mjs +6 -2
  321. package/esm/validation/database/unique-except-current-id.mjs.map +1 -1
  322. package/esm/validation/init.mjs +6 -2
  323. package/esm/validation/init.mjs.map +1 -1
  324. package/esm/validation/validateAll.mjs +3 -2
  325. package/esm/validation/validateAll.mjs.map +1 -1
  326. package/esm/warlock-config/types.d.mts +14 -1
  327. package/esm/warlock-config/types.d.mts.map +1 -1
  328. package/esm/warlock-config/warlock-config.manager.d.mts.map +1 -1
  329. package/esm/warlock-config/warlock-config.manager.mjs +3 -1
  330. package/esm/warlock-config/warlock-config.manager.mjs.map +1 -1
  331. package/esm/warlock-config/warn-deprecated-config.mjs +25 -0
  332. package/esm/warlock-config/warn-deprecated-config.mjs.map +1 -0
  333. package/llms-full.txt +71 -42
  334. package/llms.txt +41 -41
  335. package/package.json +21 -15
  336. package/skills/add-connector/SKILL.md +5 -1
  337. package/skills/benchmark-code/SKILL.md +1 -1
  338. package/skills/build-restful/SKILL.md +1 -1
  339. package/skills/build-url/SKILL.md +1 -1
  340. package/skills/configure-app/SKILL.md +7 -1
  341. package/skills/create-controller/SKILL.md +1 -1
  342. package/skills/create-module/SKILL.md +1 -1
  343. package/skills/define-resource/SKILL.md +1 -1
  344. package/skills/encrypt-data/SKILL.md +1 -1
  345. package/skills/hash-password/SKILL.md +1 -1
  346. package/skills/health-checks/SKILL.md +1 -1
  347. package/skills/index.md +30 -0
  348. package/skills/lower-stage3-decorators/SKILL.md +1 -1
  349. package/skills/process-image/SKILL.md +1 -1
  350. package/skills/register-route/SKILL.md +1 -1
  351. package/skills/request-memo/SKILL.md +1 -1
  352. package/skills/request-tracing/SKILL.md +1 -1
  353. package/skills/resolve-path/SKILL.md +1 -1
  354. package/skills/retry-operation/SKILL.md +1 -1
  355. package/skills/run-app/SKILL.md +1 -1
  356. package/skills/send-mail/SKILL.md +1 -1
  357. package/skills/send-response/SKILL.md +1 -1
  358. package/skills/store-file/SKILL.md +11 -1
  359. package/skills/test-http/SKILL.md +1 -1
  360. package/skills/test-service/SKILL.md +1 -1
  361. package/skills/update-packages/SKILL.md +1 -1
  362. package/skills/upload-file/SKILL.md +1 -1
  363. package/skills/use-app-context/SKILL.md +1 -1
  364. package/skills/use-localization/SKILL.md +1 -1
  365. package/skills/use-middleware/SKILL.md +7 -2
  366. package/skills/use-model-transformers/SKILL.md +1 -1
  367. package/skills/use-repository/SKILL.md +1 -1
  368. package/skills/use-request-locals/SKILL.md +1 -1
  369. package/skills/validate-input/SKILL.md +1 -1
  370. package/skills/warlock-conventions/SKILL.md +1 -1
  371. package/skills/warlock-doctor/SKILL.md +1 -1
  372. package/skills/warlock-routes/SKILL.md +1 -1
  373. package/skills/wire-socket/SKILL.md +5 -1
  374. package/skills/write-cli-command/SKILL.md +1 -1
  375. package/skills/write-middleware/SKILL.md +1 -1
  376. package/skills/write-seeder/SKILL.md +1 -1
  377. package/skills/write-use-case/SKILL.md +1 -1
  378. package/esm/_virtual/_rolldown/runtime.mjs +0 -7
@@ -2,6 +2,7 @@ import { Path } from "../utils/normalized-path.mjs";
2
2
  import { devLogSuccess } from "./dev-logger.mjs";
3
3
  import { isTimingsEnabled } from "./flags.mjs";
4
4
  import { clearFileExistsCache } from "./parse-imports.mjs";
5
+ import { isCodeFile } from "./special-file-patterns.mjs";
5
6
  import events from "@mongez/events";
6
7
 
7
8
  //#region ../core/src/dev-server/file-event-handler.ts
@@ -23,6 +24,15 @@ var FileEventHandler = class {
23
24
  this.pendingChanges = /* @__PURE__ */ new Set();
24
25
  this.pendingAdds = /* @__PURE__ */ new Set();
25
26
  this.pendingDeletes = /* @__PURE__ */ new Set();
27
+ this.batchQueue = Promise.resolve();
28
+ this.includeMatcher = () => false;
29
+ }
30
+ /**
31
+ * Non-code files in the include set (`.sql` fixtures, JSON config) can't be
32
+ * hot-reloaded, so they are reported as restart triggers instead.
33
+ */
34
+ setIncludeMatcher(matcher) {
35
+ this.includeMatcher = matcher;
26
36
  }
27
37
  handleFileChange(absolutePath, settleMs) {
28
38
  this.recordTimingStart(settleMs);
@@ -53,7 +63,9 @@ var FileEventHandler = class {
53
63
  clearTimeout(this.maxWaitTimer);
54
64
  this.maxWaitTimer = void 0;
55
65
  }
56
- this.processBatch();
66
+ this.batchQueue = this.batchQueue.then(() => this.processBatch()).catch((error) => {
67
+ console.error("[dev-server] batch processing failed", error);
68
+ });
57
69
  }
58
70
  /**
59
71
  * Mark the debounce-wait phase start on the first event of a batch, and
@@ -78,10 +90,17 @@ var FileEventHandler = class {
78
90
  this.batchStartedAt = void 0;
79
91
  this.watcherSettleMs = void 0;
80
92
  if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;
81
- const externalChanges = changes.filter(isExternalPath);
82
- const externalAdds = adds.filter(isExternalPath);
83
- const codeChanges = changes.filter((p) => !isExternalPath(p));
84
- const codeAdds = adds.filter((p) => !isExternalPath(p));
93
+ const isIncludedData = (p) => !isCodeFile(p) && this.includeMatcher(p);
94
+ const isExternal = (p) => isExternalPath(p) || isIncludedData(p);
95
+ const externalChanges = changes.filter(isExternal);
96
+ const externalAdds = adds.filter(isExternal);
97
+ const codeChanges = changes.filter((p) => !isExternal(p));
98
+ const codeAdds = adds.filter((p) => !isExternal(p));
99
+ const restartTriggers = [
100
+ ...changes,
101
+ ...adds,
102
+ ...deletes
103
+ ].filter(isIncludedData);
85
104
  if (codeAdds.length + codeChanges.length > 1) {
86
105
  await new Promise((resolve) => setTimeout(resolve, 500));
87
106
  clearFileExistsCache();
@@ -95,7 +114,8 @@ var FileEventHandler = class {
95
114
  events.trigger("dev-server:batch-complete", {
96
115
  added: [...externalAdds, ...addedCodePaths],
97
116
  changed: [...externalChanges, ...changedCodePaths],
98
- deleted: [...deletes, ...vanished],
117
+ deleted: [...deletes.filter((p) => !isIncludedData(p)), ...vanished],
118
+ restartTriggers,
99
119
  timings: isTimingsEnabled() ? {
100
120
  watcherSettleMs: watcherSettleMs ?? 0,
101
121
  debounceWaitMs: debounceWaitMs ?? 0
@@ -1 +1 @@
1
- {"version":3,"file":"file-event-handler.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-event-handler.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogSuccess } from \"./dev-logger\";\nimport type { FileManager } from \"./file-manager\";\nimport type { FileOperations } from \"./file-operations\";\nimport { FILE_PROCESSING_BATCH_SIZE, isTimingsEnabled } from \"./flags\";\nimport type { ManifestManager } from \"./manifest-manager\";\nimport { clearFileExistsCache } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\n\n/** Lets an isolated editor save reach HMR without the former fixed 50ms delay. */\nconst ISOLATED_SAVE_QUIET_WINDOW_MS = 12;\n\n/** Bounds a sustained formatter or checkout stream so it cannot postpone HMR forever. */\nconst BATCH_MAX_WAIT_MS = 60;\n\n/**\n * Receives raw watcher events and processes them in a single debounced batch.\n * Order within a batch: adds → changes → deletes, so changes can reference\n * newly-added files and deletes fire last.\n */\nexport class FileEventHandler {\n private pendingChanges = new Set<string>();\n private pendingAdds = new Set<string>();\n private pendingDeletes = new Set<string>();\n\n /** When this batch's first watcher event arrived — the debounce-wait phase start. */\n private batchStartedAt?: number;\n\n /** Slowest watcher-settle duration seen so far this batch, when `devServer.timings` is on. */\n private watcherSettleMs?: number;\n\n private debounceTimer?: ReturnType<typeof setTimeout>;\n private maxWaitTimer?: ReturnType<typeof setTimeout>;\n\n constructor(\n private readonly fileOperations: FileOperations,\n private readonly manifest: ManifestManager,\n private readonly dependencyGraph: DependencyGraph,\n private readonly files: Map<string, FileManager>,\n ) {}\n\n public handleFileChange(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingChanges.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileAdd(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingAdds.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileDelete(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingDeletes.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n private schedulePendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n }\n\n this.debounceTimer = setTimeout(() => this.flushPendingEvents(), ISOLATED_SAVE_QUIET_WINDOW_MS);\n\n if (this.maxWaitTimer === undefined) {\n this.maxWaitTimer = setTimeout(() => this.flushPendingEvents(), BATCH_MAX_WAIT_MS);\n }\n }\n\n private flushPendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = undefined;\n }\n\n if (this.maxWaitTimer !== undefined) {\n clearTimeout(this.maxWaitTimer);\n this.maxWaitTimer = undefined;\n }\n\n void this.processBatch();\n }\n\n /**\n * Mark the debounce-wait phase start on the first event of a batch, and\n * track the slowest watcher-settle duration seen this batch. `settleMs`\n * only arrives when `devServer.timings` is on (see `FilesWatcher`), so\n * this is a no-op past the cheap `performance.now()` call when it's off.\n */\n private recordTimingStart(settleMs?: number): void {\n if (this.batchStartedAt === undefined) {\n this.batchStartedAt = performance.now();\n }\n\n if (settleMs === undefined) return;\n\n this.watcherSettleMs =\n this.watcherSettleMs === undefined ? settleMs : Math.max(this.watcherSettleMs, settleMs);\n }\n\n private async processBatch(): Promise<void> {\n const changes = Array.from(this.pendingChanges);\n const adds = Array.from(this.pendingAdds);\n const deletes = Array.from(this.pendingDeletes);\n\n this.pendingChanges.clear();\n this.pendingAdds.clear();\n this.pendingDeletes.clear();\n\n const debounceWaitMs =\n this.batchStartedAt !== undefined ? performance.now() - this.batchStartedAt : undefined;\n const watcherSettleMs = this.watcherSettleMs;\n this.batchStartedAt = undefined;\n this.watcherSettleMs = undefined;\n\n if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;\n\n // Both .env files and warlock.config.ts live outside src/ — they should\n // never enter the dep graph, only ride along in the batch event so the\n // dev server can react (config reload / restart warning).\n const externalChanges = changes.filter(isExternalPath);\n const externalAdds = adds.filter(isExternalPath);\n const codeChanges = changes.filter((p) => !isExternalPath(p));\n const codeAdds = adds.filter((p) => !isExternalPath(p));\n\n // Multi-file batches can race the filesystem on Windows.\n if (codeAdds.length + codeChanges.length > 1) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n clearFileExistsCache();\n }\n\n const { added: addedCodePaths, vanished } = await this.processBatchAdds(codeAdds);\n const changedCodePaths = await this.processBatchChanges(codeChanges);\n await this.processBatchDeletes(deletes);\n\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n\n // Emit only the code paths that genuinely changed (hash differs). A no-op\n // save — an editor that fsyncs without writing — reports no change and is\n // dropped here, where we still know the pre-change hash. (Doing this\n // downstream is impossible: the source has already been overwritten, so a\n // content compare always looks unchanged — which is exactly why emptying a\n // file used to silently skip HMR.) External paths (.env / warlock.config.ts)\n // ride along untouched so the dev server can still react to them. Paths\n // that vanished between the add event and their read/stat (a rename or\n // move racing the filesystem) are folded into `deleted` instead of\n // `added` — they were already unwound as deletions in processBatchAdds.\n events.trigger(\"dev-server:batch-complete\", {\n added: [...externalAdds, ...addedCodePaths],\n changed: [...externalChanges, ...changedCodePaths],\n deleted: [...deletes, ...vanished],\n timings: isTimingsEnabled()\n ? { watcherSettleMs: watcherSettleMs ?? 0, debounceWaitMs: debounceWaitMs ?? 0 }\n : undefined,\n });\n }\n\n /**\n * Reprocess each changed file and return only the paths that genuinely\n * changed. `updateFile` returns false when the on-disk hash matches the\n * last-processed hash (e.g. an editor that fsyncs on save without writing),\n * so those no-ops are kept out of the reload batch. Emptying a file changes\n * its hash, so it is correctly reported as changed.\n */\n private async processBatchChanges(relativePaths: string[]): Promise<string[]> {\n const changed: string[] = [];\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n if (await this.fileOperations.updateFile(path)) {\n changed.push(path);\n }\n });\n return changed;\n }\n\n /**\n * Process pending add events. Returns the paths that genuinely became\n * available (`added`) separately from paths that no longer existed by the\n * time they were read/stat'd (`vanished`) — the latter is a rename or move\n * racing the filesystem, not a failure, so it is unwound as a deletion\n * instead of being logged as an error.\n */\n private async processBatchAdds(\n relativePaths: string[],\n ): Promise<{ added: string[]; vanished: string[] }> {\n const added: string[] = [];\n const vanished: string[] = [];\n\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n try {\n const fileManager = await this.fileOperations.addFile(path);\n\n if (fileManager.state === \"deleted\") {\n await this.fileOperations.deleteFile(path);\n vanished.push(path);\n return;\n }\n\n added.push(path);\n devLogSuccess(`Added file: ${path}`);\n } catch (error) {\n console.error(`Failed to add file ${path}:`, error);\n }\n });\n\n return { added, vanished };\n }\n\n private async processBatchDeletes(relativePaths: string[]): Promise<void> {\n for (const relativePath of relativePaths) {\n await this.fileOperations.deleteFile(relativePath);\n devLogSuccess(`Deleted file: ${relativePath}`);\n }\n }\n}\n\nfunction isEnvFile(path: string): boolean {\n const basename = path.split(\"/\").pop() || path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/** Paths watched but never added to the dependency graph. */\nfunction isExternalPath(path: string): boolean {\n return isEnvFile(path) || path === \"warlock.config.ts\";\n}\n\nasync function runInBatches<T>(\n items: T[],\n size: number,\n fn: (item: T) => Promise<unknown>,\n): Promise<void> {\n if (items.length === 0) return;\n for (let i = 0; i < items.length; i += size) {\n await Promise.all(items.slice(i, i + size).map(fn));\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"file-event-handler.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-event-handler.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport type { DependencyGraph } from \"./dependency-graph\";\nimport { devLogSuccess } from \"./dev-logger\";\nimport type { FileManager } from \"./file-manager\";\nimport type { FileOperations } from \"./file-operations\";\nimport { FILE_PROCESSING_BATCH_SIZE, isTimingsEnabled } from \"./flags\";\nimport type { ManifestManager } from \"./manifest-manager\";\nimport { clearFileExistsCache } from \"./parse-imports\";\nimport { isCodeFile } from \"./special-file-patterns\";\nimport { Path } from \"../utils/normalized-path\";\n\n/** Lets an isolated editor save reach HMR without the former fixed 50ms delay. */\nconst ISOLATED_SAVE_QUIET_WINDOW_MS = 12;\n\n/** Bounds a sustained formatter or checkout stream so it cannot postpone HMR forever. */\nconst BATCH_MAX_WAIT_MS = 60;\n\n/**\n * Receives raw watcher events and processes them in a single debounced batch.\n * Order within a batch: adds → changes → deletes, so changes can reference\n * newly-added files and deletes fire last.\n */\nexport class FileEventHandler {\n private pendingChanges = new Set<string>();\n private pendingAdds = new Set<string>();\n private pendingDeletes = new Set<string>();\n\n /** When this batch's first watcher event arrived — the debounce-wait phase start. */\n private batchStartedAt?: number;\n\n /** Slowest watcher-settle duration seen so far this batch, when `devServer.timings` is on. */\n private watcherSettleMs?: number;\n\n /** Tail of the batch-processing chain; keeps batches strictly sequential. */\n private batchQueue: Promise<void> = Promise.resolve();\n\n private debounceTimer?: ReturnType<typeof setTimeout>;\n private maxWaitTimer?: ReturnType<typeof setTimeout>;\n\n constructor(\n private readonly fileOperations: FileOperations,\n private readonly manifest: ManifestManager,\n private readonly dependencyGraph: DependencyGraph,\n private readonly files: Map<string, FileManager>,\n ) {}\n\n /** Matches paths the user listed in `devServer.watch.include`. */\n private includeMatcher: (path: string) => boolean = () => false;\n\n /**\n * Non-code files in the include set (`.sql` fixtures, JSON config) can't be\n * hot-reloaded, so they are reported as restart triggers instead.\n */\n public setIncludeMatcher(matcher: (path: string) => boolean): void {\n this.includeMatcher = matcher;\n }\n\n public handleFileChange(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingChanges.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileAdd(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingAdds.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n public handleFileDelete(absolutePath: string, settleMs?: number): void {\n this.recordTimingStart(settleMs);\n this.pendingDeletes.add(Path.toRelative(absolutePath));\n this.schedulePendingEvents();\n }\n\n private schedulePendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n }\n\n this.debounceTimer = setTimeout(() => this.flushPendingEvents(), ISOLATED_SAVE_QUIET_WINDOW_MS);\n\n if (this.maxWaitTimer === undefined) {\n this.maxWaitTimer = setTimeout(() => this.flushPendingEvents(), BATCH_MAX_WAIT_MS);\n }\n }\n\n private flushPendingEvents(): void {\n if (this.debounceTimer !== undefined) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = undefined;\n }\n\n if (this.maxWaitTimer !== undefined) {\n clearTimeout(this.maxWaitTimer);\n this.maxWaitTimer = undefined;\n }\n\n // Serialize: a batch arriving while the previous one is in flight queues\n // behind it (pending sets keep coalescing meanwhile) instead of racing it.\n this.batchQueue = this.batchQueue\n .then(() => this.processBatch())\n .catch((error) => {\n console.error(\"[dev-server] batch processing failed\", error);\n });\n }\n\n /**\n * Mark the debounce-wait phase start on the first event of a batch, and\n * track the slowest watcher-settle duration seen this batch. `settleMs`\n * only arrives when `devServer.timings` is on (see `FilesWatcher`), so\n * this is a no-op past the cheap `performance.now()` call when it's off.\n */\n private recordTimingStart(settleMs?: number): void {\n if (this.batchStartedAt === undefined) {\n this.batchStartedAt = performance.now();\n }\n\n if (settleMs === undefined) return;\n\n this.watcherSettleMs =\n this.watcherSettleMs === undefined ? settleMs : Math.max(this.watcherSettleMs, settleMs);\n }\n\n private async processBatch(): Promise<void> {\n const changes = Array.from(this.pendingChanges);\n const adds = Array.from(this.pendingAdds);\n const deletes = Array.from(this.pendingDeletes);\n\n this.pendingChanges.clear();\n this.pendingAdds.clear();\n this.pendingDeletes.clear();\n\n const debounceWaitMs =\n this.batchStartedAt !== undefined ? performance.now() - this.batchStartedAt : undefined;\n const watcherSettleMs = this.watcherSettleMs;\n this.batchStartedAt = undefined;\n this.watcherSettleMs = undefined;\n\n if (changes.length === 0 && adds.length === 0 && deletes.length === 0) return;\n\n // Both .env files and warlock.config.ts live outside src/ — they should\n // never enter the dep graph, only ride along in the batch event so the\n // dev server can react (config reload / restart warning).\n const isIncludedData = (p: string) => !isCodeFile(p) && this.includeMatcher(p);\n const isExternal = (p: string) => isExternalPath(p) || isIncludedData(p);\n const externalChanges = changes.filter(isExternal);\n const externalAdds = adds.filter(isExternal);\n const codeChanges = changes.filter((p) => !isExternal(p));\n const codeAdds = adds.filter((p) => !isExternal(p));\n const restartTriggers = [...changes, ...adds, ...deletes].filter(isIncludedData);\n\n // Multi-file batches can race the filesystem on Windows.\n if (codeAdds.length + codeChanges.length > 1) {\n await new Promise((resolve) => setTimeout(resolve, 500));\n clearFileExistsCache();\n }\n\n const { added: addedCodePaths, vanished } = await this.processBatchAdds(codeAdds);\n const changedCodePaths = await this.processBatchChanges(codeChanges);\n await this.processBatchDeletes(deletes);\n\n this.fileOperations.updateFileDependents();\n this.fileOperations.syncFilesToManifest();\n await this.manifest.save();\n\n // Emit only the code paths that genuinely changed (hash differs). A no-op\n // save — an editor that fsyncs without writing — reports no change and is\n // dropped here, where we still know the pre-change hash. (Doing this\n // downstream is impossible: the source has already been overwritten, so a\n // content compare always looks unchanged — which is exactly why emptying a\n // file used to silently skip HMR.) External paths (.env / warlock.config.ts)\n // ride along untouched so the dev server can still react to them. Paths\n // that vanished between the add event and their read/stat (a rename or\n // move racing the filesystem) are folded into `deleted` instead of\n // `added` — they were already unwound as deletions in processBatchAdds.\n events.trigger(\"dev-server:batch-complete\", {\n added: [...externalAdds, ...addedCodePaths],\n changed: [...externalChanges, ...changedCodePaths],\n deleted: [...deletes.filter((p) => !isIncludedData(p)), ...vanished],\n restartTriggers,\n timings: isTimingsEnabled()\n ? { watcherSettleMs: watcherSettleMs ?? 0, debounceWaitMs: debounceWaitMs ?? 0 }\n : undefined,\n });\n }\n\n /**\n * Reprocess each changed file and return only the paths that genuinely\n * changed. `updateFile` returns false when the on-disk hash matches the\n * last-processed hash (e.g. an editor that fsyncs on save without writing),\n * so those no-ops are kept out of the reload batch. Emptying a file changes\n * its hash, so it is correctly reported as changed.\n */\n private async processBatchChanges(relativePaths: string[]): Promise<string[]> {\n const changed: string[] = [];\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n if (await this.fileOperations.updateFile(path)) {\n changed.push(path);\n }\n });\n return changed;\n }\n\n /**\n * Process pending add events. Returns the paths that genuinely became\n * available (`added`) separately from paths that no longer existed by the\n * time they were read/stat'd (`vanished`) — the latter is a rename or move\n * racing the filesystem, not a failure, so it is unwound as a deletion\n * instead of being logged as an error.\n */\n private async processBatchAdds(\n relativePaths: string[],\n ): Promise<{ added: string[]; vanished: string[] }> {\n const added: string[] = [];\n const vanished: string[] = [];\n\n await runInBatches(relativePaths, FILE_PROCESSING_BATCH_SIZE, async (path) => {\n try {\n const fileManager = await this.fileOperations.addFile(path);\n\n if (fileManager.state === \"deleted\") {\n await this.fileOperations.deleteFile(path);\n vanished.push(path);\n return;\n }\n\n added.push(path);\n devLogSuccess(`Added file: ${path}`);\n } catch (error) {\n console.error(`Failed to add file ${path}:`, error);\n }\n });\n\n return { added, vanished };\n }\n\n private async processBatchDeletes(relativePaths: string[]): Promise<void> {\n for (const relativePath of relativePaths) {\n await this.fileOperations.deleteFile(relativePath);\n devLogSuccess(`Deleted file: ${relativePath}`);\n }\n }\n}\n\nfunction isEnvFile(path: string): boolean {\n const basename = path.split(\"/\").pop() || path;\n return basename === \".env\" || basename.startsWith(\".env.\");\n}\n\n/** Paths watched but never added to the dependency graph. */\nfunction isExternalPath(path: string): boolean {\n return isEnvFile(path) || path === \"warlock.config.ts\";\n}\n\nasync function runInBatches<T>(\n items: T[],\n size: number,\n fn: (item: T) => Promise<unknown>,\n): Promise<void> {\n if (items.length === 0) return;\n for (let i = 0; i < items.length; i += size) {\n await Promise.all(items.slice(i, i + size).map(fn));\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"file-manager.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"mappings":";;;;KASY,eAAA;;AAAZ;;;;AAA2B;AAyB3B;;cAAa,WAAA;EAAA,SAgCO,YAAA;EACT,KAAA,EAAO,GAAA,SAAY,WAAA;EACnB,cAAA,EAAgB,cAAA;EAjClB,YAAA;EACA,YAAA;EACA,IAAA;EACA,MAAA;EAiBM;EAdN,YAAA,EAAY,GAAA;EAsBL;;;;EAhBP,oBAAA,EAAoB,GAAA;EAwBkB;EArBtC,SAAA,EAAS,GAAA;EAmCiB;EAhC1B,UAAA,EAAU,GAAA;EAEV,IAAA,EAAM,QAAA;EAwDsD;EArD5D,OAAA,EAAS,eAAA;EAoIuC;EAjIhD,cAAA;EAEA,KAAA,EAAO,SAAA;cAGI,YAAA,UACT,KAAA,EAAO,GAAA,SAAY,WAAA,GACnB,cAAA,EAAgB,cAAA;EAGlB,UAAA,CAAW,OAAA,EAAS,eAAA,GAAkB,eAAA;EAMtC,YAAA;EAXW;;;;EAmBL,IAAA,CAAK,YAAA,GAAe,OAAA,CAAQ,YAAA,IAAgB,OAAA;EAjBhC;;;;;;;;;;;;;;;EA2CZ,OAAA;IAAU;EAAA;IAAmB,KAAA;EAAA,IAAyB,OAAA;EAhD5D;;;;;;EAAA,UA+FS,qBAAA,IAAyB,OAAA;EA1FhB;;;;;EAAA,UA0HT,gBAAA,CAAiB,YAAA,EAAc,OAAA,CAAQ,YAAA,IAAgB,OAAA;EAAA,UA6B7D,cAAA;EAsBH,UAAA,IAAc,YAAA;AAAA"}
1
+ {"version":3,"file":"file-manager.d.mts","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"mappings":";;;;KASY,eAAA;;AAAZ;;;;AAA2B;AAyB3B;;cAAa,WAAA;EAAA,SAgCO,YAAA;EACT,KAAA,EAAO,GAAA,SAAY,WAAA;EACnB,cAAA,EAAgB,cAAA;EAjClB,YAAA;EACA,YAAA;EACA,IAAA;EACA,MAAA;EAiBM;EAdN,YAAA,EAAY,GAAA;EAsBL;;;;EAhBP,oBAAA,EAAoB,GAAA;EAwBkB;EArBtC,SAAA,EAAS,GAAA;EAmCiB;EAhC1B,UAAA,EAAU,GAAA;EAEV,IAAA,EAAM,QAAA;EAwDsD;EArD5D,OAAA,EAAS,eAAA;EAoIuC;EAjIhD,cAAA;EAEA,KAAA,EAAO,SAAA;cAGI,YAAA,UACT,KAAA,EAAO,GAAA,SAAY,WAAA,GACnB,cAAA,EAAgB,cAAA;EAGlB,UAAA,CAAW,OAAA,EAAS,eAAA,GAAkB,eAAA;EAMtC,YAAA;EAXW;;;;EAmBL,IAAA,CAAK,YAAA,GAAe,OAAA,CAAQ,YAAA,IAAgB,OAAA;EAjBhC;;;;;;;;;;;;;;;EA2CZ,OAAA;IAAU;EAAA;IAAmB,KAAA;EAAA,IAAyB,OAAA;EAhD5D;;;;;;EAAA,UA+FS,qBAAA,IAAyB,OAAA;EA1FhB;;;;;EAAA,UA0HT,gBAAA,CAAiB,YAAA,EAAc,OAAA,CAAQ,YAAA,IAAgB,OAAA;EAAA,UA6B7D,cAAA;EAuBH,UAAA,IAAc,YAAA;AAAA"}
@@ -151,13 +151,13 @@ var FileManager = class {
151
151
  }
152
152
  detectFileType() {
153
153
  const path = this.relativePath;
154
- if (path.includes("main.ts") || path.includes("main.tsx")) this.type = "main";
154
+ if (/(^|\/)main\.tsx?$/.test(path)) this.type = "main";
155
+ else if (/\.model\.tsx?$/.test(path)) this.type = "model";
155
156
  else if (path.startsWith("src/config/")) this.type = "config";
156
157
  else if (path.endsWith("routes.ts") || path.endsWith("routes.tsx")) this.type = "route";
157
158
  else if (path.includes("/events/")) this.type = "event";
158
159
  else if (path.includes("controller")) this.type = "controller";
159
160
  else if (path.includes("service")) this.type = "service";
160
- else if (path.endsWith(".model.ts")) this.type = "model";
161
161
  else this.type = "other";
162
162
  }
163
163
  toManifest() {
@@ -1 +1 @@
1
- {"version":3,"file":"file-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { getFileAsync, lastModifiedAsync } from \"@warlock.js/fs\";\nimport crypto from \"crypto\";\nimport { DEV_SERVER_EVENTS } from \"./events\";\nimport { type FileOperations } from \"./file-operations\";\nimport { isTypeOnlyFile, parseImports } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\nimport type { FileManifest, FileState, FileType } from \"./types\";\n\nexport type CleanupFunction = () => void;\n\n/**\n * Whether `error` is a Node `ENOENT` (path does not exist) error. Used to\n * distinguish a file that has genuinely vanished between the watcher event\n * and its read/stat (a rename or move racing the filesystem) from a real\n * failure such as `EACCES` or a parse error, which must still surface.\n */\nfunction isEnoentError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * FileManager — per-file metadata for the dependency graph.\n *\n * The loader hook transpiles and caches on-demand at import time, so this\n * class never touches disk for anything beyond reading source to hash it\n * and parse its imports. Its job is keeping the metadata that the\n * orchestrator needs to invalidate dependents on change.\n */\nexport class FileManager {\n public relativePath = \"\";\n public lastModified = 0;\n public hash = \"\";\n public source = \"\";\n\n /** Files this file imports (relative paths). */\n public dependencies = new Set<string>();\n\n /**\n * Subset of `dependencies` reached only through type-only imports — a single\n * runtime occurrence makes the whole edge runtime.\n */\n public typeOnlyDependencies = new Set<string>();\n\n /** Original specifier → resolved absolute path. */\n public importMap = new Map<string, string>();\n\n /** Files that import this file (relative paths). */\n public dependents = new Set<string>();\n\n public type: FileType | undefined;\n\n /** Cleanup hooks the loaded module registered for itself. */\n public cleanup: CleanupFunction[] = [];\n\n /** Whether the source has zero runtime imports/exports. */\n public isTypeOnlyFile = false;\n\n public state: FileState = \"idle\";\n\n public constructor(\n public readonly absolutePath: string,\n public files: Map<string, FileManager>,\n public fileOperations: FileOperations,\n ) {}\n\n public addCleanup(cleanup: CleanupFunction | CleanupFunction[]) {\n const next = Array.isArray(cleanup) ? cleanup : [cleanup];\n this.cleanup.push(...next);\n this.cleanup = [...new Set(this.cleanup)];\n }\n\n public resetCleanup() {\n this.cleanup = [];\n }\n\n /**\n * Initial setup. Restores from manifest if the hash still matches,\n * otherwise re-processes from disk.\n */\n public async init(fileManifest?: Partial<FileManifest>): Promise<void> {\n this.relativePath = Path.toRelative(this.absolutePath);\n this.detectFileType();\n\n if (fileManifest) {\n await this.initFromManifest(fileManifest);\n } else {\n await this.process();\n }\n }\n\n /**\n * Read source, hash, parse imports, emit ready. The only file-system\n * touch happens here; transpilation is the loader hook's job.\n *\n * Both the read and the stat happen up front, before anything on this\n * instance is mutated. A path that disappears between the watcher event\n * and either of those calls (`ENOENT`) — typically a rename or move\n * racing the filesystem — is treated as a deletion rather than a thrown\n * error, and never leaves `source`/`hash` pointing at content that then\n * vanished. A real failure (`EACCES`, a permissions error, etc.) still\n * throws.\n *\n * @param force - re-parse even when the hash matches.\n * @returns true if the file was (re)parsed.\n */\n public async process({ force = false }: { force?: boolean } = {}): Promise<boolean> {\n if (!this.relativePath) this.relativePath = Path.toRelative(this.absolutePath);\n if (!this.type) this.detectFileType();\n\n this.state = \"loading\";\n\n let newSource: string;\n let modifiedTime: Date;\n\n try {\n newSource = await getFileAsync(this.absolutePath);\n modifiedTime = await lastModifiedAsync(this.absolutePath);\n } catch (error) {\n if (isEnoentError(error)) {\n this.state = \"deleted\";\n return false;\n }\n\n throw error;\n }\n\n const newHash = crypto.createHash(\"sha256\").update(newSource).digest(\"hex\");\n\n if (!force && newHash === this.hash) {\n this.state = \"ready\";\n return false;\n }\n\n this.source = newSource;\n this.hash = newHash;\n this.lastModified = modifiedTime.getTime();\n\n await this.rebuildImportMetadata();\n this.isTypeOnlyFile = isTypeOnlyFile(this.source);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n\n return true;\n }\n\n /**\n * Parse the current source and refresh `importMap`, `dependencies`, and\n * `typeOnlyDependencies`. A dependency is classified type-only iff every\n * import/export statement that resolves to it is type-only — a single\n * runtime reference makes the whole edge runtime.\n */\n protected async rebuildImportMetadata(): Promise<void> {\n const resolved = await parseImports(this.source, this.absolutePath);\n\n this.importMap = new Map();\n this.dependencies = new Set();\n this.typeOnlyDependencies = new Set();\n\n const runtimePaths = new Set<string>();\n\n for (const [originalPath, { absolutePath, isTypeOnly }] of resolved) {\n this.importMap.set(originalPath, absolutePath);\n\n const relativePath = Path.toRelative(absolutePath);\n this.dependencies.add(relativePath);\n\n if (!isTypeOnly) {\n runtimePaths.add(relativePath);\n }\n }\n\n for (const dependency of this.dependencies) {\n if (!runtimePaths.has(dependency)) {\n this.typeOnlyDependencies.add(dependency);\n }\n }\n }\n\n /**\n * Restore from a cached manifest entry. If the on-disk hash matches the\n * manifest hash, dep-graph metadata is restored from the manifest without\n * re-parsing. Otherwise the file is reprocessed.\n */\n protected async initFromManifest(fileManifest: Partial<FileManifest>): Promise<void> {\n this.type = fileManifest.type;\n\n this.state = \"loading\";\n\n try {\n this.source = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return;\n }\n\n const currentHash = crypto.createHash(\"sha256\").update(this.source).digest(\"hex\");\n\n if (currentHash !== fileManifest.hash) {\n await this.process({ force: true });\n return;\n }\n\n this.hash = fileManifest.hash!;\n this.lastModified = fileManifest.lastModified!;\n this.dependencies = new Set(fileManifest.dependencies || []);\n this.typeOnlyDependencies = new Set(fileManifest.typeOnlyDependencies || []);\n this.dependents = new Set(fileManifest.dependents || []);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n }\n\n protected detectFileType(): void {\n const path = this.relativePath;\n\n if (path.includes(\"main.ts\") || path.includes(\"main.tsx\")) {\n this.type = \"main\";\n } else if (path.startsWith(\"src/config/\")) {\n this.type = \"config\";\n } else if (path.endsWith(\"routes.ts\") || path.endsWith(\"routes.tsx\")) {\n this.type = \"route\";\n } else if (path.includes(\"/events/\")) {\n this.type = \"event\";\n } else if (path.includes(\"controller\")) {\n this.type = \"controller\";\n } else if (path.includes(\"service\")) {\n this.type = \"service\";\n } else if (path.endsWith(\".model.ts\")) {\n this.type = \"model\";\n } else {\n this.type = \"other\";\n }\n }\n\n public toManifest(): FileManifest {\n return {\n absolutePath: this.absolutePath,\n relativePath: this.relativePath,\n lastModified: this.lastModified,\n hash: this.hash,\n dependencies: Array.from(this.dependencies),\n typeOnlyDependencies: Array.from(this.typeOnlyDependencies),\n dependents: Array.from(this.dependents),\n type: this.type!,\n };\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"file-manager.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/file-manager.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { getFileAsync, lastModifiedAsync } from \"@warlock.js/fs\";\nimport crypto from \"crypto\";\nimport { DEV_SERVER_EVENTS } from \"./events\";\nimport { type FileOperations } from \"./file-operations\";\nimport { isTypeOnlyFile, parseImports } from \"./parse-imports\";\nimport { Path } from \"../utils/normalized-path\";\nimport type { FileManifest, FileState, FileType } from \"./types\";\n\nexport type CleanupFunction = () => void;\n\n/**\n * Whether `error` is a Node `ENOENT` (path does not exist) error. Used to\n * distinguish a file that has genuinely vanished between the watcher event\n * and its read/stat (a rename or move racing the filesystem) from a real\n * failure such as `EACCES` or a parse error, which must still surface.\n */\nfunction isEnoentError(error: unknown): boolean {\n return (\n typeof error === \"object\" &&\n error !== null &&\n \"code\" in error &&\n (error as NodeJS.ErrnoException).code === \"ENOENT\"\n );\n}\n\n/**\n * FileManager — per-file metadata for the dependency graph.\n *\n * The loader hook transpiles and caches on-demand at import time, so this\n * class never touches disk for anything beyond reading source to hash it\n * and parse its imports. Its job is keeping the metadata that the\n * orchestrator needs to invalidate dependents on change.\n */\nexport class FileManager {\n public relativePath = \"\";\n public lastModified = 0;\n public hash = \"\";\n public source = \"\";\n\n /** Files this file imports (relative paths). */\n public dependencies = new Set<string>();\n\n /**\n * Subset of `dependencies` reached only through type-only imports — a single\n * runtime occurrence makes the whole edge runtime.\n */\n public typeOnlyDependencies = new Set<string>();\n\n /** Original specifier → resolved absolute path. */\n public importMap = new Map<string, string>();\n\n /** Files that import this file (relative paths). */\n public dependents = new Set<string>();\n\n public type: FileType | undefined;\n\n /** Cleanup hooks the loaded module registered for itself. */\n public cleanup: CleanupFunction[] = [];\n\n /** Whether the source has zero runtime imports/exports. */\n public isTypeOnlyFile = false;\n\n public state: FileState = \"idle\";\n\n public constructor(\n public readonly absolutePath: string,\n public files: Map<string, FileManager>,\n public fileOperations: FileOperations,\n ) {}\n\n public addCleanup(cleanup: CleanupFunction | CleanupFunction[]) {\n const next = Array.isArray(cleanup) ? cleanup : [cleanup];\n this.cleanup.push(...next);\n this.cleanup = [...new Set(this.cleanup)];\n }\n\n public resetCleanup() {\n this.cleanup = [];\n }\n\n /**\n * Initial setup. Restores from manifest if the hash still matches,\n * otherwise re-processes from disk.\n */\n public async init(fileManifest?: Partial<FileManifest>): Promise<void> {\n this.relativePath = Path.toRelative(this.absolutePath);\n this.detectFileType();\n\n if (fileManifest) {\n await this.initFromManifest(fileManifest);\n } else {\n await this.process();\n }\n }\n\n /**\n * Read source, hash, parse imports, emit ready. The only file-system\n * touch happens here; transpilation is the loader hook's job.\n *\n * Both the read and the stat happen up front, before anything on this\n * instance is mutated. A path that disappears between the watcher event\n * and either of those calls (`ENOENT`) — typically a rename or move\n * racing the filesystem — is treated as a deletion rather than a thrown\n * error, and never leaves `source`/`hash` pointing at content that then\n * vanished. A real failure (`EACCES`, a permissions error, etc.) still\n * throws.\n *\n * @param force - re-parse even when the hash matches.\n * @returns true if the file was (re)parsed.\n */\n public async process({ force = false }: { force?: boolean } = {}): Promise<boolean> {\n if (!this.relativePath) this.relativePath = Path.toRelative(this.absolutePath);\n if (!this.type) this.detectFileType();\n\n this.state = \"loading\";\n\n let newSource: string;\n let modifiedTime: Date;\n\n try {\n newSource = await getFileAsync(this.absolutePath);\n modifiedTime = await lastModifiedAsync(this.absolutePath);\n } catch (error) {\n if (isEnoentError(error)) {\n this.state = \"deleted\";\n return false;\n }\n\n throw error;\n }\n\n const newHash = crypto.createHash(\"sha256\").update(newSource).digest(\"hex\");\n\n if (!force && newHash === this.hash) {\n this.state = \"ready\";\n return false;\n }\n\n this.source = newSource;\n this.hash = newHash;\n this.lastModified = modifiedTime.getTime();\n\n await this.rebuildImportMetadata();\n this.isTypeOnlyFile = isTypeOnlyFile(this.source);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n\n return true;\n }\n\n /**\n * Parse the current source and refresh `importMap`, `dependencies`, and\n * `typeOnlyDependencies`. A dependency is classified type-only iff every\n * import/export statement that resolves to it is type-only — a single\n * runtime reference makes the whole edge runtime.\n */\n protected async rebuildImportMetadata(): Promise<void> {\n const resolved = await parseImports(this.source, this.absolutePath);\n\n this.importMap = new Map();\n this.dependencies = new Set();\n this.typeOnlyDependencies = new Set();\n\n const runtimePaths = new Set<string>();\n\n for (const [originalPath, { absolutePath, isTypeOnly }] of resolved) {\n this.importMap.set(originalPath, absolutePath);\n\n const relativePath = Path.toRelative(absolutePath);\n this.dependencies.add(relativePath);\n\n if (!isTypeOnly) {\n runtimePaths.add(relativePath);\n }\n }\n\n for (const dependency of this.dependencies) {\n if (!runtimePaths.has(dependency)) {\n this.typeOnlyDependencies.add(dependency);\n }\n }\n }\n\n /**\n * Restore from a cached manifest entry. If the on-disk hash matches the\n * manifest hash, dep-graph metadata is restored from the manifest without\n * re-parsing. Otherwise the file is reprocessed.\n */\n protected async initFromManifest(fileManifest: Partial<FileManifest>): Promise<void> {\n this.type = fileManifest.type;\n\n this.state = \"loading\";\n\n try {\n this.source = await getFileAsync(this.absolutePath);\n } catch {\n this.state = \"deleted\";\n return;\n }\n\n const currentHash = crypto.createHash(\"sha256\").update(this.source).digest(\"hex\");\n\n if (currentHash !== fileManifest.hash) {\n await this.process({ force: true });\n return;\n }\n\n this.hash = fileManifest.hash!;\n this.lastModified = fileManifest.lastModified!;\n this.dependencies = new Set(fileManifest.dependencies || []);\n this.typeOnlyDependencies = new Set(fileManifest.typeOnlyDependencies || []);\n this.dependents = new Set(fileManifest.dependents || []);\n\n this.state = \"ready\";\n events.trigger(DEV_SERVER_EVENTS.FILE_READY, this);\n }\n\n protected detectFileType(): void {\n const path = this.relativePath;\n\n if (/(^|\\/)main\\.tsx?$/.test(path)) {\n this.type = \"main\";\n } else if (/\\.model\\.tsx?$/.test(path)) {\n // Before the folder-name checks below: `events/models/x.model.ts` is a model.\n this.type = \"model\";\n } else if (path.startsWith(\"src/config/\")) {\n this.type = \"config\";\n } else if (path.endsWith(\"routes.ts\") || path.endsWith(\"routes.tsx\")) {\n this.type = \"route\";\n } else if (path.includes(\"/events/\")) {\n this.type = \"event\";\n } else if (path.includes(\"controller\")) {\n this.type = \"controller\";\n } else if (path.includes(\"service\")) {\n this.type = \"service\";\n } else {\n this.type = \"other\";\n }\n }\n\n public toManifest(): FileManifest {\n return {\n absolutePath: this.absolutePath,\n relativePath: this.relativePath,\n lastModified: this.lastModified,\n hash: this.hash,\n dependencies: Array.from(this.dependencies),\n typeOnlyDependencies: Array.from(this.typeOnlyDependencies),\n dependents: Array.from(this.dependents),\n type: this.type!,\n };\n }\n}\n"],"mappings":"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"}
@@ -8,7 +8,7 @@ import { FileEventHandler } from "./file-event-handler.mjs";
8
8
  import { FileManager } from "./file-manager.mjs";
9
9
  import { ensureWarlockDirectory, getFilesFromDirectory } from "./utils.mjs";
10
10
  import { FileOperations } from "./file-operations.mjs";
11
- import { FilesWatcher } from "./files-watcher.mjs";
11
+ import { FilesWatcher, createIgnoredMatcher } from "./files-watcher.mjs";
12
12
  import { EslintHealthChecker } from "./health-checker/checkers/eslint-health-checker.mjs";
13
13
  import { TypescriptHealthChecker } from "./health-checker/checkers/typescript-health-checker.mjs";
14
14
  import { FilesHealthcareManager } from "./health-checker/files-healthcare.manager.mjs";
@@ -20,6 +20,7 @@ import { packageJsonManager } from "./package-json-manager.mjs";
20
20
  import { SpecialFilesCollector } from "./special-files-collector.mjs";
21
21
  import { colors } from "@mongez/copper";
22
22
  import { init } from "es-module-lexer";
23
+ import { fileURLToPath } from "node:url";
23
24
 
24
25
  //#region ../core/src/dev-server/files-orchestrator.ts
25
26
  /**
@@ -173,6 +174,7 @@ var FilesOrchestrator = class {
173
174
  this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));
174
175
  this.filesWatcher.onFileDelete((p, settleMs) => this.eventHandler.handleFileDelete(p, settleMs));
175
176
  const watchConfig = warlockConfigManager.get("devServer")?.watch;
177
+ if (watchConfig?.include?.length) this.eventHandler.setIncludeMatcher(createIgnoredMatcher(watchConfig.include));
176
178
  await this.filesWatcher.watch(watchConfig);
177
179
  }
178
180
  };
@@ -1 +1 @@
1
- {"version":3,"file":"files-orchestrator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-orchestrator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { init } from \"es-module-lexer\";\r\nimport type { MessagePort } from \"node:worker_threads\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { DependencyGraph } from \"./dependency-graph\";\r\nimport { devLogDim, devLogSuccess } from \"./dev-logger\";\r\nimport { FileEventHandler } from \"./file-event-handler\";\r\nimport { FileManager } from \"./file-manager\";\r\nimport { FileOperations } from \"./file-operations\";\r\nimport { FilesWatcher } from \"./files-watcher\";\r\nimport { FILE_PROCESSING_BATCH_SIZE } from \"./flags\";\r\nimport { EslintHealthChecker } from \"./health-checker/checkers/eslint-health-checker\";\r\nimport { TypescriptHealthChecker } from \"./health-checker/checkers/typescript-health-checker\";\r\nimport type { FileHealthCheckerContract } from \"./health-checker/file-health-checker.contract\";\r\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\r\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\r\nimport { registerLoader } from \"./loader/register-loader.js\";\r\nimport { ManifestManager } from \"./manifest-manager\";\r\nimport { ModuleLoader } from \"./module-loader\";\r\nimport { packageJsonManager } from \"./package-json-manager\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { SpecialFilesCollector } from \"./special-files-collector\";\r\nimport { tsconfigManager } from \"./tsconfig-manager\";\r\nimport { ensureWarlockDirectory, getFilesFromDirectory } from \"./utils\";\r\n\r\n/**\r\n * Register a cleanup callback that fires before the current module is\r\n * unloaded by HMR. Stack-inspects the caller so user code doesn't need to\r\n * thread the FileManager through.\r\n */\r\nexport function onCleanup(callback: () => any) {\r\n if (globalThis.__currentModuleFile) {\r\n globalThis.__currentModuleFile.addCleanup(callback);\r\n return;\r\n }\r\n\r\n const stack = new Error().stack;\r\n const callerLine = stack?.split(\"\\n\")[2];\r\n const match = callerLine?.match(/\\((.+?):\\d+:\\d+\\)/) || callerLine?.match(/at (.+?):\\d+:\\d+/);\r\n const callerFile = match?.[1];\r\n\r\n if (!callerFile) return;\r\n const fileManager = filesOrchestrator.files.get(Path.toRelative(callerFile));\r\n fileManager?.addCleanup(callback);\r\n}\r\n\r\n/**\r\n * Top-level coordinator for the dev server's file system.\r\n *\r\n * Owns: file discovery, the dependency graph, the manifest, the special-files\r\n * index, the file watcher, and the lifecycle of the ESM loader hook (which\r\n * provides cache-busting via `?v=N` version tokens).\r\n */\r\nexport class FilesOrchestrator {\r\n public readonly filesWatcher = new FilesWatcher();\r\n public readonly files = new Map<string, FileManager>();\r\n private readonly manifest = new ManifestManager(this.files);\r\n private readonly dependencyGraph = new DependencyGraph();\r\n private readonly healthCheckerManager = new FilesHealthcareManager(this.files);\r\n public readonly specialFilesCollector = new SpecialFilesCollector();\r\n public readonly moduleLoader = new ModuleLoader(this.specialFilesCollector);\r\n public readonly fileOperations: FileOperations;\r\n private readonly eventHandler: FileEventHandler;\r\n\r\n /** Main-thread end of the MessageChannel to the loader hook worker. */\r\n private loaderPort: MessagePort | null = null;\r\n\r\n /** Resolve callbacks for pending `flushVersionBumps()` calls. */\r\n private readonly pendingFlushes: Array<() => void> = [];\r\n\r\n public isInitialized = false;\r\n\r\n public constructor() {\r\n this.fileOperations = new FileOperations(\r\n this.files,\r\n this.dependencyGraph,\r\n this.manifest,\r\n this.specialFilesCollector,\r\n );\r\n\r\n this.eventHandler = new FileEventHandler(\r\n this.fileOperations,\r\n this.manifest,\r\n this.dependencyGraph,\r\n this.files,\r\n );\r\n }\r\n\r\n public async add(relativePath: string): Promise<FileManager> {\r\n return this.fileOperations.addFile(relativePath);\r\n }\r\n\r\n public async load<T>(relativePath: string, type = \"other\") {\r\n // Guarantee the ESM loader hook is registered before we import anything.\r\n // `load()` is the dynamic-import entrypoint, so it is the one place that\r\n // must satisfy `init()`'s documented precondition (\"Must be called before\r\n // any user src/ module is dynamically imported\") on its own — it cannot\r\n // assume a caller ran `init()` first.\r\n //\r\n // The CLI command-resolution path proves why: `cliCommandsLoader.load()`\r\n // imports a project command module during command *lookup* (before the\r\n // command's own `execute()` runs), whereas `loadPreloaders()` only calls\r\n // `init()` from *inside* `execute()`. So a `warlock <command>` whose\r\n // command file imports an `app/*` alias or a `.ts` sibling was imported\r\n // with no hook registered and died on `ERR_MODULE_NOT_FOUND` (f9d8f99f).\r\n //\r\n // `init()` is idempotent (guarded by `isInitialized`), so this is a no-op\r\n // on the dev-server path, which already calls it explicitly at startup.\r\n await this.init();\r\n\r\n const fileManager = await this.add(relativePath);\r\n return this.moduleLoader.loadModule<T>(fileManager, fileManager.type || type);\r\n }\r\n\r\n public getDependencyGraph(): DependencyGraph {\r\n return this.dependencyGraph;\r\n }\r\n\r\n public getInvalidationChain(file: string): string[] {\r\n return this.dependencyGraph.getInvalidationChain(file);\r\n }\r\n\r\n public getFiles(): Map<string, FileManager> {\r\n return this.files;\r\n }\r\n\r\n public getHealthCheckerManager(): FilesHealthcareManager {\r\n return this.healthCheckerManager;\r\n }\r\n\r\n /**\r\n * Initialise managers and register the ESM loader hook. Must be called\r\n * before any user `src/` module is dynamically imported.\r\n */\r\n public async init() {\r\n if (this.isInitialized) return;\r\n this.isInitialized = true;\r\n\r\n await init;\r\n await Promise.all([tsconfigManager.init(), packageJsonManager.init()]);\r\n\r\n // The loader hook writes itself into .warlock/, so ensure the dir exists.\r\n await ensureWarlockDirectory();\r\n\r\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\r\n const transpileInit = buildTranspileInit(\r\n tsconfigManager.tsconfig?.compilerOptions,\r\n devServerConfig?.transpileCacheDebug === true,\r\n );\r\n\r\n this.loaderPort = await registerLoader(transpileInit);\r\n\r\n this.loaderPort.on(\"message\", (msg: { type: string }) => {\r\n if (msg.type === \"sync-ack\") {\r\n this.pendingFlushes.shift()?.();\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Tell the hook worker a file changed. The next `import()` of it will get\r\n * a fresh `?v=N` URL → Node cache miss → fresh content.\r\n */\r\n public bumpVersion(absolutePath: string): void {\r\n this.loaderPort?.postMessage({ type: \"bump\", absolutePath });\r\n }\r\n\r\n /**\r\n * Wait until the hook worker has processed every pending bump.\r\n *\r\n * `postMessage` is async — without this, a follow-up `import()` may\r\n * resolve before the worker increments the counter and Node will return\r\n * the cached old module.\r\n */\r\n public flushVersionBumps(): Promise<void> {\r\n if (!this.loaderPort) return Promise.resolve();\r\n\r\n return new Promise<void>((resolve) => {\r\n this.pendingFlushes.push(resolve);\r\n this.loaderPort!.postMessage({ type: \"sync\" });\r\n });\r\n }\r\n\r\n /**\r\n * Discover files on disk, reconcile against the manifest, build the dep\r\n * graph, and persist the new manifest. Idempotent.\r\n */\r\n public async initializeAll() {\r\n const [filesInFilesystem, manifestExists] = await Promise.all([\r\n this.getAllFilesFromFilesystem(),\r\n this.manifest.init(),\r\n ]);\r\n\r\n if (!manifestExists) {\r\n await this.processFiles(filesInFilesystem);\r\n } else {\r\n await this.reconcileFiles(filesInFilesystem);\r\n }\r\n\r\n this.dependencyGraph.build(this.files);\r\n this.fileOperations.updateFileDependents();\r\n this.fileOperations.syncFilesToManifest();\r\n await this.manifest.save();\r\n }\r\n\r\n public async checkHealth(files: { added: string[]; changed: string[]; deleted: string[] }) {\r\n const filesToCheck = [...files.added, ...files.changed]\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n const filesToDelete = files.deleted\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n await this.healthCheckerManager.onFileChanges(filesToCheck);\r\n this.healthCheckerManager.removeFiles(filesToDelete);\r\n this.healthCheckerManager.checkFiles(filesToCheck);\r\n }\r\n\r\n public async startCheckingHealth(healthCheckers?: FileHealthCheckerContract[]): Promise<void> {\r\n devLogDim(\"Started File Health Checks in the background.\");\r\n\r\n const checkers = healthCheckers ?? [new TypescriptHealthChecker(), new EslintHealthChecker()];\r\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\r\n await this.healthCheckerManager.validateAllFiles();\r\n }\r\n\r\n /**\r\n * Glob the src directory, returning relative paths.\r\n */\r\n public async getAllFilesFromFilesystem(): Promise<string[]> {\r\n const absolutePaths = await getFilesFromDirectory();\r\n return absolutePaths.map((absPath) => Path.toRelative(absPath));\r\n }\r\n\r\n /**\r\n * Process every file from scratch — used when no manifest exists.\r\n */\r\n private async processFiles(filePaths: string[]) {\r\n devLogDim(`processing ${filePaths.length} files...`);\r\n await runInBatches(filePaths, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.process();\r\n });\r\n devLogSuccess(`processed ${filePaths.length} files`);\r\n }\r\n\r\n private async reconcileFiles(filesInFilesystem: string[]) {\r\n const filesInManifest = new Set(this.manifest.getAllFilePaths());\r\n const filesInFilesystemSet = new Set(filesInFilesystem);\r\n\r\n const newFiles = filesInFilesystem.filter((f) => !filesInManifest.has(f));\r\n const deletedFiles = Array.from(filesInManifest).filter((f) => !filesInFilesystemSet.has(f));\r\n const existingFiles = filesInFilesystem.filter((f) => filesInManifest.has(f));\r\n\r\n if (newFiles.length > 0 || deletedFiles.length > 0) {\r\n devLogDim(\r\n `reconciling: ${colors.green(newFiles.length)} new, ${colors.red(deletedFiles.length)} deleted, ${colors.blue(existingFiles.length)} existing`,\r\n );\r\n }\r\n\r\n await this.processFiles(newFiles);\r\n\r\n for (const relativePath of deletedFiles) {\r\n this.manifest.removeFile(relativePath);\r\n this.files.delete(relativePath);\r\n }\r\n\r\n await runInBatches(existingFiles, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.init(this.manifest.getFile(relativePath));\r\n });\r\n }\r\n\r\n public async watchFiles() {\r\n devLogSuccess(\"watching for file changes (not serving yet)\");\r\n\r\n this.filesWatcher.onFileChange((p, settleMs) => this.eventHandler.handleFileChange(p, settleMs));\r\n this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));\r\n this.filesWatcher.onFileDelete((p, settleMs) => this.eventHandler.handleFileDelete(p, settleMs));\r\n\r\n const watchConfig = warlockConfigManager.get(\"devServer\")?.watch;\r\n await this.filesWatcher.watch(watchConfig);\r\n }\r\n}\r\n\r\nasync function runInBatches<T>(\r\n items: T[],\r\n size: number,\r\n fn: (item: T) => Promise<unknown>,\r\n): Promise<void> {\r\n if (items.length === 0) return;\r\n for (let i = 0; i < items.length; i += size) {\r\n await Promise.all(items.slice(i, i + size).map(fn));\r\n }\r\n}\r\n\r\nexport const filesOrchestrator = new FilesOrchestrator();\r\n"],"mappings":"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"}
1
+ {"version":3,"file":"files-orchestrator.mjs","names":[],"sources":["../../../../../../../core/src/dev-server/files-orchestrator.ts"],"sourcesContent":["import { colors } from \"@mongez/copper\";\r\nimport { init } from \"es-module-lexer\";\r\nimport { fileURLToPath } from \"node:url\";\r\nimport type { MessagePort } from \"node:worker_threads\";\r\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\r\nimport { DependencyGraph } from \"./dependency-graph\";\r\nimport { devLogDim, devLogSuccess } from \"./dev-logger\";\r\nimport { FileEventHandler } from \"./file-event-handler\";\r\nimport { FileManager } from \"./file-manager\";\r\nimport { FileOperations } from \"./file-operations\";\r\nimport { FilesWatcher, createIgnoredMatcher } from \"./files-watcher\";\r\nimport { FILE_PROCESSING_BATCH_SIZE } from \"./flags\";\r\nimport { EslintHealthChecker } from \"./health-checker/checkers/eslint-health-checker\";\r\nimport { TypescriptHealthChecker } from \"./health-checker/checkers/typescript-health-checker\";\r\nimport type { FileHealthCheckerContract } from \"./health-checker/file-health-checker.contract\";\r\nimport { FilesHealthcareManager } from \"./health-checker/files-healthcare.manager\";\r\nimport { buildTranspileInit } from \"./loader/build-transpile-init.js\";\r\nimport { registerLoader } from \"./loader/register-loader.js\";\r\nimport { ManifestManager } from \"./manifest-manager\";\r\nimport { ModuleLoader } from \"./module-loader\";\r\nimport { packageJsonManager } from \"./package-json-manager\";\r\nimport { Path } from \"../utils/normalized-path\";\r\nimport { SpecialFilesCollector } from \"./special-files-collector\";\r\nimport { tsconfigManager } from \"./tsconfig-manager\";\r\nimport { ensureWarlockDirectory, getFilesFromDirectory } from \"./utils\";\r\n\r\n/**\r\n * Register a cleanup callback that fires before the current module is\r\n * unloaded by HMR. Stack-inspects the caller so user code doesn't need to\r\n * thread the FileManager through.\r\n */\r\nexport function onCleanup(callback: () => any) {\r\n if (globalThis.__currentModuleFile) {\r\n globalThis.__currentModuleFile.addCleanup(callback);\r\n return;\r\n }\r\n\r\n const stack = new Error().stack;\r\n const callerLine = stack?.split(\"\\n\")[2];\r\n const match = callerLine?.match(/\\((.+?):\\d+:\\d+\\)/) || callerLine?.match(/at (.+?):\\d+:\\d+/);\r\n const rawCaller = match?.[1];\r\n\r\n // Loaded modules report `file:///…?v=N`; turn that back into a plain path.\r\n const callerFile = rawCaller?.startsWith(\"file://\")\r\n ? fileURLToPath(rawCaller.replace(/\\?.*$/, \"\"))\r\n : rawCaller;\r\n\r\n if (!callerFile) return;\r\n const fileManager = filesOrchestrator.files.get(Path.toRelative(callerFile));\r\n fileManager?.addCleanup(callback);\r\n}\r\n\r\n/**\r\n * Top-level coordinator for the dev server's file system.\r\n *\r\n * Owns: file discovery, the dependency graph, the manifest, the special-files\r\n * index, the file watcher, and the lifecycle of the ESM loader hook (which\r\n * provides cache-busting via `?v=N` version tokens).\r\n */\r\nexport class FilesOrchestrator {\r\n public readonly filesWatcher = new FilesWatcher();\r\n public readonly files = new Map<string, FileManager>();\r\n private readonly manifest = new ManifestManager(this.files);\r\n private readonly dependencyGraph = new DependencyGraph();\r\n private readonly healthCheckerManager = new FilesHealthcareManager(this.files);\r\n public readonly specialFilesCollector = new SpecialFilesCollector();\r\n public readonly moduleLoader = new ModuleLoader(this.specialFilesCollector);\r\n public readonly fileOperations: FileOperations;\r\n private readonly eventHandler: FileEventHandler;\r\n\r\n /** Main-thread end of the MessageChannel to the loader hook worker. */\r\n private loaderPort: MessagePort | null = null;\r\n\r\n /** Resolve callbacks for pending `flushVersionBumps()` calls. */\r\n private readonly pendingFlushes: Array<() => void> = [];\r\n\r\n public isInitialized = false;\r\n\r\n public constructor() {\r\n this.fileOperations = new FileOperations(\r\n this.files,\r\n this.dependencyGraph,\r\n this.manifest,\r\n this.specialFilesCollector,\r\n );\r\n\r\n this.eventHandler = new FileEventHandler(\r\n this.fileOperations,\r\n this.manifest,\r\n this.dependencyGraph,\r\n this.files,\r\n );\r\n }\r\n\r\n public async add(relativePath: string): Promise<FileManager> {\r\n return this.fileOperations.addFile(relativePath);\r\n }\r\n\r\n public async load<T>(relativePath: string, type = \"other\") {\r\n // Guarantee the ESM loader hook is registered before we import anything.\r\n // `load()` is the dynamic-import entrypoint, so it is the one place that\r\n // must satisfy `init()`'s documented precondition (\"Must be called before\r\n // any user src/ module is dynamically imported\") on its own — it cannot\r\n // assume a caller ran `init()` first.\r\n //\r\n // The CLI command-resolution path proves why: `cliCommandsLoader.load()`\r\n // imports a project command module during command *lookup* (before the\r\n // command's own `execute()` runs), whereas `loadPreloaders()` only calls\r\n // `init()` from *inside* `execute()`. So a `warlock <command>` whose\r\n // command file imports an `app/*` alias or a `.ts` sibling was imported\r\n // with no hook registered and died on `ERR_MODULE_NOT_FOUND` (f9d8f99f).\r\n //\r\n // `init()` is idempotent (guarded by `isInitialized`), so this is a no-op\r\n // on the dev-server path, which already calls it explicitly at startup.\r\n await this.init();\r\n\r\n const fileManager = await this.add(relativePath);\r\n return this.moduleLoader.loadModule<T>(fileManager, fileManager.type || type);\r\n }\r\n\r\n public getDependencyGraph(): DependencyGraph {\r\n return this.dependencyGraph;\r\n }\r\n\r\n public getInvalidationChain(file: string): string[] {\r\n return this.dependencyGraph.getInvalidationChain(file);\r\n }\r\n\r\n public getFiles(): Map<string, FileManager> {\r\n return this.files;\r\n }\r\n\r\n public getHealthCheckerManager(): FilesHealthcareManager {\r\n return this.healthCheckerManager;\r\n }\r\n\r\n /**\r\n * Initialise managers and register the ESM loader hook. Must be called\r\n * before any user `src/` module is dynamically imported.\r\n */\r\n public async init() {\r\n if (this.isInitialized) return;\r\n this.isInitialized = true;\r\n\r\n await init;\r\n await Promise.all([tsconfigManager.init(), packageJsonManager.init()]);\r\n\r\n // The loader hook writes itself into .warlock/, so ensure the dir exists.\r\n await ensureWarlockDirectory();\r\n\r\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\r\n const transpileInit = buildTranspileInit(\r\n tsconfigManager.tsconfig?.compilerOptions,\r\n devServerConfig?.transpileCacheDebug === true,\r\n );\r\n\r\n this.loaderPort = await registerLoader(transpileInit);\r\n\r\n this.loaderPort.on(\"message\", (msg: { type: string }) => {\r\n if (msg.type === \"sync-ack\") {\r\n this.pendingFlushes.shift()?.();\r\n }\r\n });\r\n }\r\n\r\n /**\r\n * Tell the hook worker a file changed. The next `import()` of it will get\r\n * a fresh `?v=N` URL → Node cache miss → fresh content.\r\n */\r\n public bumpVersion(absolutePath: string): void {\r\n this.loaderPort?.postMessage({ type: \"bump\", absolutePath });\r\n }\r\n\r\n /**\r\n * Wait until the hook worker has processed every pending bump.\r\n *\r\n * `postMessage` is async — without this, a follow-up `import()` may\r\n * resolve before the worker increments the counter and Node will return\r\n * the cached old module.\r\n */\r\n public flushVersionBumps(): Promise<void> {\r\n if (!this.loaderPort) return Promise.resolve();\r\n\r\n return new Promise<void>((resolve) => {\r\n this.pendingFlushes.push(resolve);\r\n this.loaderPort!.postMessage({ type: \"sync\" });\r\n });\r\n }\r\n\r\n /**\r\n * Discover files on disk, reconcile against the manifest, build the dep\r\n * graph, and persist the new manifest. Idempotent.\r\n */\r\n public async initializeAll() {\r\n const [filesInFilesystem, manifestExists] = await Promise.all([\r\n this.getAllFilesFromFilesystem(),\r\n this.manifest.init(),\r\n ]);\r\n\r\n if (!manifestExists) {\r\n await this.processFiles(filesInFilesystem);\r\n } else {\r\n await this.reconcileFiles(filesInFilesystem);\r\n }\r\n\r\n this.dependencyGraph.build(this.files);\r\n this.fileOperations.updateFileDependents();\r\n this.fileOperations.syncFilesToManifest();\r\n await this.manifest.save();\r\n }\r\n\r\n public async checkHealth(files: { added: string[]; changed: string[]; deleted: string[] }) {\r\n const filesToCheck = [...files.added, ...files.changed]\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n const filesToDelete = files.deleted\r\n .map((file) => this.files.get(Path.toRelative(file)))\r\n .filter((file): file is FileManager => !!file);\r\n\r\n await this.healthCheckerManager.onFileChanges(filesToCheck);\r\n this.healthCheckerManager.removeFiles(filesToDelete);\r\n this.healthCheckerManager.checkFiles(filesToCheck);\r\n }\r\n\r\n public async startCheckingHealth(healthCheckers?: FileHealthCheckerContract[]): Promise<void> {\r\n devLogDim(\"Started File Health Checks in the background.\");\r\n\r\n const checkers = healthCheckers ?? [new TypescriptHealthChecker(), new EslintHealthChecker()];\r\n await this.healthCheckerManager.setHealthCheckers(checkers).initialize();\r\n await this.healthCheckerManager.validateAllFiles();\r\n }\r\n\r\n /**\r\n * Glob the src directory, returning relative paths.\r\n */\r\n public async getAllFilesFromFilesystem(): Promise<string[]> {\r\n const absolutePaths = await getFilesFromDirectory();\r\n return absolutePaths.map((absPath) => Path.toRelative(absPath));\r\n }\r\n\r\n /**\r\n * Process every file from scratch — used when no manifest exists.\r\n */\r\n private async processFiles(filePaths: string[]) {\r\n devLogDim(`processing ${filePaths.length} files...`);\r\n await runInBatches(filePaths, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.process();\r\n });\r\n devLogSuccess(`processed ${filePaths.length} files`);\r\n }\r\n\r\n private async reconcileFiles(filesInFilesystem: string[]) {\r\n const filesInManifest = new Set(this.manifest.getAllFilePaths());\r\n const filesInFilesystemSet = new Set(filesInFilesystem);\r\n\r\n const newFiles = filesInFilesystem.filter((f) => !filesInManifest.has(f));\r\n const deletedFiles = Array.from(filesInManifest).filter((f) => !filesInFilesystemSet.has(f));\r\n const existingFiles = filesInFilesystem.filter((f) => filesInManifest.has(f));\r\n\r\n if (newFiles.length > 0 || deletedFiles.length > 0) {\r\n devLogDim(\r\n `reconciling: ${colors.green(newFiles.length)} new, ${colors.red(deletedFiles.length)} deleted, ${colors.blue(existingFiles.length)} existing`,\r\n );\r\n }\r\n\r\n await this.processFiles(newFiles);\r\n\r\n for (const relativePath of deletedFiles) {\r\n this.manifest.removeFile(relativePath);\r\n this.files.delete(relativePath);\r\n }\r\n\r\n await runInBatches(existingFiles, FILE_PROCESSING_BATCH_SIZE, async (relativePath) => {\r\n const fileManager = new FileManager(\r\n Path.toAbsolute(relativePath),\r\n this.files,\r\n this.fileOperations,\r\n );\r\n this.files.set(relativePath, fileManager);\r\n await fileManager.init(this.manifest.getFile(relativePath));\r\n });\r\n }\r\n\r\n public async watchFiles() {\r\n devLogSuccess(\"watching for file changes (not serving yet)\");\r\n\r\n this.filesWatcher.onFileChange((p, settleMs) => this.eventHandler.handleFileChange(p, settleMs));\r\n this.filesWatcher.onFileAdd((p, settleMs) => this.eventHandler.handleFileAdd(p, settleMs));\r\n this.filesWatcher.onFileDelete((p, settleMs) => this.eventHandler.handleFileDelete(p, settleMs));\r\n\r\n const watchConfig = warlockConfigManager.get(\"devServer\")?.watch;\r\n if (watchConfig?.include?.length) {\n this.eventHandler.setIncludeMatcher(createIgnoredMatcher(watchConfig.include));\n }\n\n await this.filesWatcher.watch(watchConfig);\r\n }\r\n}\r\n\r\nasync function runInBatches<T>(\r\n items: T[],\r\n size: number,\r\n fn: (item: T) => Promise<unknown>,\r\n): Promise<void> {\r\n if (items.length === 0) return;\r\n for (let i = 0; i < items.length; i += size) {\r\n await Promise.all(items.slice(i, i + size).map(fn));\r\n }\r\n}\r\n\r\nexport const filesOrchestrator = new FilesOrchestrator();\r\n"],"mappings":"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"}
@@ -5,6 +5,8 @@ import { warlockConfigManager } from "../warlock-config/warlock-config.manager.m
5
5
  import path from "node:path";
6
6
  import { Random } from "@mongez/reinforcements";
7
7
  import events from "@mongez/events";
8
+ import fs from "node:fs";
9
+ import glob from "fast-glob";
8
10
  import chokidar from "chokidar";
9
11
 
10
12
  //#region ../core/src/dev-server/files-watcher.ts
@@ -17,6 +19,58 @@ const DEFAULT_EXCLUDE = [
17
19
  "**/.warlock/**",
18
20
  "**/.git/**"
19
21
  ];
22
+ const GLOB_CHARS = /[*?{}[\]]/;
23
+ /**
24
+ * Convert a glob (`**`, `*`, `?`, `{a,b}`) to a RegExp over a forward-slash
25
+ * path. chokidar >= 4 has no glob support, so patterns are matched here
26
+ * instead of being handed to it.
27
+ */
28
+ function globToRegExp(glob) {
29
+ let source = "";
30
+ let braceDepth = 0;
31
+ for (let i = 0; i < glob.length; i++) {
32
+ const char = glob[i];
33
+ if (char === "*") if (glob[i + 1] === "*") {
34
+ i++;
35
+ if (glob[i + 1] === "/") {
36
+ i++;
37
+ source += "(?:.*/)?";
38
+ } else source += ".*";
39
+ } else source += "[^/]*";
40
+ else if (char === "?") source += "[^/]";
41
+ else if (char === "{") {
42
+ braceDepth++;
43
+ source += "(?:";
44
+ } else if (char === "}" && braceDepth > 0) {
45
+ braceDepth--;
46
+ source += ")";
47
+ } else if (char === "," && braceDepth > 0) source += "|";
48
+ else source += /[.+^$()|[\]\\]/.test(char) ? `\\${char}` : char;
49
+ }
50
+ return new RegExp(`^${source}$`);
51
+ }
52
+ /**
53
+ * Build a chokidar `ignored` predicate from glob and literal patterns. A
54
+ * pattern matches the absolute path or the project-relative path.
55
+ */
56
+ function createIgnoredMatcher(patterns) {
57
+ const matchers = patterns.map((pattern) => globToRegExp(Path.normalize(pattern)));
58
+ return (path) => {
59
+ const absolute = Path.normalize(path);
60
+ const relative = Path.toRelative(absolute);
61
+ return matchers.some((matcher) => matcher.test(absolute) || matcher.test(relative));
62
+ };
63
+ }
64
+ /**
65
+ * Expand glob `include` entries to concrete files (chokidar watches literal
66
+ * paths only); literal entries pass through unchanged.
67
+ */
68
+ function expandIncludes(include) {
69
+ return include.flatMap((entry) => GLOB_CHARS.test(entry) ? glob.sync(entry.replace(/\\/g, "/"), {
70
+ cwd: rootPath(),
71
+ absolute: true
72
+ }) : [entry]);
73
+ }
20
74
  /**
21
75
  * All .env file variants to watch
22
76
  */
@@ -42,18 +96,23 @@ var FilesWatcher = class {
42
96
  async watch(config) {
43
97
  const devServerConfig = await warlockConfigManager.lazyGet("devServer");
44
98
  const userWatchConfig = devServerConfig?.watch;
99
+ const rootEnvFiles = fs.readdirSync(rootPath(), { withFileTypes: true }).filter((entry) => entry.isFile() && entry.name.startsWith(".env")).map((entry) => entry.name);
45
100
  const basePaths = [
46
- ...ENV_FILES.map((file) => rootPath(file)),
101
+ ...[...new Set([
102
+ ...ENV_FILES,
103
+ ".env.shared",
104
+ ...rootEnvFiles
105
+ ])].map((file) => rootPath(file)),
47
106
  rootPath("warlock.config.ts"),
48
107
  srcPath()
49
108
  ];
50
- const additionalPaths = userWatchConfig?.include || config?.include || [];
109
+ const additionalPaths = expandIncludes(userWatchConfig?.include || config?.include || []);
51
110
  const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));
52
- const ignored = [
111
+ const ignored = createIgnoredMatcher([
53
112
  ...DEFAULT_EXCLUDE,
54
113
  ...userWatchConfig?.exclude || [],
55
114
  ...config?.exclude || []
56
- ];
115
+ ]);
57
116
  const timingsEnabled = devServerConfig?.timings === true;
58
117
  const watcher = chokidar.watch(paths, {
59
118
  ignoreInitial: true,
@@ -142,5 +201,5 @@ var FilesWatcher = class {
142
201
  };
143
202
 
144
203
  //#endregion
145
- export { FilesWatcher };
204
+ export { FilesWatcher, createIgnoredMatcher };
146
205
  //# sourceMappingURL=files-watcher.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"files-watcher.mjs","names":["nodePath"],"sources":["../../../../../../../core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport nodePath from \"node:path\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n const envPaths = ENV_FILES.map((file) => rootPath(file));\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = userWatchConfig?.include || config?.include || [];\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = [\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ];\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, evPath, details) => {\n // On the native (non-fsevents) handler chokidar uses on Windows/Linux,\n // `evPath` is only the changed entry's path *relative to the watched\n // directory* (see chokidar's `handler.js` `handleEvent`/`emitRaw`),\n // not the absolute path the later `add`/`change` event carries. Only\n // fsevents (macOS) hands back an already-absolute path. Reconstruct\n // the absolute path from `details.watchedPath` so the timestamp is\n // keyed the same way the stabilised event looks it up below —\n // otherwise the lookup always misses and the phase silently reads 0.\n const watchedPath = (details as { watchedPath?: string } | undefined)?.watchedPath;\n const absolutePath = watchedPath ? nodePath.resolve(watchedPath, evPath) : evPath;\n const normalized = Path.normalize(absolutePath);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n // `error` and `settleMs` share the same second-argument slot: an error\n // event's subscriber reads it as `Error`, every other event's subscriber\n // reads it as the watcher-settle duration (see the `On*Callback` types\n // above). Passing both positionally (`normalized, error, settleMs`) used\n // to leave `settleMs` in an unread third slot, so every change/add/delete\n // subscriber always received `undefined` in its `settleMs` parameter —\n // the watcher phase's timing was silently discarded here even when the\n // raw-event timestamp above resolved correctly.\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error ?? settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"files-watcher.mjs","names":["fg","nodePath"],"sources":["../../../../../../../core/src/dev-server/files-watcher.ts"],"sourcesContent":["import events from \"@mongez/events\";\nimport { Random } from \"@mongez/reinforcements\";\nimport chokidar from \"chokidar\";\nimport fg from \"fast-glob\";\nimport fs from \"node:fs\";\nimport nodePath from \"node:path\";\nimport { rootPath, srcPath } from \"../utils\";\nimport { warlockConfigManager } from \"../warlock-config/warlock-config.manager\";\nimport { Path } from \"../utils/normalized-path\";\n\ntype FileWatcherEvent = \"change\" | \"delete\" | \"add\" | \"error\" | \"addDir\" | \"unlinkDir\";\n\ntype FileChangeCallback = (filePath: string, settleMs?: number) => void;\ntype FileDeleteCallback = (filePath: string, settleMs?: number) => void;\ntype FileAddCallback = (filePath: string, settleMs?: number) => void;\ntype FileErrorCallback = (filePath: string, error: Error) => void;\ntype FileAddDirCallback = (filePath: string) => void;\ntype FileUnlinkDirCallback = (filePath: string) => void;\ntype OnFileEventCallback =\n | FileChangeCallback\n | FileDeleteCallback\n | FileAddCallback\n | FileErrorCallback\n | FileAddDirCallback\n | FileUnlinkDirCallback;\n\n/**\n * Watch configuration options\n */\nexport type WatchConfig = {\n /**\n * Glob patterns to include\n */\n include?: string[];\n /**\n * Glob patterns to exclude\n */\n exclude?: string[];\n};\n\n/**\n * Default patterns to exclude from watching\n */\nconst DEFAULT_EXCLUDE = [\"**/node_modules/**\", \"**/dist/**\", \"**/.warlock/**\", \"**/.git/**\"];\n\nconst GLOB_CHARS = /[*?{}[\\]]/;\n\n/**\n * Convert a glob (`**`, `*`, `?`, `{a,b}`) to a RegExp over a forward-slash\n * path. chokidar >= 4 has no glob support, so patterns are matched here\n * instead of being handed to it.\n */\nexport function globToRegExp(glob: string): RegExp {\n let source = \"\";\n let braceDepth = 0;\n\n for (let i = 0; i < glob.length; i++) {\n const char = glob[i]!;\n\n if (char === \"*\") {\n if (glob[i + 1] === \"*\") {\n i++;\n if (glob[i + 1] === \"/\") {\n i++;\n source += \"(?:.*/)?\";\n } else {\n source += \".*\";\n }\n } else {\n source += \"[^/]*\";\n }\n } else if (char === \"?\") {\n source += \"[^/]\";\n } else if (char === \"{\") {\n braceDepth++;\n source += \"(?:\";\n } else if (char === \"}\" && braceDepth > 0) {\n braceDepth--;\n source += \")\";\n } else if (char === \",\" && braceDepth > 0) {\n source += \"|\";\n } else {\n source += /[.+^$()|[\\]\\\\]/.test(char) ? `\\\\${char}` : char;\n }\n }\n\n return new RegExp(`^${source}$`);\n}\n\n/**\n * Build a chokidar `ignored` predicate from glob and literal patterns. A\n * pattern matches the absolute path or the project-relative path.\n */\nexport function createIgnoredMatcher(patterns: string[]): (path: string) => boolean {\n const matchers = patterns.map((pattern) => globToRegExp(Path.normalize(pattern)));\n\n return (path: string) => {\n const absolute = Path.normalize(path);\n const relative = Path.toRelative(absolute);\n\n return matchers.some((matcher) => matcher.test(absolute) || matcher.test(relative));\n };\n}\n\n/**\n * Expand glob `include` entries to concrete files (chokidar watches literal\n * paths only); literal entries pass through unchanged.\n */\nfunction expandIncludes(include: string[]): string[] {\n return include.flatMap((entry) =>\n GLOB_CHARS.test(entry)\n ? fg.sync(entry.replace(/\\\\/g, \"/\"), { cwd: rootPath(), absolute: true })\n : [entry],\n );\n}\n\n/**\n * All .env file variants to watch\n */\nconst ENV_FILES = [\n \".env\",\n \".env.local\",\n \".env.development\",\n \".env.development.local\",\n \".env.test\",\n \".env.test.local\",\n \".env.production\",\n \".env.production.local\",\n];\n\nexport class FilesWatcher {\n /**\n * File watcher id\n */\n private id = Random.string();\n\n /**\n * First-seen timestamp per path from chokidar's `raw` event, which fires\n * BEFORE `awaitWriteFinish` stabilises the change. Diffing it against the\n * stabilised `add`/`change` timestamp gives the actual watcher-settle\n * duration for the phase-timing line. Only populated when\n * `devServer.timings` is on — see `watch()` — so a disabled flag costs\n * nothing here beyond the `undefined` checks below.\n */\n private readonly rawEventTimestamps = new Map<string, number>();\n\n /**\n * Watch for files changes\n * @param config Optional watch configuration\n */\n public async watch(config?: WatchConfig) {\n // Get user config from warlock.config.ts\n const devServerConfig = await warlockConfigManager.lazyGet(\"devServer\");\n const userWatchConfig = devServerConfig?.watch;\n\n // Build paths to watch:\n // 1. All .env variants that exist\n // 2. warlock.config.ts (project-level settings; restart-required on change)\n // 3. src directory\n // 4. Any additional paths from user config\n // Also any other `.env*` at the root (`.env.shared`, `.env.staging`, …).\n const rootEnvFiles = fs\n .readdirSync(rootPath(), { withFileTypes: true })\n .filter((entry) => entry.isFile() && entry.name.startsWith(\".env\"))\n .map((entry) => entry.name);\n const envPaths = [...new Set([...ENV_FILES, \".env.shared\", ...rootEnvFiles])].map((file) =>\n rootPath(file),\n );\n const basePaths = [...envPaths, rootPath(\"warlock.config.ts\"), srcPath()];\n const additionalPaths = expandIncludes(userWatchConfig?.include || config?.include || []);\n\n const paths = [...basePaths, ...additionalPaths].map((path) => Path.normalize(path));\n\n // Merge default exclude with config exclude\n const ignored = createIgnoredMatcher([\n ...DEFAULT_EXCLUDE,\n ...(userWatchConfig?.exclude || []),\n ...(config?.exclude || []),\n ]);\n\n const timingsEnabled = devServerConfig?.timings === true;\n\n const watcher = chokidar.watch(paths, {\n ignoreInitial: true,\n ignored,\n persistent: true,\n usePolling: false, // Try native first, will fallback if needed\n interval: 100,\n binaryInterval: 300,\n awaitWriteFinish: {\n stabilityThreshold: 100,\n pollInterval: 50,\n },\n // On Windows, explicitly enable recursive watching\n depth: 99,\n });\n\n if (timingsEnabled) {\n watcher.on(\"raw\", (_event, evPath, details) => {\n // On the native (non-fsevents) handler chokidar uses on Windows/Linux,\n // `evPath` is only the changed entry's path *relative to the watched\n // directory* (see chokidar's `handler.js` `handleEvent`/`emitRaw`),\n // not the absolute path the later `add`/`change` event carries. Only\n // fsevents (macOS) hands back an already-absolute path. Reconstruct\n // the absolute path from `details.watchedPath` so the timestamp is\n // keyed the same way the stabilised event looks it up below —\n // otherwise the lookup always misses and the phase silently reads 0.\n const watchedPath = (details as { watchedPath?: string } | undefined)?.watchedPath;\n const absolutePath = watchedPath ? nodePath.resolve(watchedPath, evPath) : evPath;\n const normalized = Path.normalize(absolutePath);\n if (!this.rawEventTimestamps.has(normalized)) {\n this.rawEventTimestamps.set(normalized, performance.now());\n }\n });\n }\n\n watcher.on(\"add\", (filePath) => this.triggerEvent(\"add\", filePath));\n watcher.on(\"change\", (filePath) => this.triggerEvent(\"change\", filePath));\n watcher.on(\"unlink\", (filePath) => this.triggerEvent(\"delete\", filePath));\n watcher.on(\"addDir\", (filePath) => this.triggerEvent(\"addDir\", filePath));\n watcher.on(\"unlinkDir\", (filePath) => this.triggerEvent(\"unlinkDir\", filePath));\n // watcher.on(\"error\", (error: Error, filePath: string) =>\n // this.triggerEvent(\"error\", filePath, error),\n // );\n\n // Cleanup on process exit\n process.on(\"SIGINT\", async () => {\n await watcher.close();\n });\n }\n\n /**\n * Trigger event immediately (no debouncing here)\n * Debouncing is handled at the orchestrator level for batch processing\n */\n private triggerEvent(event: FileWatcherEvent, filePath: string, error?: Error) {\n const normalized = Path.normalize(filePath);\n const rawEventAt = this.rawEventTimestamps.get(normalized);\n let settleMs: number | undefined;\n\n if (rawEventAt !== undefined) {\n settleMs = performance.now() - rawEventAt;\n this.rawEventTimestamps.delete(normalized);\n }\n\n // `error` and `settleMs` share the same second-argument slot: an error\n // event's subscriber reads it as `Error`, every other event's subscriber\n // reads it as the watcher-settle duration (see the `On*Callback` types\n // above). Passing both positionally (`normalized, error, settleMs`) used\n // to leave `settleMs` in an unread third slot, so every change/add/delete\n // subscriber always received `undefined` in its `settleMs` parameter —\n // the watcher phase's timing was silently discarded here even when the\n // raw-event timestamp above resolved correctly.\n events.trigger(`file-watcher.${this.id}.${event}`, normalized, error ?? settleMs);\n }\n\n /**\n * On file change event\n */\n public onFileChange(callback: FileChangeCallback) {\n return this.on(\"change\", callback);\n }\n\n /**\n * On file delete event\n */\n public onFileDelete(callback: FileDeleteCallback) {\n return this.on(\"delete\", callback);\n }\n\n /**\n * On file add event\n */\n public onFileAdd(callback: FileAddCallback) {\n return this.on(\"add\", callback);\n }\n\n /**\n * On file error event\n */\n public onFileError(callback: FileErrorCallback) {\n return this.on(\"error\", callback);\n }\n\n /**\n * On file add dir event\n */\n public onDirectoryAdd(callback: FileAddDirCallback) {\n return this.on(\"addDir\", callback);\n }\n\n /**\n * On file unlink dir event\n */\n public onDirectoryRemove(callback: FileUnlinkDirCallback) {\n return this.on(\"unlinkDir\", callback);\n }\n\n /**\n * On file event\n */\n public on(event: FileWatcherEvent, callback: OnFileEventCallback) {\n return events.subscribe(`file-watcher.${this.id}.${event}`, callback);\n }\n}\n"],"mappings":"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"}
@@ -35,6 +35,7 @@ var FilesHealthcareManager = class {
35
35
  this.healthCheckers = [];
36
36
  this.workers = /* @__PURE__ */ new Map();
37
37
  this.workerInitPromises = /* @__PURE__ */ new Map();
38
+ this.workerQueues = /* @__PURE__ */ new Map();
38
39
  this.checkerStats = /* @__PURE__ */ new Map();
39
40
  }
40
41
  /**
@@ -152,24 +153,45 @@ var FilesHealthcareManager = class {
152
153
  content: f.source,
153
154
  relativePath: f.relativePath
154
155
  }));
156
+ const current = (this.workerQueues.get(checkerName) ?? Promise.resolve()).then(() => this.postCheck(checkerName, worker, files, serializedFiles));
157
+ this.workerQueues.set(checkerName, current.then(() => void 0, () => void 0));
158
+ return current;
159
+ }
160
+ /**
161
+ * Post one check to the worker and wait for its single reply.
162
+ * Resolves as healthy if the worker errors or exits so callers never hang.
163
+ */
164
+ postCheck(checkerName, worker, files, serializedFiles) {
155
165
  return new Promise((resolve) => {
166
+ const fallback = () => files.map((f) => ({
167
+ path: f.absolutePath,
168
+ relativePath: f.relativePath,
169
+ healthy: true,
170
+ errors: [],
171
+ warnings: []
172
+ }));
173
+ const cleanup = () => {
174
+ worker.off("message", handler);
175
+ worker.off("error", onFailure);
176
+ worker.off("exit", onFailure);
177
+ };
178
+ const onFailure = () => {
179
+ cleanup();
180
+ resolve(fallback());
181
+ };
156
182
  const handler = (response) => {
157
183
  if (response.type === "results") {
158
- worker.off("message", handler);
184
+ cleanup();
159
185
  this.updateStats(checkerName, response.results);
160
186
  resolve(response.results);
161
187
  } else if (response.type === "error") {
162
- worker.off("message", handler);
163
- resolve(files.map((f) => ({
164
- path: f.absolutePath,
165
- relativePath: f.relativePath,
166
- healthy: true,
167
- errors: [],
168
- warnings: []
169
- })));
188
+ cleanup();
189
+ resolve(fallback());
170
190
  }
171
191
  };
172
192
  worker.on("message", handler);
193
+ worker.on("error", onFailure);
194
+ worker.on("exit", onFailure);
173
195
  worker.postMessage({
174
196
  type: "check",
175
197
  files: serializedFiles