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.
- package/README.md +68 -80
- package/dist/api/audits/index.d.ts +10 -33
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +10 -33
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts +10 -3
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +10 -3
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.d.ts +162 -155
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +10 -3
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.d.ts +413 -0
- package/dist/api/keys/index.d.ts.map +1 -0
- package/dist/api/keys/index.js +476 -0
- package/dist/api/keys/index.js.map +1 -0
- package/dist/api/notifications/index.d.ts +10 -4
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +10 -4
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/parameters/index.d.ts +43 -50
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +30 -37
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/users/index.d.ts +1081 -760
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +2539 -218
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts +138 -132
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +12 -4
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/batch/index.d.ts +20 -40
- package/dist/batch/index.d.ts.map +1 -1
- package/dist/batch/index.js +31 -44
- package/dist/batch/index.js.map +1 -1
- package/dist/bucket/index.d.ts +440 -8
- package/dist/bucket/index.d.ts.map +1 -1
- package/dist/bucket/index.js +1861 -12
- package/dist/bucket/index.js.map +1 -1
- package/dist/cache/core/index.d.ts +179 -7
- package/dist/cache/core/index.d.ts.map +1 -1
- package/dist/cache/core/index.js +213 -7
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/redis/index.d.ts +1 -0
- package/dist/cache/redis/index.d.ts.map +1 -1
- package/dist/cache/redis/index.js +4 -0
- package/dist/cache/redis/index.js.map +1 -1
- package/dist/cli/index.d.ts +638 -5645
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2550 -368
- package/dist/cli/index.js.map +1 -1
- package/dist/command/index.d.ts +203 -45
- package/dist/command/index.d.ts.map +1 -1
- package/dist/command/index.js +2060 -71
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +70 -40
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +34 -13
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +90 -40
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +70 -40
- package/dist/core/index.native.js.map +1 -1
- package/dist/datetime/index.d.ts +15 -0
- package/dist/datetime/index.d.ts.map +1 -1
- package/dist/datetime/index.js +15 -0
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/index.d.ts +323 -20
- package/dist/email/index.d.ts.map +1 -1
- package/dist/email/index.js +1857 -7
- package/dist/email/index.js.map +1 -1
- package/dist/fake/index.d.ts +90 -8
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +91 -20
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.d.ts +11 -4
- package/dist/lock/core/index.d.ts.map +1 -1
- package/dist/lock/core/index.js +11 -4
- package/dist/lock/core/index.js.map +1 -1
- package/dist/logger/index.d.ts +17 -66
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js +14 -63
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.d.ts +10 -30
- package/dist/mcp/index.d.ts.map +1 -1
- package/dist/mcp/index.js +12 -35
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/index.browser.js +3 -3
- package/dist/orm/index.browser.js.map +1 -1
- package/dist/orm/index.bun.js +39 -20
- package/dist/orm/index.bun.js.map +1 -1
- package/dist/orm/index.d.ts +517 -540
- package/dist/orm/index.d.ts.map +1 -1
- package/dist/orm/index.js +58 -71
- package/dist/orm/index.js.map +1 -1
- package/dist/queue/core/index.d.ts +18 -10
- package/dist/queue/core/index.d.ts.map +1 -1
- package/dist/queue/core/index.js +14 -6
- package/dist/queue/core/index.js.map +1 -1
- package/dist/react/auth/index.browser.js +108 -0
- package/dist/react/auth/index.browser.js.map +1 -0
- package/dist/react/auth/index.d.ts +100 -0
- package/dist/react/auth/index.d.ts.map +1 -0
- package/dist/react/auth/index.js +145 -0
- package/dist/react/auth/index.js.map +1 -0
- package/dist/react/core/index.d.ts +469 -0
- package/dist/react/core/index.d.ts.map +1 -0
- package/dist/react/core/index.js +464 -0
- package/dist/react/core/index.js.map +1 -0
- package/dist/react/form/index.d.ts +232 -0
- package/dist/react/form/index.d.ts.map +1 -0
- package/dist/react/form/index.js +432 -0
- package/dist/react/form/index.js.map +1 -0
- package/dist/react/head/index.browser.js +423 -0
- package/dist/react/head/index.browser.js.map +1 -0
- package/dist/react/head/index.d.ts +288 -0
- package/dist/react/head/index.d.ts.map +1 -0
- package/dist/react/head/index.js +465 -0
- package/dist/react/head/index.js.map +1 -0
- package/dist/react/i18n/index.d.ts +175 -0
- package/dist/react/i18n/index.d.ts.map +1 -0
- package/dist/react/i18n/index.js +224 -0
- package/dist/react/i18n/index.js.map +1 -0
- package/dist/react/router/index.browser.js +1974 -0
- package/dist/react/router/index.browser.js.map +1 -0
- package/dist/react/router/index.d.ts +1956 -0
- package/dist/react/router/index.d.ts.map +1 -0
- package/dist/react/router/index.js +4722 -0
- package/dist/react/router/index.js.map +1 -0
- package/dist/react/websocket/index.d.ts +117 -0
- package/dist/react/websocket/index.d.ts.map +1 -0
- package/dist/react/websocket/index.js +107 -0
- package/dist/react/websocket/index.js.map +1 -0
- package/dist/redis/index.bun.js +4 -0
- package/dist/redis/index.bun.js.map +1 -1
- package/dist/redis/index.d.ts +41 -44
- package/dist/redis/index.d.ts.map +1 -1
- package/dist/redis/index.js +16 -25
- package/dist/redis/index.js.map +1 -1
- package/dist/retry/index.d.ts +11 -2
- package/dist/retry/index.d.ts.map +1 -1
- package/dist/retry/index.js +11 -2
- package/dist/retry/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +11 -2
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +11 -2
- package/dist/scheduler/index.js.map +1 -1
- package/dist/security/index.d.ts +140 -49
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +164 -32
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +12 -7
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +12 -7
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cache/index.d.ts +7 -22
- package/dist/server/cache/index.d.ts.map +1 -1
- package/dist/server/cache/index.js +7 -22
- package/dist/server/cache/index.js.map +1 -1
- package/dist/server/compress/index.d.ts +10 -2
- package/dist/server/compress/index.d.ts.map +1 -1
- package/dist/server/compress/index.js +10 -2
- package/dist/server/compress/index.js.map +1 -1
- package/dist/server/cookies/index.d.ts +40 -16
- package/dist/server/cookies/index.d.ts.map +1 -1
- package/dist/server/cookies/index.js +7 -5
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.d.ts +124 -23
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +231 -14
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/cors/index.d.ts +13 -23
- package/dist/server/cors/index.d.ts.map +1 -1
- package/dist/server/cors/index.js +7 -21
- package/dist/server/cors/index.js.map +1 -1
- package/dist/server/health/index.d.ts +8 -2
- package/dist/server/health/index.d.ts.map +1 -1
- package/dist/server/health/index.js +8 -2
- package/dist/server/health/index.js.map +1 -1
- package/dist/server/helmet/index.d.ts +11 -3
- package/dist/server/helmet/index.d.ts.map +1 -1
- package/dist/server/helmet/index.js +11 -3
- package/dist/server/helmet/index.js.map +1 -1
- package/dist/server/links/index.d.ts +11 -6
- package/dist/server/links/index.d.ts.map +1 -1
- package/dist/server/links/index.js +11 -6
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +10 -3
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +10 -3
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/multipart/index.d.ts +9 -3
- package/dist/server/multipart/index.d.ts.map +1 -1
- package/dist/server/multipart/index.js +9 -3
- package/dist/server/multipart/index.js.map +1 -1
- package/dist/server/proxy/index.d.ts +8 -2
- package/dist/server/proxy/index.d.ts.map +1 -1
- package/dist/server/proxy/index.js +8 -2
- package/dist/server/proxy/index.js.map +1 -1
- package/dist/server/rate-limit/index.d.ts +30 -35
- package/dist/server/rate-limit/index.d.ts.map +1 -1
- package/dist/server/rate-limit/index.js +18 -55
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.d.ts +137 -4
- package/dist/server/static/index.d.ts.map +1 -1
- package/dist/server/static/index.js +1853 -5
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.d.ts +309 -6
- package/dist/server/swagger/index.d.ts.map +1 -1
- package/dist/server/swagger/index.js +1854 -6
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.d.ts +309 -7
- package/dist/sms/index.d.ts.map +1 -1
- package/dist/sms/index.js +1856 -7
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js +1218 -0
- package/dist/system/index.browser.js.map +1 -0
- package/dist/{file → system}/index.d.ts +343 -16
- package/dist/system/index.d.ts.map +1 -0
- package/dist/{file → system}/index.js +419 -22
- package/dist/system/index.js.map +1 -0
- package/dist/thread/index.d.ts +11 -2
- package/dist/thread/index.d.ts.map +1 -1
- package/dist/thread/index.js +11 -2
- package/dist/thread/index.js.map +1 -1
- package/dist/topic/core/index.d.ts +12 -5
- package/dist/topic/core/index.d.ts.map +1 -1
- package/dist/topic/core/index.js +12 -5
- package/dist/topic/core/index.js.map +1 -1
- package/dist/vite/index.d.ts +5 -6272
- package/dist/vite/index.d.ts.map +1 -1
- package/dist/vite/index.js +23 -10
- package/dist/vite/index.js.map +1 -1
- package/dist/websocket/index.d.ts +12 -8
- package/dist/websocket/index.d.ts.map +1 -1
- package/dist/websocket/index.js +12 -8
- package/dist/websocket/index.js.map +1 -1
- package/package.json +82 -11
- package/src/api/audits/index.ts +10 -33
- package/src/api/files/__tests__/$bucket.spec.ts +1 -1
- package/src/api/files/controllers/AdminFileStatsController.spec.ts +1 -1
- package/src/api/files/controllers/FileController.spec.ts +1 -1
- package/src/api/files/index.ts +10 -3
- package/src/api/files/jobs/FileJobs.spec.ts +1 -1
- package/src/api/files/services/FileService.spec.ts +1 -1
- package/src/api/jobs/index.ts +10 -3
- package/src/api/keys/controllers/AdminApiKeyController.ts +75 -0
- package/src/api/keys/controllers/ApiKeyController.ts +103 -0
- package/src/api/keys/entities/apiKeyEntity.ts +41 -0
- package/src/api/keys/index.ts +49 -0
- package/src/api/keys/schemas/adminApiKeyQuerySchema.ts +7 -0
- package/src/api/keys/schemas/adminApiKeyResourceSchema.ts +17 -0
- package/src/api/keys/schemas/createApiKeyBodySchema.ts +7 -0
- package/src/api/keys/schemas/createApiKeyResponseSchema.ts +11 -0
- package/src/api/keys/schemas/listApiKeyResponseSchema.ts +15 -0
- package/src/api/keys/schemas/revokeApiKeyParamsSchema.ts +5 -0
- package/src/api/keys/schemas/revokeApiKeyResponseSchema.ts +5 -0
- package/src/api/keys/services/ApiKeyService.spec.ts +553 -0
- package/src/api/keys/services/ApiKeyService.ts +306 -0
- package/src/api/logs/TODO.md +55 -0
- package/src/api/notifications/index.ts +10 -4
- package/src/api/parameters/index.ts +9 -30
- package/src/api/parameters/primitives/$config.ts +12 -4
- package/src/api/parameters/services/ConfigStore.ts +9 -3
- package/src/api/users/__tests__/ApiKeys-integration.spec.ts +1035 -0
- package/src/api/users/__tests__/ApiKeys.spec.ts +401 -0
- package/src/api/users/index.ts +14 -3
- package/src/api/users/primitives/$realm.ts +33 -5
- package/src/api/users/providers/RealmProvider.ts +1 -12
- package/src/api/users/services/SessionService.ts +1 -1
- package/src/api/verifications/controllers/VerificationController.ts +2 -0
- package/src/api/verifications/index.ts +10 -4
- package/src/batch/index.ts +9 -36
- package/src/batch/primitives/$batch.ts +0 -8
- package/src/batch/providers/BatchProvider.ts +29 -2
- package/src/bucket/__tests__/shared.ts +1 -1
- package/src/bucket/index.ts +13 -6
- package/src/bucket/primitives/$bucket.ts +1 -1
- package/src/bucket/providers/LocalFileStorageProvider.ts +1 -1
- package/src/bucket/providers/MemoryFileStorageProvider.ts +1 -1
- package/src/cache/core/__tests__/shared.ts +30 -0
- package/src/cache/core/index.ts +11 -6
- package/src/cache/core/primitives/$cache.spec.ts +5 -0
- package/src/cache/core/providers/CacheProvider.ts +17 -0
- package/src/cache/core/providers/MemoryCacheProvider.ts +300 -1
- package/src/cache/redis/__tests__/cache-redis.spec.ts +5 -0
- package/src/cache/redis/providers/RedisCacheProvider.ts +9 -0
- package/src/cli/apps/AlephaCli.ts +1 -14
- package/src/cli/apps/AlephaPackageBuilderCli.ts +10 -1
- package/src/cli/atoms/buildOptions.ts +99 -9
- package/src/cli/commands/build.ts +150 -37
- package/src/cli/commands/db.ts +22 -18
- package/src/cli/commands/deploy.ts +1 -1
- package/src/cli/commands/dev.ts +1 -20
- package/src/cli/commands/gen/env.ts +5 -2
- package/src/cli/commands/gen/openapi.ts +5 -2
- package/src/cli/commands/init.spec.ts +588 -0
- package/src/cli/commands/init.ts +115 -58
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/commands/typecheck.ts +11 -0
- package/src/cli/providers/AppEntryProvider.ts +1 -1
- package/src/cli/providers/ViteBuildProvider.ts +8 -50
- package/src/cli/providers/ViteDevServerProvider.ts +35 -16
- package/src/cli/services/AlephaCliUtils.ts +52 -121
- package/src/cli/services/PackageManagerUtils.ts +129 -11
- package/src/cli/services/ProjectScaffolder.spec.ts +97 -0
- package/src/cli/services/ProjectScaffolder.ts +148 -81
- package/src/cli/services/ViteUtils.ts +82 -0
- package/src/cli/{assets/claudeMd.ts → templates/agentMd.ts} +37 -24
- package/src/cli/templates/apiAppSecurityTs.ts +11 -0
- package/src/cli/templates/apiIndexTs.ts +30 -0
- package/src/cli/templates/gitignore.ts +39 -0
- package/src/cli/{assets → templates}/mainCss.ts +11 -2
- package/src/cli/templates/mainServerTs.ts +33 -0
- package/src/cli/templates/webAppRouterTs.ts +74 -0
- package/src/cli/templates/webHelloComponentTsx.ts +30 -0
- package/src/command/helpers/Runner.spec.ts +139 -0
- package/src/command/helpers/Runner.ts +7 -22
- package/src/command/index.ts +12 -4
- package/src/command/providers/CliProvider.spec.ts +1392 -0
- package/src/command/providers/CliProvider.ts +320 -47
- package/src/core/Alepha.ts +34 -27
- package/src/core/__tests__/Alepha-start.spec.ts +4 -4
- package/src/core/helpers/jsonSchemaToTypeBox.spec.ts +771 -0
- package/src/core/helpers/jsonSchemaToTypeBox.ts +62 -10
- package/src/core/index.shared.ts +1 -0
- package/src/core/index.ts +20 -0
- package/src/core/providers/EventManager.spec.ts +0 -71
- package/src/core/providers/EventManager.ts +3 -15
- package/src/core/providers/Json.ts +2 -14
- package/src/datetime/index.ts +15 -0
- package/src/email/index.ts +10 -5
- package/src/email/providers/LocalEmailProvider.spec.ts +1 -1
- package/src/email/providers/LocalEmailProvider.ts +1 -1
- package/src/fake/__tests__/keyName.example.ts +1 -1
- package/src/fake/__tests__/keyName.spec.ts +5 -5
- package/src/fake/index.ts +9 -6
- package/src/fake/providers/FakeProvider.spec.ts +258 -40
- package/src/fake/providers/FakeProvider.ts +133 -19
- package/src/lock/core/index.ts +11 -4
- package/src/logger/index.ts +17 -66
- package/src/mcp/index.ts +10 -27
- package/src/mcp/transports/SseMcpTransport.ts +0 -11
- package/src/orm/__tests__/PostgresProvider.spec.ts +2 -2
- package/src/orm/index.browser.ts +2 -2
- package/src/orm/index.bun.ts +5 -3
- package/src/orm/index.ts +23 -53
- package/src/orm/providers/drivers/BunSqliteProvider.ts +5 -1
- package/src/orm/providers/drivers/CloudflareD1Provider.ts +57 -30
- package/src/orm/providers/drivers/DatabaseProvider.ts +9 -1
- package/src/orm/providers/drivers/NodeSqliteProvider.ts +4 -1
- package/src/orm/services/Repository.ts +7 -3
- package/src/queue/core/index.ts +14 -6
- package/src/react/auth/__tests__/$auth.spec.ts +202 -0
- package/src/react/auth/hooks/useAuth.ts +32 -0
- package/src/react/auth/index.browser.ts +13 -0
- package/src/react/auth/index.shared.ts +2 -0
- package/src/react/auth/index.ts +48 -0
- package/src/react/auth/providers/ReactAuthProvider.ts +16 -0
- package/src/react/auth/services/ReactAuth.ts +135 -0
- package/src/react/core/__tests__/Router.spec.tsx +169 -0
- package/src/react/core/components/ClientOnly.tsx +49 -0
- package/src/react/core/components/ErrorBoundary.tsx +73 -0
- package/src/react/core/contexts/AlephaContext.ts +7 -0
- package/src/react/core/contexts/AlephaProvider.tsx +42 -0
- package/src/react/core/hooks/useAction.browser.spec.tsx +569 -0
- package/src/react/core/hooks/useAction.ts +480 -0
- package/src/react/core/hooks/useAlepha.ts +26 -0
- package/src/react/core/hooks/useClient.ts +17 -0
- package/src/react/core/hooks/useEvents.ts +51 -0
- package/src/react/core/hooks/useInject.ts +12 -0
- package/src/react/core/hooks/useStore.ts +52 -0
- package/src/react/core/index.ts +90 -0
- package/src/react/form/components/FormState.tsx +17 -0
- package/src/react/form/errors/FormValidationError.ts +18 -0
- package/src/react/form/hooks/useForm.browser.spec.tsx +366 -0
- package/src/react/form/hooks/useForm.ts +47 -0
- package/src/react/form/hooks/useFormState.ts +130 -0
- package/src/react/form/index.ts +44 -0
- package/src/react/form/services/FormModel.ts +614 -0
- package/src/react/head/helpers/SeoExpander.spec.ts +203 -0
- package/src/react/head/helpers/SeoExpander.ts +142 -0
- package/src/react/head/hooks/useHead.spec.tsx +288 -0
- package/src/react/head/hooks/useHead.ts +62 -0
- package/src/react/head/index.browser.ts +26 -0
- package/src/react/head/index.ts +44 -0
- package/src/react/head/interfaces/Head.ts +105 -0
- package/src/react/head/primitives/$head.ts +25 -0
- package/src/react/head/providers/BrowserHeadProvider.browser.spec.ts +196 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +212 -0
- package/src/react/head/providers/HeadProvider.ts +168 -0
- package/src/react/head/providers/ServerHeadProvider.ts +31 -0
- package/src/react/i18n/__tests__/integration.spec.tsx +239 -0
- package/src/react/i18n/components/Localize.spec.tsx +357 -0
- package/src/react/i18n/components/Localize.tsx +35 -0
- package/src/react/i18n/hooks/useI18n.browser.spec.tsx +438 -0
- package/src/react/i18n/hooks/useI18n.ts +18 -0
- package/src/react/i18n/index.ts +41 -0
- package/src/react/i18n/primitives/$dictionary.ts +69 -0
- package/src/react/i18n/providers/I18nProvider.spec.ts +389 -0
- package/src/react/i18n/providers/I18nProvider.ts +278 -0
- package/src/react/router/__tests__/page-head-browser.browser.spec.ts +95 -0
- package/src/react/router/__tests__/page-head.spec.ts +48 -0
- package/src/react/router/__tests__/seo-head.spec.ts +125 -0
- package/src/react/router/atoms/ssrManifestAtom.ts +58 -0
- package/src/react/router/components/ErrorViewer.tsx +872 -0
- package/src/react/router/components/Link.tsx +23 -0
- package/src/react/router/components/NestedView.tsx +223 -0
- package/src/react/router/components/NotFound.tsx +30 -0
- package/src/react/router/constants/PAGE_PRELOAD_KEY.ts +6 -0
- package/src/react/router/contexts/RouterLayerContext.ts +12 -0
- package/src/react/router/errors/Redirection.ts +28 -0
- package/src/react/router/hooks/useActive.ts +52 -0
- package/src/react/router/hooks/useQueryParams.ts +63 -0
- package/src/react/router/hooks/useRouter.ts +20 -0
- package/src/react/router/hooks/useRouterState.ts +11 -0
- package/src/react/router/index.browser.ts +45 -0
- package/src/react/router/index.shared.ts +19 -0
- package/src/react/router/index.ts +146 -0
- package/src/react/router/primitives/$page.browser.spec.tsx +851 -0
- package/src/react/router/primitives/$page.spec.tsx +676 -0
- package/src/react/router/primitives/$page.ts +489 -0
- package/src/react/router/providers/ReactBrowserProvider.ts +312 -0
- package/src/react/router/providers/ReactBrowserRendererProvider.ts +25 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +168 -0
- package/src/react/router/providers/ReactPageProvider.ts +726 -0
- package/src/react/router/providers/ReactPreloadProvider.spec.ts +142 -0
- package/src/react/router/providers/ReactPreloadProvider.ts +85 -0
- package/src/react/router/providers/ReactServerProvider.spec.tsx +316 -0
- package/src/react/router/providers/ReactServerProvider.ts +487 -0
- package/src/react/router/providers/ReactServerTemplateProvider.spec.ts +210 -0
- package/src/react/router/providers/ReactServerTemplateProvider.ts +542 -0
- package/src/react/router/providers/SSRManifestProvider.ts +334 -0
- package/src/react/router/services/ReactPageServerService.ts +48 -0
- package/src/react/router/services/ReactPageService.ts +27 -0
- package/src/react/router/services/ReactRouter.ts +262 -0
- package/src/react/websocket/hooks/useRoom.tsx +242 -0
- package/src/react/websocket/index.ts +7 -0
- package/src/redis/__tests__/redis.spec.ts +13 -0
- package/src/redis/index.ts +9 -25
- package/src/redis/providers/BunRedisProvider.ts +9 -0
- package/src/redis/providers/NodeRedisProvider.ts +8 -0
- package/src/redis/providers/RedisProvider.ts +16 -0
- package/src/retry/index.ts +11 -2
- package/src/router/index.ts +15 -0
- package/src/scheduler/index.ts +11 -2
- package/src/security/__tests__/BasicAuth.spec.ts +2 -0
- package/src/security/__tests__/ServerSecurityProvider.spec.ts +90 -5
- package/src/security/index.ts +15 -10
- package/src/security/interfaces/IssuerResolver.ts +27 -0
- package/src/security/primitives/$issuer.ts +55 -0
- package/src/security/providers/SecurityProvider.ts +179 -0
- package/src/security/providers/ServerBasicAuthProvider.ts +6 -2
- package/src/security/providers/ServerSecurityProvider.ts +63 -41
- package/src/server/auth/index.ts +12 -7
- package/src/server/cache/index.ts +7 -22
- package/src/server/compress/index.ts +10 -2
- package/src/server/cookies/index.ts +7 -5
- package/src/server/cookies/primitives/$cookie.ts +33 -11
- package/src/server/core/index.ts +16 -6
- package/src/server/core/interfaces/ServerRequest.ts +83 -1
- package/src/server/core/primitives/$action.spec.ts +1 -1
- package/src/server/core/primitives/$action.ts +8 -3
- package/src/server/core/providers/NodeHttpServerProvider.spec.ts +9 -3
- package/src/server/core/providers/NodeHttpServerProvider.ts +9 -3
- package/src/server/core/services/ServerRequestParser.spec.ts +520 -0
- package/src/server/core/services/ServerRequestParser.ts +306 -13
- package/src/server/cors/index.ts +7 -21
- package/src/server/cors/primitives/$cors.ts +6 -2
- package/src/server/health/index.ts +8 -2
- package/src/server/helmet/index.ts +11 -3
- package/src/server/links/index.ts +11 -6
- package/src/server/metrics/index.ts +10 -3
- package/src/server/multipart/index.ts +9 -3
- package/src/server/proxy/index.ts +8 -2
- package/src/server/rate-limit/index.ts +21 -25
- package/src/server/rate-limit/primitives/$rateLimit.ts +6 -2
- package/src/server/rate-limit/providers/ServerRateLimitProvider.spec.ts +38 -14
- package/src/server/rate-limit/providers/ServerRateLimitProvider.ts +22 -56
- package/src/server/static/index.ts +8 -2
- package/src/server/static/providers/ServerStaticProvider.ts +1 -1
- package/src/server/swagger/index.ts +9 -4
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
- package/src/sms/index.ts +9 -5
- package/src/sms/providers/LocalSmsProvider.spec.ts +1 -1
- package/src/sms/providers/LocalSmsProvider.ts +1 -1
- package/src/system/index.browser.ts +36 -0
- package/src/system/index.ts +62 -0
- package/src/system/index.workerd.ts +1 -0
- package/src/{file → system}/providers/FileSystemProvider.ts +24 -0
- package/src/{file → system}/providers/MemoryFileSystemProvider.ts +116 -3
- package/src/system/providers/MemoryShellProvider.ts +164 -0
- package/src/{file → system}/providers/NodeFileSystemProvider.spec.ts +2 -2
- package/src/{file → system}/providers/NodeFileSystemProvider.ts +47 -2
- package/src/system/providers/NodeShellProvider.ts +184 -0
- package/src/system/providers/ShellProvider.ts +74 -0
- package/src/{file → system}/services/FileDetector.spec.ts +2 -2
- package/src/thread/index.ts +11 -2
- package/src/topic/core/index.ts +12 -5
- package/src/vite/tasks/buildClient.ts +2 -7
- package/src/vite/tasks/buildServer.ts +19 -13
- package/src/vite/tasks/generateCloudflare.ts +10 -7
- package/src/vite/tasks/generateDocker.ts +4 -0
- package/src/websocket/index.ts +12 -8
- package/dist/file/index.d.ts.map +0 -1
- package/dist/file/index.js.map +0 -1
- package/src/cli/assets/apiIndexTs.ts +0 -16
- package/src/cli/assets/mainServerTs.ts +0 -24
- package/src/cli/assets/webAppRouterTs.ts +0 -16
- package/src/cli/assets/webHelloComponentTsx.ts +0 -20
- package/src/cli/providers/ViteTemplateProvider.ts +0 -27
- package/src/file/index.ts +0 -43
- /package/src/cli/{assets → templates}/apiHelloControllerTs.ts +0 -0
- /package/src/cli/{assets → templates}/biomeJson.ts +0 -0
- /package/src/cli/{assets → templates}/dummySpecTs.ts +0 -0
- /package/src/cli/{assets → templates}/editorconfig.ts +0 -0
- /package/src/cli/{assets → templates}/mainBrowserTs.ts +0 -0
- /package/src/cli/{assets → templates}/tsconfigJson.ts +0 -0
- /package/src/cli/{assets → templates}/webIndexTs.ts +0 -0
- /package/src/{file → system}/errors/FileError.ts +0 -0
- /package/src/{file → system}/services/FileDetector.ts +0 -0
|
@@ -0,0 +1,487 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
$atom,
|
|
4
|
+
$env,
|
|
5
|
+
$hook,
|
|
6
|
+
$inject,
|
|
7
|
+
$use,
|
|
8
|
+
Alepha,
|
|
9
|
+
type Static,
|
|
10
|
+
t,
|
|
11
|
+
} from "alepha";
|
|
12
|
+
import { $logger } from "alepha/logger";
|
|
13
|
+
import { ServerHeadProvider } from "alepha/react/head";
|
|
14
|
+
import { type ServerHandler, ServerRouterProvider } from "alepha/server";
|
|
15
|
+
import { ServerLinksProvider } from "alepha/server/links";
|
|
16
|
+
import { ServerStaticProvider } from "alepha/server/static";
|
|
17
|
+
import { FileSystemProvider } from "alepha/system";
|
|
18
|
+
import { renderToReadableStream } from "react-dom/server";
|
|
19
|
+
import { Redirection } from "../errors/Redirection.ts";
|
|
20
|
+
import {
|
|
21
|
+
$page,
|
|
22
|
+
type PagePrimitiveRenderOptions,
|
|
23
|
+
type PagePrimitiveRenderResult,
|
|
24
|
+
} from "../primitives/$page.ts";
|
|
25
|
+
import {
|
|
26
|
+
type PageRoute,
|
|
27
|
+
ReactPageProvider,
|
|
28
|
+
type ReactRouterState,
|
|
29
|
+
} from "./ReactPageProvider.ts";
|
|
30
|
+
import { ReactServerTemplateProvider } from "./ReactServerTemplateProvider.ts";
|
|
31
|
+
import { SSRManifestProvider } from "./SSRManifestProvider.ts";
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* React server provider responsible for SSR and static file serving.
|
|
35
|
+
*
|
|
36
|
+
* Coordinates between:
|
|
37
|
+
* - ReactPageProvider: Page routing and layer resolution
|
|
38
|
+
* - ReactServerTemplateProvider: HTML template parsing and streaming
|
|
39
|
+
* - ServerHeadProvider: Head content management
|
|
40
|
+
* - SSRManifestProvider: Module preload link collection
|
|
41
|
+
*
|
|
42
|
+
* Uses `react-dom/server` under the hood.
|
|
43
|
+
*/
|
|
44
|
+
export class ReactServerProvider {
|
|
45
|
+
/**
|
|
46
|
+
* SSR response headers - pre-allocated to avoid object creation per request.
|
|
47
|
+
*/
|
|
48
|
+
protected readonly SSR_HEADERS = {
|
|
49
|
+
"content-type": "text/html",
|
|
50
|
+
"cache-control": "no-store, no-cache, must-revalidate, proxy-revalidate",
|
|
51
|
+
pragma: "no-cache",
|
|
52
|
+
expires: "0",
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
protected readonly fs = $inject(FileSystemProvider);
|
|
56
|
+
protected readonly log = $logger();
|
|
57
|
+
protected readonly alepha = $inject(Alepha);
|
|
58
|
+
protected readonly env = $env(envSchema);
|
|
59
|
+
protected readonly pageApi = $inject(ReactPageProvider);
|
|
60
|
+
protected readonly templateProvider = $inject(ReactServerTemplateProvider);
|
|
61
|
+
protected readonly serverHeadProvider = $inject(ServerHeadProvider);
|
|
62
|
+
protected readonly serverStaticProvider = $inject(ServerStaticProvider);
|
|
63
|
+
protected readonly serverRouterProvider = $inject(ServerRouterProvider);
|
|
64
|
+
protected readonly ssrManifestProvider = $inject(SSRManifestProvider);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Cached check for ServerLinksProvider - avoids has() lookup per request.
|
|
68
|
+
*/
|
|
69
|
+
protected hasServerLinksProvider = false;
|
|
70
|
+
|
|
71
|
+
protected readonly options = $use(reactServerOptions);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Configure the React server provider.
|
|
75
|
+
*/
|
|
76
|
+
public readonly onConfigure = $hook({
|
|
77
|
+
on: "configure",
|
|
78
|
+
handler: async () => {
|
|
79
|
+
const pages = this.alepha.primitives($page);
|
|
80
|
+
|
|
81
|
+
const ssrEnabled =
|
|
82
|
+
pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
83
|
+
|
|
84
|
+
this.alepha.store.set("alepha.react.server.ssr", ssrEnabled);
|
|
85
|
+
|
|
86
|
+
// production mode
|
|
87
|
+
let root = "";
|
|
88
|
+
|
|
89
|
+
// non-serverless mode only -> serve static files
|
|
90
|
+
if (!this.alepha.isServerless() && !this.alepha.isViteDev()) {
|
|
91
|
+
root = await this.getPublicDirectory();
|
|
92
|
+
if (!root) {
|
|
93
|
+
this.log.warn(
|
|
94
|
+
"Missing static files, static file server will be disabled",
|
|
95
|
+
);
|
|
96
|
+
} else {
|
|
97
|
+
this.log.debug(`Using static files from: ${root}`);
|
|
98
|
+
await this.configureStaticServer(root);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (ssrEnabled) {
|
|
103
|
+
this.registerPages();
|
|
104
|
+
this.log.info("SSR OK");
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// no SSR enabled, serve a minimal fallback
|
|
109
|
+
this.log.info("SSR is disabled");
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Register all pages as server routes.
|
|
115
|
+
*/
|
|
116
|
+
protected registerPages(): void {
|
|
117
|
+
// Set up early head content (entry assets)
|
|
118
|
+
this.setupEarlyHeadContent();
|
|
119
|
+
|
|
120
|
+
// Cache ServerLinksProvider check at startup
|
|
121
|
+
this.hasServerLinksProvider = this.alepha.has(ServerLinksProvider);
|
|
122
|
+
|
|
123
|
+
for (const page of this.pageApi.getPages()) {
|
|
124
|
+
if (page.component || page.lazy) {
|
|
125
|
+
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
126
|
+
|
|
127
|
+
this.serverRouterProvider.createRoute({
|
|
128
|
+
...page,
|
|
129
|
+
schema: undefined, // schema is handled by the page primitive provider
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: page.match,
|
|
132
|
+
handler: this.createHandler(page),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Set up early head content with entry assets.
|
|
140
|
+
*
|
|
141
|
+
* This content is sent immediately when streaming starts, before page loaders run,
|
|
142
|
+
* allowing the browser to start downloading entry.js and CSS files early.
|
|
143
|
+
*/
|
|
144
|
+
protected setupEarlyHeadContent(): void {
|
|
145
|
+
const assets = this.ssrManifestProvider.getEntryAssets();
|
|
146
|
+
const globalHead = this.serverHeadProvider.resolveGlobalHead();
|
|
147
|
+
|
|
148
|
+
const parts: string[] = [];
|
|
149
|
+
|
|
150
|
+
if (assets) {
|
|
151
|
+
for (const css of assets.css) {
|
|
152
|
+
parts.push(`<link rel="stylesheet" href="${css}" crossorigin="">`);
|
|
153
|
+
}
|
|
154
|
+
if (assets.js) {
|
|
155
|
+
parts.push(
|
|
156
|
+
`<script type="module" crossorigin="" src="${assets.js}"></script>`,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
this.templateProvider.setEarlyHeadContent(
|
|
162
|
+
parts.length > 0 ? `${parts.join("\n")}\n` : "",
|
|
163
|
+
globalHead,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
this.log.debug("Early head content set", {
|
|
167
|
+
css: assets?.css.length ?? 0,
|
|
168
|
+
js: assets?.js ? 1 : 0,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Get the public directory path where static files are located.
|
|
174
|
+
*/
|
|
175
|
+
protected async getPublicDirectory(): Promise<string> {
|
|
176
|
+
const maybe = [
|
|
177
|
+
join(process.cwd(), `dist/${this.options.publicDir}`),
|
|
178
|
+
join(process.cwd(), this.options.publicDir),
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
for (const it of maybe) {
|
|
182
|
+
if (await this.fs.exists(it)) {
|
|
183
|
+
return it;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Configure the static file server to serve files from the given root directory.
|
|
192
|
+
*/
|
|
193
|
+
protected async configureStaticServer(root: string) {
|
|
194
|
+
await this.serverStaticProvider.createStaticServer({
|
|
195
|
+
root,
|
|
196
|
+
cacheControl: {
|
|
197
|
+
maxAge: 3600,
|
|
198
|
+
immutable: true,
|
|
199
|
+
},
|
|
200
|
+
...this.options.staticServer,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Create the request handler for a page route.
|
|
206
|
+
*/
|
|
207
|
+
protected createHandler(route: PageRoute): ServerHandler {
|
|
208
|
+
return async (serverRequest) => {
|
|
209
|
+
const { url, reply, query, params } = serverRequest;
|
|
210
|
+
|
|
211
|
+
this.log.trace("Rendering page", { name: route.name });
|
|
212
|
+
|
|
213
|
+
// Initialize router state
|
|
214
|
+
const state: ReactRouterState = {
|
|
215
|
+
url,
|
|
216
|
+
params,
|
|
217
|
+
query,
|
|
218
|
+
name: route.name,
|
|
219
|
+
onError: () => null,
|
|
220
|
+
layers: [],
|
|
221
|
+
meta: {},
|
|
222
|
+
head: {},
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Set up API links if available
|
|
226
|
+
if (this.hasServerLinksProvider) {
|
|
227
|
+
this.alepha.store.set(
|
|
228
|
+
"alepha.server.request.apiLinks",
|
|
229
|
+
await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
230
|
+
user: (serverRequest as any).user, // TODO: fix type
|
|
231
|
+
authorization: serverRequest.headers.authorization,
|
|
232
|
+
}),
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Check access permissions
|
|
237
|
+
let target: PageRoute | undefined = route;
|
|
238
|
+
while (target) {
|
|
239
|
+
if (route.can && !route.can()) {
|
|
240
|
+
this.log.warn(
|
|
241
|
+
`Access to page '${route.name}' is forbidden by can() check`,
|
|
242
|
+
);
|
|
243
|
+
reply.status = 403;
|
|
244
|
+
reply.headers["content-type"] = "text/plain";
|
|
245
|
+
return "Forbidden";
|
|
246
|
+
}
|
|
247
|
+
target = target.parent;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
251
|
+
request: serverRequest,
|
|
252
|
+
state,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
// Apply SSR headers early
|
|
256
|
+
Object.assign(reply.headers, this.SSR_HEADERS);
|
|
257
|
+
|
|
258
|
+
// Resolve global head for early streaming (htmlAttributes only)
|
|
259
|
+
const globalHead = this.serverHeadProvider.resolveGlobalHead();
|
|
260
|
+
|
|
261
|
+
// Create optimized HTML stream with early head
|
|
262
|
+
const htmlStream = this.templateProvider.createEarlyHtmlStream(
|
|
263
|
+
globalHead,
|
|
264
|
+
async () => {
|
|
265
|
+
// === ASYNC WORK (runs while early head is being sent) ===
|
|
266
|
+
const result = await this.renderPage(route, state);
|
|
267
|
+
|
|
268
|
+
if (result.redirect) {
|
|
269
|
+
// Return redirect URL - template provider will inject meta refresh
|
|
270
|
+
// since HTTP headers have already been sent
|
|
271
|
+
return { redirect: result.redirect };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { state, reactStream: result.reactStream! };
|
|
275
|
+
},
|
|
276
|
+
{
|
|
277
|
+
hydration: true,
|
|
278
|
+
onError: (error) => {
|
|
279
|
+
if (error instanceof Redirection) {
|
|
280
|
+
this.log.debug("Streaming resulted in redirection", {
|
|
281
|
+
redirect: error.redirect,
|
|
282
|
+
});
|
|
283
|
+
// Can't do redirect after streaming started - already handled above
|
|
284
|
+
} else {
|
|
285
|
+
// disable logging here, it's noisy and duplicate
|
|
286
|
+
// this.log.error("HTML stream error", error);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
this.log.trace("Page streaming started (early head optimization)");
|
|
293
|
+
route.onServerResponse?.(serverRequest);
|
|
294
|
+
reply.body = htmlStream;
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Core rendering logic - shared between SSR handler and static prerendering
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Core page rendering logic shared between SSR handler and static prerendering.
|
|
304
|
+
*
|
|
305
|
+
* Handles:
|
|
306
|
+
* - Layer resolution (loaders)
|
|
307
|
+
* - Redirect detection
|
|
308
|
+
* - Head content filling
|
|
309
|
+
* - Preload link collection
|
|
310
|
+
* - React stream rendering
|
|
311
|
+
*
|
|
312
|
+
* @param route - The page route to render
|
|
313
|
+
* @param state - The router state
|
|
314
|
+
* @returns Render result with redirect or React stream
|
|
315
|
+
*/
|
|
316
|
+
protected async renderPage(
|
|
317
|
+
route: PageRoute,
|
|
318
|
+
state: ReactRouterState,
|
|
319
|
+
): Promise<{ redirect?: string; reactStream?: ReadableStream<Uint8Array> }> {
|
|
320
|
+
// Resolve page layers (loaders)
|
|
321
|
+
const { redirect } = await this.pageApi.createLayers(route, state);
|
|
322
|
+
if (redirect) {
|
|
323
|
+
this.log.debug("Resolver resulted in redirection", { redirect });
|
|
324
|
+
return { redirect };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Fill head from route config
|
|
328
|
+
this.serverHeadProvider.fillHead(state);
|
|
329
|
+
|
|
330
|
+
// Collect and inject modulepreload links for page-specific chunks
|
|
331
|
+
const preloadLinks = this.ssrManifestProvider.collectPreloadLinks(route);
|
|
332
|
+
if (preloadLinks.length > 0) {
|
|
333
|
+
state.head ??= {};
|
|
334
|
+
state.head.link = [...(state.head.link ?? []), ...preloadLinks];
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// Render React to stream
|
|
338
|
+
|
|
339
|
+
const element = this.pageApi.root(state);
|
|
340
|
+
this.alepha.store.set("alepha.react.router.state", state);
|
|
341
|
+
|
|
342
|
+
const reactStream = await renderToReadableStream(element, {
|
|
343
|
+
onError: (error: unknown) => {
|
|
344
|
+
if (error instanceof Redirection) {
|
|
345
|
+
this.log.warn("Redirect during streaming ignored", {
|
|
346
|
+
redirect: error.redirect,
|
|
347
|
+
});
|
|
348
|
+
} else {
|
|
349
|
+
// disable logging here, it's noisy and duplicate
|
|
350
|
+
// this.log.error("Streaming render error", error);
|
|
351
|
+
}
|
|
352
|
+
},
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
return { reactStream };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// ---------------------------------------------------------------------------
|
|
359
|
+
// Testing utilities - kept for backwards compatibility with tests
|
|
360
|
+
// ---------------------------------------------------------------------------
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* For testing purposes, renders a page to HTML string.
|
|
364
|
+
* Uses the same streaming code path as production, then collects to string.
|
|
365
|
+
*
|
|
366
|
+
* @param name - Page name to render
|
|
367
|
+
* @param options - Render options (params, query, html, hydration)
|
|
368
|
+
*/
|
|
369
|
+
public async render(
|
|
370
|
+
name: string,
|
|
371
|
+
options: PagePrimitiveRenderOptions = {},
|
|
372
|
+
): Promise<PagePrimitiveRenderResult> {
|
|
373
|
+
const page = this.pageApi.page(name);
|
|
374
|
+
const url = new URL(this.pageApi.url(name, options));
|
|
375
|
+
const state: ReactRouterState = {
|
|
376
|
+
url,
|
|
377
|
+
params: options.params ?? {},
|
|
378
|
+
query: options.query ?? {},
|
|
379
|
+
onError: () => null,
|
|
380
|
+
layers: [],
|
|
381
|
+
meta: {},
|
|
382
|
+
head: {},
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
this.log.trace("Rendering", { url });
|
|
386
|
+
|
|
387
|
+
await this.alepha.events.emit("react:server:render:begin", { state });
|
|
388
|
+
|
|
389
|
+
// Use shared rendering logic
|
|
390
|
+
const result = await this.renderPage(page, state);
|
|
391
|
+
|
|
392
|
+
if (result.redirect) {
|
|
393
|
+
return { state, html: "", redirect: result.redirect };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const reactStream = result.reactStream!;
|
|
397
|
+
|
|
398
|
+
// If full HTML page not requested, collect just the React content
|
|
399
|
+
if (!options.html) {
|
|
400
|
+
const html = await this.streamToString(reactStream);
|
|
401
|
+
return { state, html };
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// Create full HTML stream and collect to string
|
|
405
|
+
const htmlStream = this.templateProvider.createHtmlStream(
|
|
406
|
+
reactStream,
|
|
407
|
+
state,
|
|
408
|
+
{ hydration: options.hydration ?? true },
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
const html = await this.streamToString(htmlStream);
|
|
412
|
+
|
|
413
|
+
await this.alepha.events.emit("react:server:render:end", { state, html });
|
|
414
|
+
|
|
415
|
+
return { state, html };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Collect a ReadableStream into a string.
|
|
420
|
+
*/
|
|
421
|
+
protected async streamToString(
|
|
422
|
+
stream: ReadableStream<Uint8Array>,
|
|
423
|
+
): Promise<string> {
|
|
424
|
+
const reader = stream.getReader();
|
|
425
|
+
const decoder = new TextDecoder();
|
|
426
|
+
const chunks: string[] = [];
|
|
427
|
+
|
|
428
|
+
try {
|
|
429
|
+
while (true) {
|
|
430
|
+
const { done, value } = await reader.read();
|
|
431
|
+
if (done) break;
|
|
432
|
+
chunks.push(decoder.decode(value, { stream: true }));
|
|
433
|
+
}
|
|
434
|
+
chunks.push(decoder.decode()); // Flush remaining
|
|
435
|
+
} finally {
|
|
436
|
+
reader.releaseLock();
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return chunks.join("");
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// ---------------------------------------------------------------------------------------------------------------------
|
|
444
|
+
|
|
445
|
+
const envSchema = t.object({
|
|
446
|
+
REACT_SSR_ENABLED: t.optional(t.boolean()),
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
declare module "alepha" {
|
|
450
|
+
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
451
|
+
interface State {
|
|
452
|
+
"alepha.react.server.ssr"?: boolean;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* React server provider configuration atom
|
|
458
|
+
*/
|
|
459
|
+
export const reactServerOptions = $atom({
|
|
460
|
+
name: "alepha.react.server.options",
|
|
461
|
+
schema: t.object({
|
|
462
|
+
publicDir: t.string(),
|
|
463
|
+
staticServer: t.object({
|
|
464
|
+
disabled: t.boolean(),
|
|
465
|
+
path: t.string({
|
|
466
|
+
description: "URL path where static files will be served.",
|
|
467
|
+
}),
|
|
468
|
+
}),
|
|
469
|
+
}),
|
|
470
|
+
default: {
|
|
471
|
+
publicDir: "public",
|
|
472
|
+
staticServer: {
|
|
473
|
+
disabled: false,
|
|
474
|
+
path: "/",
|
|
475
|
+
},
|
|
476
|
+
},
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
export type ReactServerProviderOptions = Static<
|
|
480
|
+
typeof reactServerOptions.schema
|
|
481
|
+
>;
|
|
482
|
+
|
|
483
|
+
declare module "alepha" {
|
|
484
|
+
interface State {
|
|
485
|
+
[reactServerOptions.key]: ReactServerProviderOptions;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Alepha } from "alepha";
|
|
2
|
+
import { $head } from "alepha/react/head";
|
|
3
|
+
import { HttpClient, ServerProvider } from "alepha/server";
|
|
4
|
+
import { describe, it } from "vitest";
|
|
5
|
+
import { ssrManifestAtom } from "../atoms/ssrManifestAtom.ts";
|
|
6
|
+
import { $page } from "../index.ts";
|
|
7
|
+
|
|
8
|
+
describe("ReactServerTemplateProvider", () => {
|
|
9
|
+
describe("streaming", () => {
|
|
10
|
+
class App {
|
|
11
|
+
head = $head({
|
|
12
|
+
htmlAttributes: { lang: "en" },
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
home = $page({
|
|
16
|
+
path: "/",
|
|
17
|
+
head: {
|
|
18
|
+
title: "Test Page",
|
|
19
|
+
meta: [{ name: "description", content: "Test description" }],
|
|
20
|
+
},
|
|
21
|
+
component: () => "Hello World",
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
withLoader = $page({
|
|
25
|
+
path: "/with-loader",
|
|
26
|
+
head: { title: "Loader Page" },
|
|
27
|
+
loader: async () => ({ data: "loaded" }),
|
|
28
|
+
component: ({ data }) => `Data: ${data}`,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
it("should stream complete HTML document with correct structure", async ({
|
|
33
|
+
expect,
|
|
34
|
+
}) => {
|
|
35
|
+
const alepha = Alepha.create({
|
|
36
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
37
|
+
}).with(App);
|
|
38
|
+
|
|
39
|
+
await alepha.start();
|
|
40
|
+
|
|
41
|
+
const server = alepha.inject(ServerProvider);
|
|
42
|
+
const http = alepha.inject(HttpClient);
|
|
43
|
+
|
|
44
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
45
|
+
|
|
46
|
+
// Verify HTML structure
|
|
47
|
+
expect(response.data).toContain("<!DOCTYPE html>");
|
|
48
|
+
expect(response.data).toContain('<html lang="en">');
|
|
49
|
+
expect(response.data).toContain("<head>");
|
|
50
|
+
expect(response.data).toContain('<meta charset="UTF-8">');
|
|
51
|
+
expect(response.data).toContain('<meta name="viewport"');
|
|
52
|
+
expect(response.data).toContain("<title>Test Page</title>");
|
|
53
|
+
expect(response.data).toContain(
|
|
54
|
+
'<meta name="description" content="Test description">',
|
|
55
|
+
);
|
|
56
|
+
expect(response.data).toContain("</head>");
|
|
57
|
+
expect(response.data).toContain("<body>");
|
|
58
|
+
expect(response.data).toContain('<div id="root">');
|
|
59
|
+
expect(response.data).toContain("Hello World");
|
|
60
|
+
expect(response.data).toContain("</div>");
|
|
61
|
+
expect(response.data).toContain("</body>");
|
|
62
|
+
expect(response.data).toContain("</html>");
|
|
63
|
+
|
|
64
|
+
await alepha.stop();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should include hydration data when enabled", async ({ expect }) => {
|
|
68
|
+
const alepha = Alepha.create({
|
|
69
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
70
|
+
}).with(App);
|
|
71
|
+
|
|
72
|
+
await alepha.start();
|
|
73
|
+
|
|
74
|
+
const server = alepha.inject(ServerProvider);
|
|
75
|
+
const http = alepha.inject(HttpClient);
|
|
76
|
+
|
|
77
|
+
const response = await http.fetch(`${server.hostname}/with-loader`);
|
|
78
|
+
|
|
79
|
+
// Verify hydration script is present
|
|
80
|
+
expect(response.data).toContain("<script>window.__ssr=");
|
|
81
|
+
expect(response.data).toContain("</script>");
|
|
82
|
+
|
|
83
|
+
// Verify hydration data structure
|
|
84
|
+
expect(response.data).toMatch(/window\.__ssr=\{.*"layers".*\}/);
|
|
85
|
+
|
|
86
|
+
await alepha.stop();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("should include entry assets in head when manifest is available", async ({
|
|
90
|
+
expect,
|
|
91
|
+
}) => {
|
|
92
|
+
const alepha = Alepha.create({
|
|
93
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
94
|
+
}).with(App);
|
|
95
|
+
|
|
96
|
+
// Set up mock SSR manifest
|
|
97
|
+
alepha.store.set(ssrManifestAtom, {
|
|
98
|
+
client: {
|
|
99
|
+
"src/entry.tsx": {
|
|
100
|
+
file: "assets/entry.abc123.js",
|
|
101
|
+
isEntry: true,
|
|
102
|
+
css: ["assets/style.def456.css"],
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await alepha.start();
|
|
108
|
+
|
|
109
|
+
const server = alepha.inject(ServerProvider);
|
|
110
|
+
const http = alepha.inject(HttpClient);
|
|
111
|
+
|
|
112
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
113
|
+
|
|
114
|
+
// Verify entry assets are in the head
|
|
115
|
+
expect(response.data).toContain(
|
|
116
|
+
'<link rel="stylesheet" href="/assets/style.def456.css" crossorigin="">',
|
|
117
|
+
);
|
|
118
|
+
expect(response.data).toContain(
|
|
119
|
+
'<script type="module" crossorigin="" src="/assets/entry.abc123.js"></script>',
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
await alepha.stop();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("should handle pages with loaders correctly", async ({ expect }) => {
|
|
126
|
+
const alepha = Alepha.create({
|
|
127
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
128
|
+
}).with(App);
|
|
129
|
+
|
|
130
|
+
await alepha.start();
|
|
131
|
+
|
|
132
|
+
const server = alepha.inject(ServerProvider);
|
|
133
|
+
const http = alepha.inject(HttpClient);
|
|
134
|
+
|
|
135
|
+
const response = await http.fetch(`${server.hostname}/with-loader`);
|
|
136
|
+
|
|
137
|
+
// Verify loader data is rendered
|
|
138
|
+
expect(response.data).toContain("Data: loaded");
|
|
139
|
+
|
|
140
|
+
await alepha.stop();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should set correct content-type header", async ({ expect }) => {
|
|
144
|
+
const alepha = Alepha.create({
|
|
145
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
146
|
+
}).with(App);
|
|
147
|
+
|
|
148
|
+
await alepha.start();
|
|
149
|
+
|
|
150
|
+
const server = alepha.inject(ServerProvider);
|
|
151
|
+
const http = alepha.inject(HttpClient);
|
|
152
|
+
|
|
153
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
154
|
+
|
|
155
|
+
expect(response.headers.get("content-type")).toBe("text/html");
|
|
156
|
+
|
|
157
|
+
await alepha.stop();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("should set cache-control headers for SSR responses", async ({
|
|
161
|
+
expect,
|
|
162
|
+
}) => {
|
|
163
|
+
const alepha = Alepha.create({
|
|
164
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
165
|
+
}).with(App);
|
|
166
|
+
|
|
167
|
+
await alepha.start();
|
|
168
|
+
|
|
169
|
+
const server = alepha.inject(ServerProvider);
|
|
170
|
+
const http = alepha.inject(HttpClient);
|
|
171
|
+
|
|
172
|
+
const response = await http.fetch(`${server.hostname}/`);
|
|
173
|
+
|
|
174
|
+
expect(response.headers.get("cache-control")).toContain("no-store");
|
|
175
|
+
|
|
176
|
+
await alepha.stop();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
describe("error handling", () => {
|
|
181
|
+
class ErrorApp {
|
|
182
|
+
errorPage = $page({
|
|
183
|
+
path: "/error",
|
|
184
|
+
loader: async () => {
|
|
185
|
+
throw new Error("Loader error");
|
|
186
|
+
},
|
|
187
|
+
component: () => "Should not render",
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it("should render error when loader throws", async ({ expect }) => {
|
|
192
|
+
const alepha = Alepha.create({
|
|
193
|
+
env: { LOG_LEVEL: "error", SERVER_PORT: 0 },
|
|
194
|
+
}).with(ErrorApp);
|
|
195
|
+
|
|
196
|
+
await alepha.start();
|
|
197
|
+
|
|
198
|
+
const server = alepha.inject(ServerProvider);
|
|
199
|
+
const http = alepha.inject(HttpClient);
|
|
200
|
+
|
|
201
|
+
const response = await http.fetch(`${server.hostname}/error`);
|
|
202
|
+
|
|
203
|
+
// Should still return a valid HTML response with error
|
|
204
|
+
expect(response.data).toContain("<!DOCTYPE html>");
|
|
205
|
+
expect(response.data).toContain("Loader error");
|
|
206
|
+
|
|
207
|
+
await alepha.stop();
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
});
|