alepha 0.15.0 → 0.15.2

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 (551) hide show
  1. package/README.md +43 -98
  2. package/dist/api/audits/index.d.ts +630 -653
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +12 -35
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +365 -358
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +12 -5
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +255 -248
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/jobs/index.js +10 -3
  13. package/dist/api/jobs/index.js.map +1 -1
  14. package/dist/api/keys/index.d.ts +413 -0
  15. package/dist/api/keys/index.d.ts.map +1 -0
  16. package/dist/api/keys/index.js +476 -0
  17. package/dist/api/keys/index.js.map +1 -0
  18. package/dist/api/notifications/index.browser.js +4 -4
  19. package/dist/api/notifications/index.browser.js.map +1 -1
  20. package/dist/api/notifications/index.d.ts +84 -78
  21. package/dist/api/notifications/index.d.ts.map +1 -1
  22. package/dist/api/notifications/index.js +14 -8
  23. package/dist/api/notifications/index.js.map +1 -1
  24. package/dist/api/parameters/index.d.ts +528 -535
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +30 -37
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/api/users/index.d.ts +1221 -910
  29. package/dist/api/users/index.d.ts.map +1 -1
  30. package/dist/api/users/index.js +2556 -248
  31. package/dist/api/users/index.js.map +1 -1
  32. package/dist/api/verifications/index.d.ts +142 -136
  33. package/dist/api/verifications/index.d.ts.map +1 -1
  34. package/dist/api/verifications/index.js +12 -4
  35. package/dist/api/verifications/index.js.map +1 -1
  36. package/dist/batch/index.d.ts +142 -162
  37. package/dist/batch/index.d.ts.map +1 -1
  38. package/dist/batch/index.js +31 -44
  39. package/dist/batch/index.js.map +1 -1
  40. package/dist/bucket/index.d.ts +595 -171
  41. package/dist/bucket/index.d.ts.map +1 -1
  42. package/dist/bucket/index.js +1856 -12
  43. package/dist/bucket/index.js.map +1 -1
  44. package/dist/cache/core/index.d.ts +225 -53
  45. package/dist/cache/core/index.d.ts.map +1 -1
  46. package/dist/cache/core/index.js +213 -7
  47. package/dist/cache/core/index.js.map +1 -1
  48. package/dist/cache/redis/index.d.ts +1 -0
  49. package/dist/cache/redis/index.d.ts.map +1 -1
  50. package/dist/cache/redis/index.js +6 -2
  51. package/dist/cache/redis/index.js.map +1 -1
  52. package/dist/cli/index.d.ts +834 -226
  53. package/dist/cli/index.d.ts.map +1 -1
  54. package/dist/cli/index.js +2872 -417
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/command/index.d.ts +458 -310
  57. package/dist/command/index.d.ts.map +1 -1
  58. package/dist/command/index.js +2011 -76
  59. package/dist/command/index.js.map +1 -1
  60. package/dist/core/index.browser.js +309 -97
  61. package/dist/core/index.browser.js.map +1 -1
  62. package/dist/core/index.d.ts +796 -701
  63. package/dist/core/index.d.ts.map +1 -1
  64. package/dist/core/index.js +329 -97
  65. package/dist/core/index.js.map +1 -1
  66. package/dist/core/index.native.js +309 -97
  67. package/dist/core/index.native.js.map +1 -1
  68. package/dist/datetime/index.d.ts +59 -44
  69. package/dist/datetime/index.d.ts.map +1 -1
  70. package/dist/datetime/index.js +15 -0
  71. package/dist/datetime/index.js.map +1 -1
  72. package/dist/email/index.d.ts +314 -19
  73. package/dist/email/index.d.ts.map +1 -1
  74. package/dist/email/index.js +1852 -7
  75. package/dist/email/index.js.map +1 -1
  76. package/dist/fake/index.d.ts +5500 -5418
  77. package/dist/fake/index.d.ts.map +1 -1
  78. package/dist/fake/index.js +113 -42
  79. package/dist/fake/index.js.map +1 -1
  80. package/dist/lock/core/index.d.ts +219 -212
  81. package/dist/lock/core/index.d.ts.map +1 -1
  82. package/dist/lock/core/index.js +11 -4
  83. package/dist/lock/core/index.js.map +1 -1
  84. package/dist/lock/redis/index.d.ts.map +1 -1
  85. package/dist/logger/index.d.ts +41 -90
  86. package/dist/logger/index.d.ts.map +1 -1
  87. package/dist/logger/index.js +15 -68
  88. package/dist/logger/index.js.map +1 -1
  89. package/dist/mcp/index.d.ts +228 -230
  90. package/dist/mcp/index.d.ts.map +1 -1
  91. package/dist/mcp/index.js +32 -31
  92. package/dist/mcp/index.js.map +1 -1
  93. package/dist/orm/index.browser.js +12 -12
  94. package/dist/orm/index.browser.js.map +1 -1
  95. package/dist/orm/index.bun.js +90 -80
  96. package/dist/orm/index.bun.js.map +1 -1
  97. package/dist/orm/index.d.ts +1434 -1459
  98. package/dist/orm/index.d.ts.map +1 -1
  99. package/dist/orm/index.js +112 -130
  100. package/dist/orm/index.js.map +1 -1
  101. package/dist/queue/core/index.d.ts +262 -254
  102. package/dist/queue/core/index.d.ts.map +1 -1
  103. package/dist/queue/core/index.js +14 -6
  104. package/dist/queue/core/index.js.map +1 -1
  105. package/dist/queue/redis/index.d.ts.map +1 -1
  106. package/dist/react/auth/index.browser.js +108 -0
  107. package/dist/react/auth/index.browser.js.map +1 -0
  108. package/dist/react/auth/index.d.ts +100 -0
  109. package/dist/react/auth/index.d.ts.map +1 -0
  110. package/dist/react/auth/index.js +145 -0
  111. package/dist/react/auth/index.js.map +1 -0
  112. package/dist/react/core/index.d.ts +469 -0
  113. package/dist/react/core/index.d.ts.map +1 -0
  114. package/dist/react/core/index.js +464 -0
  115. package/dist/react/core/index.js.map +1 -0
  116. package/dist/react/form/index.d.ts +232 -0
  117. package/dist/react/form/index.d.ts.map +1 -0
  118. package/dist/react/form/index.js +432 -0
  119. package/dist/react/form/index.js.map +1 -0
  120. package/dist/react/head/index.browser.js +423 -0
  121. package/dist/react/head/index.browser.js.map +1 -0
  122. package/dist/react/head/index.d.ts +288 -0
  123. package/dist/react/head/index.d.ts.map +1 -0
  124. package/dist/react/head/index.js +465 -0
  125. package/dist/react/head/index.js.map +1 -0
  126. package/dist/react/i18n/index.d.ts +175 -0
  127. package/dist/react/i18n/index.d.ts.map +1 -0
  128. package/dist/react/i18n/index.js +224 -0
  129. package/dist/react/i18n/index.js.map +1 -0
  130. package/dist/react/router/index.browser.js +1980 -0
  131. package/dist/react/router/index.browser.js.map +1 -0
  132. package/dist/react/router/index.d.ts +2068 -0
  133. package/dist/react/router/index.d.ts.map +1 -0
  134. package/dist/react/router/index.js +4932 -0
  135. package/dist/react/router/index.js.map +1 -0
  136. package/dist/react/websocket/index.d.ts +117 -0
  137. package/dist/react/websocket/index.d.ts.map +1 -0
  138. package/dist/react/websocket/index.js +107 -0
  139. package/dist/react/websocket/index.js.map +1 -0
  140. package/dist/redis/index.bun.js +4 -0
  141. package/dist/redis/index.bun.js.map +1 -1
  142. package/dist/redis/index.d.ts +127 -130
  143. package/dist/redis/index.d.ts.map +1 -1
  144. package/dist/redis/index.js +16 -25
  145. package/dist/redis/index.js.map +1 -1
  146. package/dist/retry/index.d.ts +80 -71
  147. package/dist/retry/index.d.ts.map +1 -1
  148. package/dist/retry/index.js +11 -2
  149. package/dist/retry/index.js.map +1 -1
  150. package/dist/router/index.d.ts +6 -6
  151. package/dist/router/index.d.ts.map +1 -1
  152. package/dist/scheduler/index.d.ts +119 -28
  153. package/dist/scheduler/index.d.ts.map +1 -1
  154. package/dist/scheduler/index.js +404 -3
  155. package/dist/scheduler/index.js.map +1 -1
  156. package/dist/security/index.d.ts +642 -228
  157. package/dist/security/index.d.ts.map +1 -1
  158. package/dist/security/index.js +1579 -37
  159. package/dist/security/index.js.map +1 -1
  160. package/dist/server/auth/index.d.ts +1141 -111
  161. package/dist/server/auth/index.d.ts.map +1 -1
  162. package/dist/server/auth/index.js +1261 -25
  163. package/dist/server/auth/index.js.map +1 -1
  164. package/dist/server/cache/index.d.ts +63 -78
  165. package/dist/server/cache/index.d.ts.map +1 -1
  166. package/dist/server/cache/index.js +7 -22
  167. package/dist/server/cache/index.js.map +1 -1
  168. package/dist/server/compress/index.d.ts +13 -5
  169. package/dist/server/compress/index.d.ts.map +1 -1
  170. package/dist/server/compress/index.js +10 -2
  171. package/dist/server/compress/index.js.map +1 -1
  172. package/dist/server/cookies/index.d.ts +46 -22
  173. package/dist/server/cookies/index.d.ts.map +1 -1
  174. package/dist/server/cookies/index.js +7 -5
  175. package/dist/server/cookies/index.js.map +1 -1
  176. package/dist/server/core/index.d.ts +307 -196
  177. package/dist/server/core/index.d.ts.map +1 -1
  178. package/dist/server/core/index.js +271 -38
  179. package/dist/server/core/index.js.map +1 -1
  180. package/dist/server/cors/index.d.ts +24 -34
  181. package/dist/server/cors/index.d.ts.map +1 -1
  182. package/dist/server/cors/index.js +7 -21
  183. package/dist/server/cors/index.js.map +1 -1
  184. package/dist/server/health/index.d.ts +25 -19
  185. package/dist/server/health/index.d.ts.map +1 -1
  186. package/dist/server/health/index.js +8 -2
  187. package/dist/server/health/index.js.map +1 -1
  188. package/dist/server/helmet/index.d.ts +13 -5
  189. package/dist/server/helmet/index.d.ts.map +1 -1
  190. package/dist/server/helmet/index.js +11 -3
  191. package/dist/server/helmet/index.js.map +1 -1
  192. package/dist/server/links/index.browser.js +9 -1
  193. package/dist/server/links/index.browser.js.map +1 -1
  194. package/dist/server/links/index.d.ts +133 -128
  195. package/dist/server/links/index.d.ts.map +1 -1
  196. package/dist/server/links/index.js +24 -11
  197. package/dist/server/links/index.js.map +1 -1
  198. package/dist/server/metrics/index.d.ts +524 -4
  199. package/dist/server/metrics/index.d.ts.map +1 -1
  200. package/dist/server/metrics/index.js +4472 -7
  201. package/dist/server/metrics/index.js.map +1 -1
  202. package/dist/server/multipart/index.d.ts +15 -9
  203. package/dist/server/multipart/index.d.ts.map +1 -1
  204. package/dist/server/multipart/index.js +9 -3
  205. package/dist/server/multipart/index.js.map +1 -1
  206. package/dist/server/proxy/index.d.ts +110 -104
  207. package/dist/server/proxy/index.d.ts.map +1 -1
  208. package/dist/server/proxy/index.js +8 -2
  209. package/dist/server/proxy/index.js.map +1 -1
  210. package/dist/server/rate-limit/index.d.ts +46 -51
  211. package/dist/server/rate-limit/index.d.ts.map +1 -1
  212. package/dist/server/rate-limit/index.js +18 -55
  213. package/dist/server/rate-limit/index.js.map +1 -1
  214. package/dist/server/static/index.d.ts +181 -48
  215. package/dist/server/static/index.d.ts.map +1 -1
  216. package/dist/server/static/index.js +1848 -5
  217. package/dist/server/static/index.js.map +1 -1
  218. package/dist/server/swagger/index.d.ts +348 -53
  219. package/dist/server/swagger/index.d.ts.map +1 -1
  220. package/dist/server/swagger/index.js +1849 -6
  221. package/dist/server/swagger/index.js.map +1 -1
  222. package/dist/sms/index.d.ts +312 -18
  223. package/dist/sms/index.d.ts.map +1 -1
  224. package/dist/sms/index.js +1854 -10
  225. package/dist/sms/index.js.map +1 -1
  226. package/dist/system/index.browser.js +496 -0
  227. package/dist/system/index.browser.js.map +1 -0
  228. package/dist/system/index.d.ts +1158 -0
  229. package/dist/system/index.d.ts.map +1 -0
  230. package/dist/{file → system}/index.js +412 -20
  231. package/dist/system/index.js.map +1 -0
  232. package/dist/thread/index.d.ts +82 -73
  233. package/dist/thread/index.d.ts.map +1 -1
  234. package/dist/thread/index.js +13 -4
  235. package/dist/thread/index.js.map +1 -1
  236. package/dist/topic/core/index.d.ts +330 -323
  237. package/dist/topic/core/index.d.ts.map +1 -1
  238. package/dist/topic/core/index.js +12 -5
  239. package/dist/topic/core/index.js.map +1 -1
  240. package/dist/topic/redis/index.d.ts +6 -6
  241. package/dist/topic/redis/index.d.ts.map +1 -1
  242. package/dist/vite/index.d.ts +163 -5825
  243. package/dist/vite/index.d.ts.map +1 -1
  244. package/dist/vite/index.js +130 -477
  245. package/dist/vite/index.js.map +1 -1
  246. package/dist/websocket/index.browser.js +3 -3
  247. package/dist/websocket/index.browser.js.map +1 -1
  248. package/dist/websocket/index.d.ts +287 -283
  249. package/dist/websocket/index.d.ts.map +1 -1
  250. package/dist/websocket/index.js +15 -11
  251. package/dist/websocket/index.js.map +1 -1
  252. package/package.json +86 -17
  253. package/src/api/audits/index.ts +10 -33
  254. package/src/api/files/__tests__/$bucket.spec.ts +1 -1
  255. package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
  256. package/src/api/files/controllers/FileController.spec.ts +1 -1
  257. package/src/api/files/index.ts +10 -3
  258. package/src/api/files/jobs/FileJobs.spec.ts +1 -1
  259. package/src/api/files/services/FileService.spec.ts +1 -1
  260. package/src/api/jobs/index.ts +10 -3
  261. package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
  262. package/src/api/keys/controllers/ApiKeyController.ts +103 -0
  263. package/src/api/keys/entities/apiKeyEntity.ts +41 -0
  264. package/src/api/keys/index.ts +49 -0
  265. package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
  266. package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
  267. package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
  268. package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
  269. package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
  270. package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
  271. package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
  272. package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
  273. package/src/api/keys/services/ApiKeyService.ts +306 -0
  274. package/src/api/logs/TODO.md +52 -0
  275. package/src/api/notifications/index.ts +10 -4
  276. package/src/api/parameters/index.ts +9 -30
  277. package/src/api/parameters/primitives/$config.ts +12 -4
  278. package/src/api/parameters/services/ConfigStore.ts +9 -3
  279. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
  280. package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
  281. package/src/api/users/index.ts +14 -3
  282. package/src/api/users/primitives/$realm.ts +33 -5
  283. package/src/api/users/providers/RealmProvider.ts +1 -12
  284. package/src/api/users/services/SessionService.ts +1 -11
  285. package/src/api/verifications/controllers/VerificationController.ts +2 -0
  286. package/src/api/verifications/index.ts +10 -4
  287. package/src/batch/index.ts +9 -36
  288. package/src/batch/primitives/$batch.ts +0 -8
  289. package/src/batch/providers/BatchProvider.ts +29 -2
  290. package/src/bucket/__tests__/shared.ts +1 -1
  291. package/src/bucket/index.ts +13 -6
  292. package/src/bucket/primitives/$bucket.ts +1 -1
  293. package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
  294. package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
  295. package/src/cache/core/__tests__/shared.ts +30 -0
  296. package/src/cache/core/index.ts +11 -6
  297. package/src/cache/core/primitives/$cache.spec.ts +5 -0
  298. package/src/cache/core/providers/CacheProvider.ts +17 -0
  299. package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
  300. package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
  301. package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
  302. package/src/cli/apps/AlephaCli.ts +3 -16
  303. package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -2
  304. package/src/cli/atoms/appEntryOptions.ts +13 -0
  305. package/src/cli/atoms/buildOptions.ts +1 -1
  306. package/src/cli/atoms/changelogOptions.ts +1 -1
  307. package/src/cli/commands/build.ts +64 -52
  308. package/src/cli/commands/db.ts +17 -11
  309. package/src/cli/commands/deploy.ts +1 -1
  310. package/src/cli/commands/dev.ts +13 -49
  311. package/src/cli/commands/gen/env.ts +6 -3
  312. package/src/cli/commands/gen/openapi.ts +5 -2
  313. package/src/cli/commands/init.spec.ts +544 -0
  314. package/src/cli/commands/init.ts +101 -58
  315. package/src/cli/commands/lint.ts +8 -2
  316. package/src/cli/commands/typecheck.ts +11 -0
  317. package/src/cli/defineConfig.ts +9 -0
  318. package/src/cli/index.ts +2 -1
  319. package/src/cli/providers/AppEntryProvider.ts +131 -0
  320. package/src/cli/providers/ViteBuildProvider.ts +40 -0
  321. package/src/cli/providers/ViteDevServerProvider.ts +378 -0
  322. package/src/cli/services/AlephaCliUtils.ts +39 -93
  323. package/src/cli/services/PackageManagerUtils.ts +140 -17
  324. package/src/cli/services/ProjectScaffolder.ts +169 -101
  325. package/src/cli/services/ViteUtils.ts +82 -0
  326. package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +41 -28
  327. package/src/cli/{assets → templates}/apiHelloControllerTs.ts +2 -1
  328. package/src/cli/{assets → templates}/biomeJson.ts +2 -1
  329. package/src/cli/{assets → templates}/dummySpecTs.ts +2 -1
  330. package/src/cli/{assets → templates}/editorconfig.ts +2 -1
  331. package/src/cli/templates/gitignore.ts +39 -0
  332. package/src/cli/{assets → templates}/mainBrowserTs.ts +2 -1
  333. package/src/cli/templates/mainCss.ts +33 -0
  334. package/src/cli/templates/mainServerTs.ts +33 -0
  335. package/src/cli/{assets → templates}/tsconfigJson.ts +2 -1
  336. package/src/cli/templates/webAppRouterTs.ts +50 -0
  337. package/src/cli/templates/webHelloComponentTsx.ts +20 -0
  338. package/src/command/helpers/Runner.spec.ts +4 -0
  339. package/src/command/helpers/Runner.ts +3 -21
  340. package/src/command/index.ts +12 -4
  341. package/src/command/providers/CliProvider.spec.ts +1067 -0
  342. package/src/command/providers/CliProvider.ts +203 -40
  343. package/src/core/Alepha.ts +3 -9
  344. package/src/core/__tests__/Alepha-start.spec.ts +4 -4
  345. package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
  346. package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
  347. package/src/core/index.shared.ts +1 -0
  348. package/src/core/index.ts +20 -0
  349. package/src/core/primitives/$module.ts +12 -0
  350. package/src/core/providers/EventManager.spec.ts +0 -71
  351. package/src/core/providers/EventManager.ts +3 -15
  352. package/src/core/providers/Json.ts +2 -14
  353. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
  354. package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
  355. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  356. package/src/datetime/index.ts +15 -0
  357. package/src/email/index.ts +10 -5
  358. package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
  359. package/src/email/providers/LocalEmailProvider.ts +1 -1
  360. package/src/fake/__tests__/keyName.example.ts +1 -1
  361. package/src/fake/__tests__/keyName.spec.ts +5 -5
  362. package/src/fake/index.ts +9 -6
  363. package/src/fake/providers/FakeProvider.spec.ts +258 -40
  364. package/src/fake/providers/FakeProvider.ts +133 -19
  365. package/src/lock/core/index.ts +11 -4
  366. package/src/logger/index.ts +17 -66
  367. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  368. package/src/mcp/errors/McpError.ts +30 -0
  369. package/src/mcp/index.ts +13 -27
  370. package/src/mcp/transports/SseMcpTransport.ts +6 -7
  371. package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
  372. package/src/orm/index.browser.ts +2 -2
  373. package/src/orm/index.bun.ts +4 -2
  374. package/src/orm/index.ts +21 -47
  375. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  376. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -0
  377. package/src/orm/services/Repository.ts +18 -3
  378. package/src/queue/core/index.ts +14 -6
  379. package/src/react/auth/__tests__/$auth.spec.ts +202 -0
  380. package/src/react/auth/hooks/useAuth.ts +32 -0
  381. package/src/react/auth/index.browser.ts +13 -0
  382. package/src/react/auth/index.shared.ts +2 -0
  383. package/src/react/auth/index.ts +48 -0
  384. package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
  385. package/src/react/auth/services/ReactAuth.ts +135 -0
  386. package/src/react/core/__tests__/Router.spec.tsx +169 -0
  387. package/src/react/core/components/ClientOnly.tsx +49 -0
  388. package/src/react/core/components/ErrorBoundary.tsx +73 -0
  389. package/src/react/core/contexts/AlephaContext.ts +7 -0
  390. package/src/react/core/contexts/AlephaProvider.tsx +42 -0
  391. package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
  392. package/src/react/core/hooks/useAction.ts +480 -0
  393. package/src/react/core/hooks/useAlepha.ts +26 -0
  394. package/src/react/core/hooks/useClient.ts +17 -0
  395. package/src/react/core/hooks/useEvents.ts +51 -0
  396. package/src/react/core/hooks/useInject.ts +12 -0
  397. package/src/react/core/hooks/useStore.ts +52 -0
  398. package/src/react/core/index.ts +90 -0
  399. package/src/react/form/components/FormState.tsx +17 -0
  400. package/src/react/form/errors/FormValidationError.ts +18 -0
  401. package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
  402. package/src/react/form/hooks/useForm.ts +47 -0
  403. package/src/react/form/hooks/useFormState.ts +130 -0
  404. package/src/react/form/index.ts +44 -0
  405. package/src/react/form/services/FormModel.ts +614 -0
  406. package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
  407. package/src/react/head/helpers/SeoExpander.ts +142 -0
  408. package/src/react/head/hooks/useHead.spec.tsx +288 -0
  409. package/src/react/head/hooks/useHead.ts +62 -0
  410. package/src/react/head/index.browser.ts +26 -0
  411. package/src/react/head/index.ts +44 -0
  412. package/src/react/head/interfaces/Head.ts +105 -0
  413. package/src/react/head/primitives/$head.ts +25 -0
  414. package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  415. package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
  416. package/src/react/head/providers/HeadProvider.ts +168 -0
  417. package/src/react/head/providers/ServerHeadProvider.ts +31 -0
  418. package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
  419. package/src/react/i18n/components/Localize.spec.tsx +357 -0
  420. package/src/react/i18n/components/Localize.tsx +35 -0
  421. package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  422. package/src/react/i18n/hooks/useI18n.ts +18 -0
  423. package/src/react/i18n/index.ts +41 -0
  424. package/src/react/i18n/primitives/$dictionary.ts +69 -0
  425. package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
  426. package/src/react/i18n/providers/I18nProvider.ts +278 -0
  427. package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
  428. package/src/react/router/__tests__/page-head.spec.ts +48 -0
  429. package/src/react/router/__tests__/seo-head.spec.ts +125 -0
  430. package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
  431. package/src/react/router/components/ErrorViewer.tsx +872 -0
  432. package/src/react/router/components/Link.tsx +23 -0
  433. package/src/react/router/components/NestedView.tsx +223 -0
  434. package/src/react/router/components/NotFound.tsx +30 -0
  435. package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  436. package/src/react/router/contexts/RouterLayerContext.ts +12 -0
  437. package/src/react/router/errors/Redirection.ts +28 -0
  438. package/src/react/router/hooks/useActive.ts +52 -0
  439. package/src/react/router/hooks/useQueryParams.ts +63 -0
  440. package/src/react/router/hooks/useRouter.ts +20 -0
  441. package/src/react/router/hooks/useRouterState.ts +11 -0
  442. package/src/react/router/index.browser.ts +45 -0
  443. package/src/react/router/index.shared.ts +19 -0
  444. package/src/react/router/index.ts +142 -0
  445. package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
  446. package/src/react/router/primitives/$page.spec.tsx +708 -0
  447. package/src/react/router/primitives/$page.ts +497 -0
  448. package/src/react/router/providers/ReactBrowserProvider.ts +309 -0
  449. package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
  450. package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
  451. package/src/react/router/providers/ReactPageProvider.ts +726 -0
  452. package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
  453. package/src/react/router/providers/ReactServerProvider.ts +558 -0
  454. package/src/react/router/providers/ReactServerTemplateProvider.ts +979 -0
  455. package/src/react/router/providers/SSRManifestProvider.ts +334 -0
  456. package/src/react/router/services/ReactPageServerService.ts +48 -0
  457. package/src/react/router/services/ReactPageService.ts +27 -0
  458. package/src/react/router/services/ReactRouter.ts +262 -0
  459. package/src/react/websocket/hooks/useRoom.tsx +242 -0
  460. package/src/react/websocket/index.ts +7 -0
  461. package/src/redis/__tests__/redis.spec.ts +13 -0
  462. package/src/redis/index.ts +9 -25
  463. package/src/redis/providers/BunRedisProvider.ts +9 -0
  464. package/src/redis/providers/NodeRedisProvider.ts +8 -0
  465. package/src/redis/providers/RedisProvider.ts +16 -0
  466. package/src/retry/index.ts +11 -2
  467. package/src/router/index.ts +15 -0
  468. package/src/scheduler/index.ts +11 -2
  469. package/src/security/__tests__/BasicAuth.spec.ts +2 -0
  470. package/src/security/__tests__/ServerSecurityProvider.spec.ts +13 -5
  471. package/src/security/index.ts +15 -10
  472. package/src/security/interfaces/IssuerResolver.ts +27 -0
  473. package/src/security/primitives/$issuer.ts +55 -0
  474. package/src/security/providers/SecurityProvider.ts +179 -0
  475. package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
  476. package/src/security/providers/ServerSecurityProvider.ts +36 -22
  477. package/src/server/auth/index.ts +12 -7
  478. package/src/server/cache/index.ts +7 -22
  479. package/src/server/compress/index.ts +10 -2
  480. package/src/server/cookies/index.ts +7 -5
  481. package/src/server/cookies/primitives/$cookie.ts +33 -11
  482. package/src/server/core/index.ts +17 -7
  483. package/src/server/core/interfaces/ServerRequest.ts +83 -1
  484. package/src/server/core/primitives/$action.spec.ts +1 -1
  485. package/src/server/core/primitives/$action.ts +8 -3
  486. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  487. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  488. package/src/server/core/providers/NodeHttpServerProvider.ts +77 -22
  489. package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
  490. package/src/server/core/providers/ServerProvider.ts +9 -12
  491. package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
  492. package/src/server/core/services/ServerRequestParser.ts +306 -13
  493. package/src/server/cors/index.ts +7 -21
  494. package/src/server/cors/primitives/$cors.ts +6 -2
  495. package/src/server/health/index.ts +8 -2
  496. package/src/server/helmet/index.ts +11 -3
  497. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  498. package/src/server/links/index.browser.ts +2 -0
  499. package/src/server/links/index.ts +13 -6
  500. package/src/server/metrics/index.ts +10 -3
  501. package/src/server/multipart/index.ts +9 -3
  502. package/src/server/proxy/index.ts +8 -2
  503. package/src/server/rate-limit/index.ts +21 -25
  504. package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
  505. package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
  506. package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
  507. package/src/server/static/index.ts +8 -2
  508. package/src/server/static/providers/ServerStaticProvider.ts +1 -1
  509. package/src/server/swagger/index.ts +9 -4
  510. package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
  511. package/src/sms/index.ts +9 -5
  512. package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
  513. package/src/sms/providers/LocalSmsProvider.ts +1 -1
  514. package/src/system/index.browser.ts +11 -0
  515. package/src/system/index.ts +62 -0
  516. package/src/{file → system}/providers/FileSystemProvider.ts +16 -0
  517. package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
  518. package/src/system/providers/MemoryShellProvider.ts +164 -0
  519. package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
  520. package/src/{file → system}/providers/NodeFileSystemProvider.ts +36 -0
  521. package/src/system/providers/NodeShellProvider.ts +184 -0
  522. package/src/system/providers/ShellProvider.ts +74 -0
  523. package/src/{file → system}/services/FileDetector.spec.ts +2 -2
  524. package/src/thread/index.ts +11 -2
  525. package/src/topic/core/index.ts +12 -5
  526. package/src/vite/index.ts +3 -2
  527. package/src/vite/tasks/buildClient.ts +2 -8
  528. package/src/vite/tasks/buildServer.ts +84 -21
  529. package/src/vite/tasks/copyAssets.ts +5 -4
  530. package/src/vite/tasks/generateSitemap.ts +64 -23
  531. package/src/vite/tasks/index.ts +0 -2
  532. package/src/vite/tasks/prerenderPages.ts +49 -24
  533. package/src/websocket/index.ts +12 -8
  534. package/dist/file/index.d.ts +0 -839
  535. package/dist/file/index.d.ts.map +0 -1
  536. package/dist/file/index.js.map +0 -1
  537. package/src/cli/assets/indexHtml.ts +0 -15
  538. package/src/cli/assets/mainServerTs.ts +0 -24
  539. package/src/cli/assets/webAppRouterTs.ts +0 -15
  540. package/src/cli/assets/webHelloComponentTsx.ts +0 -16
  541. package/src/cli/commands/format.ts +0 -23
  542. package/src/file/index.ts +0 -43
  543. package/src/vite/helpers/boot.ts +0 -117
  544. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  545. package/src/vite/tasks/devServer.ts +0 -71
  546. package/src/vite/tasks/runAlepha.ts +0 -270
  547. /package/dist/orm/{chunk-DtkW-qnP.js → chunk-DH6iiROE.js} +0 -0
  548. /package/src/cli/{assets → templates}/apiIndexTs.ts +0 -0
  549. /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
  550. /package/src/{file → system}/errors/FileError.ts +0 -0
  551. /package/src/{file → system}/services/FileDetector.ts +0 -0
@@ -0,0 +1,4932 @@
1
+ import { $atom, $env, $hook, $inject, $module, $use, Alepha, AlephaError, Json, KIND, Primitive, createPrimitive, isFileLike, t } from "alepha";
2
+ import { AlephaDateTime, DateTimeProvider } from "alepha/datetime";
3
+ import { AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, useAlepha, useEvents, useInject, useStore } from "alepha/react";
4
+ import { AlephaServer, ServerProvider, ServerRouterProvider } from "alepha/server";
5
+ import { AlephaServerCache } from "alepha/server/cache";
6
+ import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "alepha/server/links";
7
+ import { $logger } from "alepha/logger";
8
+ import { StrictMode, createContext, createElement, memo, use, useEffect, useRef, useState } from "react";
9
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
+ import { join } from "node:path";
11
+ import { BrowserHeadProvider, ServerHeadProvider } from "alepha/react/head";
12
+ import { ServerStaticProvider } from "alepha/server/static";
13
+ import { createReadStream } from "node:fs";
14
+ import { access, copyFile, cp, mkdir, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
15
+ import { PassThrough, Readable } from "node:stream";
16
+ import { fileURLToPath } from "node:url";
17
+ import { exec, spawn } from "node:child_process";
18
+ import { renderToReadableStream, renderToString } from "react-dom/server";
19
+ import { RouterProvider } from "alepha/router";
20
+
21
+ //#region ../../src/react/router/constants/PAGE_PRELOAD_KEY.ts
22
+ /**
23
+ * Symbol key for SSR module preloading path.
24
+ * Using Symbol.for() allows the Vite plugin to inject this at build time.
25
+ * @internal
26
+ */
27
+ const PAGE_PRELOAD_KEY = Symbol.for("alepha.page.preload");
28
+
29
+ //#endregion
30
+ //#region ../../src/react/router/services/ReactPageService.ts
31
+ /**
32
+ * $page methods interface.
33
+ */
34
+ var ReactPageService = class {
35
+ fetch(pathname, options = {}) {
36
+ throw new AlephaError("Fetch is not available for this environment.");
37
+ }
38
+ render(name, options = {}) {
39
+ throw new AlephaError("Render is not available for this environment.");
40
+ }
41
+ };
42
+
43
+ //#endregion
44
+ //#region ../../src/react/router/primitives/$page.ts
45
+ /**
46
+ * Main primitive for defining a React route in the application.
47
+ *
48
+ * The $page primitive is the core building block for creating type-safe, SSR-enabled React routes.
49
+ * It provides a declarative way to define pages with powerful features:
50
+ *
51
+ * **Routing & Navigation**
52
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
53
+ * - Nested routing with parent-child relationships
54
+ * - Type-safe URL parameter and query string validation
55
+ *
56
+ * **Data Loading**
57
+ * - Server-side data fetching with the `loader` function
58
+ * - Automatic serialization and hydration for SSR
59
+ * - Access to request context, URL params, and parent data
60
+ *
61
+ * **Component Loading**
62
+ * - Direct component rendering or lazy loading for code splitting
63
+ * - Client-only rendering when browser APIs are needed
64
+ * - Automatic fallback handling during hydration
65
+ *
66
+ * **Performance Optimization**
67
+ * - Static generation for pre-rendered pages at build time
68
+ * - Server-side caching with configurable TTL and providers
69
+ * - Code splitting through lazy component loading
70
+ *
71
+ * **Error Handling**
72
+ * - Custom error handlers with support for redirects
73
+ * - Hierarchical error handling (child → parent)
74
+ * - HTTP status code handling (404, 401, etc.)
75
+ *
76
+ * **Page Animations**
77
+ * - CSS-based enter/exit animations
78
+ * - Dynamic animations based on page state
79
+ * - Custom timing and easing functions
80
+ *
81
+ * **Lifecycle Management**
82
+ * - Server response hooks for headers and status codes
83
+ * - Page leave handlers for cleanup (browser only)
84
+ * - Permission-based access control
85
+ *
86
+ * @example Simple page with data fetching
87
+ * ```typescript
88
+ * const userProfile = $page({
89
+ * path: "/users/:id",
90
+ * schema: {
91
+ * params: t.object({ id: t.integer() }),
92
+ * query: t.object({ tab: t.optional(t.text()) })
93
+ * },
94
+ * loader: async ({ params }) => {
95
+ * const user = await userApi.getUser(params.id);
96
+ * return { user };
97
+ * },
98
+ * lazy: () => import("./UserProfile.tsx")
99
+ * });
100
+ * ```
101
+ *
102
+ * @example Nested routing with error handling
103
+ * ```typescript
104
+ * const projectSection = $page({
105
+ * path: "/projects/:id",
106
+ * children: () => [projectBoard, projectSettings],
107
+ * loader: async ({ params }) => {
108
+ * const project = await projectApi.get(params.id);
109
+ * return { project };
110
+ * },
111
+ * errorHandler: (error) => {
112
+ * if (HttpError.is(error, 404)) {
113
+ * return <ProjectNotFound />;
114
+ * }
115
+ * }
116
+ * });
117
+ * ```
118
+ *
119
+ * @example Static generation with caching
120
+ * ```typescript
121
+ * const blogPost = $page({
122
+ * path: "/blog/:slug",
123
+ * static: {
124
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
125
+ * },
126
+ * loader: async ({ params }) => {
127
+ * const post = await loadPost(params.slug);
128
+ * return { post };
129
+ * }
130
+ * });
131
+ * ```
132
+ */
133
+ const $page = (options) => {
134
+ return createPrimitive(PagePrimitive, options);
135
+ };
136
+ var PagePrimitive = class extends Primitive {
137
+ reactPageService = $inject(ReactPageService);
138
+ onInit() {
139
+ if (this.options.static) this.options.cache ??= { store: {
140
+ provider: "memory",
141
+ ttl: [1, "week"]
142
+ } };
143
+ }
144
+ get name() {
145
+ return this.options.name ?? this.config.propertyKey;
146
+ }
147
+ /**
148
+ * For testing or build purposes.
149
+ *
150
+ * This will render the page (HTML layout included or not) and return the HTML + context.
151
+ * Only valid for server-side rendering, it will throw an error if called on the client-side.
152
+ */
153
+ async render(options) {
154
+ return this.reactPageService.render(this.name, options);
155
+ }
156
+ async fetch(options) {
157
+ return this.reactPageService.fetch(this.options.path || "", options);
158
+ }
159
+ match(url) {
160
+ return false;
161
+ }
162
+ pathname(config) {
163
+ return this.options.path || "";
164
+ }
165
+ };
166
+ $page[KIND] = PagePrimitive;
167
+
168
+ //#endregion
169
+ //#region ../../src/react/router/components/ErrorViewer.tsx
170
+ const isBrowser = typeof window !== "undefined";
171
+ /**
172
+ * Error viewer component - Terminal/brutalist aesthetic
173
+ */
174
+ const ErrorViewer = ({ error, alepha }) => {
175
+ const [expanded, setExpanded] = useState(false);
176
+ const [showNodeModules, setShowNodeModules] = useState(false);
177
+ const [visible, setVisible] = useState(false);
178
+ const containerRef = useRef(null);
179
+ const isProduction = alepha.isProduction();
180
+ useEffect(() => {
181
+ const timer = setTimeout(() => setVisible(true), 10);
182
+ return () => clearTimeout(timer);
183
+ }, []);
184
+ useEffect(() => {
185
+ if (!isBrowser) return;
186
+ const handler = (e) => {
187
+ if (e.key === "c" && !e.metaKey && !e.ctrlKey) copyToClipboard(error.stack || error.message);
188
+ };
189
+ window.addEventListener("keydown", handler);
190
+ return () => window.removeEventListener("keydown", handler);
191
+ }, [error]);
192
+ if (isProduction) return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
193
+ const frames = parseStackTrace(error.stack);
194
+ const appFrames = frames.filter((f) => !f.isNodeModules);
195
+ const nodeModulesFrames = frames.filter((f) => f.isNodeModules);
196
+ const visibleAppFrames = expanded ? appFrames : appFrames.slice(0, 5);
197
+ const hiddenAppCount = appFrames.length - 5;
198
+ const timestamp = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
199
+ hour12: false,
200
+ hour: "2-digit",
201
+ minute: "2-digit",
202
+ second: "2-digit"
203
+ });
204
+ return /* @__PURE__ */ jsxs("div", {
205
+ ref: containerRef,
206
+ style: {
207
+ ...styles.overlay,
208
+ opacity: visible ? 1 : 0
209
+ },
210
+ role: "alertdialog",
211
+ "aria-modal": "true",
212
+ "aria-labelledby": "error-viewer-title",
213
+ children: [/* @__PURE__ */ jsx("div", {
214
+ style: styles.scanlines,
215
+ "aria-hidden": "true"
216
+ }), /* @__PURE__ */ jsxs("div", {
217
+ style: {
218
+ ...styles.container,
219
+ transform: visible ? "translateY(0)" : "translateY(-20px)",
220
+ opacity: visible ? 1 : 0
221
+ },
222
+ children: [
223
+ /* @__PURE__ */ jsxs("div", {
224
+ style: styles.terminalBar,
225
+ children: [
226
+ /* @__PURE__ */ jsxs("div", {
227
+ style: styles.terminalDots,
228
+ children: [
229
+ /* @__PURE__ */ jsx("span", { style: {
230
+ ...styles.dot,
231
+ backgroundColor: "#ff5f57"
232
+ } }),
233
+ /* @__PURE__ */ jsx("span", { style: {
234
+ ...styles.dot,
235
+ backgroundColor: "#febc2e"
236
+ } }),
237
+ /* @__PURE__ */ jsx("span", { style: {
238
+ ...styles.dot,
239
+ backgroundColor: "#28c840"
240
+ } })
241
+ ]
242
+ }),
243
+ /* @__PURE__ */ jsx("div", {
244
+ style: styles.terminalTitle,
245
+ children: /* @__PURE__ */ jsxs("span", {
246
+ style: styles.terminalTitleText,
247
+ children: ["error — ", timestamp]
248
+ })
249
+ }),
250
+ /* @__PURE__ */ jsxs("div", {
251
+ style: styles.terminalActions,
252
+ children: [/* @__PURE__ */ jsx("kbd", {
253
+ style: styles.kbd,
254
+ children: "C"
255
+ }), /* @__PURE__ */ jsx("span", {
256
+ style: styles.kbdLabel,
257
+ children: "copy"
258
+ })]
259
+ })
260
+ ]
261
+ }),
262
+ /* @__PURE__ */ jsx(Header, { error }),
263
+ /* @__PURE__ */ jsxs("div", {
264
+ style: styles.stackSection,
265
+ children: [/* @__PURE__ */ jsxs("div", {
266
+ style: styles.stackHeader,
267
+ children: [/* @__PURE__ */ jsx("span", {
268
+ style: styles.stackHeaderText,
269
+ children: "STACK TRACE"
270
+ }), /* @__PURE__ */ jsxs("span", {
271
+ style: styles.stackCount,
272
+ children: [
273
+ appFrames.length,
274
+ " frames",
275
+ nodeModulesFrames.length > 0 && ` · ${nodeModulesFrames.length} in node_modules`
276
+ ]
277
+ })]
278
+ }), /* @__PURE__ */ jsxs("div", {
279
+ style: styles.frameList,
280
+ children: [
281
+ visibleAppFrames.map((frame, i) => /* @__PURE__ */ jsx(StackFrameRow, {
282
+ frame,
283
+ index: i
284
+ }, `${frame.raw}-${i}`)),
285
+ hiddenAppCount > 0 && !expanded && /* @__PURE__ */ jsx(ExpandButton, {
286
+ onClick: () => setExpanded(true),
287
+ label: `Show ${hiddenAppCount} more frames`
288
+ }),
289
+ expanded && hiddenAppCount > 0 && /* @__PURE__ */ jsx(ExpandButton, {
290
+ onClick: () => setExpanded(false),
291
+ label: "Collapse"
292
+ }),
293
+ nodeModulesFrames.length > 0 && /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs("button", {
294
+ type: "button",
295
+ onClick: () => setShowNodeModules(!showNodeModules),
296
+ style: styles.nodeModulesToggle,
297
+ children: [
298
+ /* @__PURE__ */ jsx("span", {
299
+ style: styles.nodeModulesIcon,
300
+ children: showNodeModules ? "▼" : "▶"
301
+ }),
302
+ /* @__PURE__ */ jsx("span", {
303
+ style: styles.nodeModulesLabel,
304
+ children: "node_modules"
305
+ }),
306
+ /* @__PURE__ */ jsx("span", {
307
+ style: styles.nodeModulesCount,
308
+ children: nodeModulesFrames.length
309
+ })
310
+ ]
311
+ }), showNodeModules && /* @__PURE__ */ jsx("div", {
312
+ style: styles.nodeModulesFrames,
313
+ children: nodeModulesFrames.map((frame, i) => /* @__PURE__ */ jsx(StackFrameRow, {
314
+ frame,
315
+ index: appFrames.length + i,
316
+ dimmed: true
317
+ }, `nm-${frame.raw}-${i}`))
318
+ })] })
319
+ ]
320
+ })]
321
+ }),
322
+ /* @__PURE__ */ jsx("div", {
323
+ style: styles.footer,
324
+ children: /* @__PURE__ */ jsxs("span", {
325
+ style: styles.footerText,
326
+ children: [
327
+ "Press ",
328
+ /* @__PURE__ */ jsx("kbd", {
329
+ style: styles.kbdInline,
330
+ children: "C"
331
+ }),
332
+ " to copy stack trace"
333
+ ]
334
+ })
335
+ })
336
+ ]
337
+ })]
338
+ });
339
+ };
340
+ var ErrorViewer_default = ErrorViewer;
341
+ function parseStackTrace(stack) {
342
+ if (!stack) return [];
343
+ const lines = stack.split("\n").slice(1);
344
+ const frames = [];
345
+ for (const line of lines) {
346
+ const trimmed = line.trim();
347
+ if (!trimmed.startsWith("at ")) continue;
348
+ const frame = parseStackLine(trimmed);
349
+ if (frame) frames.push(frame);
350
+ }
351
+ return frames;
352
+ }
353
+ function parseStackLine(line) {
354
+ const isNodeModules = line.includes("node_modules") || line.includes("node:");
355
+ const withFn = line.match(/^at\s+(.+?)\s+\((.+):(\d+):(\d+)\)$/);
356
+ if (withFn) return {
357
+ fn: withFn[1],
358
+ file: withFn[2],
359
+ line: withFn[3],
360
+ col: withFn[4],
361
+ raw: line,
362
+ isNodeModules
363
+ };
364
+ const withoutFn = line.match(/^at\s+(.+):(\d+):(\d+)$/);
365
+ if (withoutFn) return {
366
+ fn: "<anonymous>",
367
+ file: withoutFn[1],
368
+ line: withoutFn[2],
369
+ col: withoutFn[3],
370
+ raw: line,
371
+ isNodeModules
372
+ };
373
+ return {
374
+ fn: "",
375
+ file: line.replace(/^at\s+/, ""),
376
+ line: "",
377
+ col: "",
378
+ raw: line,
379
+ isNodeModules
380
+ };
381
+ }
382
+ function copyToClipboard(text) {
383
+ if (!isBrowser || !navigator.clipboard) return Promise.resolve(false);
384
+ return navigator.clipboard.writeText(text).then(() => true).catch(() => false);
385
+ }
386
+ /**
387
+ * Header with error badge and message
388
+ */
389
+ function Header({ error }) {
390
+ const [copied, setCopied] = useState(false);
391
+ const [hovered, setHovered] = useState(false);
392
+ useEffect(() => {
393
+ if (!copied) return;
394
+ const timer = setTimeout(() => setCopied(false), 2e3);
395
+ return () => clearTimeout(timer);
396
+ }, [copied]);
397
+ const handleCopy = async () => {
398
+ if (await copyToClipboard(error.stack || error.message)) setCopied(true);
399
+ };
400
+ return /* @__PURE__ */ jsxs("div", {
401
+ style: styles.header,
402
+ children: [/* @__PURE__ */ jsxs("div", {
403
+ style: styles.headerRow,
404
+ children: [/* @__PURE__ */ jsxs("div", {
405
+ style: styles.errorIndicator,
406
+ children: [/* @__PURE__ */ jsx("div", { style: styles.errorGlow }), /* @__PURE__ */ jsx("div", {
407
+ style: styles.errorBadge,
408
+ children: error.name
409
+ })]
410
+ }), /* @__PURE__ */ jsx("button", {
411
+ type: "button",
412
+ onClick: handleCopy,
413
+ onMouseEnter: () => setHovered(true),
414
+ onMouseLeave: () => setHovered(false),
415
+ style: {
416
+ ...styles.copyBtn,
417
+ ...hovered ? styles.copyBtnHover : {}
418
+ },
419
+ children: copied ? "✓ Copied" : "Copy"
420
+ })]
421
+ }), /* @__PURE__ */ jsx("h1", {
422
+ id: "error-viewer-title",
423
+ style: styles.message,
424
+ children: error.message
425
+ })]
426
+ });
427
+ }
428
+ /**
429
+ * Single stack frame row
430
+ */
431
+ function StackFrameRow({ frame, index, dimmed = false }) {
432
+ const [hovered, setHovered] = useState(false);
433
+ const isFirst = index === 0 && !dimmed;
434
+ const fileName = frame.file.split("/").pop() || frame.file;
435
+ const dirPath = frame.file.substring(0, frame.file.length - fileName.length);
436
+ const vsCodeLink = frame.file && frame.line ? `vscode://file${frame.file}:${frame.line}:${frame.col || 1}` : null;
437
+ const content = /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("div", {
438
+ style: {
439
+ ...styles.frameIndex,
440
+ color: isFirst ? "#ff6b6b" : dimmed ? "#555" : "#666"
441
+ },
442
+ children: String(index + 1).padStart(2, "0")
443
+ }), /* @__PURE__ */ jsxs("div", {
444
+ style: styles.frameContent,
445
+ children: [frame.fn && /* @__PURE__ */ jsx("div", {
446
+ style: {
447
+ ...styles.fnName,
448
+ color: dimmed ? "#888" : "#f0f0f0"
449
+ },
450
+ children: formatFunctionName(frame.fn)
451
+ }), /* @__PURE__ */ jsxs("div", {
452
+ style: styles.filePath,
453
+ children: [
454
+ /* @__PURE__ */ jsx("span", {
455
+ style: {
456
+ ...styles.dirPath,
457
+ opacity: dimmed ? .6 : .8
458
+ },
459
+ children: dirPath
460
+ }),
461
+ /* @__PURE__ */ jsx("span", {
462
+ style: {
463
+ ...styles.fileName,
464
+ color: dimmed ? "#5a9aba" : "#7cc4eb"
465
+ },
466
+ children: fileName
467
+ }),
468
+ frame.line && /* @__PURE__ */ jsxs("span", {
469
+ style: {
470
+ ...styles.lineCol,
471
+ color: dimmed ? "#9a8a40" : "#e5b83a"
472
+ },
473
+ children: [
474
+ ":",
475
+ frame.line,
476
+ frame.col && `:${frame.col}`
477
+ ]
478
+ })
479
+ ]
480
+ })]
481
+ })] });
482
+ const rowStyles = {
483
+ ...styles.frame,
484
+ ...isFirst ? styles.frameFirst : {},
485
+ backgroundColor: hovered ? "rgba(255,255,255,0.03)" : "transparent"
486
+ };
487
+ if (vsCodeLink && isBrowser) return /* @__PURE__ */ jsx("a", {
488
+ href: vsCodeLink,
489
+ style: {
490
+ ...rowStyles,
491
+ textDecoration: "none"
492
+ },
493
+ onMouseEnter: () => setHovered(true),
494
+ onMouseLeave: () => setHovered(false),
495
+ children: content
496
+ });
497
+ return /* @__PURE__ */ jsx("div", {
498
+ style: rowStyles,
499
+ children: content
500
+ });
501
+ }
502
+ /**
503
+ * Format function name with syntax highlighting
504
+ */
505
+ function formatFunctionName(fn) {
506
+ const asyncMatch = fn.match(/^(async\s+)?(.+)$/);
507
+ if (asyncMatch?.[1]) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("span", {
508
+ style: { color: "#c678dd" },
509
+ children: "async "
510
+ }), /* @__PURE__ */ jsx("span", { children: asyncMatch[2] })] });
511
+ const methodMatch = fn.match(/^(.+)\.([^.]+)$/);
512
+ if (methodMatch) return /* @__PURE__ */ jsxs(Fragment, { children: [
513
+ /* @__PURE__ */ jsx("span", {
514
+ style: { color: "#e5c07b" },
515
+ children: methodMatch[1]
516
+ }),
517
+ /* @__PURE__ */ jsx("span", {
518
+ style: { color: "#666" },
519
+ children: "."
520
+ }),
521
+ /* @__PURE__ */ jsx("span", { children: methodMatch[2] })
522
+ ] });
523
+ return fn;
524
+ }
525
+ /**
526
+ * Expand/collapse button
527
+ */
528
+ function ExpandButton({ onClick, label }) {
529
+ const [hovered, setHovered] = useState(false);
530
+ return /* @__PURE__ */ jsx("button", {
531
+ type: "button",
532
+ onClick,
533
+ onMouseEnter: () => setHovered(true),
534
+ onMouseLeave: () => setHovered(false),
535
+ style: {
536
+ ...styles.expandBtn,
537
+ backgroundColor: hovered ? "rgba(255,255,255,0.05)" : "transparent",
538
+ color: hovered ? "#aaa" : "#777"
539
+ },
540
+ children: label
541
+ });
542
+ }
543
+ /**
544
+ * Production error view - minimal, user-friendly
545
+ */
546
+ function ErrorViewerProduction() {
547
+ const [hovered, setHovered] = useState(false);
548
+ const handleReload = () => {
549
+ if (isBrowser) window.location.reload();
550
+ };
551
+ return /* @__PURE__ */ jsx("div", {
552
+ style: styles.overlay,
553
+ role: "alertdialog",
554
+ "aria-modal": "true",
555
+ children: /* @__PURE__ */ jsxs("div", {
556
+ style: styles.prodContainer,
557
+ children: [
558
+ /* @__PURE__ */ jsx("div", {
559
+ style: styles.prodIcon,
560
+ children: /* @__PURE__ */ jsxs("svg", {
561
+ width: "32",
562
+ height: "32",
563
+ viewBox: "0 0 24 24",
564
+ fill: "none",
565
+ stroke: "currentColor",
566
+ strokeWidth: "2",
567
+ children: [
568
+ /* @__PURE__ */ jsx("circle", {
569
+ cx: "12",
570
+ cy: "12",
571
+ r: "10"
572
+ }),
573
+ /* @__PURE__ */ jsx("line", {
574
+ x1: "12",
575
+ y1: "8",
576
+ x2: "12",
577
+ y2: "12"
578
+ }),
579
+ /* @__PURE__ */ jsx("line", {
580
+ x1: "12",
581
+ y1: "16",
582
+ x2: "12.01",
583
+ y2: "16"
584
+ })
585
+ ]
586
+ })
587
+ }),
588
+ /* @__PURE__ */ jsx("h1", {
589
+ style: styles.prodTitle,
590
+ children: "Something went wrong"
591
+ }),
592
+ /* @__PURE__ */ jsx("p", {
593
+ style: styles.prodMessage,
594
+ children: "We encountered an unexpected error. Please try again."
595
+ }),
596
+ /* @__PURE__ */ jsx("button", {
597
+ type: "button",
598
+ onClick: handleReload,
599
+ onMouseEnter: () => setHovered(true),
600
+ onMouseLeave: () => setHovered(false),
601
+ style: {
602
+ ...styles.prodButton,
603
+ backgroundColor: hovered ? "#333" : "#222",
604
+ borderColor: hovered ? "#555" : "#444"
605
+ },
606
+ children: "Reload page"
607
+ })
608
+ ]
609
+ })
610
+ });
611
+ }
612
+ const MONO_FONT = "ui-monospace, \"JetBrains Mono\", \"Fira Code\", SFMono-Regular, Menlo, Monaco, Consolas, monospace";
613
+ const styles = {
614
+ overlay: {
615
+ position: "fixed",
616
+ inset: 0,
617
+ backgroundColor: "rgba(0, 0, 0, 0.92)",
618
+ display: "flex",
619
+ alignItems: "flex-start",
620
+ justifyContent: "center",
621
+ padding: "40px 20px",
622
+ overflow: "auto",
623
+ fontFamily: MONO_FONT,
624
+ fontSize: "13px",
625
+ zIndex: 99999,
626
+ transition: "opacity 0.2s ease-out"
627
+ },
628
+ scanlines: {
629
+ position: "fixed",
630
+ inset: 0,
631
+ background: "repeating-linear-gradient(0deg, transparent, transparent 2px, rgba(0,0,0,0.1) 2px, rgba(0,0,0,0.1) 4px)",
632
+ pointerEvents: "none",
633
+ zIndex: 1e5
634
+ },
635
+ container: {
636
+ width: "100%",
637
+ maxWidth: "900px",
638
+ backgroundColor: "#0d0d0d",
639
+ borderRadius: "8px",
640
+ overflow: "hidden",
641
+ boxShadow: "0 0 0 1px #333, 0 25px 80px -12px rgba(0, 0, 0, 0.8)",
642
+ transition: "transform 0.3s ease-out, opacity 0.3s ease-out"
643
+ },
644
+ terminalBar: {
645
+ display: "flex",
646
+ alignItems: "center",
647
+ padding: "12px 16px",
648
+ backgroundColor: "#1a1a1a",
649
+ borderBottom: "1px solid #333"
650
+ },
651
+ terminalDots: {
652
+ display: "flex",
653
+ gap: "8px"
654
+ },
655
+ dot: {
656
+ width: "12px",
657
+ height: "12px",
658
+ borderRadius: "50%"
659
+ },
660
+ terminalTitle: {
661
+ flex: 1,
662
+ textAlign: "center"
663
+ },
664
+ terminalTitleText: {
665
+ color: "#777",
666
+ fontSize: "12px",
667
+ letterSpacing: "0.5px"
668
+ },
669
+ terminalActions: {
670
+ display: "flex",
671
+ alignItems: "center",
672
+ gap: "6px"
673
+ },
674
+ kbd: {
675
+ display: "inline-block",
676
+ padding: "2px 6px",
677
+ backgroundColor: "#2a2a2a",
678
+ borderRadius: "4px",
679
+ fontSize: "11px",
680
+ color: "#aaa",
681
+ border: "1px solid #444"
682
+ },
683
+ kbdInline: {
684
+ display: "inline-block",
685
+ padding: "1px 5px",
686
+ backgroundColor: "#222",
687
+ borderRadius: "3px",
688
+ fontSize: "11px",
689
+ color: "#888",
690
+ border: "1px solid #444",
691
+ marginLeft: "4px",
692
+ marginRight: "4px"
693
+ },
694
+ kbdLabel: {
695
+ color: "#777",
696
+ fontSize: "11px"
697
+ },
698
+ header: {
699
+ padding: "24px",
700
+ borderBottom: "1px solid #333"
701
+ },
702
+ headerRow: {
703
+ display: "flex",
704
+ alignItems: "center",
705
+ justifyContent: "space-between",
706
+ marginBottom: "16px"
707
+ },
708
+ errorIndicator: {
709
+ position: "relative",
710
+ display: "inline-flex"
711
+ },
712
+ errorGlow: {
713
+ position: "absolute",
714
+ inset: "-4px",
715
+ background: "radial-gradient(ellipse at center, rgba(255,80,80,0.3) 0%, transparent 70%)",
716
+ borderRadius: "12px",
717
+ filter: "blur(8px)"
718
+ },
719
+ errorBadge: {
720
+ position: "relative",
721
+ display: "inline-block",
722
+ padding: "6px 14px",
723
+ backgroundColor: "#3d1a1a",
724
+ color: "#ff7b7b",
725
+ fontSize: "12px",
726
+ fontWeight: 600,
727
+ borderRadius: "6px",
728
+ border: "1px solid #5a2828",
729
+ letterSpacing: "0.5px"
730
+ },
731
+ copyBtn: {
732
+ padding: "8px 14px",
733
+ backgroundColor: "transparent",
734
+ color: "#888",
735
+ fontSize: "12px",
736
+ fontWeight: 500,
737
+ borderWidth: "1px",
738
+ borderStyle: "solid",
739
+ borderColor: "#444",
740
+ borderRadius: "6px",
741
+ cursor: "pointer",
742
+ transition: "all 0.15s",
743
+ fontFamily: MONO_FONT
744
+ },
745
+ copyBtnHover: {
746
+ backgroundColor: "#252525",
747
+ color: "#bbb",
748
+ borderColor: "#555"
749
+ },
750
+ message: {
751
+ margin: 0,
752
+ fontSize: "18px",
753
+ fontWeight: 400,
754
+ color: "#e8e8e8",
755
+ lineHeight: 1.6,
756
+ wordBreak: "break-word",
757
+ fontFamily: MONO_FONT
758
+ },
759
+ stackSection: { borderTop: "1px solid #2a2a2a" },
760
+ stackHeader: {
761
+ display: "flex",
762
+ alignItems: "center",
763
+ justifyContent: "space-between",
764
+ padding: "14px 24px",
765
+ borderBottom: "1px solid #2a2a2a"
766
+ },
767
+ stackHeaderText: {
768
+ fontSize: "10px",
769
+ fontWeight: 600,
770
+ color: "#666",
771
+ letterSpacing: "1.5px"
772
+ },
773
+ stackCount: {
774
+ fontSize: "11px",
775
+ color: "#555"
776
+ },
777
+ frameList: {
778
+ display: "flex",
779
+ flexDirection: "column"
780
+ },
781
+ frame: {
782
+ display: "flex",
783
+ alignItems: "flex-start",
784
+ padding: "12px 24px",
785
+ borderBottom: "1px solid #222",
786
+ transition: "background-color 0.1s",
787
+ cursor: "pointer"
788
+ },
789
+ frameFirst: {
790
+ backgroundColor: "rgba(255, 80, 80, 0.08)",
791
+ borderLeft: "2px solid #ff6b6b"
792
+ },
793
+ frameIndex: {
794
+ width: "28px",
795
+ flexShrink: 0,
796
+ fontSize: "11px",
797
+ fontWeight: 500,
798
+ fontFamily: MONO_FONT
799
+ },
800
+ frameContent: {
801
+ flex: 1,
802
+ minWidth: 0
803
+ },
804
+ fnName: {
805
+ fontSize: "13px",
806
+ fontWeight: 500,
807
+ marginBottom: "4px",
808
+ fontFamily: MONO_FONT
809
+ },
810
+ filePath: {
811
+ fontSize: "12px",
812
+ color: "#888",
813
+ fontFamily: MONO_FONT,
814
+ wordBreak: "break-all"
815
+ },
816
+ dirPath: { color: "#666" },
817
+ fileName: { color: "#7cc4eb" },
818
+ lineCol: { color: "#e5b83a" },
819
+ expandBtn: {
820
+ width: "100%",
821
+ padding: "14px 24px",
822
+ backgroundColor: "transparent",
823
+ color: "#777",
824
+ fontSize: "12px",
825
+ fontWeight: 500,
826
+ border: "none",
827
+ borderTop: "1px solid #2a2a2a",
828
+ cursor: "pointer",
829
+ textAlign: "left",
830
+ transition: "all 0.15s",
831
+ fontFamily: MONO_FONT
832
+ },
833
+ nodeModulesToggle: {
834
+ display: "flex",
835
+ alignItems: "center",
836
+ gap: "10px",
837
+ width: "100%",
838
+ padding: "12px 24px",
839
+ backgroundColor: "#0a0a0a",
840
+ color: "#666",
841
+ fontSize: "11px",
842
+ fontWeight: 500,
843
+ border: "none",
844
+ borderTop: "1px solid #2a2a2a",
845
+ cursor: "pointer",
846
+ textAlign: "left",
847
+ fontFamily: MONO_FONT
848
+ },
849
+ nodeModulesIcon: {
850
+ fontSize: "8px",
851
+ color: "#555"
852
+ },
853
+ nodeModulesLabel: {
854
+ flex: 1,
855
+ letterSpacing: "0.5px"
856
+ },
857
+ nodeModulesCount: { color: "#555" },
858
+ nodeModulesFrames: { backgroundColor: "#080808" },
859
+ footer: {
860
+ padding: "14px 24px",
861
+ borderTop: "1px solid #2a2a2a",
862
+ backgroundColor: "#0a0a0a"
863
+ },
864
+ footerText: {
865
+ fontSize: "11px",
866
+ color: "#555"
867
+ },
868
+ prodContainer: {
869
+ textAlign: "center",
870
+ padding: "60px 40px",
871
+ backgroundColor: "#0d0d0d",
872
+ borderRadius: "8px",
873
+ maxWidth: "400px",
874
+ border: "1px solid #333"
875
+ },
876
+ prodIcon: {
877
+ width: "64px",
878
+ height: "64px",
879
+ margin: "0 auto 24px",
880
+ color: "#666",
881
+ display: "flex",
882
+ alignItems: "center",
883
+ justifyContent: "center"
884
+ },
885
+ prodTitle: {
886
+ margin: "0 0 12px",
887
+ fontSize: "18px",
888
+ fontWeight: 500,
889
+ color: "#f0f0f0",
890
+ fontFamily: MONO_FONT
891
+ },
892
+ prodMessage: {
893
+ margin: "0 0 28px",
894
+ fontSize: "13px",
895
+ color: "#888",
896
+ lineHeight: 1.6,
897
+ fontFamily: MONO_FONT
898
+ },
899
+ prodButton: {
900
+ padding: "12px 24px",
901
+ backgroundColor: "#222",
902
+ color: "#bbb",
903
+ fontSize: "13px",
904
+ fontWeight: 500,
905
+ borderWidth: "1px",
906
+ borderStyle: "solid",
907
+ borderColor: "#444",
908
+ borderRadius: "6px",
909
+ cursor: "pointer",
910
+ transition: "all 0.15s",
911
+ fontFamily: MONO_FONT
912
+ }
913
+ };
914
+
915
+ //#endregion
916
+ //#region ../../src/react/router/contexts/RouterLayerContext.ts
917
+ const RouterLayerContext = createContext(void 0);
918
+
919
+ //#endregion
920
+ //#region ../../src/react/router/errors/Redirection.ts
921
+ /**
922
+ * Used for Redirection during the page loading.
923
+ *
924
+ * Depends on the context, it can be thrown or just returned.
925
+ *
926
+ * @example
927
+ * ```ts
928
+ * import { Redirection } from "alepha/react";
929
+ *
930
+ * const MyPage = $page({
931
+ * loader: async () => {
932
+ * if (needRedirect) {
933
+ * throw new Redirection("/new-path");
934
+ * }
935
+ * },
936
+ * });
937
+ * ```
938
+ */
939
+ var Redirection = class extends AlephaError {
940
+ redirect;
941
+ constructor(redirect) {
942
+ super("Redirection");
943
+ this.redirect = redirect;
944
+ }
945
+ };
946
+
947
+ //#endregion
948
+ //#region ../../src/react/router/hooks/useRouterState.ts
949
+ const useRouterState = () => {
950
+ const [state] = useStore("alepha.react.router.state");
951
+ if (!state) throw new AlephaError("Missing react router state");
952
+ return state;
953
+ };
954
+
955
+ //#endregion
956
+ //#region ../../src/react/router/components/NestedView.tsx
957
+ /**
958
+ * A component that renders the current view of the nested router layer.
959
+ *
960
+ * To be simple, it renders the `element` of the current child page of a parent page.
961
+ *
962
+ * @example
963
+ * ```tsx
964
+ * import { NestedView } from "alepha/react";
965
+ *
966
+ * class App {
967
+ * parent = $page({
968
+ * component: () => <NestedView />,
969
+ * });
970
+ *
971
+ * child = $page({
972
+ * parent: this.root,
973
+ * component: () => <div>Child Page</div>,
974
+ * });
975
+ * }
976
+ * ```
977
+ */
978
+ const NestedView = (props) => {
979
+ const routerLayer = use(RouterLayerContext);
980
+ const index = routerLayer?.index ?? 0;
981
+ const onError = routerLayer?.onError;
982
+ const state = useRouterState();
983
+ const alepha = useAlepha();
984
+ const [view, setView] = useState(state.layers[index]?.element);
985
+ const [animation, setAnimation] = useState("");
986
+ const animationExitDuration = useRef(0);
987
+ const animationExitNow = useRef(0);
988
+ useEvents({
989
+ "react:transition:begin": async ({ previous, state }) => {
990
+ const layer = previous.layers[index];
991
+ if (!layer) return;
992
+ if (`${state.url.pathname}/`.startsWith(`${layer.path}/`)) return;
993
+ const animationExit = parseAnimation(layer.route?.animation, state, "exit");
994
+ if (animationExit) {
995
+ const duration = animationExit.duration || 200;
996
+ animationExitNow.current = Date.now();
997
+ animationExitDuration.current = duration;
998
+ setAnimation(animationExit.animation);
999
+ } else {
1000
+ animationExitNow.current = 0;
1001
+ animationExitDuration.current = 0;
1002
+ setAnimation("");
1003
+ }
1004
+ },
1005
+ "react:transition:end": async ({ state }) => {
1006
+ const layer = state.layers[index];
1007
+ if (animationExitNow.current) {
1008
+ const duration = animationExitDuration.current;
1009
+ const diff = Date.now() - animationExitNow.current;
1010
+ if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
1011
+ }
1012
+ if (!layer?.cache) {
1013
+ setView(layer?.element);
1014
+ const animationEnter = parseAnimation(layer?.route?.animation, state, "enter");
1015
+ if (animationEnter) setAnimation(animationEnter.animation);
1016
+ else setAnimation("");
1017
+ }
1018
+ }
1019
+ }, []);
1020
+ let element = view ?? props.children ?? null;
1021
+ if (animation) element = /* @__PURE__ */ jsx("div", {
1022
+ style: {
1023
+ display: "flex",
1024
+ flex: 1,
1025
+ height: "100%",
1026
+ width: "100%",
1027
+ position: "relative",
1028
+ overflow: "hidden"
1029
+ },
1030
+ children: /* @__PURE__ */ jsx("div", {
1031
+ style: {
1032
+ height: "100%",
1033
+ width: "100%",
1034
+ display: "flex",
1035
+ animation
1036
+ },
1037
+ children: element
1038
+ })
1039
+ });
1040
+ if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
1041
+ if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
1042
+ fallback: props.errorBoundary,
1043
+ children: element
1044
+ });
1045
+ const fallback = (error) => {
1046
+ const result = onError?.(error, state) ?? /* @__PURE__ */ jsx(ErrorViewer_default, {
1047
+ error,
1048
+ alepha
1049
+ });
1050
+ if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
1051
+ return result;
1052
+ };
1053
+ return /* @__PURE__ */ jsx(ErrorBoundary, {
1054
+ fallback,
1055
+ children: element
1056
+ });
1057
+ };
1058
+ var NestedView_default = memo(NestedView);
1059
+ function parseAnimation(animationLike, state, type = "enter") {
1060
+ if (!animationLike) return;
1061
+ const DEFAULT_DURATION = 300;
1062
+ const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
1063
+ if (typeof animation === "string") {
1064
+ if (type === "exit") return;
1065
+ return {
1066
+ duration: DEFAULT_DURATION,
1067
+ animation: `${DEFAULT_DURATION}ms ${animation}`
1068
+ };
1069
+ }
1070
+ if (typeof animation === "object") {
1071
+ const anim = animation[type];
1072
+ const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
1073
+ const name = typeof anim === "object" ? anim.name : anim;
1074
+ if (type === "exit") return {
1075
+ duration,
1076
+ animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
1077
+ };
1078
+ return {
1079
+ duration,
1080
+ animation: `${duration}ms ${typeof anim === "object" ? anim.timing ?? "" : ""} ${name}`
1081
+ };
1082
+ }
1083
+ }
1084
+
1085
+ //#endregion
1086
+ //#region ../../src/react/router/components/NotFound.tsx
1087
+ /**
1088
+ * Default 404 Not Found page component.
1089
+ */
1090
+ const NotFound = (props) => /* @__PURE__ */ jsxs("div", {
1091
+ style: {
1092
+ width: "100%",
1093
+ minHeight: "90vh",
1094
+ boxSizing: "border-box",
1095
+ display: "flex",
1096
+ flexDirection: "column",
1097
+ justifyContent: "center",
1098
+ alignItems: "center",
1099
+ textAlign: "center",
1100
+ fontFamily: "system-ui, -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, sans-serif",
1101
+ padding: "2rem",
1102
+ ...props.style
1103
+ },
1104
+ children: [/* @__PURE__ */ jsx("div", {
1105
+ style: {
1106
+ fontSize: "6rem",
1107
+ fontWeight: 200,
1108
+ lineHeight: 1
1109
+ },
1110
+ children: "404"
1111
+ }), /* @__PURE__ */ jsx("div", {
1112
+ style: {
1113
+ fontSize: "0.875rem",
1114
+ marginTop: "1rem",
1115
+ opacity: .6
1116
+ },
1117
+ children: "Page not found"
1118
+ })]
1119
+ });
1120
+ var NotFound_default = NotFound;
1121
+
1122
+ //#endregion
1123
+ //#region ../../src/react/router/providers/ReactPageProvider.ts
1124
+ const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
1125
+ /**
1126
+ * Handle page routes for React applications. (Browser and Server)
1127
+ */
1128
+ var ReactPageProvider = class {
1129
+ log = $logger();
1130
+ env = $env(envSchema$1);
1131
+ alepha = $inject(Alepha);
1132
+ pages = [];
1133
+ getPages() {
1134
+ return this.pages;
1135
+ }
1136
+ getConcretePages() {
1137
+ const pages = [];
1138
+ for (const page of this.pages) {
1139
+ if (page.children && page.children.length > 0) continue;
1140
+ const fullPath = this.pathname(page.name);
1141
+ if (fullPath.includes(":") || fullPath.includes("*")) {
1142
+ if (typeof page.static === "object") {
1143
+ const entries = page.static.entries;
1144
+ if (entries && entries.length > 0) for (const entry of entries) {
1145
+ const params = entry.params;
1146
+ const path = this.compile(page.path ?? "", params);
1147
+ if (!path.includes(":") && !path.includes("*")) pages.push({
1148
+ ...page,
1149
+ name: params[Object.keys(params)[0]],
1150
+ staticName: page.name,
1151
+ path,
1152
+ ...entry
1153
+ });
1154
+ }
1155
+ }
1156
+ continue;
1157
+ }
1158
+ pages.push(page);
1159
+ }
1160
+ return pages;
1161
+ }
1162
+ page(name) {
1163
+ for (const page of this.pages) if (page.name === name) return page;
1164
+ throw new AlephaError(`Page '${name}' not found`);
1165
+ }
1166
+ pathname(name, options = {}) {
1167
+ const page = this.page(name);
1168
+ if (!page) throw new Error(`Page ${name} not found`);
1169
+ let url = page.path ?? "";
1170
+ let parent = page.parent;
1171
+ while (parent) {
1172
+ url = `${parent.path ?? ""}/${url}`;
1173
+ parent = parent.parent;
1174
+ }
1175
+ url = this.compile(url, options.params ?? {});
1176
+ if (options.query) {
1177
+ const query = new URLSearchParams(options.query);
1178
+ if (query.toString()) url += `?${query.toString()}`;
1179
+ }
1180
+ return url.replace(/\/\/+/g, "/") || "/";
1181
+ }
1182
+ url(name, options = {}) {
1183
+ return new URL(this.pathname(name, options), options.host ?? `http://localhost`);
1184
+ }
1185
+ root(state) {
1186
+ const root = createElement(AlephaContext.Provider, { value: this.alepha }, createElement(NestedView_default, {}, state.layers[0]?.element));
1187
+ if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
1188
+ return root;
1189
+ }
1190
+ convertStringObjectToObject = (schema, value) => {
1191
+ if (t.schema.isObject(schema) && typeof value === "object") {
1192
+ for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
1193
+ value[key] = this.alepha.codec.decode(schema.properties[key], decodeURIComponent(value[key]));
1194
+ } catch (e) {}
1195
+ }
1196
+ return value;
1197
+ };
1198
+ /**
1199
+ * Create a new RouterState based on a given route and request.
1200
+ * This method resolves the layers for the route, applying any query and params schemas defined in the route.
1201
+ * It also handles errors and redirects.
1202
+ */
1203
+ async createLayers(route, state, previous = []) {
1204
+ let context = {};
1205
+ const stack = [{ route }];
1206
+ let parent = route.parent;
1207
+ while (parent) {
1208
+ stack.unshift({ route: parent });
1209
+ parent = parent.parent;
1210
+ }
1211
+ let forceRefresh = false;
1212
+ for (let i = 0; i < stack.length; i++) {
1213
+ const it = stack[i];
1214
+ const route = it.route;
1215
+ const config = {};
1216
+ try {
1217
+ this.convertStringObjectToObject(route.schema?.query, state.query);
1218
+ config.query = route.schema?.query ? this.alepha.codec.decode(route.schema.query, state.query) : {};
1219
+ } catch (e) {
1220
+ it.error = e;
1221
+ break;
1222
+ }
1223
+ try {
1224
+ config.params = route.schema?.params ? this.alepha.codec.decode(route.schema.params, state.params) : {};
1225
+ } catch (e) {
1226
+ it.error = e;
1227
+ break;
1228
+ }
1229
+ it.config = { ...config };
1230
+ if (previous?.[i] && !forceRefresh && previous[i].name === route.name) {
1231
+ const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
1232
+ if (JSON.stringify({
1233
+ part: url(previous[i].part),
1234
+ params: previous[i].config?.params ?? {}
1235
+ }) === JSON.stringify({
1236
+ part: url(route.path),
1237
+ params: config.params ?? {}
1238
+ })) {
1239
+ it.props = previous[i].props;
1240
+ it.error = previous[i].error;
1241
+ it.cache = true;
1242
+ context = {
1243
+ ...context,
1244
+ ...it.props
1245
+ };
1246
+ continue;
1247
+ }
1248
+ forceRefresh = true;
1249
+ }
1250
+ if (!route.loader) continue;
1251
+ try {
1252
+ const args = Object.create(state);
1253
+ Object.assign(args, config, context);
1254
+ const props = await route.loader?.(args) ?? {};
1255
+ it.props = { ...props };
1256
+ context = {
1257
+ ...context,
1258
+ ...props
1259
+ };
1260
+ } catch (e) {
1261
+ if (e instanceof Redirection) return { redirect: e.redirect };
1262
+ this.log.error("Page loader has failed", e);
1263
+ it.error = e;
1264
+ break;
1265
+ }
1266
+ }
1267
+ let acc = "";
1268
+ for (let i = 0; i < stack.length; i++) {
1269
+ const it = stack[i];
1270
+ const props = it.props ?? {};
1271
+ const params = { ...it.config?.params };
1272
+ for (const key of Object.keys(params)) params[key] = String(params[key]);
1273
+ acc += "/";
1274
+ acc += it.route.path ? this.compile(it.route.path, params) : "";
1275
+ const path = acc.replace(/\/+/, "/");
1276
+ const localErrorHandler = this.getErrorHandler(it.route);
1277
+ if (localErrorHandler) {
1278
+ const onErrorParent = state.onError;
1279
+ state.onError = (error, context) => {
1280
+ const result = localErrorHandler(error, context);
1281
+ if (result === void 0) return onErrorParent(error, context);
1282
+ return result;
1283
+ };
1284
+ }
1285
+ if (!it.error) try {
1286
+ const element = await this.createElement(it.route, {
1287
+ ...it.route.props ? it.route.props() : {},
1288
+ ...props,
1289
+ ...context
1290
+ });
1291
+ state.layers.push({
1292
+ name: it.route.name,
1293
+ props,
1294
+ part: it.route.path,
1295
+ config: it.config,
1296
+ element: this.renderView(i + 1, path, element, it.route),
1297
+ index: i + 1,
1298
+ path,
1299
+ route: it.route,
1300
+ cache: it.cache
1301
+ });
1302
+ } catch (e) {
1303
+ it.error = e;
1304
+ }
1305
+ if (it.error) try {
1306
+ let element = await state.onError(it.error, state);
1307
+ if (element === void 0) throw it.error;
1308
+ if (element instanceof Redirection) return { redirect: element.redirect };
1309
+ if (element === null) element = this.renderError(it.error);
1310
+ state.layers.push({
1311
+ props,
1312
+ error: it.error,
1313
+ name: it.route.name,
1314
+ part: it.route.path,
1315
+ config: it.config,
1316
+ element: this.renderView(i + 1, path, element, it.route),
1317
+ index: i + 1,
1318
+ path,
1319
+ route: it.route
1320
+ });
1321
+ break;
1322
+ } catch (e) {
1323
+ if (e instanceof Redirection) return { redirect: e.redirect };
1324
+ throw e;
1325
+ }
1326
+ }
1327
+ return { state };
1328
+ }
1329
+ getErrorHandler(route) {
1330
+ if (route.errorHandler) return route.errorHandler;
1331
+ let parent = route.parent;
1332
+ while (parent) {
1333
+ if (parent.errorHandler) return parent.errorHandler;
1334
+ parent = parent.parent;
1335
+ }
1336
+ }
1337
+ async createElement(page, props) {
1338
+ if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
1339
+ if (page.lazy) return createElement((await page.lazy()).default, props);
1340
+ if (page.component) return createElement(page.component, props);
1341
+ }
1342
+ renderError(error) {
1343
+ return createElement(ErrorViewer_default, {
1344
+ error,
1345
+ alepha: this.alepha
1346
+ });
1347
+ }
1348
+ renderEmptyView() {
1349
+ return createElement(NestedView_default, {});
1350
+ }
1351
+ href(page, params = {}) {
1352
+ const found = this.pages.find((it) => it.name === page.options.name);
1353
+ if (!found) throw new AlephaError(`Page ${page.options.name} not found`);
1354
+ let url = found.path ?? "";
1355
+ let parent = found.parent;
1356
+ while (parent) {
1357
+ url = `${parent.path ?? ""}/${url}`;
1358
+ parent = parent.parent;
1359
+ }
1360
+ url = this.compile(url, params);
1361
+ return url.replace(/\/\/+/g, "/") || "/";
1362
+ }
1363
+ compile(path, params = {}) {
1364
+ for (const [key, value] of Object.entries(params)) path = path.replace(`:${key}`, value);
1365
+ return path;
1366
+ }
1367
+ renderView(index, path, view, page) {
1368
+ view ??= this.renderEmptyView();
1369
+ const element = page.client ? createElement(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
1370
+ return createElement(RouterLayerContext.Provider, { value: {
1371
+ index,
1372
+ path,
1373
+ onError: this.getErrorHandler(page) ?? ((error) => this.renderError(error))
1374
+ } }, element);
1375
+ }
1376
+ configure = $hook({
1377
+ on: "configure",
1378
+ handler: () => {
1379
+ let hasNotFoundHandler = false;
1380
+ const pages = this.alepha.primitives($page);
1381
+ const hasParent = (it) => {
1382
+ if (it.options.parent) return true;
1383
+ for (const page of pages) if ((page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : []).includes(it)) return true;
1384
+ };
1385
+ for (const page of pages) {
1386
+ if (page.options.path === "/*") hasNotFoundHandler = true;
1387
+ if (hasParent(page)) continue;
1388
+ this.add(this.map(pages, page));
1389
+ }
1390
+ if (!hasNotFoundHandler && pages.length > 0) this.add({
1391
+ path: "/*",
1392
+ name: "notFound",
1393
+ cache: true,
1394
+ component: NotFound_default,
1395
+ onServerResponse: ({ reply }) => {
1396
+ reply.status = 404;
1397
+ }
1398
+ });
1399
+ }
1400
+ });
1401
+ map(pages, target) {
1402
+ const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
1403
+ const getChildrenFromParent = (it) => {
1404
+ const children = [];
1405
+ for (const page of pages) if (page.options.parent === it) children.push(page);
1406
+ return children;
1407
+ };
1408
+ children.push(...getChildrenFromParent(target));
1409
+ return {
1410
+ ...target.options,
1411
+ name: target.name,
1412
+ parent: void 0,
1413
+ children: children.map((it) => this.map(pages, it))
1414
+ };
1415
+ }
1416
+ add(entry) {
1417
+ if (this.alepha.isReady()) throw new AlephaError("Router is already initialized");
1418
+ entry.name ??= this.nextId();
1419
+ const page = entry;
1420
+ page.match = this.createMatch(page);
1421
+ this.pages.push(page);
1422
+ if (page.children) for (const child of page.children) {
1423
+ child.parent = page;
1424
+ this.add(child);
1425
+ }
1426
+ }
1427
+ createMatch(page) {
1428
+ let url = page.path ?? "/";
1429
+ let target = page.parent;
1430
+ while (target) {
1431
+ url = `${target.path ?? ""}/${url}`;
1432
+ target = target.parent;
1433
+ }
1434
+ let path = url.replace(/\/\/+/g, "/");
1435
+ if (path.endsWith("/") && path !== "/") path = path.slice(0, -1);
1436
+ return path;
1437
+ }
1438
+ _next = 0;
1439
+ nextId() {
1440
+ this._next += 1;
1441
+ return `P${this._next}`;
1442
+ }
1443
+ };
1444
+ const isPageRoute = (it) => {
1445
+ return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
1446
+ };
1447
+
1448
+ //#endregion
1449
+ //#region ../../src/system/providers/FileSystemProvider.ts
1450
+ /**
1451
+ * FileSystem interface providing utilities for working with files.
1452
+ */
1453
+ var FileSystemProvider = class {};
1454
+
1455
+ //#endregion
1456
+ //#region ../../src/system/providers/MemoryFileSystemProvider.ts
1457
+ /**
1458
+ * In-memory implementation of FileSystemProvider for testing.
1459
+ *
1460
+ * This provider stores all files and directories in memory, making it ideal for
1461
+ * unit tests that need to verify file operations without touching the real file system.
1462
+ *
1463
+ * @example
1464
+ * ```typescript
1465
+ * // In tests, substitute the real FileSystemProvider with MemoryFileSystemProvider
1466
+ * const alepha = Alepha.create().with({
1467
+ * provide: FileSystemProvider,
1468
+ * use: MemoryFileSystemProvider,
1469
+ * });
1470
+ *
1471
+ * // Run code that uses FileSystemProvider
1472
+ * const service = alepha.inject(MyService);
1473
+ * await service.saveFile("test.txt", "Hello World");
1474
+ *
1475
+ * // Verify the file was written
1476
+ * const memoryFs = alepha.inject(MemoryFileSystemProvider);
1477
+ * expect(memoryFs.files.get("test.txt")?.toString()).toBe("Hello World");
1478
+ * ```
1479
+ */
1480
+ var MemoryFileSystemProvider = class {
1481
+ json = $inject(Json);
1482
+ /**
1483
+ * In-memory storage for files (path -> content)
1484
+ */
1485
+ files = /* @__PURE__ */ new Map();
1486
+ /**
1487
+ * In-memory storage for directories
1488
+ */
1489
+ directories = /* @__PURE__ */ new Set();
1490
+ /**
1491
+ * Track mkdir calls for test assertions
1492
+ */
1493
+ mkdirCalls = [];
1494
+ /**
1495
+ * Track writeFile calls for test assertions
1496
+ */
1497
+ writeFileCalls = [];
1498
+ /**
1499
+ * Track readFile calls for test assertions
1500
+ */
1501
+ readFileCalls = [];
1502
+ /**
1503
+ * Track rm calls for test assertions
1504
+ */
1505
+ rmCalls = [];
1506
+ /**
1507
+ * Track join calls for test assertions
1508
+ */
1509
+ joinCalls = [];
1510
+ /**
1511
+ * Error to throw on mkdir (for testing error handling)
1512
+ */
1513
+ mkdirError = null;
1514
+ /**
1515
+ * Error to throw on writeFile (for testing error handling)
1516
+ */
1517
+ writeFileError = null;
1518
+ /**
1519
+ * Error to throw on readFile (for testing error handling)
1520
+ */
1521
+ readFileError = null;
1522
+ constructor(options = {}) {
1523
+ this.mkdirError = options.mkdirError ?? null;
1524
+ this.writeFileError = options.writeFileError ?? null;
1525
+ this.readFileError = options.readFileError ?? null;
1526
+ }
1527
+ /**
1528
+ * Join path segments using forward slashes.
1529
+ * Uses Node's path.join for proper normalization (handles .. and .)
1530
+ */
1531
+ join(...paths) {
1532
+ this.joinCalls.push(paths);
1533
+ return join(...paths);
1534
+ }
1535
+ /**
1536
+ * Create a FileLike object from various sources.
1537
+ */
1538
+ createFile(options) {
1539
+ if ("path" in options) {
1540
+ const filePath = options.path;
1541
+ const buffer = this.files.get(filePath);
1542
+ if (buffer === void 0) throw new Error(`ENOENT: no such file or directory, open '${filePath}'`);
1543
+ return {
1544
+ name: options.name ?? filePath.split("/").pop() ?? "file",
1545
+ type: options.type ?? "application/octet-stream",
1546
+ size: buffer.byteLength,
1547
+ lastModified: Date.now(),
1548
+ stream: () => {
1549
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1550
+ },
1551
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1552
+ text: async () => buffer.toString("utf-8")
1553
+ };
1554
+ }
1555
+ if ("buffer" in options) {
1556
+ const buffer = options.buffer;
1557
+ return {
1558
+ name: options.name ?? "file",
1559
+ type: options.type ?? "application/octet-stream",
1560
+ size: buffer.byteLength,
1561
+ lastModified: Date.now(),
1562
+ stream: () => {
1563
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1564
+ },
1565
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1566
+ text: async () => buffer.toString("utf-8")
1567
+ };
1568
+ }
1569
+ if ("text" in options) {
1570
+ const buffer = Buffer.from(options.text, "utf-8");
1571
+ return {
1572
+ name: options.name ?? "file.txt",
1573
+ type: options.type ?? "text/plain",
1574
+ size: buffer.byteLength,
1575
+ lastModified: Date.now(),
1576
+ stream: () => {
1577
+ throw new Error("Stream not implemented in MemoryFileSystemProvider");
1578
+ },
1579
+ arrayBuffer: async () => buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength),
1580
+ text: async () => options.text
1581
+ };
1582
+ }
1583
+ throw new Error("MemoryFileSystemProvider.createFile: unsupported options. Only buffer and text are supported.");
1584
+ }
1585
+ /**
1586
+ * Remove a file or directory from memory.
1587
+ */
1588
+ async rm(path, options) {
1589
+ this.rmCalls.push({
1590
+ path,
1591
+ options
1592
+ });
1593
+ if (!(this.files.has(path) || this.directories.has(path)) && !options?.force) throw new Error(`ENOENT: no such file or directory, rm '${path}'`);
1594
+ if (this.directories.has(path)) if (options?.recursive) {
1595
+ this.directories.delete(path);
1596
+ for (const filePath of this.files.keys()) if (filePath.startsWith(`${path}/`)) this.files.delete(filePath);
1597
+ for (const dirPath of this.directories) if (dirPath.startsWith(`${path}/`)) this.directories.delete(dirPath);
1598
+ } else throw new Error(`EISDIR: illegal operation on a directory, rm '${path}'`);
1599
+ else this.files.delete(path);
1600
+ }
1601
+ /**
1602
+ * Copy a file or directory in memory.
1603
+ */
1604
+ async cp(src, dest, options) {
1605
+ if (this.directories.has(src)) {
1606
+ if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
1607
+ this.directories.add(dest);
1608
+ for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
1609
+ const newPath = filePath.replace(src, dest);
1610
+ this.files.set(newPath, Buffer.from(content));
1611
+ }
1612
+ } else if (this.files.has(src)) {
1613
+ const content = this.files.get(src);
1614
+ this.files.set(dest, Buffer.from(content));
1615
+ } else throw new Error(`ENOENT: no such file or directory, cp '${src}'`);
1616
+ }
1617
+ /**
1618
+ * Move/rename a file or directory in memory.
1619
+ */
1620
+ async mv(src, dest) {
1621
+ if (this.directories.has(src)) {
1622
+ this.directories.delete(src);
1623
+ this.directories.add(dest);
1624
+ for (const [filePath, content] of this.files) if (filePath.startsWith(`${src}/`)) {
1625
+ const newPath = filePath.replace(src, dest);
1626
+ this.files.delete(filePath);
1627
+ this.files.set(newPath, content);
1628
+ }
1629
+ } else if (this.files.has(src)) {
1630
+ const content = this.files.get(src);
1631
+ this.files.delete(src);
1632
+ this.files.set(dest, content);
1633
+ } else throw new Error(`ENOENT: no such file or directory, mv '${src}'`);
1634
+ }
1635
+ /**
1636
+ * Create a directory in memory.
1637
+ */
1638
+ async mkdir(path, options) {
1639
+ this.mkdirCalls.push({
1640
+ path,
1641
+ options
1642
+ });
1643
+ if (this.mkdirError) throw this.mkdirError;
1644
+ if (this.directories.has(path) && !options?.recursive) throw new Error(`EEXIST: file already exists, mkdir '${path}'`);
1645
+ this.directories.add(path);
1646
+ if (options?.recursive) {
1647
+ const parts = path.split("/").filter(Boolean);
1648
+ let current = "";
1649
+ for (const part of parts) {
1650
+ current = current ? `${current}/${part}` : part;
1651
+ this.directories.add(current);
1652
+ }
1653
+ }
1654
+ }
1655
+ /**
1656
+ * List files in a directory.
1657
+ */
1658
+ async ls(path, options) {
1659
+ const normalizedPath = path.replace(/\/$/, "");
1660
+ const entries = /* @__PURE__ */ new Set();
1661
+ for (const filePath of this.files.keys()) if (filePath.startsWith(`${normalizedPath}/`)) {
1662
+ const relativePath = filePath.slice(normalizedPath.length + 1);
1663
+ const parts = relativePath.split("/");
1664
+ if (options?.recursive) entries.add(relativePath);
1665
+ else entries.add(parts[0]);
1666
+ }
1667
+ for (const dirPath of this.directories) if (dirPath.startsWith(`${normalizedPath}/`) && dirPath !== normalizedPath) {
1668
+ const relativePath = dirPath.slice(normalizedPath.length + 1);
1669
+ const parts = relativePath.split("/");
1670
+ if (options?.recursive) entries.add(relativePath);
1671
+ else if (parts.length === 1) entries.add(parts[0]);
1672
+ }
1673
+ let result = Array.from(entries);
1674
+ if (!options?.hidden) result = result.filter((entry) => !entry.startsWith("."));
1675
+ return result.sort();
1676
+ }
1677
+ /**
1678
+ * Check if a file or directory exists in memory.
1679
+ */
1680
+ async exists(path) {
1681
+ return this.files.has(path) || this.directories.has(path);
1682
+ }
1683
+ /**
1684
+ * Read a file from memory.
1685
+ */
1686
+ async readFile(path) {
1687
+ this.readFileCalls.push(path);
1688
+ if (this.readFileError) throw this.readFileError;
1689
+ const content = this.files.get(path);
1690
+ if (!content) throw new Error(`ENOENT: no such file or directory, open '${path}'`);
1691
+ return content;
1692
+ }
1693
+ /**
1694
+ * Read a file from memory as text.
1695
+ */
1696
+ async readTextFile(path) {
1697
+ return (await this.readFile(path)).toString("utf-8");
1698
+ }
1699
+ /**
1700
+ * Read a file from memory as JSON.
1701
+ */
1702
+ async readJsonFile(path) {
1703
+ const text = await this.readTextFile(path);
1704
+ return this.json.parse(text);
1705
+ }
1706
+ /**
1707
+ * Write a file to memory.
1708
+ */
1709
+ async writeFile(path, data) {
1710
+ const dataStr = typeof data === "string" ? data : data instanceof Buffer || data instanceof Uint8Array ? data.toString("utf-8") : await data.text();
1711
+ this.writeFileCalls.push({
1712
+ path,
1713
+ data: dataStr
1714
+ });
1715
+ if (this.writeFileError) throw this.writeFileError;
1716
+ const buffer = typeof data === "string" ? Buffer.from(data, "utf-8") : data instanceof Buffer ? data : data instanceof Uint8Array ? Buffer.from(data) : Buffer.from(await data.text(), "utf-8");
1717
+ this.files.set(path, buffer);
1718
+ }
1719
+ /**
1720
+ * Reset all in-memory state (useful between tests).
1721
+ */
1722
+ reset() {
1723
+ this.files.clear();
1724
+ this.directories.clear();
1725
+ this.mkdirCalls = [];
1726
+ this.writeFileCalls = [];
1727
+ this.readFileCalls = [];
1728
+ this.rmCalls = [];
1729
+ this.joinCalls = [];
1730
+ this.mkdirError = null;
1731
+ this.writeFileError = null;
1732
+ this.readFileError = null;
1733
+ }
1734
+ /**
1735
+ * Check if a file was written during the test.
1736
+ *
1737
+ * @example
1738
+ * ```typescript
1739
+ * expect(fs.wasWritten("/project/tsconfig.json")).toBe(true);
1740
+ * ```
1741
+ */
1742
+ wasWritten(path) {
1743
+ return this.writeFileCalls.some((call) => call.path === path);
1744
+ }
1745
+ /**
1746
+ * Check if a file was written with content matching a pattern.
1747
+ *
1748
+ * @example
1749
+ * ```typescript
1750
+ * expect(fs.wasWrittenMatching("/project/tsconfig.json", /extends/)).toBe(true);
1751
+ * ```
1752
+ */
1753
+ wasWrittenMatching(path, pattern) {
1754
+ const call = this.writeFileCalls.find((c) => c.path === path);
1755
+ return call ? pattern.test(call.data) : false;
1756
+ }
1757
+ /**
1758
+ * Check if a file was read during the test.
1759
+ *
1760
+ * @example
1761
+ * ```typescript
1762
+ * expect(fs.wasRead("/project/package.json")).toBe(true);
1763
+ * ```
1764
+ */
1765
+ wasRead(path) {
1766
+ return this.readFileCalls.includes(path);
1767
+ }
1768
+ /**
1769
+ * Check if a file was deleted during the test.
1770
+ *
1771
+ * @example
1772
+ * ```typescript
1773
+ * expect(fs.wasDeleted("/project/old-file.txt")).toBe(true);
1774
+ * ```
1775
+ */
1776
+ wasDeleted(path) {
1777
+ return this.rmCalls.some((call) => call.path === path);
1778
+ }
1779
+ /**
1780
+ * Get the content of a file as a string (convenience method for testing).
1781
+ */
1782
+ getFileContent(path) {
1783
+ return this.files.get(path)?.toString("utf-8");
1784
+ }
1785
+ };
1786
+
1787
+ //#endregion
1788
+ //#region ../../src/system/providers/MemoryShellProvider.ts
1789
+ /**
1790
+ * In-memory implementation of ShellProvider for testing.
1791
+ *
1792
+ * Records all commands that would be executed without actually running them.
1793
+ * Can be configured to return specific outputs or throw errors for testing.
1794
+ *
1795
+ * @example
1796
+ * ```typescript
1797
+ * // In tests, substitute the real ShellProvider with MemoryShellProvider
1798
+ * const alepha = Alepha.create().with({
1799
+ * provide: ShellProvider,
1800
+ * use: MemoryShellProvider,
1801
+ * });
1802
+ *
1803
+ * // Configure mock behavior
1804
+ * const shell = alepha.inject(MemoryShellProvider);
1805
+ * shell.configure({
1806
+ * outputs: { "echo hello": "hello\n" },
1807
+ * errors: { "failing-cmd": "Command failed" },
1808
+ * });
1809
+ *
1810
+ * // Or use the fluent API
1811
+ * shell.outputs.set("another-cmd", "output");
1812
+ * shell.errors.set("another-error", "Error message");
1813
+ *
1814
+ * // Run code that uses ShellProvider
1815
+ * const service = alepha.inject(MyService);
1816
+ * await service.doSomething();
1817
+ *
1818
+ * // Verify commands were called
1819
+ * expect(shell.calls).toHaveLength(2);
1820
+ * expect(shell.calls[0].command).toBe("yarn install");
1821
+ * ```
1822
+ */
1823
+ var MemoryShellProvider = class {
1824
+ /**
1825
+ * All recorded shell calls.
1826
+ */
1827
+ calls = [];
1828
+ /**
1829
+ * Simulated outputs for specific commands.
1830
+ */
1831
+ outputs = /* @__PURE__ */ new Map();
1832
+ /**
1833
+ * Commands that should throw an error.
1834
+ */
1835
+ errors = /* @__PURE__ */ new Map();
1836
+ /**
1837
+ * Commands considered installed in the system PATH.
1838
+ */
1839
+ installedCommands = /* @__PURE__ */ new Set();
1840
+ /**
1841
+ * Configure the mock with predefined outputs, errors, and installed commands.
1842
+ */
1843
+ configure(options) {
1844
+ if (options.outputs) for (const [cmd, output] of Object.entries(options.outputs)) this.outputs.set(cmd, output);
1845
+ if (options.errors) for (const [cmd, error] of Object.entries(options.errors)) this.errors.set(cmd, error);
1846
+ if (options.installedCommands) for (const cmd of options.installedCommands) this.installedCommands.add(cmd);
1847
+ return this;
1848
+ }
1849
+ /**
1850
+ * Record command and return simulated output.
1851
+ */
1852
+ async run(command, options = {}) {
1853
+ this.calls.push({
1854
+ command,
1855
+ options
1856
+ });
1857
+ const errorMsg = this.errors.get(command);
1858
+ if (errorMsg) throw new Error(errorMsg);
1859
+ return this.outputs.get(command) ?? "";
1860
+ }
1861
+ /**
1862
+ * Check if a specific command was called.
1863
+ */
1864
+ wasCalled(command) {
1865
+ return this.calls.some((call) => call.command === command);
1866
+ }
1867
+ /**
1868
+ * Check if a command matching a pattern was called.
1869
+ */
1870
+ wasCalledMatching(pattern) {
1871
+ return this.calls.some((call) => pattern.test(call.command));
1872
+ }
1873
+ /**
1874
+ * Get all calls matching a pattern.
1875
+ */
1876
+ getCallsMatching(pattern) {
1877
+ return this.calls.filter((call) => pattern.test(call.command));
1878
+ }
1879
+ /**
1880
+ * Check if a command is installed.
1881
+ */
1882
+ async isInstalled(command) {
1883
+ return this.installedCommands.has(command);
1884
+ }
1885
+ /**
1886
+ * Reset all recorded state.
1887
+ */
1888
+ reset() {
1889
+ this.calls = [];
1890
+ this.outputs.clear();
1891
+ this.errors.clear();
1892
+ this.installedCommands.clear();
1893
+ }
1894
+ };
1895
+
1896
+ //#endregion
1897
+ //#region ../../src/system/services/FileDetector.ts
1898
+ /**
1899
+ * Service for detecting file types and getting content types.
1900
+ *
1901
+ * @example
1902
+ * ```typescript
1903
+ * const detector = alepha.inject(FileDetector);
1904
+ *
1905
+ * // Get content type from filename
1906
+ * const mimeType = detector.getContentType("image.png"); // "image/png"
1907
+ *
1908
+ * // Detect file type by magic bytes
1909
+ * const stream = createReadStream('image.png');
1910
+ * const result = await detector.detectFileType(stream, 'image.png');
1911
+ * console.log(result.mimeType); // 'image/png'
1912
+ * console.log(result.verified); // true if magic bytes match
1913
+ * ```
1914
+ */
1915
+ var FileDetector = class FileDetector {
1916
+ /**
1917
+ * Magic byte signatures for common file formats.
1918
+ * Each signature is represented as an array of bytes or null (wildcard).
1919
+ */
1920
+ static MAGIC_BYTES = {
1921
+ png: [{
1922
+ signature: [
1923
+ 137,
1924
+ 80,
1925
+ 78,
1926
+ 71,
1927
+ 13,
1928
+ 10,
1929
+ 26,
1930
+ 10
1931
+ ],
1932
+ mimeType: "image/png"
1933
+ }],
1934
+ jpg: [
1935
+ {
1936
+ signature: [
1937
+ 255,
1938
+ 216,
1939
+ 255,
1940
+ 224
1941
+ ],
1942
+ mimeType: "image/jpeg"
1943
+ },
1944
+ {
1945
+ signature: [
1946
+ 255,
1947
+ 216,
1948
+ 255,
1949
+ 225
1950
+ ],
1951
+ mimeType: "image/jpeg"
1952
+ },
1953
+ {
1954
+ signature: [
1955
+ 255,
1956
+ 216,
1957
+ 255,
1958
+ 226
1959
+ ],
1960
+ mimeType: "image/jpeg"
1961
+ },
1962
+ {
1963
+ signature: [
1964
+ 255,
1965
+ 216,
1966
+ 255,
1967
+ 227
1968
+ ],
1969
+ mimeType: "image/jpeg"
1970
+ },
1971
+ {
1972
+ signature: [
1973
+ 255,
1974
+ 216,
1975
+ 255,
1976
+ 232
1977
+ ],
1978
+ mimeType: "image/jpeg"
1979
+ }
1980
+ ],
1981
+ jpeg: [
1982
+ {
1983
+ signature: [
1984
+ 255,
1985
+ 216,
1986
+ 255,
1987
+ 224
1988
+ ],
1989
+ mimeType: "image/jpeg"
1990
+ },
1991
+ {
1992
+ signature: [
1993
+ 255,
1994
+ 216,
1995
+ 255,
1996
+ 225
1997
+ ],
1998
+ mimeType: "image/jpeg"
1999
+ },
2000
+ {
2001
+ signature: [
2002
+ 255,
2003
+ 216,
2004
+ 255,
2005
+ 226
2006
+ ],
2007
+ mimeType: "image/jpeg"
2008
+ },
2009
+ {
2010
+ signature: [
2011
+ 255,
2012
+ 216,
2013
+ 255,
2014
+ 227
2015
+ ],
2016
+ mimeType: "image/jpeg"
2017
+ },
2018
+ {
2019
+ signature: [
2020
+ 255,
2021
+ 216,
2022
+ 255,
2023
+ 232
2024
+ ],
2025
+ mimeType: "image/jpeg"
2026
+ }
2027
+ ],
2028
+ gif: [{
2029
+ signature: [
2030
+ 71,
2031
+ 73,
2032
+ 70,
2033
+ 56,
2034
+ 55,
2035
+ 97
2036
+ ],
2037
+ mimeType: "image/gif"
2038
+ }, {
2039
+ signature: [
2040
+ 71,
2041
+ 73,
2042
+ 70,
2043
+ 56,
2044
+ 57,
2045
+ 97
2046
+ ],
2047
+ mimeType: "image/gif"
2048
+ }],
2049
+ webp: [{
2050
+ signature: [
2051
+ 82,
2052
+ 73,
2053
+ 70,
2054
+ 70,
2055
+ null,
2056
+ null,
2057
+ null,
2058
+ null,
2059
+ 87,
2060
+ 69,
2061
+ 66,
2062
+ 80
2063
+ ],
2064
+ mimeType: "image/webp"
2065
+ }],
2066
+ bmp: [{
2067
+ signature: [66, 77],
2068
+ mimeType: "image/bmp"
2069
+ }],
2070
+ ico: [{
2071
+ signature: [
2072
+ 0,
2073
+ 0,
2074
+ 1,
2075
+ 0
2076
+ ],
2077
+ mimeType: "image/x-icon"
2078
+ }],
2079
+ tiff: [{
2080
+ signature: [
2081
+ 73,
2082
+ 73,
2083
+ 42,
2084
+ 0
2085
+ ],
2086
+ mimeType: "image/tiff"
2087
+ }, {
2088
+ signature: [
2089
+ 77,
2090
+ 77,
2091
+ 0,
2092
+ 42
2093
+ ],
2094
+ mimeType: "image/tiff"
2095
+ }],
2096
+ tif: [{
2097
+ signature: [
2098
+ 73,
2099
+ 73,
2100
+ 42,
2101
+ 0
2102
+ ],
2103
+ mimeType: "image/tiff"
2104
+ }, {
2105
+ signature: [
2106
+ 77,
2107
+ 77,
2108
+ 0,
2109
+ 42
2110
+ ],
2111
+ mimeType: "image/tiff"
2112
+ }],
2113
+ pdf: [{
2114
+ signature: [
2115
+ 37,
2116
+ 80,
2117
+ 68,
2118
+ 70,
2119
+ 45
2120
+ ],
2121
+ mimeType: "application/pdf"
2122
+ }],
2123
+ zip: [
2124
+ {
2125
+ signature: [
2126
+ 80,
2127
+ 75,
2128
+ 3,
2129
+ 4
2130
+ ],
2131
+ mimeType: "application/zip"
2132
+ },
2133
+ {
2134
+ signature: [
2135
+ 80,
2136
+ 75,
2137
+ 5,
2138
+ 6
2139
+ ],
2140
+ mimeType: "application/zip"
2141
+ },
2142
+ {
2143
+ signature: [
2144
+ 80,
2145
+ 75,
2146
+ 7,
2147
+ 8
2148
+ ],
2149
+ mimeType: "application/zip"
2150
+ }
2151
+ ],
2152
+ rar: [{
2153
+ signature: [
2154
+ 82,
2155
+ 97,
2156
+ 114,
2157
+ 33,
2158
+ 26,
2159
+ 7
2160
+ ],
2161
+ mimeType: "application/vnd.rar"
2162
+ }],
2163
+ "7z": [{
2164
+ signature: [
2165
+ 55,
2166
+ 122,
2167
+ 188,
2168
+ 175,
2169
+ 39,
2170
+ 28
2171
+ ],
2172
+ mimeType: "application/x-7z-compressed"
2173
+ }],
2174
+ tar: [{
2175
+ signature: [
2176
+ 117,
2177
+ 115,
2178
+ 116,
2179
+ 97,
2180
+ 114
2181
+ ],
2182
+ mimeType: "application/x-tar"
2183
+ }],
2184
+ gz: [{
2185
+ signature: [31, 139],
2186
+ mimeType: "application/gzip"
2187
+ }],
2188
+ tgz: [{
2189
+ signature: [31, 139],
2190
+ mimeType: "application/gzip"
2191
+ }],
2192
+ mp3: [
2193
+ {
2194
+ signature: [255, 251],
2195
+ mimeType: "audio/mpeg"
2196
+ },
2197
+ {
2198
+ signature: [255, 243],
2199
+ mimeType: "audio/mpeg"
2200
+ },
2201
+ {
2202
+ signature: [255, 242],
2203
+ mimeType: "audio/mpeg"
2204
+ },
2205
+ {
2206
+ signature: [
2207
+ 73,
2208
+ 68,
2209
+ 51
2210
+ ],
2211
+ mimeType: "audio/mpeg"
2212
+ }
2213
+ ],
2214
+ wav: [{
2215
+ signature: [
2216
+ 82,
2217
+ 73,
2218
+ 70,
2219
+ 70,
2220
+ null,
2221
+ null,
2222
+ null,
2223
+ null,
2224
+ 87,
2225
+ 65,
2226
+ 86,
2227
+ 69
2228
+ ],
2229
+ mimeType: "audio/wav"
2230
+ }],
2231
+ ogg: [{
2232
+ signature: [
2233
+ 79,
2234
+ 103,
2235
+ 103,
2236
+ 83
2237
+ ],
2238
+ mimeType: "audio/ogg"
2239
+ }],
2240
+ flac: [{
2241
+ signature: [
2242
+ 102,
2243
+ 76,
2244
+ 97,
2245
+ 67
2246
+ ],
2247
+ mimeType: "audio/flac"
2248
+ }],
2249
+ mp4: [
2250
+ {
2251
+ signature: [
2252
+ null,
2253
+ null,
2254
+ null,
2255
+ null,
2256
+ 102,
2257
+ 116,
2258
+ 121,
2259
+ 112
2260
+ ],
2261
+ mimeType: "video/mp4"
2262
+ },
2263
+ {
2264
+ signature: [
2265
+ null,
2266
+ null,
2267
+ null,
2268
+ null,
2269
+ 102,
2270
+ 116,
2271
+ 121,
2272
+ 112,
2273
+ 105,
2274
+ 115,
2275
+ 111,
2276
+ 109
2277
+ ],
2278
+ mimeType: "video/mp4"
2279
+ },
2280
+ {
2281
+ signature: [
2282
+ null,
2283
+ null,
2284
+ null,
2285
+ null,
2286
+ 102,
2287
+ 116,
2288
+ 121,
2289
+ 112,
2290
+ 109,
2291
+ 112,
2292
+ 52,
2293
+ 50
2294
+ ],
2295
+ mimeType: "video/mp4"
2296
+ }
2297
+ ],
2298
+ webm: [{
2299
+ signature: [
2300
+ 26,
2301
+ 69,
2302
+ 223,
2303
+ 163
2304
+ ],
2305
+ mimeType: "video/webm"
2306
+ }],
2307
+ avi: [{
2308
+ signature: [
2309
+ 82,
2310
+ 73,
2311
+ 70,
2312
+ 70,
2313
+ null,
2314
+ null,
2315
+ null,
2316
+ null,
2317
+ 65,
2318
+ 86,
2319
+ 73,
2320
+ 32
2321
+ ],
2322
+ mimeType: "video/x-msvideo"
2323
+ }],
2324
+ mov: [{
2325
+ signature: [
2326
+ null,
2327
+ null,
2328
+ null,
2329
+ null,
2330
+ 102,
2331
+ 116,
2332
+ 121,
2333
+ 112,
2334
+ 113,
2335
+ 116,
2336
+ 32,
2337
+ 32
2338
+ ],
2339
+ mimeType: "video/quicktime"
2340
+ }],
2341
+ mkv: [{
2342
+ signature: [
2343
+ 26,
2344
+ 69,
2345
+ 223,
2346
+ 163
2347
+ ],
2348
+ mimeType: "video/x-matroska"
2349
+ }],
2350
+ docx: [{
2351
+ signature: [
2352
+ 80,
2353
+ 75,
2354
+ 3,
2355
+ 4
2356
+ ],
2357
+ mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
2358
+ }],
2359
+ xlsx: [{
2360
+ signature: [
2361
+ 80,
2362
+ 75,
2363
+ 3,
2364
+ 4
2365
+ ],
2366
+ mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
2367
+ }],
2368
+ pptx: [{
2369
+ signature: [
2370
+ 80,
2371
+ 75,
2372
+ 3,
2373
+ 4
2374
+ ],
2375
+ mimeType: "application/vnd.openxmlformats-officedocument.presentationml.presentation"
2376
+ }],
2377
+ doc: [{
2378
+ signature: [
2379
+ 208,
2380
+ 207,
2381
+ 17,
2382
+ 224,
2383
+ 161,
2384
+ 177,
2385
+ 26,
2386
+ 225
2387
+ ],
2388
+ mimeType: "application/msword"
2389
+ }],
2390
+ xls: [{
2391
+ signature: [
2392
+ 208,
2393
+ 207,
2394
+ 17,
2395
+ 224,
2396
+ 161,
2397
+ 177,
2398
+ 26,
2399
+ 225
2400
+ ],
2401
+ mimeType: "application/vnd.ms-excel"
2402
+ }],
2403
+ ppt: [{
2404
+ signature: [
2405
+ 208,
2406
+ 207,
2407
+ 17,
2408
+ 224,
2409
+ 161,
2410
+ 177,
2411
+ 26,
2412
+ 225
2413
+ ],
2414
+ mimeType: "application/vnd.ms-powerpoint"
2415
+ }]
2416
+ };
2417
+ /**
2418
+ * All possible format signatures for checking against actual file content
2419
+ */
2420
+ static ALL_SIGNATURES = Object.entries(FileDetector.MAGIC_BYTES).flatMap(([ext, signatures]) => signatures.map((sig) => ({
2421
+ ext,
2422
+ ...sig
2423
+ })));
2424
+ /**
2425
+ * MIME type map for file extensions.
2426
+ *
2427
+ * Can be used to get the content type of file based on its extension.
2428
+ * Feel free to add more mime types in your project!
2429
+ */
2430
+ static mimeMap = {
2431
+ json: "application/json",
2432
+ txt: "text/plain",
2433
+ html: "text/html",
2434
+ htm: "text/html",
2435
+ xml: "application/xml",
2436
+ csv: "text/csv",
2437
+ pdf: "application/pdf",
2438
+ md: "text/markdown",
2439
+ markdown: "text/markdown",
2440
+ rtf: "application/rtf",
2441
+ css: "text/css",
2442
+ js: "application/javascript",
2443
+ mjs: "application/javascript",
2444
+ ts: "application/typescript",
2445
+ jsx: "text/jsx",
2446
+ tsx: "text/tsx",
2447
+ zip: "application/zip",
2448
+ rar: "application/vnd.rar",
2449
+ "7z": "application/x-7z-compressed",
2450
+ tar: "application/x-tar",
2451
+ gz: "application/gzip",
2452
+ tgz: "application/gzip",
2453
+ png: "image/png",
2454
+ jpg: "image/jpeg",
2455
+ jpeg: "image/jpeg",
2456
+ gif: "image/gif",
2457
+ webp: "image/webp",
2458
+ svg: "image/svg+xml",
2459
+ bmp: "image/bmp",
2460
+ ico: "image/x-icon",
2461
+ tiff: "image/tiff",
2462
+ tif: "image/tiff",
2463
+ mp3: "audio/mpeg",
2464
+ wav: "audio/wav",
2465
+ ogg: "audio/ogg",
2466
+ m4a: "audio/mp4",
2467
+ aac: "audio/aac",
2468
+ flac: "audio/flac",
2469
+ mp4: "video/mp4",
2470
+ webm: "video/webm",
2471
+ avi: "video/x-msvideo",
2472
+ mov: "video/quicktime",
2473
+ wmv: "video/x-ms-wmv",
2474
+ flv: "video/x-flv",
2475
+ mkv: "video/x-matroska",
2476
+ doc: "application/msword",
2477
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
2478
+ xls: "application/vnd.ms-excel",
2479
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
2480
+ ppt: "application/vnd.ms-powerpoint",
2481
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
2482
+ woff: "font/woff",
2483
+ woff2: "font/woff2",
2484
+ ttf: "font/ttf",
2485
+ otf: "font/otf",
2486
+ eot: "application/vnd.ms-fontobject"
2487
+ };
2488
+ /**
2489
+ * Reverse MIME type map for looking up extensions from MIME types.
2490
+ * Prefers shorter, more common extensions when multiple exist.
2491
+ */
2492
+ static reverseMimeMap = (() => {
2493
+ const reverse = {};
2494
+ for (const [ext, mimeType] of Object.entries(FileDetector.mimeMap)) if (!reverse[mimeType]) reverse[mimeType] = ext;
2495
+ return reverse;
2496
+ })();
2497
+ /**
2498
+ * Returns the file extension for a given MIME type.
2499
+ *
2500
+ * @param mimeType - The MIME type to look up
2501
+ * @returns The file extension (without dot), or "bin" if not found
2502
+ *
2503
+ * @example
2504
+ * ```typescript
2505
+ * const detector = alepha.inject(FileDetector);
2506
+ * const ext = detector.getExtensionFromMimeType("image/png"); // "png"
2507
+ * const ext2 = detector.getExtensionFromMimeType("application/octet-stream"); // "bin"
2508
+ * ```
2509
+ */
2510
+ getExtensionFromMimeType(mimeType) {
2511
+ return FileDetector.reverseMimeMap[mimeType] || "bin";
2512
+ }
2513
+ /**
2514
+ * Returns the content type of file based on its filename.
2515
+ *
2516
+ * @param filename - The filename to check
2517
+ * @returns The MIME type
2518
+ *
2519
+ * @example
2520
+ * ```typescript
2521
+ * const detector = alepha.inject(FileDetector);
2522
+ * const mimeType = detector.getContentType("image.png"); // "image/png"
2523
+ * ```
2524
+ */
2525
+ getContentType(filename) {
2526
+ const ext = filename.toLowerCase().split(".").pop() || "";
2527
+ return FileDetector.mimeMap[ext] || "application/octet-stream";
2528
+ }
2529
+ /**
2530
+ * Detects the file type by checking magic bytes against the stream content.
2531
+ *
2532
+ * @param stream - The readable stream to check
2533
+ * @param filename - The filename (used to get the extension)
2534
+ * @returns File type information including MIME type, extension, and verification status
2535
+ *
2536
+ * @example
2537
+ * ```typescript
2538
+ * const detector = alepha.inject(FileDetector);
2539
+ * const stream = createReadStream('image.png');
2540
+ * const result = await detector.detectFileType(stream, 'image.png');
2541
+ * console.log(result.mimeType); // 'image/png'
2542
+ * console.log(result.verified); // true if magic bytes match
2543
+ * ```
2544
+ */
2545
+ async detectFileType(stream, filename) {
2546
+ const expectedMimeType = this.getContentType(filename);
2547
+ const lastDotIndex = filename.lastIndexOf(".");
2548
+ const ext = lastDotIndex > 0 ? filename.substring(lastDotIndex + 1).toLowerCase() : "";
2549
+ const { buffer, stream: newStream } = await this.peekBytes(stream, 16);
2550
+ const expectedSignatures = FileDetector.MAGIC_BYTES[ext];
2551
+ if (expectedSignatures) {
2552
+ for (const { signature, mimeType } of expectedSignatures) if (this.matchesSignature(buffer, signature)) return {
2553
+ mimeType,
2554
+ extension: ext,
2555
+ verified: true,
2556
+ stream: newStream
2557
+ };
2558
+ }
2559
+ for (const { ext: detectedExt, signature, mimeType } of FileDetector.ALL_SIGNATURES) if (detectedExt !== ext && this.matchesSignature(buffer, signature)) return {
2560
+ mimeType,
2561
+ extension: detectedExt,
2562
+ verified: true,
2563
+ stream: newStream
2564
+ };
2565
+ return {
2566
+ mimeType: expectedMimeType,
2567
+ extension: ext,
2568
+ verified: false,
2569
+ stream: newStream
2570
+ };
2571
+ }
2572
+ /**
2573
+ * Reads all bytes from a stream and returns the first N bytes along with a new stream containing all data.
2574
+ * This approach reads the entire stream upfront to avoid complex async handling issues.
2575
+ *
2576
+ * @protected
2577
+ */
2578
+ async peekBytes(stream, numBytes) {
2579
+ const chunks = [];
2580
+ for await (const chunk of stream) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
2581
+ const allData = Buffer.concat(chunks);
2582
+ return {
2583
+ buffer: allData.subarray(0, numBytes),
2584
+ stream: Readable.from(allData)
2585
+ };
2586
+ }
2587
+ /**
2588
+ * Checks if a buffer matches a magic byte signature.
2589
+ *
2590
+ * @protected
2591
+ */
2592
+ matchesSignature(buffer, signature) {
2593
+ if (buffer.length < signature.length) return false;
2594
+ for (let i = 0; i < signature.length; i++) if (signature[i] !== null && buffer[i] !== signature[i]) return false;
2595
+ return true;
2596
+ }
2597
+ };
2598
+
2599
+ //#endregion
2600
+ //#region ../../src/system/providers/NodeFileSystemProvider.ts
2601
+ /**
2602
+ * Node.js implementation of FileSystem interface.
2603
+ *
2604
+ * @example
2605
+ * ```typescript
2606
+ * const fs = alepha.inject(NodeFileSystemProvider);
2607
+ *
2608
+ * // Create from URL
2609
+ * const file1 = fs.createFile({ url: "file:///path/to/file.png" });
2610
+ *
2611
+ * // Create from Buffer
2612
+ * const file2 = fs.createFile({ buffer: Buffer.from("hello"), name: "hello.txt" });
2613
+ *
2614
+ * // Create from text
2615
+ * const file3 = fs.createFile({ text: "Hello, world!", name: "greeting.txt" });
2616
+ *
2617
+ * // File operations
2618
+ * await fs.mkdir("/tmp/mydir", { recursive: true });
2619
+ * await fs.cp("/src/file.txt", "/dest/file.txt");
2620
+ * await fs.mv("/old/path.txt", "/new/path.txt");
2621
+ * const files = await fs.ls("/tmp");
2622
+ * await fs.rm("/tmp/file.txt");
2623
+ * ```
2624
+ */
2625
+ var NodeFileSystemProvider = class {
2626
+ detector = $inject(FileDetector);
2627
+ json = $inject(Json);
2628
+ join(...paths) {
2629
+ return join(...paths);
2630
+ }
2631
+ /**
2632
+ * Creates a FileLike object from various sources.
2633
+ *
2634
+ * @param options - Options for creating the file
2635
+ * @returns A FileLike object
2636
+ *
2637
+ * @example
2638
+ * ```typescript
2639
+ * const fs = alepha.inject(NodeFileSystemProvider);
2640
+ *
2641
+ * // From URL
2642
+ * const file1 = fs.createFile({ url: "https://example.com/image.png" });
2643
+ *
2644
+ * // From Buffer
2645
+ * const file2 = fs.createFile({
2646
+ * buffer: Buffer.from("hello"),
2647
+ * name: "hello.txt",
2648
+ * type: "text/plain"
2649
+ * });
2650
+ *
2651
+ * // From text
2652
+ * const file3 = fs.createFile({ text: "Hello!", name: "greeting.txt" });
2653
+ *
2654
+ * // From stream with detection
2655
+ * const stream = createReadStream("/path/to/file.png");
2656
+ * const file4 = fs.createFile({ stream, name: "image.png" });
2657
+ * ```
2658
+ */
2659
+ createFile(options) {
2660
+ if ("path" in options) {
2661
+ const path = options.path;
2662
+ const filename = path.split("/").pop() || "file";
2663
+ return this.createFileFromUrl(`file://${path}`, {
2664
+ type: options.type,
2665
+ name: options.name || filename
2666
+ });
2667
+ }
2668
+ if ("url" in options) return this.createFileFromUrl(options.url, {
2669
+ type: options.type,
2670
+ name: options.name
2671
+ });
2672
+ if ("response" in options) {
2673
+ if (!options.response.body) throw new AlephaError("Response has no body stream");
2674
+ const res = options.response;
2675
+ const sizeHeader = res.headers.get("content-length");
2676
+ const size = sizeHeader ? parseInt(sizeHeader, 10) : void 0;
2677
+ let name = options.name;
2678
+ const contentDisposition = res.headers.get("content-disposition");
2679
+ if (contentDisposition && !name) {
2680
+ const match = contentDisposition.match(/filename="?([^"]+)"?/);
2681
+ if (match) name = match[1];
2682
+ }
2683
+ const type = options.type || res.headers.get("content-type") || void 0;
2684
+ return this.createFileFromStream(options.response.body, {
2685
+ type,
2686
+ name,
2687
+ size
2688
+ });
2689
+ }
2690
+ if ("file" in options) return this.createFileFromWebFile(options.file, {
2691
+ type: options.type,
2692
+ name: options.name,
2693
+ size: options.size
2694
+ });
2695
+ if ("buffer" in options) return this.createFileFromBuffer(options.buffer, {
2696
+ type: options.type,
2697
+ name: options.name
2698
+ });
2699
+ if ("arrayBuffer" in options) return this.createFileFromBuffer(Buffer.from(options.arrayBuffer), {
2700
+ type: options.type,
2701
+ name: options.name
2702
+ });
2703
+ if ("text" in options) return this.createFileFromBuffer(Buffer.from(options.text, "utf-8"), {
2704
+ type: options.type || "text/plain",
2705
+ name: options.name || "file.txt"
2706
+ });
2707
+ if ("stream" in options) return this.createFileFromStream(options.stream, {
2708
+ type: options.type,
2709
+ name: options.name,
2710
+ size: options.size
2711
+ });
2712
+ throw new AlephaError("Invalid createFile options: no valid source provided");
2713
+ }
2714
+ /**
2715
+ * Removes a file or directory.
2716
+ *
2717
+ * @param path - The path to remove
2718
+ * @param options - Remove options
2719
+ *
2720
+ * @example
2721
+ * ```typescript
2722
+ * const fs = alepha.inject(NodeFileSystemProvider);
2723
+ *
2724
+ * // Remove a file
2725
+ * await fs.rm("/tmp/file.txt");
2726
+ *
2727
+ * // Remove a directory recursively
2728
+ * await fs.rm("/tmp/mydir", { recursive: true });
2729
+ *
2730
+ * // Remove with force (no error if doesn't exist)
2731
+ * await fs.rm("/tmp/maybe-exists.txt", { force: true });
2732
+ * ```
2733
+ */
2734
+ async rm(path, options) {
2735
+ await rm(path, options);
2736
+ }
2737
+ /**
2738
+ * Copies a file or directory.
2739
+ *
2740
+ * @param src - Source path
2741
+ * @param dest - Destination path
2742
+ * @param options - Copy options
2743
+ *
2744
+ * @example
2745
+ * ```typescript
2746
+ * const fs = alepha.inject(NodeFileSystemProvider);
2747
+ *
2748
+ * // Copy a file
2749
+ * await fs.cp("/src/file.txt", "/dest/file.txt");
2750
+ *
2751
+ * // Copy a directory recursively
2752
+ * await fs.cp("/src/dir", "/dest/dir", { recursive: true });
2753
+ *
2754
+ * // Copy with force (overwrite existing)
2755
+ * await fs.cp("/src/file.txt", "/dest/file.txt", { force: true });
2756
+ * ```
2757
+ */
2758
+ async cp(src, dest, options) {
2759
+ if ((await stat(src)).isDirectory()) {
2760
+ if (!options?.recursive) throw new Error(`Cannot copy directory without recursive option: ${src}`);
2761
+ await cp(src, dest, {
2762
+ recursive: true,
2763
+ force: options?.force ?? false
2764
+ });
2765
+ } else await copyFile(src, dest);
2766
+ }
2767
+ /**
2768
+ * Moves/renames a file or directory.
2769
+ *
2770
+ * @param src - Source path
2771
+ * @param dest - Destination path
2772
+ *
2773
+ * @example
2774
+ * ```typescript
2775
+ * const fs = alepha.inject(NodeFileSystemProvider);
2776
+ *
2777
+ * // Move/rename a file
2778
+ * await fs.mv("/old/path.txt", "/new/path.txt");
2779
+ *
2780
+ * // Move a directory
2781
+ * await fs.mv("/old/dir", "/new/dir");
2782
+ * ```
2783
+ */
2784
+ async mv(src, dest) {
2785
+ await rename(src, dest);
2786
+ }
2787
+ /**
2788
+ * Creates a directory.
2789
+ *
2790
+ * @param path - The directory path to create
2791
+ * @param options - Mkdir options
2792
+ *
2793
+ * @example
2794
+ * ```typescript
2795
+ * const fs = alepha.inject(NodeFileSystemProvider);
2796
+ *
2797
+ * // Create a directory
2798
+ * await fs.mkdir("/tmp/mydir");
2799
+ *
2800
+ * // Create nested directories
2801
+ * await fs.mkdir("/tmp/path/to/dir", { recursive: true });
2802
+ *
2803
+ * // Create with specific permissions
2804
+ * await fs.mkdir("/tmp/mydir", { mode: 0o755 });
2805
+ * ```
2806
+ */
2807
+ async mkdir(path, options) {
2808
+ await mkdir(path, options);
2809
+ }
2810
+ /**
2811
+ * Lists files in a directory.
2812
+ *
2813
+ * @param path - The directory path to list
2814
+ * @param options - List options
2815
+ * @returns Array of filenames
2816
+ *
2817
+ * @example
2818
+ * ```typescript
2819
+ * const fs = alepha.inject(NodeFileSystemProvider);
2820
+ *
2821
+ * // List files in a directory
2822
+ * const files = await fs.ls("/tmp");
2823
+ * console.log(files); // ["file1.txt", "file2.txt", "subdir"]
2824
+ *
2825
+ * // List with hidden files
2826
+ * const allFiles = await fs.ls("/tmp", { hidden: true });
2827
+ *
2828
+ * // List recursively
2829
+ * const allFilesRecursive = await fs.ls("/tmp", { recursive: true });
2830
+ * ```
2831
+ */
2832
+ async ls(path, options) {
2833
+ const entries = await readdir(path);
2834
+ const filteredEntries = options?.hidden ? entries : entries.filter((e) => !e.startsWith("."));
2835
+ if (options?.recursive) {
2836
+ const allFiles = [];
2837
+ for (const entry of filteredEntries) {
2838
+ const fullPath = join(path, entry);
2839
+ if ((await stat(fullPath)).isDirectory()) {
2840
+ allFiles.push(entry);
2841
+ const subFiles = await this.ls(fullPath, options);
2842
+ allFiles.push(...subFiles.map((f) => join(entry, f)));
2843
+ } else allFiles.push(entry);
2844
+ }
2845
+ return allFiles;
2846
+ }
2847
+ return filteredEntries;
2848
+ }
2849
+ /**
2850
+ * Checks if a file or directory exists.
2851
+ *
2852
+ * @param path - The path to check
2853
+ * @returns True if the path exists, false otherwise
2854
+ *
2855
+ * @example
2856
+ * ```typescript
2857
+ * const fs = alepha.inject(NodeFileSystemProvider);
2858
+ *
2859
+ * if (await fs.exists("/tmp/file.txt")) {
2860
+ * console.log("File exists");
2861
+ * }
2862
+ * ```
2863
+ */
2864
+ async exists(path) {
2865
+ try {
2866
+ await access(path);
2867
+ return true;
2868
+ } catch {
2869
+ return false;
2870
+ }
2871
+ }
2872
+ /**
2873
+ * Reads the content of a file.
2874
+ *
2875
+ * @param path - The file path to read
2876
+ * @returns The file content as a Buffer
2877
+ *
2878
+ * @example
2879
+ * ```typescript
2880
+ * const fs = alepha.inject(NodeFileSystemProvider);
2881
+ *
2882
+ * const buffer = await fs.readFile("/tmp/file.txt");
2883
+ * console.log(buffer.toString("utf-8"));
2884
+ * ```
2885
+ */
2886
+ async readFile(path) {
2887
+ return await readFile(path);
2888
+ }
2889
+ /**
2890
+ * Writes data to a file.
2891
+ *
2892
+ * @param path - The file path to write to
2893
+ * @param data - The data to write (Buffer or string)
2894
+ *
2895
+ * @example
2896
+ * ```typescript
2897
+ * const fs = alepha.inject(NodeFileSystemProvider);
2898
+ *
2899
+ * // Write string
2900
+ * await fs.writeFile("/tmp/file.txt", "Hello, world!");
2901
+ *
2902
+ * // Write Buffer
2903
+ * await fs.writeFile("/tmp/file.bin", Buffer.from([0x01, 0x02, 0x03]));
2904
+ * ```
2905
+ */
2906
+ async writeFile(path, data) {
2907
+ if (isFileLike(data)) {
2908
+ await writeFile(path, Readable.from(data.stream()));
2909
+ return;
2910
+ }
2911
+ await writeFile(path, data);
2912
+ }
2913
+ /**
2914
+ * Reads the content of a file as a string.
2915
+ *
2916
+ * @param path - The file path to read
2917
+ * @returns The file content as a string
2918
+ *
2919
+ * @example
2920
+ * ```typescript
2921
+ * const fs = alepha.inject(NodeFileSystemProvider);
2922
+ * const content = await fs.readTextFile("/tmp/file.txt");
2923
+ * ```
2924
+ */
2925
+ async readTextFile(path) {
2926
+ return (await this.readFile(path)).toString("utf-8");
2927
+ }
2928
+ /**
2929
+ * Reads the content of a file as JSON.
2930
+ *
2931
+ * @param path - The file path to read
2932
+ * @returns The parsed JSON content
2933
+ *
2934
+ * @example
2935
+ * ```typescript
2936
+ * const fs = alepha.inject(NodeFileSystemProvider);
2937
+ * const config = await fs.readJsonFile<{ name: string }>("/tmp/config.json");
2938
+ * ```
2939
+ */
2940
+ async readJsonFile(path) {
2941
+ const text = await this.readTextFile(path);
2942
+ return this.json.parse(text);
2943
+ }
2944
+ /**
2945
+ * Creates a FileLike object from a Web File.
2946
+ *
2947
+ * @protected
2948
+ */
2949
+ createFileFromWebFile(source, options = {}) {
2950
+ const name = options.name ?? source.name;
2951
+ return {
2952
+ name,
2953
+ type: options.type ?? (source.type || this.detector.getContentType(name)),
2954
+ size: options.size ?? source.size ?? 0,
2955
+ lastModified: source.lastModified || Date.now(),
2956
+ stream: () => source.stream(),
2957
+ arrayBuffer: async () => {
2958
+ return await source.arrayBuffer();
2959
+ },
2960
+ text: async () => {
2961
+ return await source.text();
2962
+ }
2963
+ };
2964
+ }
2965
+ /**
2966
+ * Creates a FileLike object from a Buffer.
2967
+ *
2968
+ * @protected
2969
+ */
2970
+ createFileFromBuffer(source, options = {}) {
2971
+ const name = options.name ?? "file";
2972
+ return {
2973
+ name,
2974
+ type: options.type ?? this.detector.getContentType(options.name ?? name),
2975
+ size: source.byteLength,
2976
+ lastModified: Date.now(),
2977
+ stream: () => Readable.from(source),
2978
+ arrayBuffer: async () => {
2979
+ return this.bufferToArrayBuffer(source);
2980
+ },
2981
+ text: async () => {
2982
+ return source.toString("utf-8");
2983
+ }
2984
+ };
2985
+ }
2986
+ /**
2987
+ * Creates a FileLike object from a stream.
2988
+ *
2989
+ * @protected
2990
+ */
2991
+ createFileFromStream(source, options = {}) {
2992
+ let buffer = null;
2993
+ return {
2994
+ name: options.name ?? "file",
2995
+ type: options.type ?? this.detector.getContentType(options.name ?? "file"),
2996
+ size: options.size ?? 0,
2997
+ lastModified: Date.now(),
2998
+ stream: () => source,
2999
+ _buffer: null,
3000
+ arrayBuffer: async () => {
3001
+ buffer ??= await this.streamToBuffer(source);
3002
+ return this.bufferToArrayBuffer(buffer);
3003
+ },
3004
+ text: async () => {
3005
+ buffer ??= await this.streamToBuffer(source);
3006
+ return buffer.toString("utf-8");
3007
+ }
3008
+ };
3009
+ }
3010
+ /**
3011
+ * Creates a FileLike object from a URL.
3012
+ *
3013
+ * @protected
3014
+ */
3015
+ createFileFromUrl(url, options = {}) {
3016
+ const parsedUrl = new URL(url);
3017
+ const filename = options.name || parsedUrl.pathname.split("/").pop() || "file";
3018
+ let buffer = null;
3019
+ return {
3020
+ name: filename,
3021
+ type: options.type ?? this.detector.getContentType(filename),
3022
+ size: 0,
3023
+ lastModified: Date.now(),
3024
+ stream: () => this.createStreamFromUrl(url),
3025
+ arrayBuffer: async () => {
3026
+ buffer ??= await this.loadFromUrl(url);
3027
+ return this.bufferToArrayBuffer(buffer);
3028
+ },
3029
+ text: async () => {
3030
+ buffer ??= await this.loadFromUrl(url);
3031
+ return buffer.toString("utf-8");
3032
+ },
3033
+ filepath: url
3034
+ };
3035
+ }
3036
+ /**
3037
+ * Gets a streaming response from a URL.
3038
+ *
3039
+ * @protected
3040
+ */
3041
+ getStreamingResponse(url) {
3042
+ const stream = new PassThrough();
3043
+ fetch(url).then((res) => Readable.fromWeb(res.body).pipe(stream)).catch((err) => stream.destroy(err));
3044
+ return stream;
3045
+ }
3046
+ /**
3047
+ * Loads data from a URL.
3048
+ *
3049
+ * @protected
3050
+ */
3051
+ async loadFromUrl(url) {
3052
+ const parsedUrl = new URL(url);
3053
+ if (parsedUrl.protocol === "file:") return await readFile(fileURLToPath(url));
3054
+ else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
3055
+ const response = await fetch(url);
3056
+ if (!response.ok) throw new Error(`Failed to fetch ${url}: ${response.status} ${response.statusText}`);
3057
+ const arrayBuffer = await response.arrayBuffer();
3058
+ return Buffer.from(arrayBuffer);
3059
+ } else throw new Error(`Unsupported protocol: ${parsedUrl.protocol}`);
3060
+ }
3061
+ /**
3062
+ * Creates a stream from a URL.
3063
+ *
3064
+ * @protected
3065
+ */
3066
+ createStreamFromUrl(url) {
3067
+ const parsedUrl = new URL(url);
3068
+ if (parsedUrl.protocol === "file:") return createReadStream(fileURLToPath(url));
3069
+ else if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") return this.getStreamingResponse(url);
3070
+ else throw new AlephaError(`Unsupported protocol: ${parsedUrl.protocol}`);
3071
+ }
3072
+ /**
3073
+ * Converts a stream-like object to a Buffer.
3074
+ *
3075
+ * @protected
3076
+ */
3077
+ async streamToBuffer(streamLike) {
3078
+ const stream = streamLike instanceof Readable ? streamLike : Readable.fromWeb(streamLike);
3079
+ return new Promise((resolve, reject) => {
3080
+ const buffer = [];
3081
+ stream.on("data", (chunk) => buffer.push(Buffer.from(chunk)));
3082
+ stream.on("end", () => resolve(Buffer.concat(buffer)));
3083
+ stream.on("error", (err) => reject(new AlephaError("Error converting stream", { cause: err })));
3084
+ });
3085
+ }
3086
+ /**
3087
+ * Converts a Node.js Buffer to an ArrayBuffer.
3088
+ *
3089
+ * @protected
3090
+ */
3091
+ bufferToArrayBuffer(buffer) {
3092
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
3093
+ }
3094
+ };
3095
+
3096
+ //#endregion
3097
+ //#region ../../src/system/providers/NodeShellProvider.ts
3098
+ /**
3099
+ * Node.js implementation of ShellProvider.
3100
+ *
3101
+ * Executes shell commands using Node.js child_process module.
3102
+ * Supports binary resolution from node_modules/.bin for local packages.
3103
+ */
3104
+ var NodeShellProvider = class {
3105
+ log = $logger();
3106
+ fs = $inject(FileSystemProvider);
3107
+ /**
3108
+ * Run a shell command or binary.
3109
+ */
3110
+ async run(command, options = {}) {
3111
+ const { resolve = false, capture = false, root, env } = options;
3112
+ const cwd = root ?? process.cwd();
3113
+ this.log.debug(`Shell: ${command}`, {
3114
+ cwd,
3115
+ resolve,
3116
+ capture
3117
+ });
3118
+ let executable;
3119
+ let args;
3120
+ if (resolve) {
3121
+ const [bin, ...rest] = command.split(" ");
3122
+ executable = await this.resolveExecutable(bin, cwd);
3123
+ args = rest;
3124
+ } else [executable, ...args] = command.split(" ");
3125
+ if (capture) return this.execCapture(command, {
3126
+ cwd,
3127
+ env
3128
+ });
3129
+ return this.execInherit(executable, args, {
3130
+ cwd,
3131
+ env
3132
+ });
3133
+ }
3134
+ /**
3135
+ * Execute command with inherited stdio (streams to terminal).
3136
+ */
3137
+ async execInherit(executable, args, options) {
3138
+ const proc = spawn(executable, args, {
3139
+ stdio: "inherit",
3140
+ cwd: options.cwd,
3141
+ env: {
3142
+ ...process.env,
3143
+ ...options.env
3144
+ }
3145
+ });
3146
+ return new Promise((resolve, reject) => {
3147
+ proc.on("exit", (code) => {
3148
+ if (code === 0 || code === null) resolve("");
3149
+ else reject(new AlephaError(`Command exited with code ${code}`));
3150
+ });
3151
+ proc.on("error", reject);
3152
+ });
3153
+ }
3154
+ /**
3155
+ * Execute command and capture stdout.
3156
+ */
3157
+ execCapture(command, options) {
3158
+ return new Promise((resolve, reject) => {
3159
+ exec(command, {
3160
+ cwd: options.cwd,
3161
+ env: {
3162
+ ...process.env,
3163
+ LOG_FORMAT: "pretty",
3164
+ ...options.env
3165
+ }
3166
+ }, (err, stdout) => {
3167
+ if (err) {
3168
+ err.stdout = stdout;
3169
+ reject(err);
3170
+ } else resolve(stdout);
3171
+ });
3172
+ });
3173
+ }
3174
+ /**
3175
+ * Resolve executable path from node_modules/.bin.
3176
+ *
3177
+ * Search order:
3178
+ * 1. Local: node_modules/.bin/
3179
+ * 2. Pnpm nested: node_modules/alepha/node_modules/.bin/
3180
+ * 3. Monorepo: Walk up to 3 parent directories
3181
+ */
3182
+ async resolveExecutable(name, root) {
3183
+ const suffix = process.platform === "win32" ? ".cmd" : "";
3184
+ let execPath = await this.findExecutable(root, `node_modules/.bin/${name}${suffix}`);
3185
+ if (!execPath) execPath = await this.findExecutable(root, `node_modules/alepha/node_modules/.bin/${name}${suffix}`);
3186
+ if (!execPath) {
3187
+ let parentDir = this.fs.join(root, "..");
3188
+ for (let i = 0; i < 3; i++) {
3189
+ execPath = await this.findExecutable(parentDir, `node_modules/.bin/${name}${suffix}`);
3190
+ if (execPath) break;
3191
+ parentDir = this.fs.join(parentDir, "..");
3192
+ }
3193
+ }
3194
+ if (!execPath) throw new AlephaError(`Could not find executable for '${name}'. Make sure the package is installed.`);
3195
+ return execPath;
3196
+ }
3197
+ /**
3198
+ * Check if executable exists at path.
3199
+ */
3200
+ async findExecutable(root, relativePath) {
3201
+ const fullPath = this.fs.join(root, relativePath);
3202
+ if (await this.fs.exists(fullPath)) return fullPath;
3203
+ }
3204
+ /**
3205
+ * Check if a command is installed and available in the system PATH.
3206
+ */
3207
+ isInstalled(command) {
3208
+ return new Promise((resolve) => {
3209
+ exec(process.platform === "win32" ? `where ${command}` : `command -v ${command}`, (error) => resolve(!error));
3210
+ });
3211
+ }
3212
+ };
3213
+
3214
+ //#endregion
3215
+ //#region ../../src/system/providers/ShellProvider.ts
3216
+ /**
3217
+ * Abstract provider for executing shell commands and binaries.
3218
+ *
3219
+ * Implementations:
3220
+ * - `NodeShellProvider` - Real shell execution using Node.js child_process
3221
+ * - `MemoryShellProvider` - In-memory mock for testing
3222
+ *
3223
+ * @example
3224
+ * ```typescript
3225
+ * class MyService {
3226
+ * protected readonly shell = $inject(ShellProvider);
3227
+ *
3228
+ * async build() {
3229
+ * // Run shell command directly
3230
+ * await this.shell.run("yarn install");
3231
+ *
3232
+ * // Run local binary with resolution
3233
+ * await this.shell.run("vite build", { resolve: true });
3234
+ *
3235
+ * // Capture output
3236
+ * const output = await this.shell.run("echo hello", { capture: true });
3237
+ * }
3238
+ * }
3239
+ * ```
3240
+ */
3241
+ var ShellProvider = class {};
3242
+
3243
+ //#endregion
3244
+ //#region ../../src/system/index.ts
3245
+ /**
3246
+ * | type | quality | stability |
3247
+ * |------|---------|-----------|
3248
+ * | tooling | standard | stable |
3249
+ *
3250
+ * System-level abstractions for portable code across runtimes.
3251
+ *
3252
+ * **Features:**
3253
+ * - File system operations (read, write, exists, etc.)
3254
+ * - Shell command execution
3255
+ * - File type detection and MIME utilities
3256
+ * - Memory implementations for testing
3257
+ *
3258
+ * @module alepha.system
3259
+ */
3260
+ const AlephaSystem = $module({
3261
+ name: "alepha.system",
3262
+ primitives: [],
3263
+ services: [
3264
+ FileDetector,
3265
+ FileSystemProvider,
3266
+ MemoryFileSystemProvider,
3267
+ NodeFileSystemProvider,
3268
+ ShellProvider,
3269
+ MemoryShellProvider,
3270
+ NodeShellProvider
3271
+ ],
3272
+ register: (alepha) => alepha.with({
3273
+ optional: true,
3274
+ provide: FileSystemProvider,
3275
+ use: NodeFileSystemProvider
3276
+ }).with({
3277
+ optional: true,
3278
+ provide: ShellProvider,
3279
+ use: alepha.isTest() ? MemoryShellProvider : NodeShellProvider
3280
+ })
3281
+ });
3282
+
3283
+ //#endregion
3284
+ //#region ../../src/react/router/providers/ReactServerTemplateProvider.ts
3285
+ /**
3286
+ * Handles HTML template parsing, preprocessing, and streaming for SSR.
3287
+ *
3288
+ * Responsibilities:
3289
+ * - Parse template once at startup into logical slots
3290
+ * - Pre-encode static parts as Uint8Array for zero-copy streaming
3291
+ * - Render dynamic parts (attributes, head content) efficiently
3292
+ * - Build hydration data for client-side rehydration
3293
+ *
3294
+ * This provider is injected into ReactServerProvider to handle all
3295
+ * template-related operations, keeping ReactServerProvider focused
3296
+ * on request handling and React rendering coordination.
3297
+ */
3298
+ var ReactServerTemplateProvider = class {
3299
+ log = $logger();
3300
+ alepha = $inject(Alepha);
3301
+ /**
3302
+ * Shared TextEncoder instance - reused across all requests.
3303
+ */
3304
+ encoder = new TextEncoder();
3305
+ /**
3306
+ * Pre-encoded common strings for streaming.
3307
+ */
3308
+ ENCODED = {
3309
+ HYDRATION_PREFIX: this.encoder.encode("<script>window.__ssr="),
3310
+ HYDRATION_SUFFIX: this.encoder.encode("<\/script>"),
3311
+ EMPTY: this.encoder.encode("")
3312
+ };
3313
+ /**
3314
+ * Cached template slots - parsed once, reused for all requests.
3315
+ */
3316
+ slots = null;
3317
+ /**
3318
+ * Root element ID for React mounting.
3319
+ */
3320
+ get rootId() {
3321
+ return "root";
3322
+ }
3323
+ /**
3324
+ * Regex pattern for matching the root div and extracting its content.
3325
+ */
3326
+ get rootDivRegex() {
3327
+ return new RegExp(`<div([^>]*)\\s+id=["']${this.rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
3328
+ }
3329
+ /**
3330
+ * Extract the content inside the root div from HTML.
3331
+ *
3332
+ * @param html - Full HTML string
3333
+ * @returns The content inside the root div, or undefined if not found
3334
+ */
3335
+ extractRootContent(html) {
3336
+ return html.match(this.rootDivRegex)?.[3];
3337
+ }
3338
+ /**
3339
+ * Check if template has been parsed and slots are available.
3340
+ */
3341
+ isReady() {
3342
+ return this.slots !== null;
3343
+ }
3344
+ /**
3345
+ * Get the parsed template slots.
3346
+ * Throws if template hasn't been parsed yet.
3347
+ */
3348
+ getSlots() {
3349
+ if (!this.slots) throw new AlephaError("Template not parsed. Call parseTemplate() during configuration.");
3350
+ return this.slots;
3351
+ }
3352
+ /**
3353
+ * Parse an HTML template into logical slots for efficient streaming.
3354
+ *
3355
+ * This should be called once during server startup/configuration.
3356
+ * The parsed slots are cached and reused for all requests.
3357
+ */
3358
+ parseTemplate(template) {
3359
+ this.log.debug("Parsing template into slots");
3360
+ const rootId = this.rootId;
3361
+ const doctypeMatch = template.match(/<!DOCTYPE[^>]*>/i);
3362
+ const doctype = doctypeMatch?.[0] ?? "<!DOCTYPE html>";
3363
+ let remaining = doctypeMatch ? template.slice(doctypeMatch.index + doctypeMatch[0].length) : template;
3364
+ const htmlMatch = remaining.match(/<html([^>]*)>/i);
3365
+ const htmlAttrsStr = htmlMatch?.[1]?.trim() ?? "";
3366
+ const htmlOriginalAttrs = this.parseAttributes(htmlAttrsStr);
3367
+ remaining = htmlMatch ? remaining.slice(htmlMatch.index + htmlMatch[0].length) : remaining;
3368
+ const headMatch = remaining.match(/<head([^>]*)>([\s\S]*?)<\/head>/i);
3369
+ const headOriginalContent = headMatch?.[2]?.trim() ?? "";
3370
+ remaining = headMatch ? remaining.slice(headMatch.index + headMatch[0].length) : remaining;
3371
+ const bodyMatch = remaining.match(/<body([^>]*)>/i);
3372
+ const bodyAttrsStr = bodyMatch?.[1]?.trim() ?? "";
3373
+ const bodyOriginalAttrs = this.parseAttributes(bodyAttrsStr);
3374
+ const bodyStartIndex = bodyMatch ? bodyMatch.index + bodyMatch[0].length : 0;
3375
+ remaining = remaining.slice(bodyStartIndex);
3376
+ const rootDivRegex = new RegExp(`<div([^>]*)\\s+id=["']${rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`, "i");
3377
+ const rootMatch = remaining.match(rootDivRegex);
3378
+ let beforeRoot = "";
3379
+ let afterRoot = "";
3380
+ let rootAttrs = "";
3381
+ if (rootMatch) {
3382
+ beforeRoot = remaining.slice(0, rootMatch.index).trim();
3383
+ const rootEndIndex = rootMatch.index + rootMatch[0].length;
3384
+ const bodyCloseIndex = remaining.indexOf("</body>");
3385
+ afterRoot = bodyCloseIndex > rootEndIndex ? remaining.slice(rootEndIndex, bodyCloseIndex).trim() : "";
3386
+ rootAttrs = `${rootMatch[1] ?? ""}${rootMatch[2] ?? ""}`.trim();
3387
+ } else {
3388
+ const bodyCloseIndex = remaining.indexOf("</body>");
3389
+ if (bodyCloseIndex > 0) beforeRoot = remaining.slice(0, bodyCloseIndex).trim();
3390
+ }
3391
+ const rootOpenTag = rootAttrs ? `<div ${rootAttrs} id="${rootId}">` : `<div id="${rootId}">`;
3392
+ this.slots = {
3393
+ doctype: this.encoder.encode(`${doctype}\n`),
3394
+ htmlOpen: this.encoder.encode("<html"),
3395
+ htmlClose: this.encoder.encode(">\n"),
3396
+ headOpen: this.encoder.encode("<head>"),
3397
+ headClose: this.encoder.encode("</head>\n"),
3398
+ bodyOpen: this.encoder.encode("<body"),
3399
+ bodyClose: this.encoder.encode(">\n"),
3400
+ rootOpen: this.encoder.encode(rootOpenTag),
3401
+ rootClose: this.encoder.encode("</div>\n"),
3402
+ scriptClose: this.encoder.encode("</body>\n</html>"),
3403
+ htmlOriginalAttrs,
3404
+ bodyOriginalAttrs,
3405
+ headOriginalContent,
3406
+ beforeRoot,
3407
+ afterRoot
3408
+ };
3409
+ this.log.debug("Template parsed successfully", {
3410
+ hasHtmlAttrs: Object.keys(htmlOriginalAttrs).length > 0,
3411
+ hasBodyAttrs: Object.keys(bodyOriginalAttrs).length > 0,
3412
+ hasHeadContent: headOriginalContent.length > 0,
3413
+ hasBeforeRoot: beforeRoot.length > 0,
3414
+ hasAfterRoot: afterRoot.length > 0
3415
+ });
3416
+ return this.slots;
3417
+ }
3418
+ /**
3419
+ * Parse HTML attributes string into a record.
3420
+ *
3421
+ * Handles: key="value", key='value', key=value, and boolean key
3422
+ */
3423
+ parseAttributes(attrStr) {
3424
+ const attrs = {};
3425
+ if (!attrStr) return attrs;
3426
+ for (const match of attrStr.matchAll(/([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g)) {
3427
+ const key = match[1];
3428
+ attrs[key] = match[2] ?? match[3] ?? match[4] ?? "";
3429
+ }
3430
+ return attrs;
3431
+ }
3432
+ /**
3433
+ * Render attributes record to HTML string.
3434
+ *
3435
+ * @param attrs - Attributes to render
3436
+ * @returns HTML attribute string like ` lang="en" class="dark"`
3437
+ */
3438
+ renderAttributes(attrs) {
3439
+ const entries = Object.entries(attrs);
3440
+ if (entries.length === 0) return "";
3441
+ return entries.map(([key, value]) => ` ${key}="${this.escapeHtml(value)}"`).join("");
3442
+ }
3443
+ /**
3444
+ * Render merged HTML attributes (original + dynamic).
3445
+ */
3446
+ renderMergedHtmlAttrs(dynamicAttrs) {
3447
+ const merged = {
3448
+ ...this.getSlots().htmlOriginalAttrs,
3449
+ ...dynamicAttrs
3450
+ };
3451
+ return this.renderAttributes(merged);
3452
+ }
3453
+ /**
3454
+ * Render merged body attributes (original + dynamic).
3455
+ */
3456
+ renderMergedBodyAttrs(dynamicAttrs) {
3457
+ const merged = {
3458
+ ...this.getSlots().bodyOriginalAttrs,
3459
+ ...dynamicAttrs
3460
+ };
3461
+ return this.renderAttributes(merged);
3462
+ }
3463
+ /**
3464
+ * Render head content (title, meta, link, script tags).
3465
+ *
3466
+ * @param head - Head data to render
3467
+ * @param includeOriginal - Whether to include original head content
3468
+ * @returns HTML string with head content
3469
+ */
3470
+ renderHeadContent(head, includeOriginal = true) {
3471
+ const slots = this.getSlots();
3472
+ let content = "";
3473
+ if (includeOriginal && slots.headOriginalContent) content += slots.headOriginalContent;
3474
+ if (!head) return content;
3475
+ if (head.title) if (content.includes("<title>")) content = content.replace(/<title>.*?<\/title>/i, `<title>${this.escapeHtml(head.title)}</title>`);
3476
+ else content += `<title>${this.escapeHtml(head.title)}</title>\n`;
3477
+ if (head.meta) for (const meta of head.meta) content += this.renderMetaTag(meta);
3478
+ if (head.link) for (const link of head.link) content += this.renderLinkTag(link);
3479
+ if (head.script) for (const script of head.script) content += this.renderScriptTag(script);
3480
+ return content;
3481
+ }
3482
+ /**
3483
+ * Render a meta tag.
3484
+ */
3485
+ renderMetaTag(meta) {
3486
+ if (meta.property) return `<meta property="${this.escapeHtml(meta.property)}" content="${this.escapeHtml(meta.content)}">\n`;
3487
+ if (meta.name) return `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
3488
+ return "";
3489
+ }
3490
+ /**
3491
+ * Render a link tag.
3492
+ */
3493
+ renderLinkTag(link) {
3494
+ let tag = `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}"`;
3495
+ if (link.type) tag += ` type="${this.escapeHtml(link.type)}"`;
3496
+ if (link.as) tag += ` as="${this.escapeHtml(link.as)}"`;
3497
+ if (link.crossorigin != null) tag += " crossorigin=\"\"";
3498
+ tag += ">\n";
3499
+ return tag;
3500
+ }
3501
+ /**
3502
+ * Render a script tag.
3503
+ */
3504
+ renderScriptTag(script) {
3505
+ if (typeof script === "string") return `<script>${script}<\/script>\n`;
3506
+ const { content, ...rest } = script;
3507
+ const attrs = Object.entries(rest).filter(([, value]) => value !== false && value !== void 0).map(([key, value]) => {
3508
+ if (value === true) return key;
3509
+ return `${key}="${this.escapeHtml(String(value))}"`;
3510
+ }).join(" ");
3511
+ if (content) return attrs ? `<script ${attrs}>${content}<\/script>\n` : `<script>${content}<\/script>\n`;
3512
+ return `<script ${attrs}><\/script>\n`;
3513
+ }
3514
+ /**
3515
+ * Escape HTML special characters.
3516
+ */
3517
+ escapeHtml(str) {
3518
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
3519
+ }
3520
+ /**
3521
+ * Safely serialize data to JSON for embedding in HTML.
3522
+ * Escapes characters that could break out of script tags.
3523
+ */
3524
+ safeJsonSerialize(data) {
3525
+ return JSON.stringify(data).replace(/</g, "\\u003c").replace(/>/g, "\\u003e").replace(/&/g, "\\u0026");
3526
+ }
3527
+ /**
3528
+ * Build hydration data from router state.
3529
+ *
3530
+ * This creates the data structure that will be serialized to window.__ssr
3531
+ * for client-side rehydration.
3532
+ */
3533
+ buildHydrationData(state) {
3534
+ const { request, context, ...store } = this.alepha.context.als?.getStore() ?? {};
3535
+ const hydrationData = { layers: state.layers.map((layer) => ({
3536
+ part: layer.part,
3537
+ name: layer.name,
3538
+ config: layer.config,
3539
+ props: layer.props,
3540
+ error: layer.error ? {
3541
+ ...layer.error,
3542
+ name: layer.error.name,
3543
+ message: layer.error.message,
3544
+ stack: !this.alepha.isProduction() ? layer.error.stack : void 0
3545
+ } : void 0
3546
+ })) };
3547
+ for (const [key, value] of Object.entries(store)) if (key.charAt(0) !== "_" && key !== "alepha.react.router.state" && key !== "registry") hydrationData[key] = value;
3548
+ return hydrationData;
3549
+ }
3550
+ /**
3551
+ * Stream the body content: body tag, root div, React content, hydration, and closing tags.
3552
+ *
3553
+ * If an error occurs during React streaming, it injects error HTML instead of aborting,
3554
+ * ensuring users see an error message rather than a white screen.
3555
+ */
3556
+ async streamBodyContent(controller, reactStream, state, hydration) {
3557
+ const slots = this.getSlots();
3558
+ const encoder = this.encoder;
3559
+ const head = state.head;
3560
+ controller.enqueue(slots.bodyOpen);
3561
+ controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(head?.bodyAttributes)));
3562
+ controller.enqueue(slots.bodyClose);
3563
+ if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
3564
+ controller.enqueue(slots.rootOpen);
3565
+ const reader = reactStream.getReader();
3566
+ let streamError = null;
3567
+ try {
3568
+ while (true) {
3569
+ const { done, value } = await reader.read();
3570
+ if (done) break;
3571
+ controller.enqueue(value);
3572
+ }
3573
+ } catch (error) {
3574
+ streamError = error;
3575
+ this.log.error("Error during React stream reading", error);
3576
+ } finally {
3577
+ reader.releaseLock();
3578
+ }
3579
+ if (streamError) {
3580
+ this.injectErrorHtml(controller, encoder, slots, streamError, state, {
3581
+ headClosed: true,
3582
+ bodyStarted: true
3583
+ });
3584
+ return;
3585
+ }
3586
+ controller.enqueue(slots.rootClose);
3587
+ if (slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
3588
+ if (hydration) {
3589
+ const hydrationData = this.buildHydrationData(state);
3590
+ controller.enqueue(this.ENCODED.HYDRATION_PREFIX);
3591
+ controller.enqueue(encoder.encode(this.safeJsonSerialize(hydrationData)));
3592
+ controller.enqueue(this.ENCODED.HYDRATION_SUFFIX);
3593
+ }
3594
+ controller.enqueue(slots.scriptClose);
3595
+ }
3596
+ /**
3597
+ * Create a ReadableStream that streams the HTML template with React content.
3598
+ *
3599
+ * This is the main entry point for SSR streaming. It:
3600
+ * 1. Sends <head> immediately (browser starts downloading assets)
3601
+ * 2. Streams React content as it renders
3602
+ * 3. Appends hydration script and closing tags
3603
+ *
3604
+ * @param reactStream - ReadableStream from renderToReadableStream
3605
+ * @param state - Router state with head data
3606
+ * @param options - Streaming options
3607
+ */
3608
+ createHtmlStream(reactStream, state, options = {}) {
3609
+ const { hydration = true, onError } = options;
3610
+ const slots = this.getSlots();
3611
+ const head = state.head;
3612
+ const encoder = this.encoder;
3613
+ return new ReadableStream({ start: async (controller) => {
3614
+ try {
3615
+ controller.enqueue(slots.doctype);
3616
+ controller.enqueue(slots.htmlOpen);
3617
+ controller.enqueue(encoder.encode(this.renderMergedHtmlAttrs(head?.htmlAttributes)));
3618
+ controller.enqueue(slots.htmlClose);
3619
+ controller.enqueue(slots.headOpen);
3620
+ if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3621
+ controller.enqueue(encoder.encode(this.renderHeadContent(head)));
3622
+ controller.enqueue(slots.headClose);
3623
+ await this.streamBodyContent(controller, reactStream, state, hydration);
3624
+ controller.close();
3625
+ } catch (error) {
3626
+ onError?.(error);
3627
+ controller.error(error);
3628
+ }
3629
+ } });
3630
+ }
3631
+ /**
3632
+ * Early head content for preloading.
3633
+ *
3634
+ * Contains entry assets (JS + CSS) that are always required and can be
3635
+ * sent before page loaders run.
3636
+ */
3637
+ earlyHeadContent = "";
3638
+ /**
3639
+ * Set the early head content (entry script + CSS).
3640
+ *
3641
+ * Also strips these assets from the original head content to avoid duplicates,
3642
+ * since we're moving them to the early phase.
3643
+ *
3644
+ * Automatically prepends critical meta tags (charset, viewport) if not present
3645
+ * in $head configuration, ensuring they're sent as early as possible.
3646
+ *
3647
+ * @param content - HTML string with entry assets
3648
+ * @param globalHead - Global head configuration from $head primitives
3649
+ * @param entryAssets - Entry asset paths to strip from original head
3650
+ */
3651
+ setEarlyHeadContent(content, globalHead, entryAssets) {
3652
+ const criticalMeta = [];
3653
+ const charset = globalHead?.charset ?? "UTF-8";
3654
+ criticalMeta.push(`<meta charset="${this.escapeHtml(charset)}">`);
3655
+ const viewport = globalHead?.viewport ?? "width=device-width, initial-scale=1";
3656
+ criticalMeta.push(`<meta name="viewport" content="${this.escapeHtml(viewport)}">`);
3657
+ this.earlyHeadContent = criticalMeta.length > 0 ? `${criticalMeta.join("\n")}\n${content}` : content;
3658
+ if (entryAssets && this.slots) {
3659
+ let headContent = this.slots.headOriginalContent;
3660
+ if (entryAssets.js) {
3661
+ const scriptPattern = new RegExp(`<script[^>]*\\ssrc=["']${this.escapeRegExp(entryAssets.js)}["'][^>]*>\\s*<\/script>\\s*`, "gi");
3662
+ headContent = headContent.replace(scriptPattern, "");
3663
+ }
3664
+ for (const css of entryAssets.css) {
3665
+ const linkPattern = new RegExp(`<link[^>]*\\shref=["']${this.escapeRegExp(css)}["'][^>]*>\\s*`, "gi");
3666
+ headContent = headContent.replace(linkPattern, "");
3667
+ }
3668
+ this.slots.headOriginalContent = headContent.trim();
3669
+ }
3670
+ }
3671
+ /**
3672
+ * Escape special regex characters in a string.
3673
+ */
3674
+ escapeRegExp(str) {
3675
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3676
+ }
3677
+ /**
3678
+ * Create an optimized HTML stream with early head streaming.
3679
+ *
3680
+ * This version sends critical assets (entry.js, CSS) BEFORE page loaders run,
3681
+ * allowing the browser to start downloading them immediately.
3682
+ *
3683
+ * Flow:
3684
+ * 1. Send DOCTYPE, <html>, <head> open, entry preloads (IMMEDIATE)
3685
+ * 2. Run async work (createLayers, etc.)
3686
+ * 3. Send rest of head, body, React content, hydration
3687
+ *
3688
+ * @param globalHead - Global head with htmlAttributes (from $head primitives)
3689
+ * @param asyncWork - Async function to run between early head and rest of stream
3690
+ * @param options - Streaming options
3691
+ */
3692
+ createEarlyHtmlStream(globalHead, asyncWork, options = {}) {
3693
+ const { hydration = true, onError } = options;
3694
+ const slots = this.getSlots();
3695
+ const encoder = this.encoder;
3696
+ let headClosed = false;
3697
+ let bodyStarted = false;
3698
+ let routerState;
3699
+ return new ReadableStream({ start: async (controller) => {
3700
+ try {
3701
+ controller.enqueue(slots.doctype);
3702
+ controller.enqueue(slots.htmlOpen);
3703
+ controller.enqueue(encoder.encode(this.renderMergedHtmlAttrs(globalHead?.htmlAttributes)));
3704
+ controller.enqueue(slots.htmlClose);
3705
+ controller.enqueue(slots.headOpen);
3706
+ if (this.earlyHeadContent) controller.enqueue(encoder.encode(this.earlyHeadContent));
3707
+ const result = await asyncWork();
3708
+ if (!result || "redirect" in result) {
3709
+ if (result && "redirect" in result) {
3710
+ this.log.debug("Loader redirect detected after streaming started, using meta refresh", { redirect: result.redirect });
3711
+ controller.enqueue(encoder.encode(`<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`));
3712
+ }
3713
+ controller.enqueue(slots.headClose);
3714
+ controller.enqueue(encoder.encode("<body></body></html>"));
3715
+ controller.close();
3716
+ return;
3717
+ }
3718
+ const { state, reactStream } = result;
3719
+ routerState = state;
3720
+ controller.enqueue(encoder.encode(this.renderHeadContent(state.head)));
3721
+ controller.enqueue(slots.headClose);
3722
+ headClosed = true;
3723
+ bodyStarted = true;
3724
+ await this.streamBodyContent(controller, reactStream, state, hydration);
3725
+ controller.close();
3726
+ } catch (error) {
3727
+ onError?.(error);
3728
+ try {
3729
+ this.injectErrorHtml(controller, encoder, slots, error, routerState, {
3730
+ headClosed,
3731
+ bodyStarted
3732
+ });
3733
+ controller.close();
3734
+ } catch {
3735
+ controller.error(error);
3736
+ }
3737
+ }
3738
+ } });
3739
+ }
3740
+ /**
3741
+ * Inject error HTML into the stream when an error occurs during streaming.
3742
+ *
3743
+ * Uses the router state's onError handler to render the error component,
3744
+ * falling back to ErrorViewer if no custom handler is defined.
3745
+ * Renders using renderToString to produce static HTML.
3746
+ *
3747
+ * Since we may have already sent partial HTML (DOCTYPE, <html>, <head>),
3748
+ * we need to complete the document with an error message instead of aborting.
3749
+ *
3750
+ * Handles different states:
3751
+ * - headClosed=false, bodyStarted=false: Need to add head content, close head, open body, add error, close all
3752
+ * - headClosed=true, bodyStarted=false: Need to open body, add error, close all
3753
+ * - headClosed=true, bodyStarted=true: Already inside root div, add error, close all
3754
+ */
3755
+ injectErrorHtml(controller, encoder, slots, error, routerState, streamState) {
3756
+ if (!streamState.headClosed) {
3757
+ const headContent = this.renderHeadContent(routerState?.head);
3758
+ if (headContent) controller.enqueue(encoder.encode(headContent));
3759
+ controller.enqueue(slots.headClose);
3760
+ }
3761
+ if (!streamState.bodyStarted) {
3762
+ controller.enqueue(slots.bodyOpen);
3763
+ controller.enqueue(encoder.encode(this.renderMergedBodyAttrs(routerState?.head?.bodyAttributes)));
3764
+ controller.enqueue(slots.bodyClose);
3765
+ if (slots.beforeRoot) controller.enqueue(encoder.encode(slots.beforeRoot));
3766
+ controller.enqueue(slots.rootOpen);
3767
+ }
3768
+ const errorHtml = this.renderErrorToString(error instanceof Error ? error : new Error(String(error)), routerState);
3769
+ controller.enqueue(encoder.encode(errorHtml));
3770
+ controller.enqueue(slots.rootClose);
3771
+ if (!streamState.bodyStarted && slots.afterRoot) controller.enqueue(encoder.encode(slots.afterRoot));
3772
+ controller.enqueue(slots.scriptClose);
3773
+ }
3774
+ /**
3775
+ * Render an error to HTML string using the router's error handler.
3776
+ *
3777
+ * Falls back to ErrorViewer if:
3778
+ * - No router state is available
3779
+ * - The error handler returns null/undefined
3780
+ * - The error handler itself throws
3781
+ */
3782
+ renderErrorToString(error, routerState) {
3783
+ this.log.error("SSR rendering error", error);
3784
+ let errorElement;
3785
+ if (routerState?.onError) try {
3786
+ const result = routerState.onError(error, routerState);
3787
+ if (result instanceof Redirection) this.log.warn("Error handler returned Redirection but headers already sent", { redirect: result.redirect });
3788
+ else if (result !== null && result !== void 0) errorElement = result;
3789
+ } catch (handlerError) {
3790
+ this.log.error("Error handler threw an exception", handlerError);
3791
+ }
3792
+ if (!errorElement) errorElement = createElement(ErrorViewer_default, {
3793
+ error,
3794
+ alepha: this.alepha
3795
+ });
3796
+ const wrappedElement = createElement(AlephaContext.Provider, { value: this.alepha }, errorElement);
3797
+ try {
3798
+ return renderToString(wrappedElement);
3799
+ } catch (renderError) {
3800
+ this.log.error("Failed to render error component", renderError);
3801
+ return error.message;
3802
+ }
3803
+ }
3804
+ };
3805
+
3806
+ //#endregion
3807
+ //#region ../../src/react/router/atoms/ssrManifestAtom.ts
3808
+ /**
3809
+ * Schema for the SSR manifest atom.
3810
+ */
3811
+ const ssrManifestAtomSchema = t.object({
3812
+ base: t.optional(t.string()),
3813
+ preload: t.optional(t.record(t.string(), t.string())),
3814
+ client: t.optional(t.record(t.string(), t.object({
3815
+ file: t.string(),
3816
+ isEntry: t.optional(t.boolean()),
3817
+ imports: t.optional(t.array(t.string())),
3818
+ css: t.optional(t.array(t.string()))
3819
+ })))
3820
+ });
3821
+ /**
3822
+ * SSR Manifest atom containing all manifest data for SSR module preloading.
3823
+ *
3824
+ * This atom is populated at build time by embedding manifest data into the
3825
+ * generated index.js. This approach is optimal for serverless deployments
3826
+ * as it eliminates filesystem reads at runtime.
3827
+ *
3828
+ * The manifest includes:
3829
+ * - preload: Maps short hash keys to source paths (from viteAlephaSsrPreload)
3830
+ * - client: Maps source files to their output info (file, imports, css)
3831
+ */
3832
+ const ssrManifestAtom = $atom({
3833
+ name: "alepha.react.ssr.manifest",
3834
+ description: "SSR manifest for module preloading",
3835
+ schema: ssrManifestAtomSchema,
3836
+ default: {}
3837
+ });
3838
+
3839
+ //#endregion
3840
+ //#region ../../src/react/router/providers/SSRManifestProvider.ts
3841
+ /**
3842
+ * Provider for SSR manifest data used for module preloading.
3843
+ *
3844
+ * The manifest is populated at build time by embedding data into the
3845
+ * generated index.js via the ssrManifestAtom. This eliminates filesystem
3846
+ * reads at runtime, making it optimal for serverless deployments.
3847
+ *
3848
+ * Manifest files are generated during `vite build`:
3849
+ * - manifest.json (client manifest)
3850
+ * - preload-manifest.json (from viteAlephaSsrPreload plugin)
3851
+ */
3852
+ var SSRManifestProvider = class {
3853
+ alepha = $inject(Alepha);
3854
+ /**
3855
+ * Get the manifest from the store at runtime.
3856
+ * This ensures the manifest is available even when set after module load.
3857
+ */
3858
+ get manifest() {
3859
+ return this.alepha.store.get(ssrManifestAtom) ?? {};
3860
+ }
3861
+ /**
3862
+ * Get the base path for assets (from Vite's base config).
3863
+ * Returns empty string if base is "/" (default), otherwise returns the base path.
3864
+ */
3865
+ get base() {
3866
+ return this.manifest.base ?? "";
3867
+ }
3868
+ /**
3869
+ * Get the preload manifest.
3870
+ */
3871
+ get preloadManifest() {
3872
+ return this.manifest.preload;
3873
+ }
3874
+ /**
3875
+ * Get the client manifest.
3876
+ */
3877
+ get clientManifest() {
3878
+ return this.manifest.client;
3879
+ }
3880
+ /**
3881
+ * Resolve a preload key to its source path.
3882
+ *
3883
+ * The key is a short hash injected by viteAlephaSsrPreload plugin,
3884
+ * which maps to the full source path in the preload manifest.
3885
+ *
3886
+ * @param key - Short hash key (e.g., "a1b2c3d4")
3887
+ * @returns Source path (e.g., "src/pages/UserDetail.tsx") or undefined
3888
+ */
3889
+ resolvePreloadKey(key) {
3890
+ return this.preloadManifest?.[key];
3891
+ }
3892
+ /**
3893
+ * Get all chunks required for a source file, including transitive dependencies.
3894
+ *
3895
+ * Uses the client manifest to recursively resolve all imported chunks.
3896
+ *
3897
+ * @param sourcePath - Source file path (e.g., "src/pages/Home.tsx")
3898
+ * @returns Array of chunk URLs to preload, or empty array if not found
3899
+ */
3900
+ getChunks(sourcePath) {
3901
+ if (!this.clientManifest) return [];
3902
+ if (!this.findManifestEntry(sourcePath)) return [];
3903
+ const chunks = /* @__PURE__ */ new Set();
3904
+ const visited = /* @__PURE__ */ new Set();
3905
+ this.collectChunksRecursive(sourcePath, chunks, visited);
3906
+ return Array.from(chunks);
3907
+ }
3908
+ /**
3909
+ * Find manifest entry for a source path, trying different extensions.
3910
+ */
3911
+ findManifestEntry(sourcePath) {
3912
+ if (!this.clientManifest) return void 0;
3913
+ if (this.clientManifest[sourcePath]) return this.clientManifest[sourcePath];
3914
+ const basePath = sourcePath.replace(/\.[^.]+$/, "");
3915
+ for (const ext of [
3916
+ ".tsx",
3917
+ ".ts",
3918
+ ".jsx",
3919
+ ".js"
3920
+ ]) {
3921
+ const pathWithExt = basePath + ext;
3922
+ if (this.clientManifest[pathWithExt]) return this.clientManifest[pathWithExt];
3923
+ }
3924
+ }
3925
+ /**
3926
+ * Recursively collect all chunk URLs for a manifest entry.
3927
+ */
3928
+ collectChunksRecursive(key, chunks, visited) {
3929
+ if (visited.has(key)) return;
3930
+ visited.add(key);
3931
+ if (!this.clientManifest) return;
3932
+ const entry = this.clientManifest[key];
3933
+ if (!entry) return;
3934
+ const base = this.base;
3935
+ if (entry.file) chunks.add(`${base}/${entry.file}`);
3936
+ if (entry.css) for (const css of entry.css) chunks.add(`${base}/${css}`);
3937
+ if (entry.imports) for (const imp of entry.imports) {
3938
+ if (imp === "index.html" || imp.endsWith(".html")) continue;
3939
+ this.collectChunksRecursive(imp, chunks, visited);
3940
+ }
3941
+ }
3942
+ /**
3943
+ * Collect modulepreload links for a route and its parent chain.
3944
+ */
3945
+ collectPreloadLinks(route) {
3946
+ if (!this.isAvailable()) return [];
3947
+ const preloadPaths = [];
3948
+ let current = route;
3949
+ while (current) {
3950
+ const preloadKey = current[PAGE_PRELOAD_KEY];
3951
+ if (preloadKey) {
3952
+ const sourcePath = this.resolvePreloadKey(preloadKey);
3953
+ if (sourcePath) preloadPaths.push(sourcePath);
3954
+ }
3955
+ current = current.parent;
3956
+ }
3957
+ if (preloadPaths.length === 0) return [];
3958
+ return this.getChunksForMultiple(preloadPaths).map((href) => {
3959
+ if (href.endsWith(".css")) return {
3960
+ rel: "preload",
3961
+ href,
3962
+ as: "style",
3963
+ crossorigin: ""
3964
+ };
3965
+ return {
3966
+ rel: "modulepreload",
3967
+ href
3968
+ };
3969
+ });
3970
+ }
3971
+ /**
3972
+ * Get all chunks for multiple source files.
3973
+ *
3974
+ * @param sourcePaths - Array of source file paths
3975
+ * @returns Deduplicated array of chunk URLs
3976
+ */
3977
+ getChunksForMultiple(sourcePaths) {
3978
+ const allChunks = /* @__PURE__ */ new Set();
3979
+ for (const path of sourcePaths) {
3980
+ const chunks = this.getChunks(path);
3981
+ for (const chunk of chunks) allChunks.add(chunk);
3982
+ }
3983
+ return Array.from(allChunks);
3984
+ }
3985
+ /**
3986
+ * Check if manifest is loaded and available.
3987
+ */
3988
+ isAvailable() {
3989
+ return this.clientManifest !== void 0;
3990
+ }
3991
+ /**
3992
+ * Cached entry assets - computed once at first access.
3993
+ */
3994
+ cachedEntryAssets = null;
3995
+ /**
3996
+ * Get the entry point assets (main entry.js and associated CSS files).
3997
+ *
3998
+ * These assets are always required for all pages and can be preloaded
3999
+ * before page-specific loaders run.
4000
+ *
4001
+ * @returns Entry assets with js and css paths, or null if manifest unavailable
4002
+ */
4003
+ getEntryAssets() {
4004
+ if (this.cachedEntryAssets) return this.cachedEntryAssets;
4005
+ if (!this.clientManifest) return null;
4006
+ const base = this.base;
4007
+ for (const [key, entry] of Object.entries(this.clientManifest)) if (entry.isEntry) {
4008
+ this.cachedEntryAssets = {
4009
+ js: `${base}/${entry.file}`,
4010
+ css: entry.css?.map((css) => `${base}/${css}`) ?? []
4011
+ };
4012
+ return this.cachedEntryAssets;
4013
+ }
4014
+ return null;
4015
+ }
4016
+ /**
4017
+ * Build preload link tags for entry assets.
4018
+ *
4019
+ * @returns Array of link objects ready to be rendered
4020
+ */
4021
+ getEntryPreloadLinks() {
4022
+ const assets = this.getEntryAssets();
4023
+ if (!assets) return [];
4024
+ const links = [];
4025
+ for (const css of assets.css) links.push({
4026
+ rel: "stylesheet",
4027
+ href: css,
4028
+ crossorigin: ""
4029
+ });
4030
+ if (assets.js) links.push({
4031
+ rel: "modulepreload",
4032
+ href: assets.js
4033
+ });
4034
+ return links;
4035
+ }
4036
+ };
4037
+
4038
+ //#endregion
4039
+ //#region ../../src/react/router/providers/ReactServerProvider.ts
4040
+ /**
4041
+ * React server provider responsible for SSR and static file serving.
4042
+ *
4043
+ * Coordinates between:
4044
+ * - ReactPageProvider: Page routing and layer resolution
4045
+ * - ReactServerTemplateProvider: HTML template parsing and streaming
4046
+ * - ServerHeadProvider: Head content management
4047
+ * - SSRManifestProvider: Module preload link collection
4048
+ *
4049
+ * Uses `react-dom/server` under the hood.
4050
+ */
4051
+ var ReactServerProvider = class {
4052
+ /**
4053
+ * SSR response headers - pre-allocated to avoid object creation per request.
4054
+ */
4055
+ SSR_HEADERS = {
4056
+ "content-type": "text/html",
4057
+ "cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
4058
+ pragma: "no-cache",
4059
+ expires: "0"
4060
+ };
4061
+ fs = $inject(FileSystemProvider);
4062
+ log = $logger();
4063
+ alepha = $inject(Alepha);
4064
+ env = $env(envSchema);
4065
+ pageApi = $inject(ReactPageProvider);
4066
+ templateProvider = $inject(ReactServerTemplateProvider);
4067
+ serverHeadProvider = $inject(ServerHeadProvider);
4068
+ serverStaticProvider = $inject(ServerStaticProvider);
4069
+ serverRouterProvider = $inject(ServerRouterProvider);
4070
+ ssrManifestProvider = $inject(SSRManifestProvider);
4071
+ /**
4072
+ * Cached check for ServerLinksProvider - avoids has() lookup per request.
4073
+ */
4074
+ hasServerLinksProvider = false;
4075
+ options = $use(reactServerOptions);
4076
+ /**
4077
+ * Configure the React server provider.
4078
+ */
4079
+ onConfigure = $hook({
4080
+ on: "configure",
4081
+ handler: async () => {
4082
+ const ssrEnabled = this.alepha.primitives($page).length > 0 && this.env.REACT_SSR_ENABLED !== false;
4083
+ this.alepha.store.set("alepha.react.server.ssr", ssrEnabled);
4084
+ let root = "";
4085
+ if (!this.alepha.isServerless() && !this.alepha.isViteDev()) {
4086
+ root = await this.getPublicDirectory();
4087
+ if (!root) this.log.warn("Missing static files, static file server will be disabled");
4088
+ else {
4089
+ this.log.debug(`Using static files from: ${root}`);
4090
+ await this.configureStaticServer(root);
4091
+ }
4092
+ }
4093
+ if (ssrEnabled) {
4094
+ await this.registerPages(async () => this.template);
4095
+ this.log.info("SSR OK");
4096
+ return;
4097
+ }
4098
+ this.log.info("SSR is disabled, use History API fallback");
4099
+ this.serverRouterProvider.createRoute({
4100
+ path: "*",
4101
+ handler: async ({ url, reply }) => {
4102
+ if (url.pathname.includes(".")) {
4103
+ reply.headers["content-type"] = "text/plain";
4104
+ reply.body = "Not Found";
4105
+ reply.status = 404;
4106
+ return;
4107
+ }
4108
+ reply.headers["content-type"] = "text/html";
4109
+ return this.template;
4110
+ }
4111
+ });
4112
+ }
4113
+ });
4114
+ /**
4115
+ * Get the current HTML template.
4116
+ */
4117
+ get template() {
4118
+ return this.alepha.store.get("alepha.react.server.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body><div id='root'></div></body></html>";
4119
+ }
4120
+ /**
4121
+ * Register all pages as server routes.
4122
+ */
4123
+ async registerPages(templateLoader) {
4124
+ const template = await templateLoader();
4125
+ if (template) this.templateProvider.parseTemplate(template);
4126
+ this.setupEarlyHeadContent();
4127
+ this.hasServerLinksProvider = this.alepha.has(ServerLinksProvider);
4128
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) {
4129
+ this.log.debug(`+ ${page.match} -> ${page.name}`);
4130
+ this.serverRouterProvider.createRoute({
4131
+ ...page,
4132
+ schema: void 0,
4133
+ method: "GET",
4134
+ path: page.match,
4135
+ handler: this.createHandler(page, templateLoader)
4136
+ });
4137
+ }
4138
+ }
4139
+ /**
4140
+ * Set up early head content with entry assets.
4141
+ *
4142
+ * This content is sent immediately when streaming starts, before page loaders run,
4143
+ * allowing the browser to start downloading entry.js and CSS files early.
4144
+ *
4145
+ * Uses <script type="module"> instead of <link rel="modulepreload"> for JS
4146
+ * because the script needs to execute anyway - this way the browser starts
4147
+ * downloading, parsing, AND will execute as soon as ready.
4148
+ *
4149
+ * Also injects critical meta tags (charset, viewport) if not specified in $head,
4150
+ * and strips these assets from the original template head to avoid duplicates.
4151
+ */
4152
+ setupEarlyHeadContent() {
4153
+ const assets = this.ssrManifestProvider.getEntryAssets();
4154
+ const globalHead = this.serverHeadProvider.resolveGlobalHead();
4155
+ const parts = [];
4156
+ if (assets) {
4157
+ for (const css of assets.css) parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
4158
+ if (assets.js) parts.push(`<script type="module" crossorigin="" src="${assets.js}"><\/script>`);
4159
+ }
4160
+ this.templateProvider.setEarlyHeadContent(parts.length > 0 ? `${parts.join("\n")}\n` : "", globalHead, assets ?? void 0);
4161
+ this.log.debug("Early head content set", {
4162
+ css: assets?.css.length ?? 0,
4163
+ js: assets?.js ? 1 : 0
4164
+ });
4165
+ }
4166
+ /**
4167
+ * Get the public directory path where static files are located.
4168
+ */
4169
+ async getPublicDirectory() {
4170
+ const maybe = [join(process.cwd(), `dist/${this.options.publicDir}`), join(process.cwd(), this.options.publicDir)];
4171
+ for (const it of maybe) if (await this.fs.exists(it)) return it;
4172
+ return "";
4173
+ }
4174
+ /**
4175
+ * Configure the static file server to serve files from the given root directory.
4176
+ */
4177
+ async configureStaticServer(root) {
4178
+ await this.serverStaticProvider.createStaticServer({
4179
+ root,
4180
+ cacheControl: {
4181
+ maxAge: 3600,
4182
+ immutable: true
4183
+ },
4184
+ ...this.options.staticServer
4185
+ });
4186
+ }
4187
+ /**
4188
+ * Create the request handler for a page route.
4189
+ */
4190
+ createHandler(route, templateLoader) {
4191
+ return async (serverRequest) => {
4192
+ const { url, reply, query, params } = serverRequest;
4193
+ if (!this.templateProvider.isReady()) {
4194
+ const template = await templateLoader();
4195
+ if (!template) throw new AlephaError("Missing template for SSR rendering");
4196
+ this.templateProvider.parseTemplate(template);
4197
+ this.setupEarlyHeadContent();
4198
+ }
4199
+ this.log.trace("Rendering page", { name: route.name });
4200
+ const state = {
4201
+ url,
4202
+ params,
4203
+ query,
4204
+ name: route.name,
4205
+ onError: () => null,
4206
+ layers: [],
4207
+ meta: {},
4208
+ head: {}
4209
+ };
4210
+ if (this.hasServerLinksProvider) this.alepha.store.set("alepha.server.request.apiLinks", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
4211
+ user: serverRequest.user,
4212
+ authorization: serverRequest.headers.authorization
4213
+ }));
4214
+ let target = route;
4215
+ while (target) {
4216
+ if (route.can && !route.can()) {
4217
+ this.log.warn(`Access to page '${route.name}' is forbidden by can() check`);
4218
+ reply.status = 403;
4219
+ reply.headers["content-type"] = "text/plain";
4220
+ return "Forbidden";
4221
+ }
4222
+ target = target.parent;
4223
+ }
4224
+ await this.alepha.events.emit("react:server:render:begin", {
4225
+ request: serverRequest,
4226
+ state
4227
+ });
4228
+ Object.assign(reply.headers, this.SSR_HEADERS);
4229
+ const globalHead = this.serverHeadProvider.resolveGlobalHead();
4230
+ const htmlStream = this.templateProvider.createEarlyHtmlStream(globalHead, async () => {
4231
+ const result = await this.renderPage(route, state);
4232
+ if (result.redirect) return { redirect: result.redirect };
4233
+ return {
4234
+ state,
4235
+ reactStream: result.reactStream
4236
+ };
4237
+ }, {
4238
+ hydration: true,
4239
+ onError: (error) => {
4240
+ if (error instanceof Redirection) this.log.debug("Streaming resulted in redirection", { redirect: error.redirect });
4241
+ }
4242
+ });
4243
+ this.log.trace("Page streaming started (early head optimization)");
4244
+ route.onServerResponse?.(serverRequest);
4245
+ reply.body = htmlStream;
4246
+ };
4247
+ }
4248
+ /**
4249
+ * Core page rendering logic shared between SSR handler and static prerendering.
4250
+ *
4251
+ * Handles:
4252
+ * - Layer resolution (loaders)
4253
+ * - Redirect detection
4254
+ * - Head content filling
4255
+ * - Preload link collection
4256
+ * - React stream rendering
4257
+ *
4258
+ * @param route - The page route to render
4259
+ * @param state - The router state
4260
+ * @returns Render result with redirect or React stream
4261
+ */
4262
+ async renderPage(route, state) {
4263
+ const { redirect } = await this.pageApi.createLayers(route, state);
4264
+ if (redirect) {
4265
+ this.log.debug("Resolver resulted in redirection", { redirect });
4266
+ return { redirect };
4267
+ }
4268
+ this.serverHeadProvider.fillHead(state);
4269
+ const preloadLinks = this.ssrManifestProvider.collectPreloadLinks(route);
4270
+ if (preloadLinks.length > 0) {
4271
+ state.head ??= {};
4272
+ state.head.link = [...state.head.link ?? [], ...preloadLinks];
4273
+ }
4274
+ const element = this.pageApi.root(state);
4275
+ this.alepha.store.set("alepha.react.router.state", state);
4276
+ return { reactStream: await renderToReadableStream(element, { onError: (error) => {
4277
+ if (error instanceof Redirection) this.log.warn("Redirect during streaming ignored", { redirect: error.redirect });
4278
+ } }) };
4279
+ }
4280
+ /**
4281
+ * For testing purposes, renders a page to HTML string.
4282
+ * Uses the same streaming code path as production, then collects to string.
4283
+ *
4284
+ * @param name - Page name to render
4285
+ * @param options - Render options (params, query, html, hydration)
4286
+ */
4287
+ async render(name, options = {}) {
4288
+ const page = this.pageApi.page(name);
4289
+ const url = new URL(this.pageApi.url(name, options));
4290
+ const state = {
4291
+ url,
4292
+ params: options.params ?? {},
4293
+ query: options.query ?? {},
4294
+ onError: () => null,
4295
+ layers: [],
4296
+ meta: {},
4297
+ head: {}
4298
+ };
4299
+ this.log.trace("Rendering", { url });
4300
+ await this.alepha.events.emit("react:server:render:begin", { state });
4301
+ if (!this.templateProvider.isReady()) {
4302
+ this.templateProvider.parseTemplate(this.template);
4303
+ this.setupEarlyHeadContent();
4304
+ }
4305
+ const result = await this.renderPage(page, state);
4306
+ if (result.redirect) return {
4307
+ state,
4308
+ html: "",
4309
+ redirect: result.redirect
4310
+ };
4311
+ const reactStream = result.reactStream;
4312
+ if (!options.html) return {
4313
+ state,
4314
+ html: await this.streamToString(reactStream)
4315
+ };
4316
+ const htmlStream = this.templateProvider.createHtmlStream(reactStream, state, { hydration: options.hydration ?? true });
4317
+ const html = await this.streamToString(htmlStream);
4318
+ await this.alepha.events.emit("react:server:render:end", {
4319
+ state,
4320
+ html
4321
+ });
4322
+ return {
4323
+ state,
4324
+ html
4325
+ };
4326
+ }
4327
+ /**
4328
+ * Collect a ReadableStream into a string.
4329
+ */
4330
+ async streamToString(stream) {
4331
+ const reader = stream.getReader();
4332
+ const decoder = new TextDecoder();
4333
+ const chunks = [];
4334
+ try {
4335
+ while (true) {
4336
+ const { done, value } = await reader.read();
4337
+ if (done) break;
4338
+ chunks.push(decoder.decode(value, { stream: true }));
4339
+ }
4340
+ chunks.push(decoder.decode());
4341
+ } finally {
4342
+ reader.releaseLock();
4343
+ }
4344
+ return chunks.join("");
4345
+ }
4346
+ };
4347
+ const envSchema = t.object({ REACT_SSR_ENABLED: t.optional(t.boolean()) });
4348
+ /**
4349
+ * React server provider configuration atom
4350
+ */
4351
+ const reactServerOptions = $atom({
4352
+ name: "alepha.react.server.options",
4353
+ schema: t.object({
4354
+ publicDir: t.string(),
4355
+ staticServer: t.object({
4356
+ disabled: t.boolean(),
4357
+ path: t.string({ description: "URL path where static files will be served." })
4358
+ })
4359
+ }),
4360
+ default: {
4361
+ publicDir: "public",
4362
+ staticServer: {
4363
+ disabled: false,
4364
+ path: "/"
4365
+ }
4366
+ }
4367
+ });
4368
+
4369
+ //#endregion
4370
+ //#region ../../src/react/router/services/ReactPageServerService.ts
4371
+ /**
4372
+ * $page methods for server-side.
4373
+ */
4374
+ var ReactPageServerService = class extends ReactPageService {
4375
+ reactServerProvider = $inject(ReactServerProvider);
4376
+ templateProvider = $inject(ReactServerTemplateProvider);
4377
+ serverProvider = $inject(ServerProvider);
4378
+ async render(name, options = {}) {
4379
+ return this.reactServerProvider.render(name, options);
4380
+ }
4381
+ async fetch(pathname, options = {}) {
4382
+ const response = await fetch(`${this.serverProvider.hostname}/${pathname}`);
4383
+ const html = await response.text();
4384
+ if (options?.html) return {
4385
+ html,
4386
+ response
4387
+ };
4388
+ const rootContent = this.templateProvider.extractRootContent(html);
4389
+ if (rootContent !== void 0) return {
4390
+ html: rootContent,
4391
+ response
4392
+ };
4393
+ throw new AlephaError("Invalid HTML response");
4394
+ }
4395
+ };
4396
+
4397
+ //#endregion
4398
+ //#region ../../src/react/router/providers/ReactBrowserRouterProvider.ts
4399
+ /**
4400
+ * Implementation of AlephaRouter for React in browser environment.
4401
+ */
4402
+ var ReactBrowserRouterProvider = class extends RouterProvider {
4403
+ log = $logger();
4404
+ alepha = $inject(Alepha);
4405
+ pageApi = $inject(ReactPageProvider);
4406
+ browserHeadProvider = $inject(BrowserHeadProvider);
4407
+ add(entry) {
4408
+ this.pageApi.add(entry);
4409
+ }
4410
+ configure = $hook({
4411
+ on: "configure",
4412
+ handler: async () => {
4413
+ for (const page of this.pageApi.getPages()) if (page.component || page.lazy) this.push({
4414
+ path: page.match,
4415
+ page
4416
+ });
4417
+ }
4418
+ });
4419
+ async transition(url, previous = [], meta = {}) {
4420
+ const { pathname, search } = url;
4421
+ const state = {
4422
+ url,
4423
+ query: {},
4424
+ params: {},
4425
+ layers: [],
4426
+ onError: () => null,
4427
+ meta
4428
+ };
4429
+ await this.alepha.events.emit("react:action:begin", { type: "transition" });
4430
+ await this.alepha.events.emit("react:transition:begin", {
4431
+ previous: this.alepha.store.get("alepha.react.router.state"),
4432
+ state
4433
+ });
4434
+ try {
4435
+ const { route, params } = this.match(pathname);
4436
+ const query = {};
4437
+ if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
4438
+ state.name = route?.page.name;
4439
+ state.query = query;
4440
+ state.params = params ?? {};
4441
+ if (isPageRoute(route)) {
4442
+ const { redirect } = await this.pageApi.createLayers(route.page, state, previous);
4443
+ if (redirect) return redirect;
4444
+ }
4445
+ if (state.layers.length === 0) state.layers.push({
4446
+ name: "not-found",
4447
+ element: createElement(NotFound_default),
4448
+ index: 0,
4449
+ path: "/"
4450
+ });
4451
+ await this.alepha.events.emit("react:action:success", { type: "transition" });
4452
+ await this.alepha.events.emit("react:transition:success", { state });
4453
+ } catch (e) {
4454
+ this.log.error("Transition has failed", e);
4455
+ state.layers = [{
4456
+ name: "error",
4457
+ element: this.pageApi.renderError(e),
4458
+ index: 0,
4459
+ path: "/"
4460
+ }];
4461
+ await this.alepha.events.emit("react:action:error", {
4462
+ type: "transition",
4463
+ error: e
4464
+ });
4465
+ await this.alepha.events.emit("react:transition:error", {
4466
+ error: e,
4467
+ state
4468
+ });
4469
+ }
4470
+ if (previous) for (let i = 0; i < previous.length; i++) {
4471
+ const layer = previous[i];
4472
+ if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
4473
+ }
4474
+ for (let i = 0; i < state.layers.length; i++) {
4475
+ const layer = state.layers[i];
4476
+ if (previous?.[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onEnter?.();
4477
+ }
4478
+ this.alepha.store.set("alepha.react.router.state", state);
4479
+ await this.alepha.events.emit("react:action:end", { type: "transition" });
4480
+ await this.alepha.events.emit("react:transition:end", { state });
4481
+ this.browserHeadProvider.fillAndRenderHead(state);
4482
+ }
4483
+ root(state) {
4484
+ return this.pageApi.root(state);
4485
+ }
4486
+ };
4487
+
4488
+ //#endregion
4489
+ //#region ../../src/react/router/providers/ReactBrowserProvider.ts
4490
+ /**
4491
+ * React browser renderer configuration atom
4492
+ */
4493
+ const reactBrowserOptions = $atom({
4494
+ name: "alepha.react.browser.options",
4495
+ schema: t.object({ scrollRestoration: t.enum(["top", "manual"]) }),
4496
+ default: { scrollRestoration: "top" }
4497
+ });
4498
+ var ReactBrowserProvider = class {
4499
+ log = $logger();
4500
+ client = $inject(LinkProvider);
4501
+ alepha = $inject(Alepha);
4502
+ router = $inject(ReactBrowserRouterProvider);
4503
+ dateTimeProvider = $inject(DateTimeProvider);
4504
+ browserHeadProvider = $inject(BrowserHeadProvider);
4505
+ options = $use(reactBrowserOptions);
4506
+ get rootId() {
4507
+ return "root";
4508
+ }
4509
+ getRootElement() {
4510
+ const root = this.document.getElementById(this.rootId);
4511
+ if (root) return root;
4512
+ const div = this.document.createElement("div");
4513
+ div.id = this.rootId;
4514
+ this.document.body.prepend(div);
4515
+ return div;
4516
+ }
4517
+ transitioning;
4518
+ get state() {
4519
+ return this.alepha.store.get("alepha.react.router.state");
4520
+ }
4521
+ /**
4522
+ * Accessor for Document DOM API.
4523
+ */
4524
+ get document() {
4525
+ return window.document;
4526
+ }
4527
+ /**
4528
+ * Accessor for History DOM API.
4529
+ */
4530
+ get history() {
4531
+ return window.history;
4532
+ }
4533
+ /**
4534
+ * Accessor for Location DOM API.
4535
+ */
4536
+ get location() {
4537
+ return window.location;
4538
+ }
4539
+ get base() {
4540
+ const base = import.meta.env?.BASE_URL;
4541
+ if (!base || base === "/") return "";
4542
+ return base;
4543
+ }
4544
+ get url() {
4545
+ const url = this.location.pathname + this.location.search;
4546
+ if (this.base) return url.replace(this.base, "");
4547
+ return url;
4548
+ }
4549
+ pushState(path, replace) {
4550
+ const url = this.base + path;
4551
+ if (replace) this.history.replaceState({}, "", url);
4552
+ else this.history.pushState({}, "", url);
4553
+ }
4554
+ async invalidate(props) {
4555
+ const previous = [];
4556
+ this.log.trace("Invalidating layers");
4557
+ if (props) {
4558
+ const [key] = Object.keys(props);
4559
+ const value = props[key];
4560
+ for (const layer of this.state.layers) {
4561
+ if (layer.props?.[key]) {
4562
+ previous.push({
4563
+ ...layer,
4564
+ props: {
4565
+ ...layer.props,
4566
+ [key]: value
4567
+ }
4568
+ });
4569
+ break;
4570
+ }
4571
+ previous.push(layer);
4572
+ }
4573
+ }
4574
+ await this.render({ previous });
4575
+ }
4576
+ async go(url, options = {}) {
4577
+ this.log.trace(`Going to ${url}`, {
4578
+ url,
4579
+ options
4580
+ });
4581
+ await this.render({
4582
+ url,
4583
+ previous: options.force ? [] : this.state.layers,
4584
+ meta: options.meta
4585
+ });
4586
+ if (this.state.url.pathname + this.state.url.search !== url) {
4587
+ this.pushState(this.state.url.pathname + this.state.url.search);
4588
+ return;
4589
+ }
4590
+ this.pushState(url, options.replace);
4591
+ }
4592
+ async render(options = {}) {
4593
+ const previous = options.previous ?? this.state.layers;
4594
+ const url = options.url ?? this.url;
4595
+ const start = this.dateTimeProvider.now();
4596
+ this.transitioning = {
4597
+ to: url,
4598
+ from: this.state?.url.pathname
4599
+ };
4600
+ this.log.debug("Transitioning...", { to: url });
4601
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
4602
+ if (redirect) {
4603
+ this.log.info("Redirecting to", { redirect });
4604
+ if (redirect.startsWith("http")) window.location.href = redirect;
4605
+ else return await this.render({ url: redirect });
4606
+ }
4607
+ const ms = this.dateTimeProvider.now().diff(start);
4608
+ this.log.info(`Transition OK [${ms}ms]`, this.transitioning);
4609
+ this.transitioning = void 0;
4610
+ }
4611
+ /**
4612
+ * Get embedded layers from the server.
4613
+ */
4614
+ getHydrationState() {
4615
+ try {
4616
+ if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
4617
+ } catch (error) {
4618
+ console.error(error);
4619
+ }
4620
+ }
4621
+ onTransitionEnd = $hook({
4622
+ on: "react:transition:end",
4623
+ handler: () => {
4624
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
4625
+ this.log.trace("Restoring scroll position to top");
4626
+ window.scrollTo(0, 0);
4627
+ }
4628
+ }
4629
+ });
4630
+ ready = $hook({
4631
+ on: "ready",
4632
+ handler: async () => {
4633
+ const hydration = this.getHydrationState();
4634
+ const previous = hydration?.layers ?? [];
4635
+ if (hydration) {
4636
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.set(key, value);
4637
+ }
4638
+ await this.render({ previous });
4639
+ const element = this.router.root(this.state);
4640
+ await this.alepha.events.emit("react:browser:render", {
4641
+ element,
4642
+ root: this.getRootElement(),
4643
+ hydration,
4644
+ state: this.state
4645
+ });
4646
+ this.browserHeadProvider.fillAndRenderHead(this.state);
4647
+ window.addEventListener("popstate", () => {
4648
+ if (this.base + this.state.url.pathname === this.location.pathname) return;
4649
+ this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
4650
+ this.render();
4651
+ });
4652
+ }
4653
+ });
4654
+ };
4655
+
4656
+ //#endregion
4657
+ //#region ../../src/react/router/services/ReactRouter.ts
4658
+ /**
4659
+ * Friendly browser router API.
4660
+ *
4661
+ * Can be safely used server-side, but most methods will be no-op.
4662
+ */
4663
+ var ReactRouter = class {
4664
+ alepha = $inject(Alepha);
4665
+ pageApi = $inject(ReactPageProvider);
4666
+ get state() {
4667
+ return this.alepha.store.get("alepha.react.router.state");
4668
+ }
4669
+ get pages() {
4670
+ return this.pageApi.getPages();
4671
+ }
4672
+ get concretePages() {
4673
+ return this.pageApi.getConcretePages();
4674
+ }
4675
+ get browser() {
4676
+ if (this.alepha.isBrowser()) return this.alepha.inject(ReactBrowserProvider);
4677
+ }
4678
+ isActive(href, options = {}) {
4679
+ const current = this.state.url.pathname;
4680
+ let isActive = current === href || current === `${href}/` || `${current}/` === href;
4681
+ if (options.startWith && !isActive) isActive = current.startsWith(href);
4682
+ return isActive;
4683
+ }
4684
+ node(name, config = {}) {
4685
+ const page = this.pageApi.page(name);
4686
+ if (!page.lazy && !page.component) return {
4687
+ ...page,
4688
+ label: page.label ?? page.name,
4689
+ children: void 0
4690
+ };
4691
+ return {
4692
+ ...page,
4693
+ label: page.label ?? page.name,
4694
+ href: this.path(name, config),
4695
+ children: void 0
4696
+ };
4697
+ }
4698
+ path(name, config = {}) {
4699
+ return this.pageApi.pathname(name, {
4700
+ params: {
4701
+ ...this.state?.params,
4702
+ ...config.params
4703
+ },
4704
+ query: config.query
4705
+ });
4706
+ }
4707
+ /**
4708
+ * Reload the current page.
4709
+ * This is equivalent to calling `go()` with the current pathname and search.
4710
+ */
4711
+ async reload() {
4712
+ if (!this.browser) return;
4713
+ await this.go(this.location.pathname + this.location.search, {
4714
+ replace: true,
4715
+ force: true
4716
+ });
4717
+ }
4718
+ getURL() {
4719
+ if (!this.browser) return this.state.url;
4720
+ return new URL(this.location.href);
4721
+ }
4722
+ get location() {
4723
+ if (!this.browser) throw new Error("Browser is required");
4724
+ return this.browser.location;
4725
+ }
4726
+ get current() {
4727
+ return this.state;
4728
+ }
4729
+ get pathname() {
4730
+ return this.state.url.pathname;
4731
+ }
4732
+ get query() {
4733
+ const query = {};
4734
+ for (const [key, value] of new URLSearchParams(this.state.url.search).entries()) query[key] = String(value);
4735
+ return query;
4736
+ }
4737
+ async back() {
4738
+ this.browser?.history.back();
4739
+ }
4740
+ async forward() {
4741
+ this.browser?.history.forward();
4742
+ }
4743
+ async invalidate(props) {
4744
+ await this.browser?.invalidate(props);
4745
+ }
4746
+ async go(path, options) {
4747
+ for (const page of this.pages) if (page.name === path) {
4748
+ await this.browser?.go(this.path(path, options), options);
4749
+ return;
4750
+ }
4751
+ await this.browser?.go(path, options);
4752
+ }
4753
+ anchor(path, options = {}) {
4754
+ let href = path;
4755
+ for (const page of this.pages) if (page.name === path) {
4756
+ href = this.path(path, options);
4757
+ break;
4758
+ }
4759
+ return {
4760
+ href: this.base(href),
4761
+ onClick: (ev) => {
4762
+ ev.stopPropagation();
4763
+ ev.preventDefault();
4764
+ this.go(href, options).catch(console.error);
4765
+ }
4766
+ };
4767
+ }
4768
+ base(path) {
4769
+ const base = import.meta.env?.BASE_URL;
4770
+ if (!base || base === "/") return path;
4771
+ return base + path;
4772
+ }
4773
+ /**
4774
+ * Set query params.
4775
+ *
4776
+ * @param record
4777
+ * @param options
4778
+ */
4779
+ setQueryParams(record, options = {}) {
4780
+ const func = typeof record === "function" ? record : () => record;
4781
+ const search = new URLSearchParams(func(this.query)).toString();
4782
+ const state = search ? `${this.pathname}?${search}` : this.pathname;
4783
+ if (options.push) window.history.pushState({}, "", state);
4784
+ else window.history.replaceState({}, "", state);
4785
+ }
4786
+ };
4787
+
4788
+ //#endregion
4789
+ //#region ../../src/react/router/hooks/useRouter.ts
4790
+ /**
4791
+ * Use this hook to access the React Router instance.
4792
+ *
4793
+ * You can add a type parameter to specify the type of your application.
4794
+ * This will allow you to use the router in a typesafe way.
4795
+ *
4796
+ * @example
4797
+ * class App {
4798
+ * home = $page();
4799
+ * }
4800
+ *
4801
+ * const router = useRouter<App>();
4802
+ * router.go("home"); // typesafe
4803
+ */
4804
+ const useRouter = () => {
4805
+ return useInject(ReactRouter);
4806
+ };
4807
+
4808
+ //#endregion
4809
+ //#region ../../src/react/router/components/Link.tsx
4810
+ /**
4811
+ * Link component for client-side navigation.
4812
+ *
4813
+ * It's a simple wrapper around an anchor (`<a>`) element using the `useRouter` hook.
4814
+ */
4815
+ const Link = (props) => {
4816
+ const router = useRouter();
4817
+ return createElement("a", {
4818
+ ...props,
4819
+ ...router.anchor(props.href)
4820
+ }, props.children);
4821
+ };
4822
+ var Link_default = Link;
4823
+
4824
+ //#endregion
4825
+ //#region ../../src/react/router/hooks/useActive.ts
4826
+ /**
4827
+ * Hook to determine if a given route is active and to provide anchor props for navigation.
4828
+ * This hook refreshes on router state changes.
4829
+ */
4830
+ const useActive = (args) => {
4831
+ useRouterState();
4832
+ const router = useRouter();
4833
+ const [isPending, setPending] = useState(false);
4834
+ const options = typeof args === "string" ? { href: args } : {
4835
+ ...args,
4836
+ href: args.href
4837
+ };
4838
+ const href = options.href;
4839
+ const isActive = router.isActive(href, options);
4840
+ return {
4841
+ isPending,
4842
+ isActive,
4843
+ anchorProps: {
4844
+ href: router.base(href),
4845
+ onClick: async (ev) => {
4846
+ ev?.stopPropagation();
4847
+ ev?.preventDefault();
4848
+ if (isActive) return;
4849
+ if (isPending) return;
4850
+ setPending(true);
4851
+ try {
4852
+ await router.go(href);
4853
+ } finally {
4854
+ setPending(false);
4855
+ }
4856
+ }
4857
+ }
4858
+ };
4859
+ };
4860
+
4861
+ //#endregion
4862
+ //#region ../../src/react/router/hooks/useQueryParams.ts
4863
+ /**
4864
+ * Hook to manage query parameters in the URL using a defined schema.
4865
+ */
4866
+ const useQueryParams = (schema, options = {}) => {
4867
+ const alepha = useAlepha();
4868
+ const key = options.key ?? "q";
4869
+ const router = useRouter();
4870
+ const querystring = router.query[key];
4871
+ const [queryParams = {}, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
4872
+ useEffect(() => {
4873
+ setQueryParams(decode(alepha, schema, querystring));
4874
+ }, [querystring]);
4875
+ return [queryParams, (queryParams) => {
4876
+ setQueryParams(queryParams);
4877
+ router.setQueryParams((data) => {
4878
+ return {
4879
+ ...data,
4880
+ [key]: encode(alepha, schema, queryParams)
4881
+ };
4882
+ });
4883
+ }];
4884
+ };
4885
+ const encode = (alepha, schema, data) => {
4886
+ return btoa(JSON.stringify(alepha.codec.decode(schema, data)));
4887
+ };
4888
+ const decode = (alepha, schema, data) => {
4889
+ try {
4890
+ return alepha.codec.decode(schema, JSON.parse(atob(decodeURIComponent(data))));
4891
+ } catch {
4892
+ return;
4893
+ }
4894
+ };
4895
+
4896
+ //#endregion
4897
+ //#region ../../src/react/router/index.ts
4898
+ /**
4899
+ * Provides declarative routing with the `$page` primitive for building type-safe React routes.
4900
+ *
4901
+ * This module enables:
4902
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
4903
+ * - Nested routing with parent-child relationships
4904
+ * - Type-safe URL parameter and query string validation
4905
+ * - Server-side data fetching with the `loader` function
4906
+ * - Lazy loading and code splitting
4907
+ * - Page animations and error handling
4908
+ *
4909
+ * @see {@link $page}
4910
+ * @module alepha.react.router
4911
+ */
4912
+ const AlephaReactRouter = $module({
4913
+ name: "alepha.react.router",
4914
+ primitives: [$page],
4915
+ services: [
4916
+ ReactPageProvider,
4917
+ ReactPageService,
4918
+ ReactRouter,
4919
+ ReactServerProvider,
4920
+ ReactServerTemplateProvider,
4921
+ SSRManifestProvider,
4922
+ ReactPageServerService
4923
+ ],
4924
+ register: (alepha) => alepha.with(AlephaReact).with(AlephaDateTime).with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with({
4925
+ provide: ReactPageService,
4926
+ use: ReactPageServerService
4927
+ }).with(SSRManifestProvider).with(ReactServerTemplateProvider).with(ReactServerProvider).with(ReactPageProvider).with(ReactRouter)
4928
+ });
4929
+
4930
+ //#endregion
4931
+ export { $page, AlephaReactRouter, ErrorViewer_default as ErrorViewer, Link_default as Link, NestedView_default as NestedView, NotFound_default as NotFound, PAGE_PRELOAD_KEY, PagePrimitive, ReactBrowserProvider, ReactPageProvider, ReactPageService, ReactRouter, ReactServerProvider, ReactServerTemplateProvider, Redirection, RouterLayerContext, SSRManifestProvider, isPageRoute, reactBrowserOptions, reactServerOptions, useActive, useQueryParams, useRouter, useRouterState };
4932
+ //# sourceMappingURL=index.js.map