@fougere/core 0.2.0-alpha.2 → 0.4.0-alpha.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 (500) hide show
  1. package/README.md +3 -2
  2. package/dist/boot/AppLifecycle.d.ts +18 -0
  3. package/dist/boot/AppLifecycle.d.ts.map +1 -0
  4. package/dist/boot/AppLifecycle.js +42 -0
  5. package/dist/boot/AppLifecycle.js.map +1 -0
  6. package/dist/boot/Emissions.d.ts +126 -0
  7. package/dist/boot/Emissions.d.ts.map +1 -0
  8. package/dist/boot/Emissions.js +233 -0
  9. package/dist/boot/Emissions.js.map +1 -0
  10. package/dist/boot/HandlerFacade.d.ts +72 -0
  11. package/dist/boot/HandlerFacade.d.ts.map +1 -0
  12. package/dist/boot/HandlerFacade.js +174 -0
  13. package/dist/boot/HandlerFacade.js.map +1 -0
  14. package/dist/boot/Lifecycle.d.ts +4 -0
  15. package/dist/boot/Lifecycle.d.ts.map +1 -0
  16. package/dist/boot/Lifecycle.js +3 -0
  17. package/dist/boot/Lifecycle.js.map +1 -0
  18. package/dist/boot/ambient-port.d.ts +38 -0
  19. package/dist/boot/ambient-port.d.ts.map +1 -0
  20. package/dist/boot/ambient-port.js +2 -0
  21. package/dist/boot/ambient-port.js.map +1 -0
  22. package/dist/boot/ambient.als.d.ts +3 -0
  23. package/dist/boot/ambient.als.d.ts.map +1 -0
  24. package/dist/boot/ambient.als.js +34 -0
  25. package/dist/boot/ambient.als.js.map +1 -0
  26. package/dist/boot/ambient.queue.d.ts +10 -0
  27. package/dist/boot/ambient.queue.d.ts.map +1 -0
  28. package/dist/boot/ambient.queue.js +60 -0
  29. package/dist/boot/ambient.queue.js.map +1 -0
  30. package/dist/boot/apply.d.ts +22 -0
  31. package/dist/boot/apply.d.ts.map +1 -0
  32. package/dist/boot/apply.js +38 -0
  33. package/dist/boot/apply.js.map +1 -0
  34. package/dist/boot/auth.d.ts +53 -0
  35. package/dist/boot/auth.d.ts.map +1 -0
  36. package/dist/boot/auth.js +2 -0
  37. package/dist/boot/auth.js.map +1 -0
  38. package/dist/{binding.d.ts → boot/binding.d.ts} +14 -17
  39. package/dist/boot/binding.d.ts.map +1 -0
  40. package/dist/{binding.js → boot/binding.js} +19 -10
  41. package/dist/boot/binding.js.map +1 -0
  42. package/dist/{boot.d.ts → boot/boot.d.ts} +25 -5
  43. package/dist/boot/boot.d.ts.map +1 -0
  44. package/dist/boot/boot.js +70 -0
  45. package/dist/boot/boot.js.map +1 -0
  46. package/dist/boot/bootstrap.d.ts.map +1 -0
  47. package/dist/boot/bootstrap.js +722 -0
  48. package/dist/boot/bootstrap.js.map +1 -0
  49. package/dist/boot/frame.d.ts +44 -0
  50. package/dist/boot/frame.d.ts.map +1 -0
  51. package/dist/boot/frame.js +172 -0
  52. package/dist/boot/frame.js.map +1 -0
  53. package/dist/boot/ownership.d.ts +42 -0
  54. package/dist/boot/ownership.d.ts.map +1 -0
  55. package/dist/boot/ownership.js +127 -0
  56. package/dist/boot/ownership.js.map +1 -0
  57. package/dist/boot/ports.d.ts +17 -0
  58. package/dist/boot/ports.d.ts.map +1 -0
  59. package/dist/boot/ports.js +57 -0
  60. package/dist/boot/ports.js.map +1 -0
  61. package/dist/{remote.d.ts → boot/remote.d.ts} +11 -3
  62. package/dist/boot/remote.d.ts.map +1 -0
  63. package/dist/{remote.js → boot/remote.js} +26 -28
  64. package/dist/boot/remote.js.map +1 -0
  65. package/dist/{seed.d.ts → boot/seed.d.ts} +13 -1
  66. package/dist/boot/seed.d.ts.map +1 -0
  67. package/dist/{seed.js → boot/seed.js} +24 -3
  68. package/dist/boot/seed.js.map +1 -0
  69. package/dist/boot/together.d.ts +26 -0
  70. package/dist/boot/together.d.ts.map +1 -0
  71. package/dist/boot/together.js +163 -0
  72. package/dist/boot/together.js.map +1 -0
  73. package/dist/boot/types.d.ts +260 -0
  74. package/dist/boot/types.d.ts.map +1 -0
  75. package/dist/boot/types.js.map +1 -0
  76. package/dist/builtins/config.d.ts +9 -0
  77. package/dist/builtins/config.d.ts.map +1 -1
  78. package/dist/builtins/config.js +16 -1
  79. package/dist/builtins/config.js.map +1 -1
  80. package/dist/builtins/logger.d.ts +37 -4
  81. package/dist/builtins/logger.d.ts.map +1 -1
  82. package/dist/builtins/logger.js +66 -7
  83. package/dist/builtins/logger.js.map +1 -1
  84. package/dist/config-loader.d.ts +39 -35
  85. package/dist/config-loader.d.ts.map +1 -1
  86. package/dist/config-loader.js +15 -54
  87. package/dist/config-loader.js.map +1 -1
  88. package/dist/contract/Call.d.ts +10 -0
  89. package/dist/contract/Call.d.ts.map +1 -0
  90. package/dist/contract/Call.js +15 -0
  91. package/dist/contract/Call.js.map +1 -0
  92. package/dist/contract/Invocation.d.ts +27 -0
  93. package/dist/contract/Invocation.d.ts.map +1 -0
  94. package/dist/contract/Invocation.js +59 -0
  95. package/dist/contract/Invocation.js.map +1 -0
  96. package/dist/contract/RouteAddress.d.ts +17 -0
  97. package/dist/contract/RouteAddress.d.ts.map +1 -0
  98. package/dist/contract/RouteAddress.js +38 -0
  99. package/dist/contract/RouteAddress.js.map +1 -0
  100. package/dist/contract.d.ts +25 -19
  101. package/dist/contract.d.ts.map +1 -1
  102. package/dist/contract.js +25 -10
  103. package/dist/contract.js.map +1 -1
  104. package/dist/crypto/encoding.d.ts +25 -0
  105. package/dist/crypto/encoding.d.ts.map +1 -0
  106. package/dist/crypto/encoding.js +58 -0
  107. package/dist/crypto/encoding.js.map +1 -0
  108. package/dist/crypto/node.d.ts +4 -0
  109. package/dist/crypto/node.d.ts.map +1 -0
  110. package/dist/crypto/node.js +25 -0
  111. package/dist/crypto/node.js.map +1 -0
  112. package/dist/crypto/port.d.ts +42 -0
  113. package/dist/crypto/port.d.ts.map +1 -0
  114. package/dist/crypto/port.js +19 -0
  115. package/dist/crypto/port.js.map +1 -0
  116. package/dist/crypto/webcrypto.d.ts +4 -0
  117. package/dist/crypto/webcrypto.d.ts.map +1 -0
  118. package/dist/crypto/webcrypto.js +34 -0
  119. package/dist/crypto/webcrypto.js.map +1 -0
  120. package/dist/dispatch/ArgumentResolver.d.ts +10 -0
  121. package/dist/dispatch/ArgumentResolver.d.ts.map +1 -0
  122. package/dist/dispatch/ArgumentResolver.js +12 -0
  123. package/dist/dispatch/ArgumentResolver.js.map +1 -0
  124. package/dist/dispatch/ArrayResult.d.ts +3 -0
  125. package/dist/dispatch/ArrayResult.d.ts.map +1 -0
  126. package/dist/dispatch/ArrayResult.js +11 -0
  127. package/dist/dispatch/ArrayResult.js.map +1 -0
  128. package/dist/dispatch/DispatchEvent.d.ts +18 -0
  129. package/dist/dispatch/DispatchEvent.d.ts.map +1 -0
  130. package/dist/dispatch/DispatchEvent.js +30 -0
  131. package/dist/dispatch/DispatchEvent.js.map +1 -0
  132. package/dist/dispatch/DispatchLifecycle.d.ts +9 -0
  133. package/dist/dispatch/DispatchLifecycle.d.ts.map +1 -0
  134. package/dist/dispatch/DispatchLifecycle.js +25 -0
  135. package/dist/dispatch/DispatchLifecycle.js.map +1 -0
  136. package/dist/dispatch/DispatchPort.d.ts +6 -0
  137. package/dist/dispatch/DispatchPort.d.ts.map +1 -0
  138. package/dist/dispatch/DispatchPort.js +2 -0
  139. package/dist/dispatch/DispatchPort.js.map +1 -0
  140. package/dist/dispatch/Dispatcher.d.ts +16 -0
  141. package/dist/dispatch/Dispatcher.d.ts.map +1 -0
  142. package/dist/dispatch/Dispatcher.js +44 -0
  143. package/dist/dispatch/Dispatcher.js.map +1 -0
  144. package/dist/dispatch/InFlight.d.ts +12 -0
  145. package/dist/dispatch/InFlight.d.ts.map +1 -0
  146. package/dist/dispatch/InFlight.js +46 -0
  147. package/dist/dispatch/InFlight.js.map +1 -0
  148. package/dist/dispatch/InputValidator.d.ts +7 -0
  149. package/dist/dispatch/InputValidator.d.ts.map +1 -0
  150. package/dist/dispatch/InputValidator.js +22 -0
  151. package/dist/dispatch/InputValidator.js.map +1 -0
  152. package/dist/dispatch/LocalRoutePolicy.d.ts +11 -0
  153. package/dist/dispatch/LocalRoutePolicy.d.ts.map +1 -0
  154. package/dist/dispatch/LocalRoutePolicy.js +32 -0
  155. package/dist/dispatch/LocalRoutePolicy.js.map +1 -0
  156. package/dist/dispatch/OperationExecutor.d.ts +25 -0
  157. package/dist/dispatch/OperationExecutor.d.ts.map +1 -0
  158. package/dist/dispatch/OperationExecutor.js +32 -0
  159. package/dist/dispatch/OperationExecutor.js.map +1 -0
  160. package/dist/dispatch/OperationRoute.d.ts +13 -0
  161. package/dist/dispatch/OperationRoute.d.ts.map +1 -0
  162. package/dist/dispatch/OperationRoute.js +15 -0
  163. package/dist/dispatch/OperationRoute.js.map +1 -0
  164. package/dist/dispatch/OutputProjector.d.ts +12 -0
  165. package/dist/dispatch/OutputProjector.d.ts.map +1 -0
  166. package/dist/dispatch/OutputProjector.js +30 -0
  167. package/dist/dispatch/OutputProjector.js.map +1 -0
  168. package/dist/dispatch/OutputView.d.ts +8 -0
  169. package/dist/dispatch/OutputView.d.ts.map +1 -0
  170. package/dist/dispatch/OutputView.js +10 -0
  171. package/dist/dispatch/OutputView.js.map +1 -0
  172. package/dist/dispatch/PresenterArgumentResolver.d.ts +12 -0
  173. package/dist/dispatch/PresenterArgumentResolver.d.ts.map +1 -0
  174. package/dist/dispatch/PresenterArgumentResolver.js +20 -0
  175. package/dist/dispatch/PresenterArgumentResolver.js.map +1 -0
  176. package/dist/dispatch/PresenterExecutor.d.ts +11 -0
  177. package/dist/dispatch/PresenterExecutor.d.ts.map +1 -0
  178. package/dist/dispatch/PresenterExecutor.js +56 -0
  179. package/dist/dispatch/PresenterExecutor.js.map +1 -0
  180. package/dist/dispatch/RemoteRouteResolver.d.ts +13 -0
  181. package/dist/dispatch/RemoteRouteResolver.d.ts.map +1 -0
  182. package/dist/dispatch/RemoteRouteResolver.js +18 -0
  183. package/dist/dispatch/RemoteRouteResolver.js.map +1 -0
  184. package/dist/dispatch/Route.d.ts +10 -0
  185. package/dist/dispatch/Route.d.ts.map +1 -0
  186. package/dist/dispatch/Route.js +2 -0
  187. package/dist/dispatch/Route.js.map +1 -0
  188. package/dist/dispatch/RouteNotFoundError.d.ts +10 -0
  189. package/dist/dispatch/RouteNotFoundError.d.ts.map +1 -0
  190. package/dist/dispatch/RouteNotFoundError.js +24 -0
  191. package/dist/dispatch/RouteNotFoundError.js.map +1 -0
  192. package/dist/dispatch/RoutePolicy.d.ts +8 -0
  193. package/dist/dispatch/RoutePolicy.d.ts.map +1 -0
  194. package/dist/dispatch/RoutePolicy.js +2 -0
  195. package/dist/dispatch/RoutePolicy.js.map +1 -0
  196. package/dist/dispatch/RouteRegistry.d.ts +18 -0
  197. package/dist/dispatch/RouteRegistry.d.ts.map +1 -0
  198. package/dist/dispatch/RouteRegistry.js +62 -0
  199. package/dist/dispatch/RouteRegistry.js.map +1 -0
  200. package/dist/dispatch/RouteResolver.d.ts +7 -0
  201. package/dist/dispatch/RouteResolver.d.ts.map +1 -0
  202. package/dist/dispatch/RouteResolver.js +2 -0
  203. package/dist/dispatch/RouteResolver.js.map +1 -0
  204. package/dist/dispatch/StorageGuard.d.ts +15 -0
  205. package/dist/dispatch/StorageGuard.d.ts.map +1 -0
  206. package/dist/dispatch/StorageGuard.js +63 -0
  207. package/dist/dispatch/StorageGuard.js.map +1 -0
  208. package/dist/effective-operation.d.ts +90 -0
  209. package/dist/effective-operation.d.ts.map +1 -0
  210. package/dist/effective-operation.js +449 -0
  211. package/dist/effective-operation.js.map +1 -0
  212. package/dist/emit.d.ts +2 -2
  213. package/dist/emit.d.ts.map +1 -1
  214. package/dist/emit.js +2 -2
  215. package/dist/emit.js.map +1 -1
  216. package/dist/entry/DynamicFacade.d.ts +10 -0
  217. package/dist/entry/DynamicFacade.d.ts.map +1 -0
  218. package/dist/entry/DynamicFacade.js +27 -0
  219. package/dist/entry/DynamicFacade.js.map +1 -0
  220. package/dist/entry/FacadeEntry.d.ts +11 -0
  221. package/dist/entry/FacadeEntry.d.ts.map +1 -0
  222. package/dist/entry/FacadeEntry.js +26 -0
  223. package/dist/entry/FacadeEntry.js.map +1 -0
  224. package/dist/entry/TransportEntry.d.ts +13 -0
  225. package/dist/entry/TransportEntry.d.ts.map +1 -0
  226. package/dist/entry/TransportEntry.js +24 -0
  227. package/dist/entry/TransportEntry.js.map +1 -0
  228. package/dist/frond-config.d.ts +24 -7
  229. package/dist/frond-config.d.ts.map +1 -1
  230. package/dist/frond-config.js.map +1 -1
  231. package/dist/graph.d.ts +1 -1
  232. package/dist/graph.d.ts.map +1 -1
  233. package/dist/graph.js +3 -2
  234. package/dist/graph.js.map +1 -1
  235. package/dist/identity-keys.d.ts +14 -0
  236. package/dist/identity-keys.d.ts.map +1 -0
  237. package/dist/identity-keys.js +37 -0
  238. package/dist/identity-keys.js.map +1 -0
  239. package/dist/identity.d.ts +58 -0
  240. package/dist/identity.d.ts.map +1 -0
  241. package/dist/identity.js +182 -0
  242. package/dist/identity.js.map +1 -0
  243. package/dist/imports.d.ts +2 -2
  244. package/dist/imports.d.ts.map +1 -1
  245. package/dist/imports.js +7 -3
  246. package/dist/imports.js.map +1 -1
  247. package/dist/index.d.ts +57 -38
  248. package/dist/index.d.ts.map +1 -1
  249. package/dist/index.js +38 -32
  250. package/dist/index.js.map +1 -1
  251. package/dist/loader.d.ts +16 -0
  252. package/dist/loader.d.ts.map +1 -0
  253. package/dist/loader.js +25 -0
  254. package/dist/loader.js.map +1 -0
  255. package/dist/node.d.ts +21 -0
  256. package/dist/node.d.ts.map +1 -0
  257. package/dist/node.js +24 -0
  258. package/dist/node.js.map +1 -0
  259. package/dist/orm.d.ts +69 -8
  260. package/dist/orm.d.ts.map +1 -1
  261. package/dist/orm.js +35 -2
  262. package/dist/orm.js.map +1 -1
  263. package/dist/prefab/collector.d.ts +29 -0
  264. package/dist/prefab/collector.d.ts.map +1 -0
  265. package/dist/prefab/collector.js +34 -0
  266. package/dist/prefab/collector.js.map +1 -0
  267. package/dist/{crud.d.ts → prefab/crud.d.ts} +7 -8
  268. package/dist/prefab/crud.d.ts.map +1 -0
  269. package/dist/{crud.js → prefab/crud.js} +39 -5
  270. package/dist/prefab/crud.js.map +1 -0
  271. package/dist/prefab/mirror.d.ts +52 -0
  272. package/dist/prefab/mirror.d.ts.map +1 -0
  273. package/dist/prefab/mirror.js +66 -0
  274. package/dist/prefab/mirror.js.map +1 -0
  275. package/dist/prefab/prefab.d.ts +15 -0
  276. package/dist/prefab/prefab.d.ts.map +1 -0
  277. package/dist/prefab/prefab.js +19 -0
  278. package/dist/prefab/prefab.js.map +1 -0
  279. package/dist/{presenter.d.ts → prefab/presenter.d.ts} +6 -17
  280. package/dist/prefab/presenter.d.ts.map +1 -0
  281. package/dist/{presenter.js → prefab/presenter.js} +5 -23
  282. package/dist/prefab/presenter.js.map +1 -0
  283. package/dist/prefab/repository.d.ts +104 -0
  284. package/dist/prefab/repository.d.ts.map +1 -0
  285. package/dist/prefab/repository.js +62 -0
  286. package/dist/prefab/repository.js.map +1 -0
  287. package/dist/scan/Fronds.d.ts +45 -0
  288. package/dist/scan/Fronds.d.ts.map +1 -0
  289. package/dist/scan/Fronds.js +66 -0
  290. package/dist/scan/Fronds.js.map +1 -0
  291. package/dist/scan/conventions.d.ts +74 -0
  292. package/dist/scan/conventions.d.ts.map +1 -0
  293. package/dist/scan/conventions.js +82 -0
  294. package/dist/scan/conventions.js.map +1 -0
  295. package/dist/scan/emit.d.ts +14 -0
  296. package/dist/scan/emit.d.ts.map +1 -0
  297. package/dist/scan/emit.js +212 -0
  298. package/dist/scan/emit.js.map +1 -0
  299. package/dist/scan/frond.d.ts +224 -0
  300. package/dist/scan/frond.d.ts.map +1 -0
  301. package/dist/scan/frond.js +2 -0
  302. package/dist/scan/frond.js.map +1 -0
  303. package/dist/{handler-parser.d.ts → scan/handler-parser.d.ts} +20 -9
  304. package/dist/scan/handler-parser.d.ts.map +1 -0
  305. package/dist/scan/handler-parser.js +665 -0
  306. package/dist/scan/handler-parser.js.map +1 -0
  307. package/dist/scan/scan-cache.d.ts +16 -0
  308. package/dist/scan/scan-cache.d.ts.map +1 -0
  309. package/dist/{scan-cache.js → scan/scan-cache.js} +27 -20
  310. package/dist/scan/scan-cache.js.map +1 -0
  311. package/dist/scan/scanner.d.ts +20 -0
  312. package/dist/scan/scanner.d.ts.map +1 -0
  313. package/dist/{scanner.js → scan/scanner.js} +212 -167
  314. package/dist/scan/scanner.js.map +1 -0
  315. package/dist/verify.d.ts +16 -3
  316. package/dist/verify.d.ts.map +1 -1
  317. package/dist/verify.js +21 -18
  318. package/dist/verify.js.map +1 -1
  319. package/dist/{call.d.ts → wire/call.d.ts} +76 -7
  320. package/dist/wire/call.d.ts.map +1 -0
  321. package/dist/{call.js → wire/call.js} +53 -80
  322. package/dist/wire/call.js.map +1 -0
  323. package/dist/{middleware.d.ts → wire/errors.d.ts} +18 -19
  324. package/dist/wire/errors.d.ts.map +1 -0
  325. package/dist/{middleware.js → wire/errors.js} +26 -14
  326. package/dist/wire/errors.js.map +1 -0
  327. package/dist/{errors.d.ts → wire/http-error.d.ts} +5 -6
  328. package/dist/wire/http-error.d.ts.map +1 -0
  329. package/dist/{errors.js → wire/http-error.js} +7 -6
  330. package/dist/wire/http-error.js.map +1 -0
  331. package/dist/wire/invocation.d.ts +4 -0
  332. package/dist/wire/invocation.d.ts.map +1 -0
  333. package/dist/wire/invocation.js +3 -0
  334. package/dist/wire/invocation.js.map +1 -0
  335. package/dist/{middleware/logger.d.ts → wire/loggerMiddleware.d.ts} +3 -3
  336. package/dist/wire/loggerMiddleware.d.ts.map +1 -0
  337. package/dist/{middleware/logger.js → wire/loggerMiddleware.js} +2 -2
  338. package/dist/wire/loggerMiddleware.js.map +1 -0
  339. package/dist/wire/middleware.d.ts +29 -0
  340. package/dist/wire/middleware.d.ts.map +1 -0
  341. package/dist/wire/middleware.js +20 -0
  342. package/dist/wire/middleware.js.map +1 -0
  343. package/dist/{operation.d.ts → wire/operation.d.ts} +39 -10
  344. package/dist/wire/operation.d.ts.map +1 -0
  345. package/dist/wire/operation.js +167 -0
  346. package/dist/wire/operation.js.map +1 -0
  347. package/package.json +27 -8
  348. package/src/boot/AppLifecycle.ts +50 -0
  349. package/src/boot/Emissions.ts +263 -0
  350. package/src/boot/HandlerFacade.ts +242 -0
  351. package/src/boot/Lifecycle.ts +3 -0
  352. package/src/boot/ambient-port.ts +41 -0
  353. package/src/boot/ambient.als.ts +43 -0
  354. package/src/boot/ambient.queue.ts +75 -0
  355. package/src/boot/apply.ts +47 -0
  356. package/src/boot/auth.ts +55 -0
  357. package/src/boot/binding.ts +182 -0
  358. package/src/boot/boot.ts +125 -0
  359. package/src/boot/bootstrap.ts +839 -0
  360. package/src/boot/frame.ts +210 -0
  361. package/src/boot/ownership.ts +140 -0
  362. package/src/boot/ports.ts +66 -0
  363. package/src/boot/remote.ts +179 -0
  364. package/src/boot/seed.ts +158 -0
  365. package/src/boot/together.ts +215 -0
  366. package/src/boot/types.ts +261 -0
  367. package/src/builtins/config.ts +19 -0
  368. package/src/builtins/logger.ts +175 -0
  369. package/src/config-loader.ts +146 -0
  370. package/src/contract/Call.ts +18 -0
  371. package/src/contract/Invocation.ts +72 -0
  372. package/src/contract/RouteAddress.ts +49 -0
  373. package/src/contract.ts +74 -0
  374. package/src/crypto/encoding.ts +59 -0
  375. package/src/crypto/node.ts +30 -0
  376. package/src/crypto/port.ts +45 -0
  377. package/src/crypto/webcrypto.ts +55 -0
  378. package/src/define.ts +10 -0
  379. package/src/dispatch/ArgumentResolver.ts +17 -0
  380. package/src/dispatch/ArrayResult.ts +10 -0
  381. package/src/dispatch/DispatchEvent.ts +38 -0
  382. package/src/dispatch/DispatchLifecycle.ts +23 -0
  383. package/src/dispatch/DispatchPort.ts +6 -0
  384. package/src/dispatch/Dispatcher.ts +48 -0
  385. package/src/dispatch/InFlight.ts +49 -0
  386. package/src/dispatch/InputValidator.ts +28 -0
  387. package/src/dispatch/LocalRoutePolicy.ts +34 -0
  388. package/src/dispatch/OperationExecutor.ts +60 -0
  389. package/src/dispatch/OperationRoute.ts +18 -0
  390. package/src/dispatch/OutputProjector.ts +32 -0
  391. package/src/dispatch/OutputView.ts +9 -0
  392. package/src/dispatch/PresenterArgumentResolver.ts +25 -0
  393. package/src/dispatch/PresenterExecutor.ts +55 -0
  394. package/src/dispatch/RemoteRouteResolver.ts +21 -0
  395. package/src/dispatch/Route.ts +11 -0
  396. package/src/dispatch/RouteNotFoundError.ts +29 -0
  397. package/src/dispatch/RoutePolicy.ts +8 -0
  398. package/src/dispatch/RouteRegistry.ts +73 -0
  399. package/src/dispatch/RouteResolver.ts +7 -0
  400. package/src/dispatch/StorageGuard.ts +71 -0
  401. package/src/effective-operation.ts +597 -0
  402. package/src/emit.ts +90 -0
  403. package/src/entry/DynamicFacade.ts +28 -0
  404. package/src/entry/FacadeEntry.ts +32 -0
  405. package/src/entry/TransportEntry.ts +30 -0
  406. package/src/frond-config.ts +138 -0
  407. package/src/graph.ts +180 -0
  408. package/src/identity-keys.ts +43 -0
  409. package/src/identity.ts +235 -0
  410. package/src/imports.ts +123 -0
  411. package/src/index.ts +106 -0
  412. package/src/loader.ts +41 -0
  413. package/src/node.ts +28 -0
  414. package/src/orm.ts +252 -0
  415. package/src/prefab/collector.ts +36 -0
  416. package/src/prefab/crud.ts +188 -0
  417. package/src/prefab/mirror.ts +122 -0
  418. package/src/prefab/prefab.ts +46 -0
  419. package/src/prefab/presenter.ts +63 -0
  420. package/src/prefab/repository.ts +170 -0
  421. package/src/scan/Fronds.ts +73 -0
  422. package/src/scan/conventions.ts +122 -0
  423. package/src/scan/emit.ts +231 -0
  424. package/src/scan/frond.ts +235 -0
  425. package/src/scan/handler-parser.ts +818 -0
  426. package/src/scan/scan-cache.ts +115 -0
  427. package/src/scan/scanner.ts +758 -0
  428. package/src/verify.ts +193 -0
  429. package/src/wire/call.ts +369 -0
  430. package/src/wire/errors.ts +124 -0
  431. package/src/wire/http-error.ts +77 -0
  432. package/src/wire/invocation.ts +10 -0
  433. package/src/wire/loggerMiddleware.ts +29 -0
  434. package/src/wire/middleware.ts +47 -0
  435. package/src/wire/operation.ts +258 -0
  436. package/dist/binding.d.ts.map +0 -1
  437. package/dist/binding.js.map +0 -1
  438. package/dist/boot.d.ts.map +0 -1
  439. package/dist/boot.js +0 -53
  440. package/dist/boot.js.map +0 -1
  441. package/dist/bootstrap.d.ts.map +0 -1
  442. package/dist/bootstrap.js +0 -879
  443. package/dist/bootstrap.js.map +0 -1
  444. package/dist/call.d.ts.map +0 -1
  445. package/dist/call.js.map +0 -1
  446. package/dist/collector.d.ts +0 -28
  447. package/dist/collector.d.ts.map +0 -1
  448. package/dist/collector.js +0 -32
  449. package/dist/collector.js.map +0 -1
  450. package/dist/crud.d.ts.map +0 -1
  451. package/dist/crud.js.map +0 -1
  452. package/dist/egress.d.ts +0 -52
  453. package/dist/egress.d.ts.map +0 -1
  454. package/dist/egress.js +0 -186
  455. package/dist/egress.js.map +0 -1
  456. package/dist/errors.d.ts.map +0 -1
  457. package/dist/errors.js.map +0 -1
  458. package/dist/handler-parser.d.ts.map +0 -1
  459. package/dist/handler-parser.js +0 -505
  460. package/dist/handler-parser.js.map +0 -1
  461. package/dist/invocation.d.ts +0 -20
  462. package/dist/invocation.d.ts.map +0 -1
  463. package/dist/invocation.js +0 -15
  464. package/dist/invocation.js.map +0 -1
  465. package/dist/middleware/error-handler.d.ts +0 -15
  466. package/dist/middleware/error-handler.d.ts.map +0 -1
  467. package/dist/middleware/error-handler.js +0 -29
  468. package/dist/middleware/error-handler.js.map +0 -1
  469. package/dist/middleware/logger.d.ts.map +0 -1
  470. package/dist/middleware/logger.js.map +0 -1
  471. package/dist/middleware.d.ts.map +0 -1
  472. package/dist/middleware.js.map +0 -1
  473. package/dist/mirror.d.ts +0 -93
  474. package/dist/mirror.d.ts.map +0 -1
  475. package/dist/mirror.js +0 -96
  476. package/dist/mirror.js.map +0 -1
  477. package/dist/operation.d.ts.map +0 -1
  478. package/dist/operation.js +0 -95
  479. package/dist/operation.js.map +0 -1
  480. package/dist/presenter.d.ts.map +0 -1
  481. package/dist/presenter.js.map +0 -1
  482. package/dist/remote.d.ts.map +0 -1
  483. package/dist/remote.js.map +0 -1
  484. package/dist/repository.d.ts +0 -55
  485. package/dist/repository.d.ts.map +0 -1
  486. package/dist/repository.js +0 -26
  487. package/dist/repository.js.map +0 -1
  488. package/dist/scan-cache.d.ts +0 -10
  489. package/dist/scan-cache.d.ts.map +0 -1
  490. package/dist/scan-cache.js.map +0 -1
  491. package/dist/scanner.d.ts +0 -34
  492. package/dist/scanner.d.ts.map +0 -1
  493. package/dist/scanner.js.map +0 -1
  494. package/dist/seed.d.ts.map +0 -1
  495. package/dist/seed.js.map +0 -1
  496. package/dist/types.d.ts +0 -421
  497. package/dist/types.d.ts.map +0 -1
  498. package/dist/types.js.map +0 -1
  499. /package/dist/{bootstrap.d.ts → boot/bootstrap.d.ts} +0 -0
  500. /package/dist/{types.js → boot/types.js} +0 -0
@@ -0,0 +1,758 @@
1
+ import { readdir, readFile } from 'node:fs/promises';
2
+ import { existsSync, type Dirent } from 'node:fs';
3
+ import { join, dirname, basename, resolve as resolvePath } from 'node:path';
4
+ import type { FrondDescriptor, ProviderEntry, EntityEntry, HandlerEntry, PresenterEntry, CollectorEntry, SeedEntry, ScanResult, ScanDiagnostic } from './frond.js';
5
+ import { ANONYMOUS_SCHEMA_NAME, type SchemaView } from '@fougere/schema';
6
+ import type { OperationContract, OperationsMap } from '../wire/operation.js';
7
+ import { cardinalityOf } from '../wire/operation.js';
8
+ import { computeBindingPlan } from '../boot/binding.js';
9
+ import {
10
+ parseAllHandlerMethods,
11
+ parsePresenterMethods,
12
+ parseConstructorParams,
13
+ resetTypePrograms,
14
+ seedTypeProgram,
15
+ type ParsedType,
16
+ } from './handler-parser.js';
17
+ import { loadFrondConfig } from '../frond-config.js';
18
+ import { emitKeyOf } from '../emit.js';
19
+ import { getPresenterFields } from '../prefab/presenter.js';
20
+ import { ormKeyOf, togetherKeyOf } from '../orm.js';
21
+ import { targetOf, viewsOf, outputOf } from '../prefab/prefab.js';
22
+ import { ownedBy, repositoryKeyOf } from '../prefab/repository.js';
23
+ import { lowerFirst } from '@fougere/schema';
24
+ import { Fronds } from './Fronds.js';
25
+ import { getModuleLoader } from '../loader.js';
26
+ import {
27
+ type Conventions, type ConventionsInput,
28
+ DEFAULT_CONVENTIONS, resolveConventions, frondPackage, providerDirsOf, frondDirsOf,
29
+ } from './conventions.js';
30
+
31
+ // FS
32
+
33
+ /**
34
+ * What this scan run could not do. Reset by {@link scanProject}, which owns a run.
35
+ *
36
+ * Module-scoped like the loader and the cache root above: the scanner already has a
37
+ * notion of "the current run". Three sites record — measured — so a per-run object
38
+ * would carry twenty methods to spare one line of reset.
39
+ */
40
+ let diagnostics: ScanDiagnostic[] = [];
41
+
42
+ function record(d: ScanDiagnostic): void {
43
+ diagnostics.push(d);
44
+ }
45
+
46
+ /**
47
+ * An absent convention directory is the ordinary case — a frond without
48
+ * `presenters/` is not a defect. Anything else (permissions, an I/O error, a path
49
+ * that is not a directory) means the scan did not look, and answering `[]` says
50
+ * it did. One `catch` used to conflate the two.
51
+ */
52
+ async function readEntries(path: string): Promise<Dirent[]> {
53
+ try {
54
+ return await readdir(path, { withFileTypes: true });
55
+ } catch (cause) {
56
+ if ((cause as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
57
+ record({
58
+ severity: 'blocking',
59
+ code: 'directory-unreadable',
60
+ filePath: path,
61
+ // The path is NOT repeated here: `filePath` carries it, and a renderer that
62
+ // prints both wraps an absolute path twice into an unreadable box.
63
+ message: 'Could not read this directory — anything it declares is missing from '
64
+ + 'the app, and nothing downstream can tell that from an empty directory.',
65
+ cause,
66
+ });
67
+ return [];
68
+ }
69
+ }
70
+
71
+ async function dirs(path: string): Promise<string[]> {
72
+ const entries = await readEntries(path);
73
+ return entries.filter((e) => e.isDirectory()).map((e) => e.name);
74
+ }
75
+
76
+ async function files(path: string): Promise<string[]> {
77
+ const entries = await readEntries(path);
78
+ return entries
79
+ .filter((e) => e.isFile() && (e.name.endsWith('.ts') || e.name.endsWith('.js')))
80
+ .map((e) => join(path, e.name));
81
+ }
82
+
83
+ // Module
84
+
85
+ async function loadModule(filePath: string): Promise<Record<string, unknown>> {
86
+ return await getModuleLoader()(filePath);
87
+ }
88
+
89
+ async function loadDefault(filePath: string): Promise<unknown> {
90
+ const mod = await loadModule(filePath);
91
+ return mod.default;
92
+ }
93
+
94
+ async function loadClass(filePath: string): Promise<ProviderEntry['ctor']> {
95
+ const ctor = await loadDefault(filePath);
96
+ if (typeof ctor !== 'function' || !ctor.prototype)
97
+ throw new Error(`${filePath}: default export is not a class`);
98
+ return ctor as ProviderEntry['ctor'];
99
+ }
100
+
101
+ function isEntityClass(value: unknown): value is SchemaView {
102
+ return typeof value === 'function' && 'getFields' in (value as any);
103
+ }
104
+
105
+ // Workspace
106
+
107
+ /** Walk up from project root to find the monorepo root (pnpm-workspace.yaml + packages/). */
108
+ function findWorkspaceRoot(from: string): string {
109
+ let dir = resolvePath(from);
110
+ while (dir !== dirname(dir)) {
111
+ if (existsSync(join(dir, 'pnpm-workspace.yaml')) && existsSync(join(dir, 'packages'))) return dir;
112
+ dir = dirname(dir);
113
+ }
114
+ return resolvePath(from); // fallback: use project root itself
115
+ }
116
+
117
+ /**
118
+ * Strip the 'Handler' suffix → the name the handler answers to. 'PostHandler' → 'post'.
119
+ *
120
+ * Nothing here checks that an entity carries the result, and nothing should: a handler
121
+ * about no stored row is ordinary. The old name of this function — `toEntityName` — is
122
+ * what let "one façade per entity" be repeated until it read as a rule.
123
+ */
124
+ function toAddress(className: string): string {
125
+ const base = className.endsWith('Handler') ? className.slice(0, -7) : className;
126
+ return lowerFirst(base);
127
+ }
128
+
129
+ // Scan
130
+
131
+ /**
132
+ * The container key a constructor parameter asks for — derived from its TYPE, not from
133
+ * how the type was spelled.
134
+ *
135
+ * `deps` used to be `p.type.name`, so the key WAS the alias's name: `type ListOrm =
136
+ * EntityOrm<List>` resolved only because someone had spelled it exactly like the
137
+ * registration key (`ListOrm`), while `type ListRepo = EntityOrm<List>` — the same type —
138
+ * typechecked and died at boot on `'ListRepo' is not registered`. And `EntityOrm<List>`
139
+ * written out in full asked for `'EntityOrm'`, which nothing registers.
140
+ *
141
+ * `EntityOrm<X>` names X's storage, so that is the key. The generic argument was already
142
+ * parsed (`ParsedType.generics`) and thrown away. Anything else keeps its own name: a
143
+ * plain service IS designated by its class name.
144
+ */
145
+ function depKeyOf(type: ParsedType): string {
146
+ // `Facade<PostHandler>` — the second port, read exactly like the first. The type names
147
+ // what arrives: not the handler (its methods take positional arguments and it is never
148
+ // injected), but the door built in front of it. Same key whether that door is the local
149
+ // façade or a doublure, which is what makes the topology invisible from a signature.
150
+ const facadeOf = type.name === 'Facade' ? type.generics?.[0]?.name : undefined;
151
+ if (facadeOf) return lowerFirst(facadeOf);
152
+
153
+ // `Emit<PostPublished>` — the third port, and the only one that names a SUBJECT rather
154
+ // than an interlocutor. Read like the other two: the type names what arrives, here a
155
+ // function that announces. Who receives it is not in the signature and never will be.
156
+ const factOf = type.name === 'Emit' ? type.generics?.[0]?.name : undefined;
157
+ if (factOf) return emitKeyOf(factOf);
158
+
159
+ // `Together<[Account, Ledger], [RateMirror]>` — the fifth reading, and the only one whose
160
+ // argument is a SET rather than one subject. The AST hands each tuple back as a single
161
+ // string, so they are split here, where every other question of "how was it written" lives.
162
+ const frame = type.name === 'Together' ? type.generics?.[0]?.name : undefined;
163
+ if (frame) return togetherKeyOf(tupleMembers(frame), tupleMembers(type.generics?.[1]?.name ?? ''));
164
+
165
+ // `RepositoryOf<Post>` — what an author writes when no repository file exists. The dual
166
+ // of the line below: one names the port, the other the holder, and both resolve to a key
167
+ // rather than to a class the author would have to invent.
168
+ const held = type.name === 'RepositoryOf' ? type.generics?.[0]?.name : undefined;
169
+ if (held) return repositoryKeyOf(held);
170
+
171
+ const target = type.name === 'EntityOrm' ? type.generics?.[0]?.name : undefined;
172
+ if (!target) return type.name;
173
+
174
+ return ormKeyOf(target);
175
+ }
176
+
177
+ /**
178
+ * `'[Account, Ledger]'` → `['Account', 'Ledger']`.
179
+ *
180
+ * The tuple was chosen over the variadic form the parser reads more cleanly, because the
181
+ * variadic one costs arities-with-defaults and a `never` filter on the TypeScript side
182
+ * while the tuple maps to `[EntityOrm<Account>, EntityOrm<Ledger>]` in one line. The
183
+ * parser does not get to decide alone; this split is what that choice costs.
184
+ */
185
+ function tupleMembers(raw: string): string[] {
186
+ return raw.replace(/^\[|\]$/g, '').split(',').map((member) => member.trim()).filter(Boolean);
187
+ }
188
+
189
+ /**
190
+ * These readings are semantic: an unchanged file can mean something different after an
191
+ * imported alias changes. The parser's TypeScript Program is the cache for one scan; a
192
+ * cache keyed only by this file's bytes would be unsound.
193
+ */
194
+ const ctorParamsOf = (filePath: string) =>
195
+ parseConstructorParams(filePath);
196
+
197
+ const presenterMethodsOf = (filePath: string) =>
198
+ parsePresenterMethods(filePath);
199
+
200
+ const handlerMethodsOf = (filePath: string, projectRoot?: string) =>
201
+ parseAllHandlerMethods(filePath, projectRoot);
202
+
203
+ async function toProvider(filePath: string): Promise<ProviderEntry> {
204
+ const ctor = await loadClass(filePath);
205
+ const params = await ctorParamsOf(filePath);
206
+ const deps = params.map((p) => depKeyOf(p.type));
207
+
208
+ // A repository inherits its constructor from `Repository(…)`, so the file declares none
209
+ // and the scan reads no parameter. The mixin knows what it was built for and says so at
210
+ // runtime — same escape as `Crud.__ops`, and the same reason: what a prefab fabricates,
211
+ // only the prefab can describe.
212
+ //
213
+ // An AGGREGATE owns several, and its base takes them in the declared order. It is handed
214
+ // no frame: the boundary and the unit of work are two statements, and a frame is ASKED FOR
215
+ // like anywhere else — see `prefab/repository.ts`.
216
+ const owned = ownedBy(ctor);
217
+ const target = targetOf(ctor);
218
+ if (owned.length > 1 && deps.length === 0) {
219
+ deps.push(...owned.map((entity) => ormKeyOf(lowerFirst((entity as { name: string }).name))));
220
+ } else if (target && deps.length === 0) {
221
+ deps.push(ormKeyOf(lowerFirst((target as { name: string }).name)));
222
+ }
223
+
224
+ // No `name` beside `ctor`: a provider registers under `ctor.name`, which is what
225
+ // `depKeyOf` returns since it reads the type as written. The camelCase field that
226
+ // used to sit here called itself the registration key and was one nowhere.
227
+ return { ctor, deps, filePath };
228
+ }
229
+
230
+ async function toEntityEntry(filePath: string): Promise<EntityEntry | null> {
231
+ const exported = await loadDefault(filePath);
232
+ if (!isEntityClass(exported)) return null;
233
+ const runtimeName = (exported as { name?: string }).name;
234
+ // A derivation returned directly (`export default User.extend(...)`) carries the
235
+ // factory's own name, which the schema package stamps and exports. The file is then
236
+ // the declaration site and therefore the only name the author actually supplied;
237
+ // named classes keep winning.
238
+ const declaredName = runtimeName && runtimeName !== ANONYMOUS_SCHEMA_NAME
239
+ ? runtimeName
240
+ : basename(filePath).replace(/\.[^.]+$/, '');
241
+ const name = lowerFirst(declaredName);
242
+ return { name, entityClass: exported, filePath };
243
+ }
244
+
245
+ /**
246
+ * Resolve a ParsedType to a runtime schema if available in module exports.
247
+ * Handles arrays, generics (uses base name), and simple references.
248
+ */
249
+ function resolveSchema(type: ParsedType, moduleExports: Record<string, unknown>): SchemaView | undefined {
250
+ // An array's element type IS `type.name` — the arity rides beside it, so nothing has
251
+ // to be unwrapped here.
252
+ // For generics like Pagination<Post>, also check inner types
253
+ if (type.generics) {
254
+ for (const g of type.generics) {
255
+ const resolved = moduleExports[g.name];
256
+ if (resolved && typeof resolved === 'function' && 'getFields' in resolved) {
257
+ // `Partial<X>` in a signature IS the patch declaration (Crud.update) —
258
+ // project it onto the schema view instead of dropping the wrapper, so
259
+ // the facade validates in patch mode (absent field → untouched).
260
+ if (type.name === 'Partial' && 'partial' in resolved && typeof (resolved as any).partial === 'function') {
261
+ return (resolved as any).partial() as SchemaView;
262
+ }
263
+ return resolved as unknown as SchemaView;
264
+ }
265
+ }
266
+ }
267
+ const resolved = moduleExports[type.name];
268
+ if (resolved && typeof resolved === 'function' && 'getFields' in resolved) {
269
+ return resolved as unknown as SchemaView;
270
+ }
271
+ return undefined;
272
+ }
273
+
274
+ /**
275
+ * Parse ALL method signatures for unified binding.
276
+ *
277
+ * Resolves schemas for all params (not just the first) and stores
278
+ * full signatures for the binding algorithm.
279
+ */
280
+ async function inferOperations(
281
+ filePath: string,
282
+ handlerName: string,
283
+ moduleExports: Record<string, unknown>,
284
+ collectorTypeNames: Set<string>,
285
+ explicitInputs: ReadonlySet<string>,
286
+ declared: Record<string, OperationContract>,
287
+ projectRoot?: string,
288
+ ): Promise<OperationsMap> {
289
+ const map = new Map<string, OperationContract>();
290
+ let parsed: Awaited<ReturnType<typeof parseAllHandlerMethods>>;
291
+ try {
292
+ parsed = await handlerMethodsOf(filePath, projectRoot);
293
+ } catch (cause) {
294
+ // The handler still gets a façade — its methods exist at runtime — but with no
295
+ // contract: no binding plan, no input schema, no doc sentence. It used to
296
+ // return the empty map, so the app served a stranger's idea of the handler and
297
+ // said nothing. The operations are gone; the sentence saying so is not.
298
+ record({
299
+ severity: 'blocking',
300
+ code: 'handler-parse-failed',
301
+ filePath,
302
+ message: 'Could not parse this handler — its operations carry no contract, so the '
303
+ + 'façade serves them unbound. Not the same as a handler with no operation.',
304
+ cause,
305
+ });
306
+ return map;
307
+ }
308
+
309
+ /**
310
+ * A base class the parse could not open — an installed package, typically, whose
311
+ * source is not in the workspace. Its operations are missing from this façade, and
312
+ * the scan cannot tell whether there were any.
313
+ *
314
+ * A warning, not a refusal: an installed base class with no operation is perfectly
315
+ * ordinary, and the boot has no way to decide between the two. So it names the
316
+ * clause and stops there. Stating the contract in `frond.config.ts` is the answer
317
+ * — the third producer, which creates an op neither other producer found — and it
318
+ * silences this by making the op exist.
319
+ */
320
+ for (const base of parsed.unresolvedHeritage) {
321
+ record({
322
+ severity: 'warning',
323
+ code: 'heritage-unresolved',
324
+ filePath,
325
+ message: `Could not resolve 'extends ${base}' — any operation it declares is absent `
326
+ + `from this façade, and the scan cannot tell that from a base class with none. `
327
+ + `State the contract in frond.config.ts to put it back.`,
328
+ });
329
+ }
330
+
331
+ for (const method of parsed.methods) {
332
+ // The contract is what carries the description; `signature` is the raw material it
333
+ // was read from. Leaving it only on the signature meant every consumer had to know
334
+ // to look one level down, and only the façade did.
335
+ const meta: OperationContract = {
336
+ signature: method,
337
+ ...(method.description && { description: method.description }),
338
+ };
339
+
340
+ // A convention may omit a declaration only when it has one answer. Only values the
341
+ // caller supplies through the body are candidates: a schema-typed collector, fact or
342
+ // context parameter is not input merely because it names an entity. The old loop
343
+ // ignored provenance and assigned the first schema it met, so swapping two parameters
344
+ // silently changed the contract the façade used to judge the request body.
345
+ const binding = computeBindingPlan(method.params, collectorTypeNames);
346
+ const candidates = method.params.flatMap((param, index) => {
347
+ if (binding[index]?.source.kind !== 'body') return [];
348
+ const schema = resolveSchema(param.type, moduleExports);
349
+ return schema ? [{ param, schema }] : [];
350
+ });
351
+ if (candidates.length === 1) {
352
+ meta.input = candidates[0].schema;
353
+ } else if (
354
+ candidates.length > 1
355
+ && declared[method.name]?.input === undefined
356
+ && !explicitInputs.has(method.name)
357
+ ) {
358
+ const subject = `${handlerName}.${method.name}`;
359
+ record({
360
+ severity: 'blocking',
361
+ code: 'input-contract-ambiguous',
362
+ filePath,
363
+ subject,
364
+ message: `Cannot infer the input contract for ${subject}: ${candidates.length} entity `
365
+ + `candidates — ${candidates.map(({ param }) => `${param.name}: ${param.type.raw}`).join('; ')}. `
366
+ + `Declare operations.${method.name}.input in frond.config.ts.`,
367
+ });
368
+ }
369
+
370
+ if (method.returnType) {
371
+ meta.output = resolveSchema(method.returnType, moduleExports);
372
+ // `output` is the shape of one row; this says how many rows come back.
373
+ meta.cardinality = cardinalityOf(method.returnType);
374
+ }
375
+
376
+ map.set(method.name, meta);
377
+ }
378
+
379
+ return map;
380
+ }
381
+
382
+ async function toHandlerEntry(
383
+ filePath: string,
384
+ entityByClassName: Map<string, SchemaView>,
385
+ collectorTypeNames: Set<string>,
386
+ explicitInputs: ReadonlySet<string>,
387
+ projectRoot?: string,
388
+ surface?: string,
389
+ ): Promise<HandlerEntry> {
390
+ const mod = await loadModule(filePath);
391
+ const ctor = mod.default;
392
+ if (typeof ctor !== 'function' || !ctor.prototype)
393
+ throw new Error(`${filePath}: default export is not a class`);
394
+
395
+ // Augment module exports with known entity classes so resolveSchema
396
+ // can find them when T is substituted (e.g. T → Post after parsing Crud(Post))
397
+ const augmented: Record<string, unknown> = { ...mod };
398
+ for (const [className, entityClass] of entityByClassName) {
399
+ if (!(className in augmented)) augmented[className] = entityClass;
400
+ }
401
+
402
+ const address = toAddress(ctor.name);
403
+ const declaredOps = (ctor as { __ops?: Record<string, OperationContract> }).__ops ?? {};
404
+ const operations = await inferOperations(
405
+ filePath,
406
+ ctor.name,
407
+ augmented,
408
+ collectorTypeNames,
409
+ explicitInputs,
410
+ declaredOps,
411
+ projectRoot,
412
+ );
413
+ const ctorParams = await ctorParamsOf(filePath);
414
+ const deps = ctorParams.map((p) => depKeyOf(p.type));
415
+
416
+ // Read output override from Crud(Entity, Output) — static __output property
417
+ // A handler-wide view, when it is not simply the entity — the two are compared by
418
+ // identity, which is why both slots are read through the same door.
419
+ const subject = targetOf(ctor);
420
+ const declared = outputOf(ctor);
421
+ const outputOverride = declared && subject && declared !== subject
422
+ ? (declared as unknown as SchemaView)
423
+ : undefined;
424
+
425
+ return {
426
+ name: lowerFirst(ctor.name),
427
+ address,
428
+ ctor: ctor as ProviderEntry['ctor'],
429
+ operations,
430
+ deps,
431
+ filePath,
432
+ outputOverride,
433
+ surface,
434
+ };
435
+ }
436
+
437
+ /** Strip '.seed' suffix → entity name. 'Author.seed.ts' → 'author'. */
438
+ function toSeedEntityName(fileName: string): string {
439
+ const base = fileName.replace(/\.seed\.(ts|js)$/, '').replace(/\.(ts|js)$/, '');
440
+ return lowerFirst(base);
441
+ }
442
+
443
+ async function toSeedEntry(filePath: string): Promise<SeedEntry | null> {
444
+ const data = await loadDefault(filePath);
445
+ if (Array.isArray(data) || typeof data === 'function') {
446
+ const fileName = filePath.split('/').pop()!;
447
+ return { entityName: toSeedEntityName(fileName), data: data as SeedEntry['data'], filePath };
448
+ }
449
+ return null;
450
+ }
451
+
452
+ async function toPresenterEntry(filePath: string): Promise<PresenterEntry | null> {
453
+ const ctor = await loadClass(filePath);
454
+ const target = targetOf(ctor);
455
+ if (!target) return null;
456
+ const entityName = lowerFirst((target as any).name);
457
+ const fields = getPresenterFields(ctor);
458
+ const presenterParams = await ctorParamsOf(filePath);
459
+ const deps = presenterParams.map((p) => depKeyOf(p.type));
460
+
461
+ // Parse method return types from source
462
+ let fieldMeta: PresenterEntry['fieldMeta'] = [];
463
+ try {
464
+ const parsed = await presenterMethodsOf(filePath);
465
+ fieldMeta = parsed.map((m) => ({
466
+ name: m.name,
467
+ returnType: m.returnType?.name,
468
+ // One array level IS the page — the method answers one value per row. What is left
469
+ // over is the field's own arity: `string[]` a scalar per row, `string[][]` a list.
470
+ list: (m.returnType?.arrayDepth ?? 0) > 1,
471
+ nullable: m.returnType?.nullable,
472
+ // Everything after the rows is bound like a handler's argument.
473
+ params: m.params.slice(1),
474
+ }));
475
+
476
+ // The rows parameter is stripped just above, and nothing used to check its SHAPE.
477
+ // A computed field receives the PAGE and answers one value per row, so a method
478
+ // written `items(order: Order)` compiles and dies at the first call with a 500
479
+ // (`expected 1 value(s) for 1 row(s), got 0`) — while the information sat here, at
480
+ // the scan. Every own method of a presenter IS a computed field
481
+ // (`getPresenterFields`), so there is no private helper to spare.
482
+ //
483
+ // An ERROR at boot, not a refusal: a `blocking` scan diagnostic is logged and the
484
+ // app still starts (`bootstrap.ts`, and the comment above that loop says why). Making
485
+ // it refuse means deciding that a declaration which cannot work is an unresolved
486
+ // CONTRACT — the one thing that does stop the boot — and that is a decision, not a
487
+ // severity.
488
+ for (const method of parsed) {
489
+ const rows = method.params[0];
490
+ if (rows && rows.type.array === true) continue;
491
+ record({
492
+ severity: 'blocking',
493
+ code: 'presenter-field-not-page',
494
+ filePath,
495
+ subject: `${(ctor as { name?: string }).name ?? 'Presenter'}.${method.name}`,
496
+ message: `${method.name}(${rows ? `${rows.name}: ${rows.type.raw}` : ''}) is a computed `
497
+ + `field, so it receives the PAGE and must answer one value per row. Declare `
498
+ + `${method.name}(${rows?.name ?? 'rows'}: ${(target as { name?: string }).name ?? 'Entity'}[]) `
499
+ + `and return an array of the same length.`,
500
+ });
501
+ }
502
+ } catch { /* parse failure — fall back to untyped */ }
503
+
504
+ // Declared at runtime on the class, so it survives a scan that resolved nothing.
505
+ return { entityName, ctor, fields, fieldMeta, views: viewsOf(ctor), deps, filePath };
506
+ }
507
+
508
+ async function toCollectorEntry(filePath: string): Promise<CollectorEntry | null> {
509
+ const ctor = await loadClass(filePath);
510
+ const target = targetOf(ctor);
511
+ if (!target) return null;
512
+ // The target's NAME and nothing else — a collector reads no fields, so the class it
513
+ // was built on needs no schema.
514
+ const typeName = lowerFirst((target as any).name);
515
+ const collectorParams = await ctorParamsOf(filePath);
516
+ const deps = collectorParams.map((p) => depKeyOf(p.type));
517
+ return { typeName, ctor, deps, filePath };
518
+ }
519
+
520
+ async function scanFrond(frondPath: string, name: string, source: FrondDescriptor['source'], conventions: Conventions, projectRoot?: string): Promise<FrondDescriptor> {
521
+ const {
522
+ entities: entitiesDir, handlers: handlersDir,
523
+ presenters: presentersDir, collectors: collectorsDir, seeds: seedsDir,
524
+ } = conventions.dirs;
525
+ /**
526
+ * A convention directory, read by whoever knows the shape it holds.
527
+ *
528
+ * The same three lines were written six times — list, read in parallel, drop what the
529
+ * reader refused — which is what made `handlers/` quietly special: it grew a second
530
+ * pass for its surfaces and nothing else could.
531
+ */
532
+ const collect = async <T extends object>(
533
+ dir: string,
534
+ read: (filePath: string) => Promise<T | null>,
535
+ ): Promise<T[]> => {
536
+ const entries: (T | null)[] = await Promise.all((await files(join(frondPath, dir))).map(read));
537
+ return entries.filter((entry): entry is T => entry !== null);
538
+ };
539
+
540
+ // services/ and repositories/ — two spellings, one provider list.
541
+ const providers = (await Promise.all(providerDirsOf(conventions).map((dir) => collect(dir, toProvider)))).flat();
542
+ const entities = await collect(entitiesDir, toEntityEntry);
543
+ const collectors = await collect(collectorsDir, toCollectorEntry);
544
+ const collectorTypeNames = new Set(collectors.map((collector) => collector.typeName));
545
+ const frondConfig = await loadFrondConfig(frondPath);
546
+ const explicitInputs = new Set(
547
+ Object.entries(frondConfig?.operations ?? {})
548
+ .filter(([, contract]) => contract.input !== undefined)
549
+ .map(([operation]) => operation),
550
+ );
551
+
552
+ // Handlers resolve `T` against the entities, so those come first.
553
+ const entityByClassName = new Map(
554
+ entities.map((e) => [(e.entityClass as { name: string }).name, e.entityClass]),
555
+ );
556
+ const handlers = await collect(handlersDir, (f) =>
557
+ toHandlerEntry(f, entityByClassName, collectorTypeNames, explicitInputs, projectRoot));
558
+
559
+ // A subdirectory of handlers/ is a named surface — the one directory whose CHILDREN
560
+ // are part of the convention too.
561
+ for (const surface of await dirs(join(frondPath, handlersDir))) {
562
+ handlers.push(...await collect(join(handlersDir, surface), (f) =>
563
+ toHandlerEntry(f, entityByClassName, collectorTypeNames, explicitInputs, projectRoot, surface)));
564
+ }
565
+
566
+ const presenters = await collect(presentersDir, toPresenterEntry);
567
+ const seeds = await collect(seedsDir, toSeedEntry);
568
+
569
+ // Mark exposed entries: frond.config.ts takes precedence, then @expose decorator
570
+ if (frondConfig?.expose) {
571
+ const exposeSet = new Set(frondConfig.expose);
572
+ for (const e of entities) {
573
+ e.exposed = exposeSet.has((e.entityClass as any).name);
574
+ }
575
+ for (const h of handlers) {
576
+ h.exposed = exposeSet.has(h.ctor.name);
577
+ }
578
+ } else {
579
+ // Fallback: check @expose decorator, default to true (expose everything unless explicitly hidden)
580
+ for (const e of entities) {
581
+ e.exposed = (e.entityClass as any).__exposed !== false;
582
+ }
583
+ for (const h of handlers) {
584
+ h.exposed = (h.ctor as any).__exposed !== false;
585
+ }
586
+ }
587
+
588
+ // Flatten per-op config overrides: the only thing that needs flattening is the handler
589
+ // CLASS, which becomes its name (that is the DI key). Everything else — the surface keys
590
+ // AND the contract keys (`input`, `binding`) — travels verbatim, so a slot added to
591
+ // OperationOverride reaches its reader without a stop here. Enumerating keys by hand is
592
+ // what used to silently drop whatever was added last (the same invariant `cloneField`
593
+ // holds one layer down).
594
+ const operationsOverrides = frondConfig?.operations
595
+ ? Object.fromEntries(
596
+ Object.entries(frondConfig.operations).map(([opName, { handler, ...rest }]) => [
597
+ opName,
598
+ { ...rest, handlerName: handler?.name },
599
+ ]),
600
+ )
601
+ : undefined;
602
+
603
+ return {
604
+ name,
605
+ source,
606
+ providers,
607
+ entities,
608
+ handlers,
609
+ presenters,
610
+ collectors,
611
+ seeds,
612
+ surfaces: frondConfig?.surfaces,
613
+ reads: frondConfig?.reads,
614
+ operationsOverrides,
615
+ };
616
+ }
617
+
618
+ /**
619
+ * The frond's name — the directory, unless its `package.json` renames it. One rule for a
620
+ * frond under `fronds/` and for the root frond alike, so the root needs no second spelling.
621
+ *
622
+ * Carrying the convention is what makes a frond; the key never marked anything and
623
+ * nothing read it. It earns its keep as the one thing the directory cannot say: that
624
+ * `fronds/blog-v2/` serves the frond still called `blog` — so a rename on disk does not
625
+ * rename the entity keys, the `@fronds/*` import or a `remotes:` entry.
626
+ */
627
+ async function frondNameOf(frondPath: string, dirName: string): Promise<string> {
628
+ try {
629
+ const pkg = JSON.parse(await readFile(join(frondPath, 'package.json'), 'utf8')) as {
630
+ fougere?: { frond?: unknown };
631
+ };
632
+ const declared = pkg.fougere?.frond;
633
+ return typeof declared === 'string' && declared.length > 0 ? declared : dirName;
634
+ } catch {
635
+ return dirName;
636
+ }
637
+ }
638
+
639
+ /**
640
+ * A frond is a directory carrying the convention, and the project root is one such
641
+ * directory — so a single-domain app writes `entities/` next to `app/` and never names
642
+ * anything. `fronds/` is not the definition, it is where the OTHERS live: the root frond
643
+ * stays put when a second domain appears, which is what makes flattening free instead of
644
+ * a deferred move.
645
+ *
646
+ * `entities/` is the test, not "any convention directory". `services/` and `repositories/`
647
+ * are ordinary top-level names in projects that never heard of Fougere, and a domain
648
+ * without a single entity is not a domain.
649
+ */
650
+ async function rootFrondOf(root: string, workspaceRoot: string, conventions: Conventions): Promise<FrondDescriptor | null> {
651
+ if ((await files(join(root, conventions.dirs.entities))).length === 0) return null;
652
+ const name = await frondNameOf(root, basename(resolvePath(root)));
653
+ return scanFrond(root, name, { path: root, package: frondPackage(name, conventions) }, conventions, workspaceRoot);
654
+ }
655
+
656
+ /**
657
+ * `@fronds/<name>` → the directory it names, for every frond of a project.
658
+ *
659
+ * The framework states this convention — `FrondSource.package` has always spelled it, and
660
+ * `fougere sync` writes it into a consumer's tsconfig — and until now its own reader could
661
+ * not resolve it. The Nuxt module registered a Vite alias, so a `.vue` page could import
662
+ * `@fronds/blog/entities/Post`, while the SCAN loaded sources through a bare jiti: a frond
663
+ * naming its neighbour got `Cannot find module '@fronds/user/entities/User.js'`. So the one
664
+ * form that survives a split was the one form that did not run.
665
+ *
666
+ * Hand it to the module loader — `createJiti(url, { alias: await frondAliases(root) })` —
667
+ * and the named form resolves everywhere the framework reads. A directory listing and a
668
+ * `package.json` read, no parsing: it is deliberately callable BEFORE the loader exists,
669
+ * which is what makes the chicken-and-egg go away.
670
+ */
671
+ export async function frondAliases(root: string, conventions: Conventions = DEFAULT_CONVENTIONS): Promise<Record<string, string>> {
672
+ const frondsDir = join(root, conventions.fronds);
673
+ const aliases: Record<string, string> = {};
674
+
675
+ // The root itself is a frond when it carries `entities/` — same rule as the scan.
676
+ if ((await files(join(root, conventions.dirs.entities))).length > 0) {
677
+ aliases[frondPackage(await frondNameOf(root, basename(resolvePath(root))), conventions)] = resolvePath(root);
678
+ }
679
+ for (const dir of await dirs(frondsDir)) {
680
+ const path = join(frondsDir, dir);
681
+ aliases[frondPackage(await frondNameOf(path, dir), conventions)] = resolvePath(path);
682
+ }
683
+
684
+ /**
685
+ * A SYNCED frond answers to the same name.
686
+ *
687
+ * `fougere sync` writes `.fougere/remotes/<name>/` and registers it in `remotes.json`,
688
+ * which the Nuxt module already reads to alias `@fronds/<name>`. Doing it here too is
689
+ * what makes the convention mean ONE thing: a consumer writes `@fronds/blog/entities/Post`
690
+ * and never learns whether that frond is on this disk or was fetched from a card.
691
+ *
692
+ * A local frond wins a name collision — its source is the truth, a synced copy is a
693
+ * mirror of somebody else's.
694
+ */
695
+ try {
696
+ const registry = JSON.parse(
697
+ await readFile(join(root, '.fougere', 'remotes.json'), 'utf8'),
698
+ ) as Record<string, { path?: string }>;
699
+ for (const [name, entry] of Object.entries(registry)) {
700
+ if (typeof entry?.path !== 'string' || aliases[frondPackage(name, conventions)]) continue;
701
+ aliases[frondPackage(name, conventions)] = resolvePath(entry.path);
702
+ }
703
+ } catch {
704
+ // No registry, or an unreadable one: nothing was synced here. Not this function's
705
+ // complaint — `sync` owns that file and reports on it.
706
+ }
707
+
708
+ return aliases;
709
+ }
710
+
711
+ export async function scanProject(
712
+ root: string,
713
+ filter?: string[],
714
+ conventionsInput?: ConventionsInput,
715
+ ): Promise<ScanResult> {
716
+ const conventions = resolveConventions(conventionsInput);
717
+ resetTypePrograms();
718
+ // A run owns its findings: two scans in one process (a test suite, a watcher)
719
+ // must not inherit each other's.
720
+ diagnostics = [];
721
+
722
+ const frondsDir = join(root, conventions.fronds);
723
+ const dirNames = await dirs(frondsDir);
724
+ // Resolve workspace root (parent of packages/) for package import resolution
725
+ // For monorepo: root is the project dir (e.g. demos/nuxt-blog), workspace root is the repo root
726
+ const workspaceRoot = findWorkspaceRoot(root);
727
+
728
+ // One program for this run. Seeded here because the parser rebuilds on every root it
729
+ // has not seen, and a frond lives outside its project's tsconfig `include`. Both keys
730
+ // are seeded: heritage reads under the workspace root, a constructor under none.
731
+ const declarations = (await Promise.all(
732
+ [root, ...dirNames.map((dir) => join(frondsDir, dir))].flatMap((frondPath) =>
733
+ frondDirsOf(conventions).map((dir) => files(join(frondPath, dir)))),
734
+ )).flat();
735
+ await seedTypeProgram(declarations, workspaceRoot);
736
+ await seedTypeProgram(declarations);
737
+
738
+ const [rootFrond, under] = await Promise.all([
739
+ rootFrondOf(root, workspaceRoot, conventions),
740
+ Promise.all(
741
+ dirNames.map(async (dir) => {
742
+ const name = await frondNameOf(join(frondsDir, dir), dir);
743
+ return scanFrond(
744
+ join(frondsDir, dir), name,
745
+ { path: join(frondsDir, dir), package: frondPackage(name, conventions) },
746
+ conventions,
747
+ workspaceRoot,
748
+ );
749
+ }),
750
+ ),
751
+ ]);
752
+
753
+ // The app's own domain first, then the ones it took in.
754
+ const all = rootFrond ? [rootFrond, ...under] : under;
755
+ const fronds = Fronds.scanned(filter ? all.filter((f) => filter.includes(f.name)) : all);
756
+
757
+ return { fronds, diagnostics };
758
+ }