alepha 0.15.0 → 0.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (551) hide show
  1. package/README.md +43 -98
  2. package/dist/api/audits/index.d.ts +630 -653
  3. package/dist/api/audits/index.d.ts.map +1 -1
  4. package/dist/api/audits/index.js +12 -35
  5. package/dist/api/audits/index.js.map +1 -1
  6. package/dist/api/files/index.d.ts +365 -358
  7. package/dist/api/files/index.d.ts.map +1 -1
  8. package/dist/api/files/index.js +12 -5
  9. package/dist/api/files/index.js.map +1 -1
  10. package/dist/api/jobs/index.d.ts +255 -248
  11. package/dist/api/jobs/index.d.ts.map +1 -1
  12. package/dist/api/jobs/index.js +10 -3
  13. package/dist/api/jobs/index.js.map +1 -1
  14. package/dist/api/keys/index.d.ts +413 -0
  15. package/dist/api/keys/index.d.ts.map +1 -0
  16. package/dist/api/keys/index.js +476 -0
  17. package/dist/api/keys/index.js.map +1 -0
  18. package/dist/api/notifications/index.browser.js +4 -4
  19. package/dist/api/notifications/index.browser.js.map +1 -1
  20. package/dist/api/notifications/index.d.ts +84 -78
  21. package/dist/api/notifications/index.d.ts.map +1 -1
  22. package/dist/api/notifications/index.js +14 -8
  23. package/dist/api/notifications/index.js.map +1 -1
  24. package/dist/api/parameters/index.d.ts +528 -535
  25. package/dist/api/parameters/index.d.ts.map +1 -1
  26. package/dist/api/parameters/index.js +30 -37
  27. package/dist/api/parameters/index.js.map +1 -1
  28. package/dist/api/users/index.d.ts +1221 -910
  29. package/dist/api/users/index.d.ts.map +1 -1
  30. package/dist/api/users/index.js +2556 -248
  31. package/dist/api/users/index.js.map +1 -1
  32. package/dist/api/verifications/index.d.ts +142 -136
  33. package/dist/api/verifications/index.d.ts.map +1 -1
  34. package/dist/api/verifications/index.js +12 -4
  35. package/dist/api/verifications/index.js.map +1 -1
  36. package/dist/batch/index.d.ts +142 -162
  37. package/dist/batch/index.d.ts.map +1 -1
  38. package/dist/batch/index.js +31 -44
  39. package/dist/batch/index.js.map +1 -1
  40. package/dist/bucket/index.d.ts +595 -171
  41. package/dist/bucket/index.d.ts.map +1 -1
  42. package/dist/bucket/index.js +1856 -12
  43. package/dist/bucket/index.js.map +1 -1
  44. package/dist/cache/core/index.d.ts +225 -53
  45. package/dist/cache/core/index.d.ts.map +1 -1
  46. package/dist/cache/core/index.js +213 -7
  47. package/dist/cache/core/index.js.map +1 -1
  48. package/dist/cache/redis/index.d.ts +1 -0
  49. package/dist/cache/redis/index.d.ts.map +1 -1
  50. package/dist/cache/redis/index.js +6 -2
  51. package/dist/cache/redis/index.js.map +1 -1
  52. package/dist/cli/index.d.ts +834 -226
  53. package/dist/cli/index.d.ts.map +1 -1
  54. package/dist/cli/index.js +2872 -417
  55. package/dist/cli/index.js.map +1 -1
  56. package/dist/command/index.d.ts +458 -310
  57. package/dist/command/index.d.ts.map +1 -1
  58. package/dist/command/index.js +2011 -76
  59. package/dist/command/index.js.map +1 -1
  60. package/dist/core/index.browser.js +309 -97
  61. package/dist/core/index.browser.js.map +1 -1
  62. package/dist/core/index.d.ts +796 -701
  63. package/dist/core/index.d.ts.map +1 -1
  64. package/dist/core/index.js +329 -97
  65. package/dist/core/index.js.map +1 -1
  66. package/dist/core/index.native.js +309 -97
  67. package/dist/core/index.native.js.map +1 -1
  68. package/dist/datetime/index.d.ts +59 -44
  69. package/dist/datetime/index.d.ts.map +1 -1
  70. package/dist/datetime/index.js +15 -0
  71. package/dist/datetime/index.js.map +1 -1
  72. package/dist/email/index.d.ts +314 -19
  73. package/dist/email/index.d.ts.map +1 -1
  74. package/dist/email/index.js +1852 -7
  75. package/dist/email/index.js.map +1 -1
  76. package/dist/fake/index.d.ts +5500 -5418
  77. package/dist/fake/index.d.ts.map +1 -1
  78. package/dist/fake/index.js +113 -42
  79. package/dist/fake/index.js.map +1 -1
  80. package/dist/lock/core/index.d.ts +219 -212
  81. package/dist/lock/core/index.d.ts.map +1 -1
  82. package/dist/lock/core/index.js +11 -4
  83. package/dist/lock/core/index.js.map +1 -1
  84. package/dist/lock/redis/index.d.ts.map +1 -1
  85. package/dist/logger/index.d.ts +41 -90
  86. package/dist/logger/index.d.ts.map +1 -1
  87. package/dist/logger/index.js +15 -68
  88. package/dist/logger/index.js.map +1 -1
  89. package/dist/mcp/index.d.ts +228 -230
  90. package/dist/mcp/index.d.ts.map +1 -1
  91. package/dist/mcp/index.js +32 -31
  92. package/dist/mcp/index.js.map +1 -1
  93. package/dist/orm/index.browser.js +12 -12
  94. package/dist/orm/index.browser.js.map +1 -1
  95. package/dist/orm/index.bun.js +90 -80
  96. package/dist/orm/index.bun.js.map +1 -1
  97. package/dist/orm/index.d.ts +1434 -1459
  98. package/dist/orm/index.d.ts.map +1 -1
  99. package/dist/orm/index.js +112 -130
  100. package/dist/orm/index.js.map +1 -1
  101. package/dist/queue/core/index.d.ts +262 -254
  102. package/dist/queue/core/index.d.ts.map +1 -1
  103. package/dist/queue/core/index.js +14 -6
  104. package/dist/queue/core/index.js.map +1 -1
  105. package/dist/queue/redis/index.d.ts.map +1 -1
  106. package/dist/react/auth/index.browser.js +108 -0
  107. package/dist/react/auth/index.browser.js.map +1 -0
  108. package/dist/react/auth/index.d.ts +100 -0
  109. package/dist/react/auth/index.d.ts.map +1 -0
  110. package/dist/react/auth/index.js +145 -0
  111. package/dist/react/auth/index.js.map +1 -0
  112. package/dist/react/core/index.d.ts +469 -0
  113. package/dist/react/core/index.d.ts.map +1 -0
  114. package/dist/react/core/index.js +464 -0
  115. package/dist/react/core/index.js.map +1 -0
  116. package/dist/react/form/index.d.ts +232 -0
  117. package/dist/react/form/index.d.ts.map +1 -0
  118. package/dist/react/form/index.js +432 -0
  119. package/dist/react/form/index.js.map +1 -0
  120. package/dist/react/head/index.browser.js +423 -0
  121. package/dist/react/head/index.browser.js.map +1 -0
  122. package/dist/react/head/index.d.ts +288 -0
  123. package/dist/react/head/index.d.ts.map +1 -0
  124. package/dist/react/head/index.js +465 -0
  125. package/dist/react/head/index.js.map +1 -0
  126. package/dist/react/i18n/index.d.ts +175 -0
  127. package/dist/react/i18n/index.d.ts.map +1 -0
  128. package/dist/react/i18n/index.js +224 -0
  129. package/dist/react/i18n/index.js.map +1 -0
  130. package/dist/react/router/index.browser.js +1980 -0
  131. package/dist/react/router/index.browser.js.map +1 -0
  132. package/dist/react/router/index.d.ts +2068 -0
  133. package/dist/react/router/index.d.ts.map +1 -0
  134. package/dist/react/router/index.js +4932 -0
  135. package/dist/react/router/index.js.map +1 -0
  136. package/dist/react/websocket/index.d.ts +117 -0
  137. package/dist/react/websocket/index.d.ts.map +1 -0
  138. package/dist/react/websocket/index.js +107 -0
  139. package/dist/react/websocket/index.js.map +1 -0
  140. package/dist/redis/index.bun.js +4 -0
  141. package/dist/redis/index.bun.js.map +1 -1
  142. package/dist/redis/index.d.ts +127 -130
  143. package/dist/redis/index.d.ts.map +1 -1
  144. package/dist/redis/index.js +16 -25
  145. package/dist/redis/index.js.map +1 -1
  146. package/dist/retry/index.d.ts +80 -71
  147. package/dist/retry/index.d.ts.map +1 -1
  148. package/dist/retry/index.js +11 -2
  149. package/dist/retry/index.js.map +1 -1
  150. package/dist/router/index.d.ts +6 -6
  151. package/dist/router/index.d.ts.map +1 -1
  152. package/dist/scheduler/index.d.ts +119 -28
  153. package/dist/scheduler/index.d.ts.map +1 -1
  154. package/dist/scheduler/index.js +404 -3
  155. package/dist/scheduler/index.js.map +1 -1
  156. package/dist/security/index.d.ts +642 -228
  157. package/dist/security/index.d.ts.map +1 -1
  158. package/dist/security/index.js +1579 -37
  159. package/dist/security/index.js.map +1 -1
  160. package/dist/server/auth/index.d.ts +1141 -111
  161. package/dist/server/auth/index.d.ts.map +1 -1
  162. package/dist/server/auth/index.js +1261 -25
  163. package/dist/server/auth/index.js.map +1 -1
  164. package/dist/server/cache/index.d.ts +63 -78
  165. package/dist/server/cache/index.d.ts.map +1 -1
  166. package/dist/server/cache/index.js +7 -22
  167. package/dist/server/cache/index.js.map +1 -1
  168. package/dist/server/compress/index.d.ts +13 -5
  169. package/dist/server/compress/index.d.ts.map +1 -1
  170. package/dist/server/compress/index.js +10 -2
  171. package/dist/server/compress/index.js.map +1 -1
  172. package/dist/server/cookies/index.d.ts +46 -22
  173. package/dist/server/cookies/index.d.ts.map +1 -1
  174. package/dist/server/cookies/index.js +7 -5
  175. package/dist/server/cookies/index.js.map +1 -1
  176. package/dist/server/core/index.d.ts +307 -196
  177. package/dist/server/core/index.d.ts.map +1 -1
  178. package/dist/server/core/index.js +271 -38
  179. package/dist/server/core/index.js.map +1 -1
  180. package/dist/server/cors/index.d.ts +24 -34
  181. package/dist/server/cors/index.d.ts.map +1 -1
  182. package/dist/server/cors/index.js +7 -21
  183. package/dist/server/cors/index.js.map +1 -1
  184. package/dist/server/health/index.d.ts +25 -19
  185. package/dist/server/health/index.d.ts.map +1 -1
  186. package/dist/server/health/index.js +8 -2
  187. package/dist/server/health/index.js.map +1 -1
  188. package/dist/server/helmet/index.d.ts +13 -5
  189. package/dist/server/helmet/index.d.ts.map +1 -1
  190. package/dist/server/helmet/index.js +11 -3
  191. package/dist/server/helmet/index.js.map +1 -1
  192. package/dist/server/links/index.browser.js +9 -1
  193. package/dist/server/links/index.browser.js.map +1 -1
  194. package/dist/server/links/index.d.ts +133 -128
  195. package/dist/server/links/index.d.ts.map +1 -1
  196. package/dist/server/links/index.js +24 -11
  197. package/dist/server/links/index.js.map +1 -1
  198. package/dist/server/metrics/index.d.ts +524 -4
  199. package/dist/server/metrics/index.d.ts.map +1 -1
  200. package/dist/server/metrics/index.js +4472 -7
  201. package/dist/server/metrics/index.js.map +1 -1
  202. package/dist/server/multipart/index.d.ts +15 -9
  203. package/dist/server/multipart/index.d.ts.map +1 -1
  204. package/dist/server/multipart/index.js +9 -3
  205. package/dist/server/multipart/index.js.map +1 -1
  206. package/dist/server/proxy/index.d.ts +110 -104
  207. package/dist/server/proxy/index.d.ts.map +1 -1
  208. package/dist/server/proxy/index.js +8 -2
  209. package/dist/server/proxy/index.js.map +1 -1
  210. package/dist/server/rate-limit/index.d.ts +46 -51
  211. package/dist/server/rate-limit/index.d.ts.map +1 -1
  212. package/dist/server/rate-limit/index.js +18 -55
  213. package/dist/server/rate-limit/index.js.map +1 -1
  214. package/dist/server/static/index.d.ts +181 -48
  215. package/dist/server/static/index.d.ts.map +1 -1
  216. package/dist/server/static/index.js +1848 -5
  217. package/dist/server/static/index.js.map +1 -1
  218. package/dist/server/swagger/index.d.ts +348 -53
  219. package/dist/server/swagger/index.d.ts.map +1 -1
  220. package/dist/server/swagger/index.js +1849 -6
  221. package/dist/server/swagger/index.js.map +1 -1
  222. package/dist/sms/index.d.ts +312 -18
  223. package/dist/sms/index.d.ts.map +1 -1
  224. package/dist/sms/index.js +1854 -10
  225. package/dist/sms/index.js.map +1 -1
  226. package/dist/system/index.browser.js +496 -0
  227. package/dist/system/index.browser.js.map +1 -0
  228. package/dist/system/index.d.ts +1158 -0
  229. package/dist/system/index.d.ts.map +1 -0
  230. package/dist/{file → system}/index.js +412 -20
  231. package/dist/system/index.js.map +1 -0
  232. package/dist/thread/index.d.ts +82 -73
  233. package/dist/thread/index.d.ts.map +1 -1
  234. package/dist/thread/index.js +13 -4
  235. package/dist/thread/index.js.map +1 -1
  236. package/dist/topic/core/index.d.ts +330 -323
  237. package/dist/topic/core/index.d.ts.map +1 -1
  238. package/dist/topic/core/index.js +12 -5
  239. package/dist/topic/core/index.js.map +1 -1
  240. package/dist/topic/redis/index.d.ts +6 -6
  241. package/dist/topic/redis/index.d.ts.map +1 -1
  242. package/dist/vite/index.d.ts +163 -5825
  243. package/dist/vite/index.d.ts.map +1 -1
  244. package/dist/vite/index.js +130 -477
  245. package/dist/vite/index.js.map +1 -1
  246. package/dist/websocket/index.browser.js +3 -3
  247. package/dist/websocket/index.browser.js.map +1 -1
  248. package/dist/websocket/index.d.ts +287 -283
  249. package/dist/websocket/index.d.ts.map +1 -1
  250. package/dist/websocket/index.js +15 -11
  251. package/dist/websocket/index.js.map +1 -1
  252. package/package.json +86 -17
  253. package/src/api/audits/index.ts +10 -33
  254. package/src/api/files/__tests__/$bucket.spec.ts +1 -1
  255. package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
  256. package/src/api/files/controllers/FileController.spec.ts +1 -1
  257. package/src/api/files/index.ts +10 -3
  258. package/src/api/files/jobs/FileJobs.spec.ts +1 -1
  259. package/src/api/files/services/FileService.spec.ts +1 -1
  260. package/src/api/jobs/index.ts +10 -3
  261. package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
  262. package/src/api/keys/controllers/ApiKeyController.ts +103 -0
  263. package/src/api/keys/entities/apiKeyEntity.ts +41 -0
  264. package/src/api/keys/index.ts +49 -0
  265. package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
  266. package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
  267. package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
  268. package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
  269. package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
  270. package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
  271. package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
  272. package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
  273. package/src/api/keys/services/ApiKeyService.ts +306 -0
  274. package/src/api/logs/TODO.md +52 -0
  275. package/src/api/notifications/index.ts +10 -4
  276. package/src/api/parameters/index.ts +9 -30
  277. package/src/api/parameters/primitives/$config.ts +12 -4
  278. package/src/api/parameters/services/ConfigStore.ts +9 -3
  279. package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
  280. package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
  281. package/src/api/users/index.ts +14 -3
  282. package/src/api/users/primitives/$realm.ts +33 -5
  283. package/src/api/users/providers/RealmProvider.ts +1 -12
  284. package/src/api/users/services/SessionService.ts +1 -11
  285. package/src/api/verifications/controllers/VerificationController.ts +2 -0
  286. package/src/api/verifications/index.ts +10 -4
  287. package/src/batch/index.ts +9 -36
  288. package/src/batch/primitives/$batch.ts +0 -8
  289. package/src/batch/providers/BatchProvider.ts +29 -2
  290. package/src/bucket/__tests__/shared.ts +1 -1
  291. package/src/bucket/index.ts +13 -6
  292. package/src/bucket/primitives/$bucket.ts +1 -1
  293. package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
  294. package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
  295. package/src/cache/core/__tests__/shared.ts +30 -0
  296. package/src/cache/core/index.ts +11 -6
  297. package/src/cache/core/primitives/$cache.spec.ts +5 -0
  298. package/src/cache/core/providers/CacheProvider.ts +17 -0
  299. package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
  300. package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
  301. package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
  302. package/src/cli/apps/AlephaCli.ts +3 -16
  303. package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -2
  304. package/src/cli/atoms/appEntryOptions.ts +13 -0
  305. package/src/cli/atoms/buildOptions.ts +1 -1
  306. package/src/cli/atoms/changelogOptions.ts +1 -1
  307. package/src/cli/commands/build.ts +64 -52
  308. package/src/cli/commands/db.ts +17 -11
  309. package/src/cli/commands/deploy.ts +1 -1
  310. package/src/cli/commands/dev.ts +13 -49
  311. package/src/cli/commands/gen/env.ts +6 -3
  312. package/src/cli/commands/gen/openapi.ts +5 -2
  313. package/src/cli/commands/init.spec.ts +544 -0
  314. package/src/cli/commands/init.ts +101 -58
  315. package/src/cli/commands/lint.ts +8 -2
  316. package/src/cli/commands/typecheck.ts +11 -0
  317. package/src/cli/defineConfig.ts +9 -0
  318. package/src/cli/index.ts +2 -1
  319. package/src/cli/providers/AppEntryProvider.ts +131 -0
  320. package/src/cli/providers/ViteBuildProvider.ts +40 -0
  321. package/src/cli/providers/ViteDevServerProvider.ts +378 -0
  322. package/src/cli/services/AlephaCliUtils.ts +39 -93
  323. package/src/cli/services/PackageManagerUtils.ts +140 -17
  324. package/src/cli/services/ProjectScaffolder.ts +169 -101
  325. package/src/cli/services/ViteUtils.ts +82 -0
  326. package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +41 -28
  327. package/src/cli/{assets → templates}/apiHelloControllerTs.ts +2 -1
  328. package/src/cli/{assets → templates}/biomeJson.ts +2 -1
  329. package/src/cli/{assets → templates}/dummySpecTs.ts +2 -1
  330. package/src/cli/{assets → templates}/editorconfig.ts +2 -1
  331. package/src/cli/templates/gitignore.ts +39 -0
  332. package/src/cli/{assets → templates}/mainBrowserTs.ts +2 -1
  333. package/src/cli/templates/mainCss.ts +33 -0
  334. package/src/cli/templates/mainServerTs.ts +33 -0
  335. package/src/cli/{assets → templates}/tsconfigJson.ts +2 -1
  336. package/src/cli/templates/webAppRouterTs.ts +50 -0
  337. package/src/cli/templates/webHelloComponentTsx.ts +20 -0
  338. package/src/command/helpers/Runner.spec.ts +4 -0
  339. package/src/command/helpers/Runner.ts +3 -21
  340. package/src/command/index.ts +12 -4
  341. package/src/command/providers/CliProvider.spec.ts +1067 -0
  342. package/src/command/providers/CliProvider.ts +203 -40
  343. package/src/core/Alepha.ts +3 -9
  344. package/src/core/__tests__/Alepha-start.spec.ts +4 -4
  345. package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
  346. package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
  347. package/src/core/index.shared.ts +1 -0
  348. package/src/core/index.ts +20 -0
  349. package/src/core/primitives/$module.ts +12 -0
  350. package/src/core/providers/EventManager.spec.ts +0 -71
  351. package/src/core/providers/EventManager.ts +3 -15
  352. package/src/core/providers/Json.ts +2 -14
  353. package/src/core/providers/KeylessJsonSchemaCodec.spec.ts +257 -0
  354. package/src/core/providers/KeylessJsonSchemaCodec.ts +396 -14
  355. package/src/core/providers/SchemaValidator.spec.ts +236 -0
  356. package/src/datetime/index.ts +15 -0
  357. package/src/email/index.ts +10 -5
  358. package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
  359. package/src/email/providers/LocalEmailProvider.ts +1 -1
  360. package/src/fake/__tests__/keyName.example.ts +1 -1
  361. package/src/fake/__tests__/keyName.spec.ts +5 -5
  362. package/src/fake/index.ts +9 -6
  363. package/src/fake/providers/FakeProvider.spec.ts +258 -40
  364. package/src/fake/providers/FakeProvider.ts +133 -19
  365. package/src/lock/core/index.ts +11 -4
  366. package/src/logger/index.ts +17 -66
  367. package/src/logger/providers/PrettyFormatterProvider.ts +0 -9
  368. package/src/mcp/errors/McpError.ts +30 -0
  369. package/src/mcp/index.ts +13 -27
  370. package/src/mcp/transports/SseMcpTransport.ts +6 -7
  371. package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
  372. package/src/orm/index.browser.ts +2 -2
  373. package/src/orm/index.bun.ts +4 -2
  374. package/src/orm/index.ts +21 -47
  375. package/src/orm/providers/DrizzleKitProvider.ts +3 -5
  376. package/src/orm/providers/drivers/BunSqliteProvider.ts +1 -0
  377. package/src/orm/services/Repository.ts +18 -3
  378. package/src/queue/core/index.ts +14 -6
  379. package/src/react/auth/__tests__/$auth.spec.ts +202 -0
  380. package/src/react/auth/hooks/useAuth.ts +32 -0
  381. package/src/react/auth/index.browser.ts +13 -0
  382. package/src/react/auth/index.shared.ts +2 -0
  383. package/src/react/auth/index.ts +48 -0
  384. package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
  385. package/src/react/auth/services/ReactAuth.ts +135 -0
  386. package/src/react/core/__tests__/Router.spec.tsx +169 -0
  387. package/src/react/core/components/ClientOnly.tsx +49 -0
  388. package/src/react/core/components/ErrorBoundary.tsx +73 -0
  389. package/src/react/core/contexts/AlephaContext.ts +7 -0
  390. package/src/react/core/contexts/AlephaProvider.tsx +42 -0
  391. package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
  392. package/src/react/core/hooks/useAction.ts +480 -0
  393. package/src/react/core/hooks/useAlepha.ts +26 -0
  394. package/src/react/core/hooks/useClient.ts +17 -0
  395. package/src/react/core/hooks/useEvents.ts +51 -0
  396. package/src/react/core/hooks/useInject.ts +12 -0
  397. package/src/react/core/hooks/useStore.ts +52 -0
  398. package/src/react/core/index.ts +90 -0
  399. package/src/react/form/components/FormState.tsx +17 -0
  400. package/src/react/form/errors/FormValidationError.ts +18 -0
  401. package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
  402. package/src/react/form/hooks/useForm.ts +47 -0
  403. package/src/react/form/hooks/useFormState.ts +130 -0
  404. package/src/react/form/index.ts +44 -0
  405. package/src/react/form/services/FormModel.ts +614 -0
  406. package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
  407. package/src/react/head/helpers/SeoExpander.ts +142 -0
  408. package/src/react/head/hooks/useHead.spec.tsx +288 -0
  409. package/src/react/head/hooks/useHead.ts +62 -0
  410. package/src/react/head/index.browser.ts +26 -0
  411. package/src/react/head/index.ts +44 -0
  412. package/src/react/head/interfaces/Head.ts +105 -0
  413. package/src/react/head/primitives/$head.ts +25 -0
  414. package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
  415. package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
  416. package/src/react/head/providers/HeadProvider.ts +168 -0
  417. package/src/react/head/providers/ServerHeadProvider.ts +31 -0
  418. package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
  419. package/src/react/i18n/components/Localize.spec.tsx +357 -0
  420. package/src/react/i18n/components/Localize.tsx +35 -0
  421. package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
  422. package/src/react/i18n/hooks/useI18n.ts +18 -0
  423. package/src/react/i18n/index.ts +41 -0
  424. package/src/react/i18n/primitives/$dictionary.ts +69 -0
  425. package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
  426. package/src/react/i18n/providers/I18nProvider.ts +278 -0
  427. package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
  428. package/src/react/router/__tests__/page-head.spec.ts +48 -0
  429. package/src/react/router/__tests__/seo-head.spec.ts +125 -0
  430. package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
  431. package/src/react/router/components/ErrorViewer.tsx +872 -0
  432. package/src/react/router/components/Link.tsx +23 -0
  433. package/src/react/router/components/NestedView.tsx +223 -0
  434. package/src/react/router/components/NotFound.tsx +30 -0
  435. package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
  436. package/src/react/router/contexts/RouterLayerContext.ts +12 -0
  437. package/src/react/router/errors/Redirection.ts +28 -0
  438. package/src/react/router/hooks/useActive.ts +52 -0
  439. package/src/react/router/hooks/useQueryParams.ts +63 -0
  440. package/src/react/router/hooks/useRouter.ts +20 -0
  441. package/src/react/router/hooks/useRouterState.ts +11 -0
  442. package/src/react/router/index.browser.ts +45 -0
  443. package/src/react/router/index.shared.ts +19 -0
  444. package/src/react/router/index.ts +142 -0
  445. package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
  446. package/src/react/router/primitives/$page.spec.tsx +708 -0
  447. package/src/react/router/primitives/$page.ts +497 -0
  448. package/src/react/router/providers/ReactBrowserProvider.ts +309 -0
  449. package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
  450. package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
  451. package/src/react/router/providers/ReactPageProvider.ts +726 -0
  452. package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
  453. package/src/react/router/providers/ReactServerProvider.ts +558 -0
  454. package/src/react/router/providers/ReactServerTemplateProvider.ts +979 -0
  455. package/src/react/router/providers/SSRManifestProvider.ts +334 -0
  456. package/src/react/router/services/ReactPageServerService.ts +48 -0
  457. package/src/react/router/services/ReactPageService.ts +27 -0
  458. package/src/react/router/services/ReactRouter.ts +262 -0
  459. package/src/react/websocket/hooks/useRoom.tsx +242 -0
  460. package/src/react/websocket/index.ts +7 -0
  461. package/src/redis/__tests__/redis.spec.ts +13 -0
  462. package/src/redis/index.ts +9 -25
  463. package/src/redis/providers/BunRedisProvider.ts +9 -0
  464. package/src/redis/providers/NodeRedisProvider.ts +8 -0
  465. package/src/redis/providers/RedisProvider.ts +16 -0
  466. package/src/retry/index.ts +11 -2
  467. package/src/router/index.ts +15 -0
  468. package/src/scheduler/index.ts +11 -2
  469. package/src/security/__tests__/BasicAuth.spec.ts +2 -0
  470. package/src/security/__tests__/ServerSecurityProvider.spec.ts +13 -5
  471. package/src/security/index.ts +15 -10
  472. package/src/security/interfaces/IssuerResolver.ts +27 -0
  473. package/src/security/primitives/$issuer.ts +55 -0
  474. package/src/security/providers/SecurityProvider.ts +179 -0
  475. package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
  476. package/src/security/providers/ServerSecurityProvider.ts +36 -22
  477. package/src/server/auth/index.ts +12 -7
  478. package/src/server/cache/index.ts +7 -22
  479. package/src/server/compress/index.ts +10 -2
  480. package/src/server/cookies/index.ts +7 -5
  481. package/src/server/cookies/primitives/$cookie.ts +33 -11
  482. package/src/server/core/index.ts +17 -7
  483. package/src/server/core/interfaces/ServerRequest.ts +83 -1
  484. package/src/server/core/primitives/$action.spec.ts +1 -1
  485. package/src/server/core/primitives/$action.ts +8 -3
  486. package/src/server/core/providers/BunHttpServerProvider.ts +1 -1
  487. package/src/server/core/providers/NodeHttpServerProvider.spec.ts +125 -0
  488. package/src/server/core/providers/NodeHttpServerProvider.ts +77 -22
  489. package/src/server/core/providers/ServerLoggerProvider.ts +2 -2
  490. package/src/server/core/providers/ServerProvider.ts +9 -12
  491. package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
  492. package/src/server/core/services/ServerRequestParser.ts +306 -13
  493. package/src/server/cors/index.ts +7 -21
  494. package/src/server/cors/primitives/$cors.ts +6 -2
  495. package/src/server/health/index.ts +8 -2
  496. package/src/server/helmet/index.ts +11 -3
  497. package/src/server/links/atoms/apiLinksAtom.ts +7 -0
  498. package/src/server/links/index.browser.ts +2 -0
  499. package/src/server/links/index.ts +13 -6
  500. package/src/server/metrics/index.ts +10 -3
  501. package/src/server/multipart/index.ts +9 -3
  502. package/src/server/proxy/index.ts +8 -2
  503. package/src/server/rate-limit/index.ts +21 -25
  504. package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
  505. package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
  506. package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
  507. package/src/server/static/index.ts +8 -2
  508. package/src/server/static/providers/ServerStaticProvider.ts +1 -1
  509. package/src/server/swagger/index.ts +9 -4
  510. package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
  511. package/src/sms/index.ts +9 -5
  512. package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
  513. package/src/sms/providers/LocalSmsProvider.ts +1 -1
  514. package/src/system/index.browser.ts +11 -0
  515. package/src/system/index.ts +62 -0
  516. package/src/{file → system}/providers/FileSystemProvider.ts +16 -0
  517. package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
  518. package/src/system/providers/MemoryShellProvider.ts +164 -0
  519. package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
  520. package/src/{file → system}/providers/NodeFileSystemProvider.ts +36 -0
  521. package/src/system/providers/NodeShellProvider.ts +184 -0
  522. package/src/system/providers/ShellProvider.ts +74 -0
  523. package/src/{file → system}/services/FileDetector.spec.ts +2 -2
  524. package/src/thread/index.ts +11 -2
  525. package/src/topic/core/index.ts +12 -5
  526. package/src/vite/index.ts +3 -2
  527. package/src/vite/tasks/buildClient.ts +2 -8
  528. package/src/vite/tasks/buildServer.ts +84 -21
  529. package/src/vite/tasks/copyAssets.ts +5 -4
  530. package/src/vite/tasks/generateSitemap.ts +64 -23
  531. package/src/vite/tasks/index.ts +0 -2
  532. package/src/vite/tasks/prerenderPages.ts +49 -24
  533. package/src/websocket/index.ts +12 -8
  534. package/dist/file/index.d.ts +0 -839
  535. package/dist/file/index.d.ts.map +0 -1
  536. package/dist/file/index.js.map +0 -1
  537. package/src/cli/assets/indexHtml.ts +0 -15
  538. package/src/cli/assets/mainServerTs.ts +0 -24
  539. package/src/cli/assets/webAppRouterTs.ts +0 -15
  540. package/src/cli/assets/webHelloComponentTsx.ts +0 -16
  541. package/src/cli/commands/format.ts +0 -23
  542. package/src/file/index.ts +0 -43
  543. package/src/vite/helpers/boot.ts +0 -117
  544. package/src/vite/plugins/viteAlephaDev.ts +0 -177
  545. package/src/vite/tasks/devServer.ts +0 -71
  546. package/src/vite/tasks/runAlepha.ts +0 -270
  547. /package/dist/orm/{chunk-DtkW-qnP.js → chunk-DH6iiROE.js} +0 -0
  548. /package/src/cli/{assets → templates}/apiIndexTs.ts +0 -0
  549. /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
  550. /package/src/{file → system}/errors/FileError.ts +0 -0
  551. /package/src/{file → system}/services/FileDetector.ts +0 -0
@@ -0,0 +1,979 @@
1
+ import { $inject, Alepha, AlephaError } from "alepha";
2
+ import { $logger } from "alepha/logger";
3
+ import { AlephaContext } from "alepha/react";
4
+ import type { SimpleHead } from "alepha/react/head";
5
+ import { createElement, type ReactNode } from "react";
6
+ import { renderToString } from "react-dom/server";
7
+ import ErrorViewer from "../components/ErrorViewer.tsx";
8
+ import { Redirection } from "../errors/Redirection.ts";
9
+ import type { ReactRouterState } from "./ReactPageProvider.ts";
10
+
11
+ /**
12
+ * Handles HTML template parsing, preprocessing, and streaming for SSR.
13
+ *
14
+ * Responsibilities:
15
+ * - Parse template once at startup into logical slots
16
+ * - Pre-encode static parts as Uint8Array for zero-copy streaming
17
+ * - Render dynamic parts (attributes, head content) efficiently
18
+ * - Build hydration data for client-side rehydration
19
+ *
20
+ * This provider is injected into ReactServerProvider to handle all
21
+ * template-related operations, keeping ReactServerProvider focused
22
+ * on request handling and React rendering coordination.
23
+ */
24
+ export class ReactServerTemplateProvider {
25
+ protected readonly log = $logger();
26
+ protected readonly alepha = $inject(Alepha);
27
+
28
+ /**
29
+ * Shared TextEncoder instance - reused across all requests.
30
+ */
31
+ protected readonly encoder = new TextEncoder();
32
+
33
+ /**
34
+ * Pre-encoded common strings for streaming.
35
+ */
36
+ protected readonly ENCODED = {
37
+ HYDRATION_PREFIX: this.encoder.encode("<script>window.__ssr="),
38
+ HYDRATION_SUFFIX: this.encoder.encode("</script>"),
39
+ EMPTY: this.encoder.encode(""),
40
+ } as const;
41
+
42
+ /**
43
+ * Cached template slots - parsed once, reused for all requests.
44
+ */
45
+ protected slots: TemplateSlots | null = null;
46
+
47
+ /**
48
+ * Root element ID for React mounting.
49
+ */
50
+ public get rootId(): string {
51
+ return "root";
52
+ }
53
+
54
+ /**
55
+ * Regex pattern for matching the root div and extracting its content.
56
+ */
57
+ public get rootDivRegex(): RegExp {
58
+ return new RegExp(
59
+ `<div([^>]*)\\s+id=["']${this.rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`,
60
+ "i",
61
+ );
62
+ }
63
+
64
+ /**
65
+ * Extract the content inside the root div from HTML.
66
+ *
67
+ * @param html - Full HTML string
68
+ * @returns The content inside the root div, or undefined if not found
69
+ */
70
+ public extractRootContent(html: string): string | undefined {
71
+ const match = html.match(this.rootDivRegex);
72
+ return match?.[3];
73
+ }
74
+
75
+ /**
76
+ * Check if template has been parsed and slots are available.
77
+ */
78
+ public isReady(): boolean {
79
+ return this.slots !== null;
80
+ }
81
+
82
+ /**
83
+ * Get the parsed template slots.
84
+ * Throws if template hasn't been parsed yet.
85
+ */
86
+ public getSlots(): TemplateSlots {
87
+ if (!this.slots) {
88
+ throw new AlephaError(
89
+ "Template not parsed. Call parseTemplate() during configuration.",
90
+ );
91
+ }
92
+ return this.slots;
93
+ }
94
+
95
+ /**
96
+ * Parse an HTML template into logical slots for efficient streaming.
97
+ *
98
+ * This should be called once during server startup/configuration.
99
+ * The parsed slots are cached and reused for all requests.
100
+ */
101
+ public parseTemplate(template: string): TemplateSlots {
102
+ this.log.debug("Parsing template into slots");
103
+
104
+ const rootId = this.rootId;
105
+
106
+ // Extract doctype
107
+ const doctypeMatch = template.match(/<!DOCTYPE[^>]*>/i);
108
+ const doctype = doctypeMatch?.[0] ?? "<!DOCTYPE html>";
109
+ let remaining = doctypeMatch
110
+ ? template.slice(doctypeMatch.index! + doctypeMatch[0].length)
111
+ : template;
112
+
113
+ // Extract <html> tag and attributes
114
+ const htmlMatch = remaining.match(/<html([^>]*)>/i);
115
+ const htmlAttrsStr = htmlMatch?.[1]?.trim() ?? "";
116
+ const htmlOriginalAttrs = this.parseAttributes(htmlAttrsStr);
117
+ remaining = htmlMatch
118
+ ? remaining.slice(htmlMatch.index! + htmlMatch[0].length)
119
+ : remaining;
120
+
121
+ // Extract <head> content
122
+ const headMatch = remaining.match(/<head([^>]*)>([\s\S]*?)<\/head>/i);
123
+ const headOriginalContent = headMatch?.[2]?.trim() ?? "";
124
+ remaining = headMatch
125
+ ? remaining.slice(headMatch.index! + headMatch[0].length)
126
+ : remaining;
127
+
128
+ // Extract <body> tag and attributes
129
+ const bodyMatch = remaining.match(/<body([^>]*)>/i);
130
+ const bodyAttrsStr = bodyMatch?.[1]?.trim() ?? "";
131
+ const bodyOriginalAttrs = this.parseAttributes(bodyAttrsStr);
132
+ const bodyStartIndex = bodyMatch
133
+ ? bodyMatch.index! + bodyMatch[0].length
134
+ : 0;
135
+ remaining = remaining.slice(bodyStartIndex);
136
+
137
+ // Find root div
138
+ const rootDivRegex = new RegExp(
139
+ `<div([^>]*)\\s+id=["']${rootId}["']([^>]*)>([\\s\\S]*?)<\\/div>`,
140
+ "i",
141
+ );
142
+ const rootMatch = remaining.match(rootDivRegex);
143
+
144
+ let beforeRoot = "";
145
+ let afterRoot = "";
146
+ let rootAttrs = "";
147
+
148
+ if (rootMatch) {
149
+ beforeRoot = remaining.slice(0, rootMatch.index!).trim();
150
+ const rootEndIndex = rootMatch.index! + rootMatch[0].length;
151
+ // Find </body> for afterRoot
152
+ const bodyCloseIndex = remaining.indexOf("</body>");
153
+ afterRoot =
154
+ bodyCloseIndex > rootEndIndex
155
+ ? remaining.slice(rootEndIndex, bodyCloseIndex).trim()
156
+ : "";
157
+ rootAttrs = `${rootMatch[1] ?? ""}${rootMatch[2] ?? ""}`.trim();
158
+ } else {
159
+ // No root div found - will inject one
160
+ const bodyCloseIndex = remaining.indexOf("</body>");
161
+ if (bodyCloseIndex > 0) {
162
+ beforeRoot = remaining.slice(0, bodyCloseIndex).trim();
163
+ }
164
+ }
165
+
166
+ // Build the root div opening tag
167
+ const rootOpenTag = rootAttrs
168
+ ? `<div ${rootAttrs} id="${rootId}">`
169
+ : `<div id="${rootId}">`;
170
+
171
+ this.slots = {
172
+ // Pre-encoded static parts
173
+ doctype: this.encoder.encode(`${doctype}\n`),
174
+ htmlOpen: this.encoder.encode("<html"),
175
+ htmlClose: this.encoder.encode(">\n"),
176
+ headOpen: this.encoder.encode("<head>"),
177
+ headClose: this.encoder.encode("</head>\n"),
178
+ bodyOpen: this.encoder.encode("<body"),
179
+ bodyClose: this.encoder.encode(">\n"),
180
+ rootOpen: this.encoder.encode(rootOpenTag),
181
+ rootClose: this.encoder.encode("</div>\n"),
182
+ scriptClose: this.encoder.encode("</body>\n</html>"),
183
+
184
+ // Original content for merging
185
+ htmlOriginalAttrs,
186
+ bodyOriginalAttrs,
187
+ headOriginalContent,
188
+ beforeRoot,
189
+ afterRoot,
190
+ };
191
+
192
+ this.log.debug("Template parsed successfully", {
193
+ hasHtmlAttrs: Object.keys(htmlOriginalAttrs).length > 0,
194
+ hasBodyAttrs: Object.keys(bodyOriginalAttrs).length > 0,
195
+ hasHeadContent: headOriginalContent.length > 0,
196
+ hasBeforeRoot: beforeRoot.length > 0,
197
+ hasAfterRoot: afterRoot.length > 0,
198
+ });
199
+
200
+ return this.slots;
201
+ }
202
+
203
+ /**
204
+ * Parse HTML attributes string into a record.
205
+ *
206
+ * Handles: key="value", key='value', key=value, and boolean key
207
+ */
208
+ protected parseAttributes(attrStr: string): Record<string, string> {
209
+ const attrs: Record<string, string> = {};
210
+ if (!attrStr) return attrs;
211
+
212
+ // Match: key="value", key='value', key=value, or just key (boolean)
213
+ const attrRegex = /([^\s=]+)(?:=(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
214
+
215
+ for (const match of attrStr.matchAll(attrRegex)) {
216
+ const key = match[1];
217
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
218
+ attrs[key] = value;
219
+ }
220
+
221
+ return attrs;
222
+ }
223
+
224
+ /**
225
+ * Render attributes record to HTML string.
226
+ *
227
+ * @param attrs - Attributes to render
228
+ * @returns HTML attribute string like ` lang="en" class="dark"`
229
+ */
230
+ public renderAttributes(attrs: Record<string, string>): string {
231
+ const entries = Object.entries(attrs);
232
+ if (entries.length === 0) return "";
233
+
234
+ return entries
235
+ .map(([key, value]) => ` ${key}="${this.escapeHtml(value)}"`)
236
+ .join("");
237
+ }
238
+
239
+ /**
240
+ * Render merged HTML attributes (original + dynamic).
241
+ */
242
+ public renderMergedHtmlAttrs(dynamicAttrs?: Record<string, string>): string {
243
+ const slots = this.getSlots();
244
+ const merged = { ...slots.htmlOriginalAttrs, ...dynamicAttrs };
245
+ return this.renderAttributes(merged);
246
+ }
247
+
248
+ /**
249
+ * Render merged body attributes (original + dynamic).
250
+ */
251
+ public renderMergedBodyAttrs(dynamicAttrs?: Record<string, string>): string {
252
+ const slots = this.getSlots();
253
+ const merged = { ...slots.bodyOriginalAttrs, ...dynamicAttrs };
254
+ return this.renderAttributes(merged);
255
+ }
256
+
257
+ /**
258
+ * Render head content (title, meta, link, script tags).
259
+ *
260
+ * @param head - Head data to render
261
+ * @param includeOriginal - Whether to include original head content
262
+ * @returns HTML string with head content
263
+ */
264
+ public renderHeadContent(head?: SimpleHead, includeOriginal = true): string {
265
+ const slots = this.getSlots();
266
+ let content = "";
267
+
268
+ // Include original head content first
269
+ if (includeOriginal && slots.headOriginalContent) {
270
+ content += slots.headOriginalContent;
271
+ }
272
+
273
+ if (!head) return content;
274
+
275
+ // Title - check if already exists in original content
276
+ if (head.title) {
277
+ if (content.includes("<title>")) {
278
+ // Replace existing title
279
+ content = content.replace(
280
+ /<title>.*?<\/title>/i,
281
+ `<title>${this.escapeHtml(head.title)}</title>`,
282
+ );
283
+ } else {
284
+ content += `<title>${this.escapeHtml(head.title)}</title>\n`;
285
+ }
286
+ }
287
+
288
+ // Meta tags
289
+ if (head.meta) {
290
+ for (const meta of head.meta) {
291
+ content += this.renderMetaTag(meta);
292
+ }
293
+ }
294
+
295
+ // Link tags
296
+ if (head.link) {
297
+ for (const link of head.link) {
298
+ content += this.renderLinkTag(link);
299
+ }
300
+ }
301
+
302
+ // Script tags
303
+ if (head.script) {
304
+ for (const script of head.script) {
305
+ content += this.renderScriptTag(script);
306
+ }
307
+ }
308
+
309
+ return content;
310
+ }
311
+
312
+ /**
313
+ * Render a meta tag.
314
+ */
315
+ protected renderMetaTag(meta: {
316
+ name?: string;
317
+ property?: string;
318
+ content: string;
319
+ }): string {
320
+ if (meta.property) {
321
+ return `<meta property="${this.escapeHtml(meta.property)}" content="${this.escapeHtml(meta.content)}">\n`;
322
+ }
323
+ if (meta.name) {
324
+ return `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">\n`;
325
+ }
326
+ return "";
327
+ }
328
+
329
+ /**
330
+ * Render a link tag.
331
+ */
332
+ protected renderLinkTag(link: {
333
+ rel: string;
334
+ href: string;
335
+ type?: string;
336
+ as?: string;
337
+ crossorigin?: string;
338
+ }): string {
339
+ let tag = `<link rel="${this.escapeHtml(link.rel)}" href="${this.escapeHtml(link.href)}"`;
340
+ if (link.type) {
341
+ tag += ` type="${this.escapeHtml(link.type)}"`;
342
+ }
343
+ if (link.as) {
344
+ tag += ` as="${this.escapeHtml(link.as)}"`;
345
+ }
346
+ if (link.crossorigin != null) {
347
+ tag += ' crossorigin=""';
348
+ }
349
+ tag += ">\n";
350
+ return tag;
351
+ }
352
+
353
+ /**
354
+ * Render a script tag.
355
+ */
356
+ protected renderScriptTag(
357
+ script:
358
+ | string
359
+ | (Record<string, string | boolean | undefined> & { content?: string }),
360
+ ): string {
361
+ // Handle plain string as inline script
362
+ if (typeof script === "string") {
363
+ return `<script>${script}</script>\n`;
364
+ }
365
+
366
+ const { content, ...rest } = script;
367
+ const attrs = Object.entries(rest)
368
+ .filter(([, value]) => value !== false && value !== undefined)
369
+ .map(([key, value]) => {
370
+ if (value === true) return key;
371
+ return `${key}="${this.escapeHtml(String(value))}"`;
372
+ })
373
+ .join(" ");
374
+
375
+ if (content) {
376
+ return attrs
377
+ ? `<script ${attrs}>${content}</script>\n`
378
+ : `<script>${content}</script>\n`;
379
+ }
380
+ return `<script ${attrs}></script>\n`;
381
+ }
382
+
383
+ /**
384
+ * Escape HTML special characters.
385
+ */
386
+ public escapeHtml(str: string): string {
387
+ return str
388
+ .replace(/&/g, "&amp;")
389
+ .replace(/</g, "&lt;")
390
+ .replace(/>/g, "&gt;")
391
+ .replace(/"/g, "&quot;")
392
+ .replace(/'/g, "&#039;");
393
+ }
394
+
395
+ /**
396
+ * Safely serialize data to JSON for embedding in HTML.
397
+ * Escapes characters that could break out of script tags.
398
+ */
399
+ public safeJsonSerialize(data: unknown): string {
400
+ return JSON.stringify(data)
401
+ .replace(/</g, "\\u003c")
402
+ .replace(/>/g, "\\u003e")
403
+ .replace(/&/g, "\\u0026");
404
+ }
405
+
406
+ /**
407
+ * Build hydration data from router state.
408
+ *
409
+ * This creates the data structure that will be serialized to window.__ssr
410
+ * for client-side rehydration.
411
+ */
412
+ public buildHydrationData(state: ReactRouterState): HydrationData {
413
+ const { request, context, ...store } =
414
+ this.alepha.context.als?.getStore() ?? {};
415
+
416
+ const layers = state.layers.map((layer) => ({
417
+ part: layer.part, // mandatory for previous-checking
418
+ name: layer.name, // mandatory for previous-checking
419
+ config: layer.config, // mandatory for previous-checking (contains 'query' & 'params')
420
+ props: layer.props, // our not-so-secret data cache
421
+ error: layer.error
422
+ ? {
423
+ ...layer.error,
424
+ name: layer.error.name,
425
+ message: layer.error.message,
426
+ stack: !this.alepha.isProduction() ? layer.error.stack : undefined,
427
+ }
428
+ : undefined,
429
+ }));
430
+
431
+ const hydrationData: HydrationData = {
432
+ layers,
433
+ };
434
+
435
+ for (const [key, value] of Object.entries(store)) {
436
+ if (
437
+ key.charAt(0) !== "_" &&
438
+ key !== "alepha.react.router.state" &&
439
+ key !== "registry"
440
+ ) {
441
+ hydrationData[key] = value;
442
+ }
443
+ }
444
+
445
+ return hydrationData;
446
+ }
447
+
448
+ /**
449
+ * Stream the body content: body tag, root div, React content, hydration, and closing tags.
450
+ *
451
+ * If an error occurs during React streaming, it injects error HTML instead of aborting,
452
+ * ensuring users see an error message rather than a white screen.
453
+ */
454
+ protected async streamBodyContent(
455
+ controller: ReadableStreamDefaultController<Uint8Array>,
456
+ reactStream: ReadableStream<Uint8Array>,
457
+ state: ReactRouterState,
458
+ hydration: boolean,
459
+ ): Promise<void> {
460
+ const slots = this.getSlots();
461
+ const encoder = this.encoder;
462
+ const head = state.head;
463
+
464
+ // <body ...>
465
+ controller.enqueue(slots.bodyOpen);
466
+ controller.enqueue(
467
+ encoder.encode(this.renderMergedBodyAttrs(head?.bodyAttributes)),
468
+ );
469
+ controller.enqueue(slots.bodyClose);
470
+
471
+ // Content before root (if any)
472
+ if (slots.beforeRoot) {
473
+ controller.enqueue(encoder.encode(slots.beforeRoot));
474
+ }
475
+
476
+ // <div id="root">
477
+ controller.enqueue(slots.rootOpen);
478
+
479
+ // Stream React content - catch errors from the React stream
480
+ const reader = reactStream.getReader();
481
+ let streamError: unknown = null;
482
+
483
+ try {
484
+ while (true) {
485
+ const { done, value } = await reader.read();
486
+ if (done) break;
487
+ controller.enqueue(value);
488
+ }
489
+ } catch (error) {
490
+ // React stream errored - save for error HTML injection
491
+ streamError = error;
492
+ this.log.error("Error during React stream reading", error);
493
+ } finally {
494
+ reader.releaseLock();
495
+ }
496
+
497
+ // If React stream errored, inject error HTML inside the root div
498
+ if (streamError) {
499
+ this.injectErrorHtml(controller, encoder, slots, streamError, state, {
500
+ headClosed: true,
501
+ bodyStarted: true,
502
+ });
503
+ // injectErrorHtml already closes the document, so return early
504
+ return;
505
+ }
506
+
507
+ // </div>
508
+ controller.enqueue(slots.rootClose);
509
+
510
+ // Content after root (if any)
511
+ if (slots.afterRoot) {
512
+ controller.enqueue(encoder.encode(slots.afterRoot));
513
+ }
514
+
515
+ // Hydration script
516
+ if (hydration) {
517
+ const hydrationData = this.buildHydrationData(state);
518
+ controller.enqueue(this.ENCODED.HYDRATION_PREFIX);
519
+ controller.enqueue(encoder.encode(this.safeJsonSerialize(hydrationData)));
520
+ controller.enqueue(this.ENCODED.HYDRATION_SUFFIX);
521
+ }
522
+
523
+ // </body></html>
524
+ controller.enqueue(slots.scriptClose);
525
+ }
526
+
527
+ /**
528
+ * Create a ReadableStream that streams the HTML template with React content.
529
+ *
530
+ * This is the main entry point for SSR streaming. It:
531
+ * 1. Sends <head> immediately (browser starts downloading assets)
532
+ * 2. Streams React content as it renders
533
+ * 3. Appends hydration script and closing tags
534
+ *
535
+ * @param reactStream - ReadableStream from renderToReadableStream
536
+ * @param state - Router state with head data
537
+ * @param options - Streaming options
538
+ */
539
+ public createHtmlStream(
540
+ reactStream: ReadableStream<Uint8Array>,
541
+ state: ReactRouterState,
542
+ options: {
543
+ hydration?: boolean;
544
+ onError?: (error: unknown) => void;
545
+ } = {},
546
+ ): ReadableStream<Uint8Array> {
547
+ const { hydration = true, onError } = options;
548
+ const slots = this.getSlots();
549
+ const head = state.head;
550
+ const encoder = this.encoder;
551
+
552
+ return new ReadableStream<Uint8Array>({
553
+ start: async (controller) => {
554
+ try {
555
+ // DOCTYPE
556
+ controller.enqueue(slots.doctype);
557
+
558
+ // <html ...>
559
+ controller.enqueue(slots.htmlOpen);
560
+ controller.enqueue(
561
+ encoder.encode(this.renderMergedHtmlAttrs(head?.htmlAttributes)),
562
+ );
563
+ controller.enqueue(slots.htmlClose);
564
+
565
+ // <head>...</head>
566
+ controller.enqueue(slots.headOpen);
567
+ if (this.earlyHeadContent) {
568
+ controller.enqueue(encoder.encode(this.earlyHeadContent));
569
+ }
570
+ controller.enqueue(encoder.encode(this.renderHeadContent(head)));
571
+ controller.enqueue(slots.headClose);
572
+
573
+ // Body content (body, root, React, hydration, closing tags)
574
+ await this.streamBodyContent(
575
+ controller,
576
+ reactStream,
577
+ state,
578
+ hydration,
579
+ );
580
+
581
+ controller.close();
582
+ } catch (error) {
583
+ onError?.(error);
584
+ controller.error(error);
585
+ }
586
+ },
587
+ });
588
+ }
589
+
590
+ /**
591
+ * Early head content for preloading.
592
+ *
593
+ * Contains entry assets (JS + CSS) that are always required and can be
594
+ * sent before page loaders run.
595
+ */
596
+ protected earlyHeadContent: string = "";
597
+
598
+ /**
599
+ * Set the early head content (entry script + CSS).
600
+ *
601
+ * Also strips these assets from the original head content to avoid duplicates,
602
+ * since we're moving them to the early phase.
603
+ *
604
+ * Automatically prepends critical meta tags (charset, viewport) if not present
605
+ * in $head configuration, ensuring they're sent as early as possible.
606
+ *
607
+ * @param content - HTML string with entry assets
608
+ * @param globalHead - Global head configuration from $head primitives
609
+ * @param entryAssets - Entry asset paths to strip from original head
610
+ */
611
+ public setEarlyHeadContent(
612
+ content: string,
613
+ globalHead?: SimpleHead,
614
+ entryAssets?: { js?: string; css: string[] },
615
+ ): void {
616
+ // Build early content with critical meta tags first
617
+ const criticalMeta: string[] = [];
618
+
619
+ // Add charset - use custom value from $head or default to UTF-8
620
+ const charset = globalHead?.charset ?? "UTF-8";
621
+ criticalMeta.push(`<meta charset="${this.escapeHtml(charset)}">`);
622
+
623
+ // Add viewport - use custom value from $head or default
624
+ const viewport =
625
+ globalHead?.viewport ?? "width=device-width, initial-scale=1";
626
+ criticalMeta.push(
627
+ `<meta name="viewport" content="${this.escapeHtml(viewport)}">`,
628
+ );
629
+
630
+ // Prepend critical meta tags before entry assets
631
+ this.earlyHeadContent =
632
+ criticalMeta.length > 0
633
+ ? `${criticalMeta.join("\n")}\n${content}`
634
+ : content;
635
+
636
+ // Strip entry assets from original head content to avoid duplicates
637
+ if (entryAssets && this.slots) {
638
+ let headContent = this.slots.headOriginalContent;
639
+
640
+ // Remove entry script tag
641
+ if (entryAssets.js) {
642
+ // Match script tag with this src (handles various attribute orders)
643
+ const scriptPattern = new RegExp(
644
+ `<script[^>]*\\ssrc=["']${this.escapeRegExp(entryAssets.js)}["'][^>]*>\\s*</script>\\s*`,
645
+ "gi",
646
+ );
647
+ headContent = headContent.replace(scriptPattern, "");
648
+ }
649
+
650
+ // Remove entry CSS link tags
651
+ for (const css of entryAssets.css) {
652
+ const linkPattern = new RegExp(
653
+ `<link[^>]*\\shref=["']${this.escapeRegExp(css)}["'][^>]*>\\s*`,
654
+ "gi",
655
+ );
656
+ headContent = headContent.replace(linkPattern, "");
657
+ }
658
+
659
+ this.slots.headOriginalContent = headContent.trim();
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Escape special regex characters in a string.
665
+ */
666
+ protected escapeRegExp(str: string): string {
667
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
668
+ }
669
+
670
+ /**
671
+ * Create an optimized HTML stream with early head streaming.
672
+ *
673
+ * This version sends critical assets (entry.js, CSS) BEFORE page loaders run,
674
+ * allowing the browser to start downloading them immediately.
675
+ *
676
+ * Flow:
677
+ * 1. Send DOCTYPE, <html>, <head> open, entry preloads (IMMEDIATE)
678
+ * 2. Run async work (createLayers, etc.)
679
+ * 3. Send rest of head, body, React content, hydration
680
+ *
681
+ * @param globalHead - Global head with htmlAttributes (from $head primitives)
682
+ * @param asyncWork - Async function to run between early head and rest of stream
683
+ * @param options - Streaming options
684
+ */
685
+ public createEarlyHtmlStream(
686
+ globalHead: SimpleHead,
687
+ asyncWork: () => Promise<
688
+ | {
689
+ state: ReactRouterState;
690
+ reactStream: ReadableStream<Uint8Array>;
691
+ }
692
+ | { redirect: string }
693
+ | null
694
+ >,
695
+ options: {
696
+ hydration?: boolean;
697
+ onError?: (error: unknown) => void;
698
+ } = {},
699
+ ): ReadableStream<Uint8Array> {
700
+ const { hydration = true, onError } = options;
701
+ const slots = this.getSlots();
702
+ const encoder = this.encoder;
703
+
704
+ // Track streaming state for error recovery
705
+ let headClosed = false;
706
+ let bodyStarted = false;
707
+ let routerState: ReactRouterState | undefined;
708
+
709
+ return new ReadableStream<Uint8Array>({
710
+ start: async (controller) => {
711
+ try {
712
+ // === EARLY PHASE (before async work) ===
713
+
714
+ // DOCTYPE
715
+ controller.enqueue(slots.doctype);
716
+
717
+ // <html ...> with global htmlAttributes only
718
+ controller.enqueue(slots.htmlOpen);
719
+ controller.enqueue(
720
+ encoder.encode(
721
+ this.renderMergedHtmlAttrs(globalHead?.htmlAttributes),
722
+ ),
723
+ );
724
+ controller.enqueue(slots.htmlClose);
725
+
726
+ // <head> open + entry preloads
727
+ controller.enqueue(slots.headOpen);
728
+ if (this.earlyHeadContent) {
729
+ controller.enqueue(encoder.encode(this.earlyHeadContent));
730
+ }
731
+
732
+ // === ASYNC WORK (createLayers, etc.) ===
733
+ const result = await asyncWork();
734
+
735
+ // Handle redirect - inject meta refresh since headers already sent
736
+ if (!result || "redirect" in result) {
737
+ if (result && "redirect" in result) {
738
+ this.log.debug(
739
+ "Loader redirect detected after streaming started, using meta refresh",
740
+ { redirect: result.redirect },
741
+ );
742
+ controller.enqueue(
743
+ encoder.encode(
744
+ `<meta http-equiv="refresh" content="0; url=${this.escapeHtml(result.redirect)}">\n`,
745
+ ),
746
+ );
747
+ }
748
+ controller.enqueue(slots.headClose);
749
+ controller.enqueue(encoder.encode("<body></body></html>"));
750
+ controller.close();
751
+ return;
752
+ }
753
+
754
+ const { state, reactStream } = result;
755
+ routerState = state;
756
+
757
+ // === LATE PHASE (after async work) ===
758
+
759
+ // Rest of head content (title, meta, links from loaders)
760
+ controller.enqueue(
761
+ encoder.encode(this.renderHeadContent(state.head)),
762
+ );
763
+ controller.enqueue(slots.headClose);
764
+ headClosed = true;
765
+
766
+ // Body content (body, root, React, hydration, closing tags)
767
+ bodyStarted = true;
768
+ await this.streamBodyContent(
769
+ controller,
770
+ reactStream,
771
+ state,
772
+ hydration,
773
+ );
774
+
775
+ controller.close();
776
+ } catch (error) {
777
+ onError?.(error);
778
+
779
+ // Instead of aborting the stream, inject error HTML so user sees
780
+ // an error message instead of white screen.
781
+ // React 19 streaming SSR doesn't reliably trigger ErrorBoundary,
782
+ // so we must handle it at the stream level.
783
+ try {
784
+ this.injectErrorHtml(
785
+ controller,
786
+ encoder,
787
+ slots,
788
+ error,
789
+ routerState,
790
+ { headClosed, bodyStarted },
791
+ );
792
+ controller.close();
793
+ } catch {
794
+ // If error injection fails, abort as last resort
795
+ controller.error(error);
796
+ }
797
+ }
798
+ },
799
+ });
800
+ }
801
+
802
+ /**
803
+ * Inject error HTML into the stream when an error occurs during streaming.
804
+ *
805
+ * Uses the router state's onError handler to render the error component,
806
+ * falling back to ErrorViewer if no custom handler is defined.
807
+ * Renders using renderToString to produce static HTML.
808
+ *
809
+ * Since we may have already sent partial HTML (DOCTYPE, <html>, <head>),
810
+ * we need to complete the document with an error message instead of aborting.
811
+ *
812
+ * Handles different states:
813
+ * - headClosed=false, bodyStarted=false: Need to add head content, close head, open body, add error, close all
814
+ * - headClosed=true, bodyStarted=false: Need to open body, add error, close all
815
+ * - headClosed=true, bodyStarted=true: Already inside root div, add error, close all
816
+ */
817
+ protected injectErrorHtml(
818
+ controller: ReadableStreamDefaultController<Uint8Array>,
819
+ encoder: TextEncoder,
820
+ slots: TemplateSlots,
821
+ error: unknown,
822
+ routerState: ReactRouterState | undefined,
823
+ streamState: { headClosed: boolean; bodyStarted: boolean },
824
+ ): void {
825
+ // If head not closed, add remaining head content first
826
+ if (!streamState.headClosed) {
827
+ // Include original head content (CSS, scripts) and any head from router state
828
+ const headContent = this.renderHeadContent(routerState?.head);
829
+ if (headContent) {
830
+ controller.enqueue(encoder.encode(headContent));
831
+ }
832
+ controller.enqueue(slots.headClose);
833
+ }
834
+
835
+ // If body hasn't started, we need to open body and root div
836
+ if (!streamState.bodyStarted) {
837
+ // Open body with any body attributes from state
838
+ controller.enqueue(slots.bodyOpen);
839
+ controller.enqueue(
840
+ encoder.encode(
841
+ this.renderMergedBodyAttrs(routerState?.head?.bodyAttributes),
842
+ ),
843
+ );
844
+ controller.enqueue(slots.bodyClose);
845
+
846
+ // Content before root (if any)
847
+ if (slots.beforeRoot) {
848
+ controller.enqueue(encoder.encode(slots.beforeRoot));
849
+ }
850
+
851
+ controller.enqueue(slots.rootOpen);
852
+ }
853
+
854
+ // Try to render error using router state's error handler
855
+ const errorHtml = this.renderErrorToString(
856
+ error instanceof Error ? error : new Error(String(error)),
857
+ routerState,
858
+ );
859
+
860
+ controller.enqueue(encoder.encode(errorHtml));
861
+
862
+ // Close root div
863
+ controller.enqueue(slots.rootClose);
864
+
865
+ // Content after root (if any)
866
+ if (!streamState.bodyStarted && slots.afterRoot) {
867
+ controller.enqueue(encoder.encode(slots.afterRoot));
868
+ }
869
+
870
+ // Close document
871
+ controller.enqueue(slots.scriptClose);
872
+ }
873
+
874
+ /**
875
+ * Render an error to HTML string using the router's error handler.
876
+ *
877
+ * Falls back to ErrorViewer if:
878
+ * - No router state is available
879
+ * - The error handler returns null/undefined
880
+ * - The error handler itself throws
881
+ */
882
+ protected renderErrorToString(
883
+ error: Error,
884
+ routerState: ReactRouterState | undefined,
885
+ ): string {
886
+ // Log the error with stack trace for debugging
887
+ this.log.error("SSR rendering error", error);
888
+
889
+ let errorElement: ReactNode;
890
+
891
+ // Try to use the router state's error handler
892
+ if (routerState?.onError) {
893
+ try {
894
+ const result = routerState.onError(error, routerState);
895
+
896
+ // If handler returns a Redirection, we can't handle it (headers already sent)
897
+ // Log and fall through to default error viewer
898
+ if (result instanceof Redirection) {
899
+ this.log.warn(
900
+ "Error handler returned Redirection but headers already sent",
901
+ { redirect: result.redirect },
902
+ );
903
+ } else if (result !== null && result !== undefined) {
904
+ errorElement = result;
905
+ }
906
+ } catch (handlerError) {
907
+ this.log.error("Error handler threw an exception", handlerError);
908
+ // Fall through to default error viewer
909
+ }
910
+ }
911
+
912
+ // Fall back to ErrorViewer if no element was produced
913
+ if (!errorElement) {
914
+ errorElement = createElement(ErrorViewer, {
915
+ error,
916
+ alepha: this.alepha,
917
+ });
918
+ }
919
+
920
+ // Wrap in AlephaContext.Provider so any components that need it can access it
921
+ const wrappedElement = createElement(
922
+ AlephaContext.Provider,
923
+ { value: this.alepha },
924
+ errorElement,
925
+ );
926
+
927
+ try {
928
+ return renderToString(wrappedElement);
929
+ } catch (renderError) {
930
+ // If renderToString fails, return minimal fallback HTML
931
+ this.log.error("Failed to render error component", renderError);
932
+ return error.message;
933
+ }
934
+ }
935
+ }
936
+
937
+ // ---------------------------------------------------------------------------------------------------------------------
938
+
939
+ /**
940
+ * Template slots - the template split into logical parts for efficient streaming.
941
+ *
942
+ * Static parts are pre-encoded as Uint8Array for zero-copy streaming.
943
+ * Dynamic parts (attributes, head content) are kept as strings/objects for merging.
944
+ */
945
+ export interface TemplateSlots {
946
+ // Pre-encoded static parts
947
+ doctype: Uint8Array;
948
+ htmlOpen: Uint8Array; // "<html"
949
+ htmlClose: Uint8Array; // ">"
950
+ headOpen: Uint8Array; // "<head>"
951
+ headClose: Uint8Array; // "</head>"
952
+ bodyOpen: Uint8Array; // "<body"
953
+ bodyClose: Uint8Array; // ">"
954
+ rootOpen: Uint8Array; // '<div id="root">'
955
+ rootClose: Uint8Array; // "</div>"
956
+ scriptClose: Uint8Array; // "</body></html>"
957
+
958
+ // Original content (kept for merging)
959
+ htmlOriginalAttrs: Record<string, string>;
960
+ bodyOriginalAttrs: Record<string, string>;
961
+ headOriginalContent: string;
962
+ beforeRoot: string; // content between <body> and root div
963
+ afterRoot: string; // content between root div and </body>
964
+ }
965
+
966
+ /**
967
+ * Hydration state that gets serialized to window.__ssr
968
+ */
969
+ export interface HydrationData {
970
+ layers: Array<{
971
+ data?: unknown;
972
+ error?: {
973
+ name: string;
974
+ message: string;
975
+ stack?: string;
976
+ };
977
+ }>;
978
+ [key: string]: unknown;
979
+ }