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,203 @@
1
+ import { Alepha } from "alepha";
2
+ import { describe, it } from "vitest";
3
+ import { SeoExpander } from "./SeoExpander.ts";
4
+
5
+ describe("SeoExpander", () => {
6
+ it("should expand basic SEO configuration", ({ expect }) => {
7
+ const alepha = Alepha.create();
8
+ const seoExpander = alepha.inject(SeoExpander);
9
+
10
+ const result = seoExpander.expand({
11
+ title: "My App",
12
+ description: "Build amazing apps",
13
+ url: "https://example.com/",
14
+ });
15
+
16
+ expect(result.meta).toContainEqual({
17
+ name: "description",
18
+ content: "Build amazing apps",
19
+ });
20
+ expect(result.meta).toContainEqual({
21
+ property: "og:title",
22
+ content: "My App",
23
+ });
24
+ expect(result.meta).toContainEqual({
25
+ property: "og:description",
26
+ content: "Build amazing apps",
27
+ });
28
+ expect(result.meta).toContainEqual({
29
+ property: "og:url",
30
+ content: "https://example.com/",
31
+ });
32
+ expect(result.meta).toContainEqual({
33
+ property: "og:type",
34
+ content: "website",
35
+ });
36
+ expect(result.meta).toContainEqual({
37
+ name: "twitter:title",
38
+ content: "My App",
39
+ });
40
+ expect(result.meta).toContainEqual({
41
+ name: "twitter:description",
42
+ content: "Build amazing apps",
43
+ });
44
+ expect(result.link).toContainEqual({
45
+ rel: "canonical",
46
+ href: "https://example.com/",
47
+ });
48
+ });
49
+
50
+ it("should expand full SEO configuration with image", ({ expect }) => {
51
+ const alepha = Alepha.create();
52
+ const seoExpander = alepha.inject(SeoExpander);
53
+
54
+ const result = seoExpander.expand({
55
+ title: "Alepha Framework",
56
+ description: "TypeScript framework made easy",
57
+ image: "https://alepha.dev/og-image.png",
58
+ url: "https://alepha.dev/",
59
+ siteName: "Alepha",
60
+ locale: "en_US",
61
+ type: "website",
62
+ imageWidth: 1200,
63
+ imageHeight: 630,
64
+ imageAlt: "Alepha Framework Logo",
65
+ });
66
+
67
+ // OpenGraph tags
68
+ expect(result.meta).toContainEqual({
69
+ property: "og:image",
70
+ content: "https://alepha.dev/og-image.png",
71
+ });
72
+ expect(result.meta).toContainEqual({
73
+ property: "og:image:width",
74
+ content: "1200",
75
+ });
76
+ expect(result.meta).toContainEqual({
77
+ property: "og:image:height",
78
+ content: "630",
79
+ });
80
+ expect(result.meta).toContainEqual({
81
+ property: "og:image:alt",
82
+ content: "Alepha Framework Logo",
83
+ });
84
+ expect(result.meta).toContainEqual({
85
+ property: "og:site_name",
86
+ content: "Alepha",
87
+ });
88
+ expect(result.meta).toContainEqual({
89
+ property: "og:locale",
90
+ content: "en_US",
91
+ });
92
+
93
+ // Twitter tags
94
+ expect(result.meta).toContainEqual({
95
+ name: "twitter:image",
96
+ content: "https://alepha.dev/og-image.png",
97
+ });
98
+ expect(result.meta).toContainEqual({
99
+ name: "twitter:image:alt",
100
+ content: "Alepha Framework Logo",
101
+ });
102
+ // With image, default to summary_large_image
103
+ expect(result.meta).toContainEqual({
104
+ name: "twitter:card",
105
+ content: "summary_large_image",
106
+ });
107
+ });
108
+
109
+ it("should support Twitter-specific overrides", ({ expect }) => {
110
+ const alepha = Alepha.create();
111
+ const seoExpander = alepha.inject(SeoExpander);
112
+
113
+ const result = seoExpander.expand({
114
+ title: "Base Title",
115
+ description: "Base description",
116
+ twitter: {
117
+ card: "summary",
118
+ site: "@alepha_dev",
119
+ creator: "@johndoe",
120
+ title: "Twitter-specific Title",
121
+ },
122
+ });
123
+
124
+ expect(result.meta).toContainEqual({
125
+ name: "twitter:card",
126
+ content: "summary",
127
+ });
128
+ expect(result.meta).toContainEqual({
129
+ name: "twitter:site",
130
+ content: "@alepha_dev",
131
+ });
132
+ expect(result.meta).toContainEqual({
133
+ name: "twitter:creator",
134
+ content: "@johndoe",
135
+ });
136
+ expect(result.meta).toContainEqual({
137
+ name: "twitter:title",
138
+ content: "Twitter-specific Title",
139
+ });
140
+ // OG should still use base title
141
+ expect(result.meta).toContainEqual({
142
+ property: "og:title",
143
+ content: "Base Title",
144
+ });
145
+ });
146
+
147
+ it("should support OpenGraph-specific overrides", ({ expect }) => {
148
+ const alepha = Alepha.create();
149
+ const seoExpander = alepha.inject(SeoExpander);
150
+
151
+ const result = seoExpander.expand({
152
+ title: "Base Title",
153
+ description: "Base description",
154
+ og: {
155
+ title: "OG-specific Title",
156
+ description: "OG-specific description",
157
+ },
158
+ });
159
+
160
+ expect(result.meta).toContainEqual({
161
+ property: "og:title",
162
+ content: "OG-specific Title",
163
+ });
164
+ expect(result.meta).toContainEqual({
165
+ property: "og:description",
166
+ content: "OG-specific description",
167
+ });
168
+ // Twitter should use base
169
+ expect(result.meta).toContainEqual({
170
+ name: "twitter:title",
171
+ content: "Base Title",
172
+ });
173
+ expect(result.meta).toContainEqual({
174
+ name: "twitter:description",
175
+ content: "Base description",
176
+ });
177
+ });
178
+
179
+ it("should handle article type", ({ expect }) => {
180
+ const alepha = Alepha.create();
181
+ const seoExpander = alepha.inject(SeoExpander);
182
+
183
+ const result = seoExpander.expand({
184
+ title: "Blog Post",
185
+ type: "article",
186
+ });
187
+
188
+ expect(result.meta).toContainEqual({
189
+ property: "og:type",
190
+ content: "article",
191
+ });
192
+ });
193
+
194
+ it("should return empty arrays for empty config", ({ expect }) => {
195
+ const alepha = Alepha.create();
196
+ const seoExpander = alepha.inject(SeoExpander);
197
+
198
+ const result = seoExpander.expand({});
199
+
200
+ expect(result.meta).toEqual([]);
201
+ expect(result.link).toEqual([]);
202
+ });
203
+ });
@@ -0,0 +1,142 @@
1
+ import type { Head, HeadMeta } from "../interfaces/Head.ts";
2
+
3
+ /**
4
+ * Expands Head configuration into SEO meta tags.
5
+ *
6
+ * Generates:
7
+ * - `<meta name="description">` from head.description
8
+ * - `<meta property="og:*">` OpenGraph tags
9
+ * - `<meta name="twitter:*">` Twitter Card tags
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * const helper = new SeoExpander();
14
+ * const { meta, link } = helper.expand({
15
+ * title: "My App",
16
+ * description: "Build amazing apps",
17
+ * image: "https://example.com/og.png",
18
+ * url: "https://example.com/",
19
+ * });
20
+ * ```
21
+ */
22
+ export class SeoExpander {
23
+ public expand(head: Head): {
24
+ meta: HeadMeta[];
25
+ link: Array<{ rel: string; href: string }>;
26
+ } {
27
+ const meta: HeadMeta[] = [];
28
+ const link: Array<{ rel: string; href: string }> = [];
29
+
30
+ // Only expand SEO if there's meaningful content beyond just title
31
+ const hasSeoContent =
32
+ head.description ||
33
+ head.image ||
34
+ head.url ||
35
+ head.siteName ||
36
+ head.locale ||
37
+ head.type ||
38
+ head.og ||
39
+ head.twitter;
40
+
41
+ if (!hasSeoContent) {
42
+ return { meta, link };
43
+ }
44
+
45
+ // Base description
46
+ if (head.description) {
47
+ meta.push({ name: "description", content: head.description });
48
+ }
49
+
50
+ // Canonical URL
51
+ if (head.url) {
52
+ link.push({ rel: "canonical", href: head.url });
53
+ }
54
+
55
+ // OpenGraph tags
56
+ this.expandOpenGraph(head, meta);
57
+
58
+ // Twitter Card tags
59
+ this.expandTwitter(head, meta);
60
+
61
+ return { meta, link };
62
+ }
63
+
64
+ protected expandOpenGraph(head: Head, meta: HeadMeta[]): void {
65
+ const ogTitle = head.og?.title ?? head.title;
66
+ const ogDescription = head.og?.description ?? head.description;
67
+ const ogImage = head.og?.image ?? head.image;
68
+
69
+ if (head.type || ogTitle) {
70
+ meta.push({ property: "og:type", content: head.type ?? "website" });
71
+ }
72
+ if (head.url) {
73
+ meta.push({ property: "og:url", content: head.url });
74
+ }
75
+ if (ogTitle) {
76
+ meta.push({ property: "og:title", content: ogTitle });
77
+ }
78
+ if (ogDescription) {
79
+ meta.push({ property: "og:description", content: ogDescription });
80
+ }
81
+ if (ogImage) {
82
+ meta.push({ property: "og:image", content: ogImage });
83
+ if (head.imageWidth) {
84
+ meta.push({
85
+ property: "og:image:width",
86
+ content: String(head.imageWidth),
87
+ });
88
+ }
89
+ if (head.imageHeight) {
90
+ meta.push({
91
+ property: "og:image:height",
92
+ content: String(head.imageHeight),
93
+ });
94
+ }
95
+ if (head.imageAlt) {
96
+ meta.push({ property: "og:image:alt", content: head.imageAlt });
97
+ }
98
+ }
99
+ if (head.siteName) {
100
+ meta.push({ property: "og:site_name", content: head.siteName });
101
+ }
102
+ if (head.locale) {
103
+ meta.push({ property: "og:locale", content: head.locale });
104
+ }
105
+ }
106
+
107
+ protected expandTwitter(head: Head, meta: HeadMeta[]): void {
108
+ const twitterTitle = head.twitter?.title ?? head.title;
109
+ const twitterDescription = head.twitter?.description ?? head.description;
110
+ const twitterImage = head.twitter?.image ?? head.image;
111
+
112
+ if (head.twitter?.card || twitterTitle || twitterImage) {
113
+ meta.push({
114
+ name: "twitter:card",
115
+ content:
116
+ head.twitter?.card ??
117
+ (twitterImage ? "summary_large_image" : "summary"),
118
+ });
119
+ }
120
+ if (head.url) {
121
+ meta.push({ name: "twitter:url", content: head.url });
122
+ }
123
+ if (twitterTitle) {
124
+ meta.push({ name: "twitter:title", content: twitterTitle });
125
+ }
126
+ if (twitterDescription) {
127
+ meta.push({ name: "twitter:description", content: twitterDescription });
128
+ }
129
+ if (twitterImage) {
130
+ meta.push({ name: "twitter:image", content: twitterImage });
131
+ if (head.imageAlt) {
132
+ meta.push({ name: "twitter:image:alt", content: head.imageAlt });
133
+ }
134
+ }
135
+ if (head.twitter?.site) {
136
+ meta.push({ name: "twitter:site", content: head.twitter.site });
137
+ }
138
+ if (head.twitter?.creator) {
139
+ meta.push({ name: "twitter:creator", content: head.twitter.creator });
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,288 @@
1
+ import { render } from "@testing-library/react";
2
+ import { Alepha } from "alepha";
3
+ import { AlephaContext } from "alepha/react";
4
+ import { act, type ReactNode } from "react";
5
+ import { beforeEach, describe, it, vi } from "vitest";
6
+ import type { Head } from "../index.ts";
7
+ import { useHead } from "../index.ts";
8
+
9
+ /**
10
+ * @vitest-environment jsdom
11
+ */
12
+
13
+ describe("useHead", () => {
14
+ const renderWithAlepha = (alepha: Alepha, element: ReactNode) => {
15
+ return render(
16
+ <AlephaContext.Provider value={alepha}>{element}</AlephaContext.Provider>,
17
+ );
18
+ };
19
+
20
+ beforeEach(() => {
21
+ // Reset document state before each test
22
+ document.title = "";
23
+ document.head.innerHTML = "";
24
+ document.body.removeAttribute("class");
25
+ document.body.removeAttribute("style");
26
+ document.documentElement.removeAttribute("lang");
27
+ document.documentElement.removeAttribute("class");
28
+ });
29
+
30
+ it("should set initial head options on mount", ({ expect }) => {
31
+ const alepha = Alepha.create();
32
+ const TestComponent = () => {
33
+ useHead({
34
+ title: "Test Title",
35
+ });
36
+ return <div>Test</div>;
37
+ };
38
+
39
+ renderWithAlepha(alepha, <TestComponent />);
40
+
41
+ expect(document.title).toBe("Test Title");
42
+ });
43
+
44
+ it("should return current head state and setter function", ({ expect }) => {
45
+ const alepha = Alepha.create();
46
+ let headState: Head;
47
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
48
+
49
+ const TestComponent = () => {
50
+ const [head, setHead] = useHead();
51
+ headState = head;
52
+ setHeadFn = setHead;
53
+ return <div>Test</div>;
54
+ };
55
+
56
+ renderWithAlepha(alepha, <TestComponent />);
57
+
58
+ expect(headState!).toBeDefined();
59
+ expect(setHeadFn!).toBeDefined();
60
+ expect(typeof setHeadFn!).toBe("function");
61
+ });
62
+
63
+ it("should update document title when setHead is called", ({ expect }) => {
64
+ const alepha = Alepha.create();
65
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
66
+
67
+ const TestComponent = () => {
68
+ const [, setHead] = useHead();
69
+ setHeadFn = setHead;
70
+ return <div>Test</div>;
71
+ };
72
+
73
+ renderWithAlepha(alepha, <TestComponent />);
74
+
75
+ act(() => {
76
+ setHeadFn({ title: "Updated Title" });
77
+ });
78
+
79
+ expect(document.title).toBe("Updated Title");
80
+ });
81
+
82
+ it("should update meta tags when setHead is called", ({ expect }) => {
83
+ const alepha = Alepha.create();
84
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
85
+
86
+ const TestComponent = () => {
87
+ const [, setHead] = useHead();
88
+ setHeadFn = setHead;
89
+ return <div>Test</div>;
90
+ };
91
+
92
+ renderWithAlepha(alepha, <TestComponent />);
93
+
94
+ act(() => {
95
+ setHeadFn({
96
+ meta: [
97
+ { name: "description", content: "Test Description" },
98
+ { name: "keywords", content: "test, keywords" },
99
+ ],
100
+ });
101
+ });
102
+
103
+ const descriptionMeta = document.querySelector('meta[name="description"]');
104
+ const keywordsMeta = document.querySelector('meta[name="keywords"]');
105
+
106
+ expect(descriptionMeta?.getAttribute("content")).toBe("Test Description");
107
+ expect(keywordsMeta?.getAttribute("content")).toBe("test, keywords");
108
+ });
109
+
110
+ it("should update body attributes when setHead is called", ({ expect }) => {
111
+ const alepha = Alepha.create();
112
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
113
+
114
+ const TestComponent = () => {
115
+ const [, setHead] = useHead();
116
+ setHeadFn = setHead;
117
+ return <div>Test</div>;
118
+ };
119
+
120
+ renderWithAlepha(alepha, <TestComponent />);
121
+
122
+ act(() => {
123
+ setHeadFn({
124
+ bodyAttributes: {
125
+ class: "dark-theme",
126
+ style: "background: black;",
127
+ },
128
+ });
129
+ });
130
+
131
+ expect(document.body.getAttribute("class")).toBe("dark-theme");
132
+ expect(document.body.getAttribute("style")).toBe("background: black;");
133
+ });
134
+
135
+ it("should update html attributes when setHead is called", ({ expect }) => {
136
+ const alepha = Alepha.create();
137
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
138
+
139
+ const TestComponent = () => {
140
+ const [, setHead] = useHead();
141
+ setHeadFn = setHead;
142
+ return <div>Test</div>;
143
+ };
144
+
145
+ renderWithAlepha(alepha, <TestComponent />);
146
+
147
+ act(() => {
148
+ setHeadFn({
149
+ htmlAttributes: {
150
+ lang: "en",
151
+ class: "no-js",
152
+ },
153
+ });
154
+ });
155
+
156
+ expect(document.documentElement.getAttribute("lang")).toBe("en");
157
+ expect(document.documentElement.getAttribute("class")).toBe("no-js");
158
+ });
159
+
160
+ it("should support functional updates", ({ expect }) => {
161
+ const alepha = Alepha.create();
162
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
163
+
164
+ const TestComponent = () => {
165
+ const [, setHead] = useHead({ title: "Initial Title" });
166
+ setHeadFn = setHead;
167
+ return <div>Test</div>;
168
+ };
169
+
170
+ renderWithAlepha(alepha, <TestComponent />);
171
+
172
+ expect(document.title).toBe("Initial Title");
173
+
174
+ act(() => {
175
+ setHeadFn((prev) => ({
176
+ ...prev,
177
+ title: `${prev?.title} - Updated`,
178
+ }));
179
+ });
180
+
181
+ expect(document.title).toBe("Initial Title - Updated");
182
+ });
183
+
184
+ it("should handle multiple head updates", ({ expect }) => {
185
+ const alepha = Alepha.create();
186
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
187
+
188
+ const TestComponent = () => {
189
+ const [, setHead] = useHead();
190
+ setHeadFn = setHead;
191
+ return <div>Test</div>;
192
+ };
193
+
194
+ renderWithAlepha(alepha, <TestComponent />);
195
+
196
+ act(() => {
197
+ setHeadFn({
198
+ title: "First Title",
199
+ meta: [{ name: "description", content: "First Description" }],
200
+ });
201
+ });
202
+
203
+ expect(document.title).toBe("First Title");
204
+ expect(
205
+ document
206
+ .querySelector('meta[name="description"]')
207
+ ?.getAttribute("content"),
208
+ ).toBe("First Description");
209
+
210
+ act(() => {
211
+ setHeadFn({
212
+ title: "Second Title",
213
+ meta: [{ name: "description", content: "Second Description" }],
214
+ });
215
+ });
216
+
217
+ expect(document.title).toBe("Second Title");
218
+ expect(
219
+ document
220
+ .querySelector('meta[name="description"]')
221
+ ?.getAttribute("content"),
222
+ ).toBe("Second Description");
223
+ });
224
+
225
+ it("should not crash on server-side (non-browser environment)", ({
226
+ expect,
227
+ }) => {
228
+ const alepha = Alepha.create();
229
+ // Mock isBrowser to return false
230
+ vi.spyOn(alepha, "isBrowser").mockReturnValue(false);
231
+
232
+ let headState: Head;
233
+ let setHeadFn: (head?: Head | ((previous?: Head) => Head)) => void;
234
+
235
+ const TestComponent = () => {
236
+ const [head, setHead] = useHead({ title: "Server Title" });
237
+ headState = head;
238
+ setHeadFn = setHead;
239
+ return <div>Test</div>;
240
+ };
241
+
242
+ expect(() => {
243
+ renderWithAlepha(alepha, <TestComponent />);
244
+ }).not.toThrow();
245
+
246
+ expect(headState!).toEqual({});
247
+
248
+ // setHead should not crash on server
249
+ expect(() => {
250
+ setHeadFn({ title: "New Title" });
251
+ }).not.toThrow();
252
+
253
+ // Document title should remain unchanged on server
254
+ expect(document.title).toBe("");
255
+ });
256
+
257
+ it("should get current head state from document", ({ expect }) => {
258
+ const alepha = Alepha.create();
259
+
260
+ // Pre-populate document with some head data
261
+ document.title = "Existing Title";
262
+ document.body.setAttribute("class", "existing-class");
263
+ document.documentElement.setAttribute("lang", "fr");
264
+
265
+ const meta = document.createElement("meta");
266
+ meta.setAttribute("name", "author");
267
+ meta.setAttribute("content", "John Doe");
268
+ document.head.appendChild(meta);
269
+
270
+ let headState: Head;
271
+
272
+ const TestComponent = () => {
273
+ const [head] = useHead();
274
+ headState = head;
275
+ return <div>Test</div>;
276
+ };
277
+
278
+ renderWithAlepha(alepha, <TestComponent />);
279
+
280
+ expect(headState!.title).toBe("Existing Title");
281
+ expect(headState!.bodyAttributes?.class).toBe("existing-class");
282
+ expect(headState!.htmlAttributes?.lang).toBe("fr");
283
+ expect(headState!.meta).toContainEqual({
284
+ name: "author",
285
+ content: "John Doe",
286
+ });
287
+ });
288
+ });
@@ -0,0 +1,62 @@
1
+ import { Alepha } from "alepha";
2
+ import { useInject } from "alepha/react";
3
+ import { useCallback, useEffect, useMemo } from "react";
4
+ import type { Head } from "../interfaces/Head.ts";
5
+ import { BrowserHeadProvider } from "../providers/BrowserHeadProvider.ts";
6
+
7
+ /**
8
+ * ```tsx
9
+ * const App = () => {
10
+ * const [head, setHead] = useHead({
11
+ * // will set the document title on the first render
12
+ * title: "My App",
13
+ * });
14
+ *
15
+ * return (
16
+ * // This will update the document title when the button is clicked
17
+ * <button onClick={() => setHead({ title: "Change Title" })}>
18
+ * Change Title {head.title}
19
+ * </button>
20
+ * );
21
+ * }
22
+ * ```
23
+ */
24
+ export const useHead = (options?: UseHeadOptions): UseHeadReturn => {
25
+ const alepha = useInject(Alepha);
26
+
27
+ const current = useMemo(() => {
28
+ if (!alepha.isBrowser()) {
29
+ return {};
30
+ }
31
+
32
+ return alepha.inject(BrowserHeadProvider).getHead(window.document);
33
+ }, []);
34
+
35
+ const setHead = useCallback((head?: Head | ((previous?: Head) => Head)) => {
36
+ if (!alepha.isBrowser()) {
37
+ return;
38
+ }
39
+
40
+ alepha
41
+ .inject(BrowserHeadProvider)
42
+ .renderHead(
43
+ window.document,
44
+ typeof head === "function" ? head(current) : head || {},
45
+ );
46
+ }, []);
47
+
48
+ useEffect(() => {
49
+ if (options) {
50
+ setHead(options);
51
+ }
52
+ }, []);
53
+
54
+ return [current, setHead];
55
+ };
56
+
57
+ export type UseHeadOptions = Head | ((previous?: Head) => Head);
58
+
59
+ export type UseHeadReturn = [
60
+ Head,
61
+ (head?: Head | ((previous?: Head) => Head)) => void,
62
+ ];