alepha 0.15.1 → 0.15.3

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