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