alepha 0.20.1 → 0.20.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 +0 -1
- package/assets/swagger-ui/swagger-ui-bundle.js +1 -1
- package/assets/swagger-ui/swagger-ui.css +1 -1
- package/dist/api/audits/index.browser.js +49 -0
- package/dist/api/audits/index.browser.js.map +1 -1
- package/dist/api/audits/index.d.ts.map +1 -1
- package/dist/api/audits/index.js +49 -0
- package/dist/api/audits/index.js.map +1 -1
- package/dist/api/files/index.d.ts.map +1 -1
- package/dist/api/files/index.js +2 -1
- package/dist/api/files/index.js.map +1 -1
- package/dist/api/jobs/index.browser.js +64 -148
- package/dist/api/jobs/index.browser.js.map +1 -1
- package/dist/api/jobs/index.d.ts +339 -600
- package/dist/api/jobs/index.d.ts.map +1 -1
- package/dist/api/jobs/index.js +605 -1012
- package/dist/api/jobs/index.js.map +1 -1
- package/dist/api/keys/index.js.map +1 -1
- package/dist/api/notifications/index.d.ts +79 -27
- package/dist/api/notifications/index.d.ts.map +1 -1
- package/dist/api/notifications/index.js +90 -23
- package/dist/api/notifications/index.js.map +1 -1
- package/dist/api/organizations/index.d.ts.map +1 -1
- package/dist/api/parameters/index.browser.js +37 -0
- package/dist/api/parameters/index.browser.js.map +1 -1
- package/dist/api/parameters/index.d.ts +4 -65
- package/dist/api/parameters/index.d.ts.map +1 -1
- package/dist/api/parameters/index.js +37 -0
- package/dist/api/parameters/index.js.map +1 -1
- package/dist/api/payments/index.d.ts +2 -1
- package/dist/api/payments/index.d.ts.map +1 -1
- package/dist/api/payments/index.js +4 -2
- package/dist/api/payments/index.js.map +1 -1
- package/dist/api/users/index.d.ts +225 -5199
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +15 -11
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.d.ts.map +1 -1
- package/dist/api/verifications/index.js +4 -2
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/bucket/index.js +5 -1
- package/dist/bucket/index.js.map +1 -1
- package/dist/bucket/index.workerd.js +5 -1
- package/dist/bucket/index.workerd.js.map +1 -1
- package/dist/cache/core/index.js.map +1 -1
- package/dist/cache/core/index.workerd.js.map +1 -1
- package/dist/captcha/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +225 -11681
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +732 -257
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/devtools/index.js +7 -1
- package/dist/cli/devtools/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +65 -63
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +140 -27
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/cli/vendor/index.js +15 -0
- package/dist/cli/vendor/index.js.map +1 -1
- package/dist/command/index.d.ts +1 -1
- package/dist/command/index.js +1 -1
- package/dist/command/index.js.map +1 -1
- package/dist/core/index.browser.js +6 -0
- package/dist/core/index.browser.js.map +1 -1
- package/dist/core/index.d.ts +8 -8
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +6 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.native.js +6 -0
- package/dist/core/index.native.js.map +1 -1
- package/dist/core/index.workerd.js +6 -0
- package/dist/core/index.workerd.js.map +1 -1
- package/dist/crypto/index.js.map +1 -1
- package/dist/datetime/index.js.map +1 -1
- package/dist/email/core/index.js.map +1 -1
- package/dist/email/smtp/index.js +2 -10522
- package/dist/email/smtp/index.js.map +1 -1
- package/dist/fake/index.d.ts +4 -8085
- package/dist/fake/index.d.ts.map +1 -1
- package/dist/fake/index.js +3 -33554
- package/dist/fake/index.js.map +1 -1
- package/dist/lock/core/index.js.map +1 -1
- package/dist/lock/redis/index.js.map +1 -1
- package/dist/logger/index.js +32 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +1 -1
- package/dist/orm/core/index.browser.js +1 -361
- package/dist/orm/core/index.browser.js.map +1 -1
- package/dist/orm/core/index.bun.js +14 -406
- package/dist/orm/core/index.bun.js.map +1 -1
- package/dist/orm/core/index.d.ts +96 -5117
- package/dist/orm/core/index.d.ts.map +1 -1
- package/dist/orm/core/index.js +23 -419
- package/dist/orm/core/index.js.map +1 -1
- package/dist/orm/postgres/index.bun.js +17 -20
- package/dist/orm/postgres/index.bun.js.map +1 -1
- package/dist/orm/postgres/index.d.ts +2 -613
- package/dist/orm/postgres/index.d.ts.map +1 -1
- package/dist/orm/postgres/index.js +17 -20
- package/dist/orm/postgres/index.js.map +1 -1
- package/dist/react/core/index.js.map +1 -1
- package/dist/react/form/index.d.ts +60 -1
- package/dist/react/form/index.d.ts.map +1 -1
- package/dist/react/form/index.js +86 -1
- package/dist/react/form/index.js.map +1 -1
- package/dist/react/head/index.browser.js +16 -1
- package/dist/react/head/index.browser.js.map +1 -1
- package/dist/react/head/index.d.ts +6 -0
- package/dist/react/head/index.d.ts.map +1 -1
- package/dist/react/head/index.js +16 -1
- package/dist/react/head/index.js.map +1 -1
- package/dist/react/i18n/index.js.map +1 -1
- package/dist/react/intro/index.js +22 -17
- package/dist/react/intro/index.js.map +1 -1
- package/dist/react/router/index.browser.js +78 -12
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +57 -13
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +102 -14
- package/dist/react/router/index.js.map +1 -1
- package/dist/react/testing/index.d.ts +1 -411
- package/dist/react/testing/index.d.ts.map +1 -1
- package/dist/react/testing/index.js +13 -12293
- package/dist/react/testing/index.js.map +1 -1
- package/dist/react/ui/index.d.ts +124 -0
- package/dist/react/ui/index.d.ts.map +1 -0
- package/dist/react/ui/index.js +209 -0
- package/dist/react/ui/index.js.map +1 -0
- package/dist/react/websocket/index.js.map +1 -1
- package/dist/redis/index.js.map +1 -1
- package/dist/router/index.d.ts +13 -13
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -32
- package/dist/router/index.js.map +1 -1
- package/dist/scheduler/index.d.ts +1 -83
- package/dist/scheduler/index.d.ts.map +1 -1
- package/dist/scheduler/index.js +2 -391
- package/dist/scheduler/index.js.map +1 -1
- package/dist/scheduler/index.workerd.js +2 -391
- package/dist/scheduler/index.workerd.js.map +1 -1
- package/dist/security/index.browser.js.map +1 -1
- package/dist/security/index.d.ts +2 -325
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/index.js +3 -1362
- package/dist/security/index.js.map +1 -1
- package/dist/server/auth/index.d.ts +1 -1054
- package/dist/server/auth/index.d.ts.map +1 -1
- package/dist/server/auth/index.js +16 -1224
- package/dist/server/auth/index.js.map +1 -1
- package/dist/server/cookies/index.js.map +1 -1
- package/dist/server/core/index.browser.js.map +1 -1
- package/dist/server/core/index.d.ts +1 -4
- package/dist/server/core/index.d.ts.map +1 -1
- package/dist/server/core/index.js +19 -4
- package/dist/server/core/index.js.map +1 -1
- package/dist/server/links/index.browser.js.map +1 -1
- package/dist/server/links/index.js.map +1 -1
- package/dist/server/metrics/index.d.ts +1 -514
- package/dist/server/metrics/index.d.ts.map +1 -1
- package/dist/server/metrics/index.js +4 -4356
- package/dist/server/metrics/index.js.map +1 -1
- package/dist/server/rate-limit/index.js.map +1 -1
- package/dist/server/static/index.js.map +1 -1
- package/dist/server/swagger/index.js +1 -1
- package/dist/server/swagger/index.js.map +1 -1
- package/dist/sms/index.js.map +1 -1
- package/dist/system/index.browser.js.map +1 -1
- package/dist/system/index.d.ts.map +1 -1
- package/dist/system/index.js +1 -0
- package/dist/system/index.js.map +1 -1
- package/dist/system/index.workerd.js.map +1 -1
- package/dist/topic/core/index.js +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/dist/websocket/index.browser.js +21 -0
- package/dist/websocket/index.browser.js.map +1 -1
- package/dist/websocket/index.js +21 -0
- package/dist/websocket/index.js.map +1 -1
- package/package.json +23 -37
- package/src/api/files/__tests__/FileController.spec.ts +1 -1
- package/src/api/files/jobs/FileJobs.ts +2 -1
- package/src/api/jobs/__tests__/$job.spec.ts +320 -2867
- package/src/api/jobs/controllers/AdminJobController.ts +29 -138
- package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
- package/src/api/jobs/index.browser.ts +5 -7
- package/src/api/jobs/index.ts +23 -51
- package/src/api/jobs/primitives/$job.ts +66 -58
- package/src/api/jobs/providers/JobProvider.ts +561 -566
- package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
- package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
- package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
- package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
- package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
- package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
- package/src/api/jobs/services/JobService.ts +90 -483
- package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
- package/src/api/notifications/index.ts +7 -4
- package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
- package/src/api/payments/services/PaymentService.ts +4 -2
- package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
- package/src/api/users/audits/UserAudits.ts +3 -1
- package/src/api/users/buckets/UserBuckets.ts +2 -1
- package/src/api/users/index.ts +1 -4
- package/src/api/users/jobs/UserJobs.ts +5 -4
- package/src/api/users/schemas/userQuerySchema.ts +0 -1
- package/src/api/users/services/UserService.ts +1 -5
- package/src/api/verifications/__tests__/CodeVerification.spec.ts +14 -0
- package/src/api/verifications/__tests__/LinkVerification.spec.ts +14 -0
- package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
- package/src/api/verifications/services/VerificationService.ts +1 -0
- package/src/cli/core/__tests__/init.spec.ts +209 -1
- package/src/cli/core/commands/init.ts +9 -9
- package/src/cli/core/services/PackageManagerUtils.ts +22 -12
- package/src/cli/core/services/ProjectScaffolder.ts +300 -70
- package/src/cli/core/tasks/BuildDockerTask.ts +9 -10
- package/src/cli/core/tasks/BuildServerTask.ts +8 -0
- package/src/cli/core/templates/agentMd.ts +2 -8
- package/src/cli/core/templates/apiIndexTs.ts +22 -14
- package/src/cli/core/templates/componentsJsonTs.ts +39 -0
- package/src/cli/core/templates/mainCss.ts +2 -36
- package/src/cli/core/templates/saasAdminLayoutTsx.ts +77 -0
- package/src/cli/core/templates/saasAdminPagesTsx.ts +26 -0
- package/src/cli/core/templates/saasAuthLayoutTsx.ts +20 -0
- package/src/cli/core/templates/saasAuthPagesTsx.ts +62 -0
- package/src/cli/core/templates/saasRealmProviderTs.ts +46 -0
- package/src/cli/core/templates/vitestConfigTs.ts +17 -0
- package/src/cli/core/templates/webAppRouterTs.ts +102 -82
- package/src/cli/core/templates/webIndexTs.ts +23 -1
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
- package/src/cli/platform/__tests__/SecretsCommand.spec.ts +2 -0
- package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
- package/src/cli/platform/atoms/platformOptions.ts +9 -0
- package/src/cli/platform/schemas/cloudflare.ts +3 -2
- package/src/cli/platform/services/CloudflareApi.ts +164 -25
- package/src/cli/platform/services/WranglerApi.ts +0 -17
- package/src/command/providers/CliProvider.ts +1 -1
- package/src/core/Alepha.ts +9 -0
- package/src/core/interfaces/Service.ts +3 -1
- package/src/core/providers/TypeProvider.ts +1 -1
- package/src/logger/services/Logger.ts +1 -1
- package/src/mcp/__tests__/$resource.spec.ts +1 -1
- package/src/mcp/__tests__/$tool.spec.ts +1 -1
- package/src/mcp/__tests__/McpServerProvider.spec.ts +1 -1
- package/src/orm/__tests__/$repository-tests.ts +1 -0
- package/src/orm/__tests__/orm-next-tests.ts +2 -67
- package/src/orm/__tests__/orm-next.spec.ts +0 -21
- package/src/orm/core/index.shared.ts +0 -2
- package/src/orm/core/index.ts +1 -2
- package/src/orm/core/primitives/$repository.ts +3 -6
- package/src/orm/core/providers/drivers/DatabaseProvider.ts +0 -5
- package/src/orm/core/providers/drivers/NodeSqliteProvider.ts +11 -13
- package/src/orm/core/services/ModelBuilder.ts +1 -13
- package/src/orm/core/services/Repository.ts +1 -42
- package/src/orm/core/services/SqliteModelBuilder.ts +2 -33
- package/src/orm/postgres/services/PostgresModelBuilder.ts +10 -45
- package/src/react/form/index.ts +2 -0
- package/src/react/form/services/parseField.ts +163 -0
- package/src/react/form/services/prettyName.ts +19 -0
- package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
- package/src/react/intro/components/GettingStartedAuthSlide.tsx +11 -4
- package/src/react/router/__tests__/ReactBrowserProvider.browser.spec.ts +213 -2
- package/src/react/router/primitives/$page.ts +35 -12
- package/src/react/router/providers/ReactBrowserProvider.ts +73 -0
- package/src/react/router/providers/ReactBrowserRouterProvider.ts +1 -1
- package/src/react/router/providers/ReactPreloadProvider.ts +1 -1
- package/src/react/router/providers/ReactServerProvider.ts +1 -0
- package/src/react/ui/atoms/uiAtom.ts +28 -0
- package/src/react/ui/components/ColorScheme.tsx +36 -0
- package/src/react/ui/hooks/useColorMode.ts +49 -0
- package/src/react/ui/hooks/useSidebarState.ts +26 -0
- package/src/react/ui/hooks/useTheme.ts +22 -0
- package/src/react/ui/index.ts +35 -0
- package/src/react/ui/services/UiPersistence.ts +41 -0
- package/src/router/TemplatedPathParser.ts +50 -51
- package/src/router/__tests__/RouterProvider.spec.ts +62 -0
- package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
- package/src/router/providers/RouterProvider.ts +10 -5
- package/src/scheduler/providers/CronProvider.ts +1 -1
- package/src/security/primitives/$basicAuth.ts +1 -1
- package/src/server/auth/providers/ServerAuthProvider.ts +5 -1
- package/src/server/core/interfaces/ServerRequest.ts +1 -0
- package/src/server/core/providers/ServerProvider.ts +1 -1
- package/src/server/core/providers/ServerRouterProvider.ts +2 -2
- package/src/server/core/services/HttpClient.ts +1 -1
- package/src/server/swagger/providers/ServerSwaggerProvider.ts +1 -1
- package/src/system/providers/NodeShellProvider.ts +1 -0
- package/src/topic/core/providers/TopicProvider.ts +1 -1
- package/dist/api/invitations/index.d.ts +0 -790
- package/dist/api/invitations/index.d.ts.map +0 -1
- package/dist/api/invitations/index.js +0 -662
- package/dist/api/invitations/index.js.map +0 -1
- package/dist/api/issues/index.d.ts +0 -810
- package/dist/api/issues/index.d.ts.map +0 -1
- package/dist/api/issues/index.js +0 -444
- package/dist/api/issues/index.js.map +0 -1
- package/dist/api/subscriptions/index.d.ts +0 -1692
- package/dist/api/subscriptions/index.d.ts.map +0 -1
- package/dist/api/subscriptions/index.js +0 -1867
- package/dist/api/subscriptions/index.js.map +0 -1
- package/dist/api/workflows/index.browser.js +0 -246
- package/dist/api/workflows/index.browser.js.map +0 -1
- package/dist/api/workflows/index.d.ts +0 -1618
- package/dist/api/workflows/index.d.ts.map +0 -1
- package/dist/api/workflows/index.js +0 -1495
- package/dist/api/workflows/index.js.map +0 -1
- package/dist/react/testing/chunk-DBEY4PJZ.js +0 -16
- package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
- package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
- package/src/api/invitations/controllers/InvitationController.ts +0 -84
- package/src/api/invitations/entities/invitations.ts +0 -33
- package/src/api/invitations/index.ts +0 -58
- package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
- package/src/api/invitations/providers/InvitationProvider.ts +0 -45
- package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
- package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
- package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
- package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
- package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
- package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
- package/src/api/invitations/services/InvitationService.ts +0 -556
- package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
- package/src/api/issues/controllers/AdminIssueController.ts +0 -149
- package/src/api/issues/controllers/IssueController.ts +0 -44
- package/src/api/issues/entities/issues.ts +0 -49
- package/src/api/issues/index.ts +0 -50
- package/src/api/issues/schemas/createIssueSchema.ts +0 -13
- package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
- package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
- package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
- package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
- package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
- package/src/api/issues/services/IssueService.ts +0 -264
- package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
- package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
- package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
- package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
- package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
- package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
- package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
- package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
- package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
- package/src/api/jobs/services/JobService-tests.ts +0 -157
- package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
- package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
- package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
- package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
- package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
- package/src/api/subscriptions/entities/subscriptions.ts +0 -68
- package/src/api/subscriptions/index.ts +0 -133
- package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
- package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
- package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
- package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
- package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
- package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
- package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
- package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
- package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
- package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
- package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
- package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
- package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
- package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
- package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
- package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
- package/src/api/subscriptions/services/BillingService.ts +0 -437
- package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
- package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
- package/src/api/subscriptions/services/UsageService.ts +0 -118
- package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
- package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
- package/src/api/workflows/entities/workflowExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
- package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
- package/src/api/workflows/index.browser.ts +0 -22
- package/src/api/workflows/index.ts +0 -115
- package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
- package/src/api/workflows/primitives/$workflow.ts +0 -202
- package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
- package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
- package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
- package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
- package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
- package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
- package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
- package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
- package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
- package/src/api/workflows/services/WorkflowService.ts +0 -382
- package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
- package/src/cli/core/templates/webAdminDashboardTsx.ts +0 -17
- package/src/orm/core/__tests__/parseQueryString.spec.ts +0 -196
- package/src/orm/core/helpers/parseQueryString.ts +0 -502
- package/src/orm/core/primitives/$view.ts +0 -88
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { type TSchema, TypeBoxError } from "alepha";
|
|
2
|
+
import type { BaseInputField } from "./FormModel.ts";
|
|
3
|
+
import { prettyName } from "./prettyName.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Semantic icon hint derived from schema metadata. UI layers map this
|
|
7
|
+
* to their own icon set — this module is headless and ships no JSX.
|
|
8
|
+
*/
|
|
9
|
+
export type IconHint =
|
|
10
|
+
| "email"
|
|
11
|
+
| "password"
|
|
12
|
+
| "phone"
|
|
13
|
+
| "url"
|
|
14
|
+
| "number"
|
|
15
|
+
| "calendar"
|
|
16
|
+
| "clock"
|
|
17
|
+
| "list"
|
|
18
|
+
| "text"
|
|
19
|
+
| "user"
|
|
20
|
+
| "file"
|
|
21
|
+
| "switch";
|
|
22
|
+
|
|
23
|
+
export interface FieldConstraints {
|
|
24
|
+
minLength?: number;
|
|
25
|
+
maxLength?: number;
|
|
26
|
+
minimum?: number;
|
|
27
|
+
maximum?: number;
|
|
28
|
+
pattern?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface FieldMeta {
|
|
32
|
+
id?: string;
|
|
33
|
+
label: string;
|
|
34
|
+
description?: string;
|
|
35
|
+
error?: string;
|
|
36
|
+
required: boolean;
|
|
37
|
+
type?: string;
|
|
38
|
+
format?: string;
|
|
39
|
+
isEnum: boolean;
|
|
40
|
+
isArray: boolean;
|
|
41
|
+
isObject: boolean;
|
|
42
|
+
isArrayOfObjects: boolean;
|
|
43
|
+
enum?: readonly unknown[];
|
|
44
|
+
iconHint?: IconHint;
|
|
45
|
+
constraints: FieldConstraints;
|
|
46
|
+
testId?: string;
|
|
47
|
+
schema: TSchema;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface ParseFieldOptions {
|
|
51
|
+
label?: string;
|
|
52
|
+
description?: string;
|
|
53
|
+
error?: Error;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Derives a {@link FieldMeta} from an `InputField` (from `useForm`) plus
|
|
58
|
+
* optional overrides. Pure — no React, no JSX, no UI library coupling.
|
|
59
|
+
*
|
|
60
|
+
* UI components consume this metadata to render labels, descriptions,
|
|
61
|
+
* error messages, icons, and validation constraints.
|
|
62
|
+
*/
|
|
63
|
+
export const parseField = (
|
|
64
|
+
input: BaseInputField,
|
|
65
|
+
options: ParseFieldOptions = {},
|
|
66
|
+
): FieldMeta => {
|
|
67
|
+
const schema = input.schema as TSchema & {
|
|
68
|
+
type?: string;
|
|
69
|
+
format?: string;
|
|
70
|
+
title?: string;
|
|
71
|
+
description?: string;
|
|
72
|
+
enum?: readonly unknown[];
|
|
73
|
+
minLength?: number;
|
|
74
|
+
maxLength?: number;
|
|
75
|
+
minimum?: number;
|
|
76
|
+
maximum?: number;
|
|
77
|
+
pattern?: string;
|
|
78
|
+
properties?: unknown;
|
|
79
|
+
items?: { properties?: unknown };
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const label =
|
|
83
|
+
options.label ??
|
|
84
|
+
(typeof schema.title === "string" ? schema.title : undefined) ??
|
|
85
|
+
prettyName(input.path);
|
|
86
|
+
|
|
87
|
+
const description =
|
|
88
|
+
options.description ??
|
|
89
|
+
(typeof schema.description === "string" ? schema.description : undefined);
|
|
90
|
+
|
|
91
|
+
const error =
|
|
92
|
+
options.error instanceof TypeBoxError
|
|
93
|
+
? (options.error as TypeBoxError).value?.message
|
|
94
|
+
: undefined;
|
|
95
|
+
|
|
96
|
+
const type = schema.type;
|
|
97
|
+
const format = typeof schema.format === "string" ? schema.format : undefined;
|
|
98
|
+
const isEnum = Array.isArray(schema.enum);
|
|
99
|
+
const isArray = type === "array";
|
|
100
|
+
const isObject = type === "object" && Boolean(schema.properties);
|
|
101
|
+
const isArrayOfObjects =
|
|
102
|
+
isArray && Boolean(schema.items && (schema.items as any).properties);
|
|
103
|
+
|
|
104
|
+
const name = input.props.name;
|
|
105
|
+
const iconHint = inferIconHint({ type, format, name, isEnum, isArray });
|
|
106
|
+
|
|
107
|
+
const constraints: FieldConstraints = {};
|
|
108
|
+
if (typeof schema.minLength === "number")
|
|
109
|
+
constraints.minLength = schema.minLength;
|
|
110
|
+
if (typeof schema.maxLength === "number")
|
|
111
|
+
constraints.maxLength = schema.maxLength;
|
|
112
|
+
if (typeof schema.minimum === "number") constraints.minimum = schema.minimum;
|
|
113
|
+
if (typeof schema.maximum === "number") constraints.maximum = schema.maximum;
|
|
114
|
+
if (typeof schema.pattern === "string") constraints.pattern = schema.pattern;
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
id: input.props.id,
|
|
118
|
+
label,
|
|
119
|
+
description,
|
|
120
|
+
error,
|
|
121
|
+
required: input.required,
|
|
122
|
+
type,
|
|
123
|
+
format,
|
|
124
|
+
isEnum,
|
|
125
|
+
isArray,
|
|
126
|
+
isObject,
|
|
127
|
+
isArrayOfObjects,
|
|
128
|
+
enum: schema.enum,
|
|
129
|
+
iconHint,
|
|
130
|
+
constraints,
|
|
131
|
+
testId: (input.props as Record<string, unknown>)["data-testid"] as
|
|
132
|
+
| string
|
|
133
|
+
| undefined,
|
|
134
|
+
schema: input.schema,
|
|
135
|
+
};
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const inferIconHint = (params: {
|
|
139
|
+
type?: string;
|
|
140
|
+
format?: string;
|
|
141
|
+
name?: string;
|
|
142
|
+
isEnum: boolean;
|
|
143
|
+
isArray: boolean;
|
|
144
|
+
}): IconHint | undefined => {
|
|
145
|
+
const { type, format, name, isEnum, isArray } = params;
|
|
146
|
+
|
|
147
|
+
if (format === "email") return "email";
|
|
148
|
+
if (format === "url" || format === "uri") return "url";
|
|
149
|
+
if (format === "tel" || format === "phone") return "phone";
|
|
150
|
+
if (format === "date" || format === "date-time") return "calendar";
|
|
151
|
+
if (format === "time") return "clock";
|
|
152
|
+
|
|
153
|
+
if (name?.toLowerCase().includes("password")) return "password";
|
|
154
|
+
if (name?.toLowerCase().includes("email")) return "email";
|
|
155
|
+
if (name?.toLowerCase().includes("phone")) return "phone";
|
|
156
|
+
|
|
157
|
+
if (type === "boolean") return "switch";
|
|
158
|
+
if (type === "number" || type === "integer") return "number";
|
|
159
|
+
if (isEnum || isArray) return "list";
|
|
160
|
+
if (type === "string") return "text";
|
|
161
|
+
|
|
162
|
+
return undefined;
|
|
163
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Converts a path or identifier string into a pretty display name.
|
|
3
|
+
* For paths like "/contacts/0/name", extracts just the field name "Name".
|
|
4
|
+
* Handles camelCase and snake_case conversion to Title Case.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* prettyName("/userName") // "User Name"
|
|
8
|
+
* prettyName("/contacts/0/email") // "Email"
|
|
9
|
+
* prettyName("/address/streetName") // "Street Name"
|
|
10
|
+
* prettyName("first_name") // "First Name"
|
|
11
|
+
*/
|
|
12
|
+
export const prettyName = (name: string): string => {
|
|
13
|
+
const segments = name.split("/").filter((s) => s && !/^\d+$/.test(s));
|
|
14
|
+
const fieldName = segments[segments.length - 1] || name.replaceAll("/", "");
|
|
15
|
+
return fieldName
|
|
16
|
+
.replace(/([a-z])([A-Z])/g, "$1 $2")
|
|
17
|
+
.replace(/_/g, " ")
|
|
18
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
19
|
+
};
|
|
@@ -154,10 +154,12 @@ export class BrowserHeadProvider {
|
|
|
154
154
|
| string
|
|
155
155
|
| (Record<string, string | boolean | undefined> & { content?: string }),
|
|
156
156
|
): void {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
//
|
|
157
|
+
// Plain string → inline script. Dedupe by exact content match against
|
|
158
|
+
// any existing inline script (handles SSR-emitted globals that would
|
|
159
|
+
// otherwise be re-appended on hydration).
|
|
160
160
|
if (typeof script === "string") {
|
|
161
|
+
if (this.findInlineScriptByContent(document, script)) return;
|
|
162
|
+
const el = document.createElement("script");
|
|
161
163
|
el.textContent = script;
|
|
162
164
|
document.head.appendChild(el);
|
|
163
165
|
return;
|
|
@@ -165,14 +167,18 @@ export class BrowserHeadProvider {
|
|
|
165
167
|
|
|
166
168
|
const { content, ...attrs } = script;
|
|
167
169
|
|
|
168
|
-
//
|
|
170
|
+
// src-based scripts: dedupe by src attribute (existing behaviour).
|
|
169
171
|
if (attrs.src) {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
}
|
|
172
|
+
if (document.querySelector(`script[src="${attrs.src}"]`)) return;
|
|
173
|
+
} else if (typeof attrs.id === "string") {
|
|
174
|
+
// id-based dedupe — single source of truth per id.
|
|
175
|
+
if (document.querySelector(`script#${CSS.escape(attrs.id)}`)) return;
|
|
176
|
+
} else if (content) {
|
|
177
|
+
// Inline scripts with `content` and no src/id: fall back to content match.
|
|
178
|
+
if (this.findInlineScriptByContent(document, content)) return;
|
|
174
179
|
}
|
|
175
180
|
|
|
181
|
+
const el = document.createElement("script");
|
|
176
182
|
for (const [key, value] of Object.entries(attrs)) {
|
|
177
183
|
if (value === true) {
|
|
178
184
|
el.setAttribute(key, "");
|
|
@@ -180,14 +186,29 @@ export class BrowserHeadProvider {
|
|
|
180
186
|
el.setAttribute(key, String(value));
|
|
181
187
|
}
|
|
182
188
|
}
|
|
183
|
-
|
|
184
189
|
if (content) {
|
|
185
190
|
el.textContent = content;
|
|
186
191
|
}
|
|
187
|
-
|
|
188
192
|
document.head.appendChild(el);
|
|
189
193
|
}
|
|
190
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Find an existing inline `<script>` tag (no `src`) with matching textContent.
|
|
197
|
+
* Used to make `renderScriptTag` idempotent across hydration + navigation,
|
|
198
|
+
* so SSR-emitted global scripts aren't re-appended client-side.
|
|
199
|
+
*/
|
|
200
|
+
protected findInlineScriptByContent(
|
|
201
|
+
document: Document,
|
|
202
|
+
content: string,
|
|
203
|
+
): Element | null {
|
|
204
|
+
for (const existing of document.head.querySelectorAll(
|
|
205
|
+
"script:not([src])",
|
|
206
|
+
)) {
|
|
207
|
+
if (existing.textContent === content) return existing;
|
|
208
|
+
}
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
191
212
|
protected renderMetaTag(document: Document, meta: HeadMeta): void {
|
|
192
213
|
const { content } = meta;
|
|
193
214
|
|
|
@@ -8,7 +8,7 @@ import type { GettingStartedSlide } from "./GettingStarted.tsx";
|
|
|
8
8
|
* Returns undefined if auth routes are not configured.
|
|
9
9
|
*/
|
|
10
10
|
export const useAuthSlide = (): GettingStartedSlide | undefined => {
|
|
11
|
-
const { user } = useAuth();
|
|
11
|
+
const { user, logout } = useAuth();
|
|
12
12
|
const router = useRouter();
|
|
13
13
|
|
|
14
14
|
// Check if auth routes exist
|
|
@@ -19,8 +19,6 @@ export const useAuthSlide = (): GettingStartedSlide | undefined => {
|
|
|
19
19
|
|
|
20
20
|
// User is logged in - show user info and logout option
|
|
21
21
|
if (user) {
|
|
22
|
-
const logoutAnchorProps = router.anchor(router.path("logout"));
|
|
23
|
-
|
|
24
22
|
return {
|
|
25
23
|
text: "Welcome back!",
|
|
26
24
|
sub: `You're signed in as ${user.email || user.username || "user"}.`,
|
|
@@ -33,7 +31,16 @@ export const useAuthSlide = (): GettingStartedSlide | undefined => {
|
|
|
33
31
|
num: "→",
|
|
34
32
|
text: (
|
|
35
33
|
<>
|
|
36
|
-
<a
|
|
34
|
+
<a
|
|
35
|
+
href="#"
|
|
36
|
+
onClick={(e) => {
|
|
37
|
+
e.preventDefault();
|
|
38
|
+
logout();
|
|
39
|
+
}}
|
|
40
|
+
>
|
|
41
|
+
Sign out
|
|
42
|
+
</a>{" "}
|
|
43
|
+
to test the login flow
|
|
37
44
|
</>
|
|
38
45
|
),
|
|
39
46
|
},
|
|
@@ -1,9 +1,22 @@
|
|
|
1
1
|
import { Alepha } from "alepha";
|
|
2
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
-
import {
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
ReactBrowserProvider,
|
|
5
|
+
type RouterPushOptions,
|
|
6
|
+
reactBrowserOptions,
|
|
7
|
+
} from "../providers/ReactBrowserProvider.ts";
|
|
4
8
|
|
|
5
9
|
class TestReactBrowserProvider extends ReactBrowserProvider {
|
|
6
10
|
public testGetHydrationState = this.getHydrationState.bind(this);
|
|
11
|
+
public testAttachAnchorInterceptor = this.attachAnchorInterceptor.bind(this);
|
|
12
|
+
public pushCalls: Array<{ url: string; options?: RouterPushOptions }> = [];
|
|
13
|
+
|
|
14
|
+
public override async push(
|
|
15
|
+
url: string,
|
|
16
|
+
options?: RouterPushOptions,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
this.pushCalls.push({ url, options });
|
|
19
|
+
}
|
|
7
20
|
}
|
|
8
21
|
|
|
9
22
|
describe("ReactBrowserProvider", () => {
|
|
@@ -102,4 +115,202 @@ describe("ReactBrowserProvider", () => {
|
|
|
102
115
|
expect(result?.["alepha.i18n.locale"]).toBe("en");
|
|
103
116
|
});
|
|
104
117
|
});
|
|
118
|
+
|
|
119
|
+
describe("attachAnchorInterceptor", () => {
|
|
120
|
+
let alepha: Alepha;
|
|
121
|
+
let provider: TestReactBrowserProvider;
|
|
122
|
+
let detach: () => void;
|
|
123
|
+
let container: HTMLDivElement;
|
|
124
|
+
|
|
125
|
+
const createAnchor = (
|
|
126
|
+
attrs: Record<string, string>,
|
|
127
|
+
inner?: HTMLElement,
|
|
128
|
+
): HTMLAnchorElement => {
|
|
129
|
+
const a = document.createElement("a");
|
|
130
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
131
|
+
a.setAttribute(k, v);
|
|
132
|
+
}
|
|
133
|
+
if (inner) {
|
|
134
|
+
a.appendChild(inner);
|
|
135
|
+
} else {
|
|
136
|
+
a.textContent = "link";
|
|
137
|
+
}
|
|
138
|
+
container.appendChild(a);
|
|
139
|
+
return a;
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const click = (
|
|
143
|
+
target: HTMLElement,
|
|
144
|
+
init: MouseEventInit = {},
|
|
145
|
+
): MouseEvent => {
|
|
146
|
+
const ev = new MouseEvent("click", {
|
|
147
|
+
bubbles: true,
|
|
148
|
+
cancelable: true,
|
|
149
|
+
button: 0,
|
|
150
|
+
...init,
|
|
151
|
+
});
|
|
152
|
+
target.dispatchEvent(ev);
|
|
153
|
+
return ev;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
alepha = Alepha.create();
|
|
158
|
+
provider = alepha.inject(TestReactBrowserProvider);
|
|
159
|
+
container = document.createElement("div");
|
|
160
|
+
document.body.appendChild(container);
|
|
161
|
+
detach = provider.testAttachAnchorInterceptor();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
afterEach(() => {
|
|
165
|
+
detach();
|
|
166
|
+
container.remove();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("intercepts plain internal /foo anchor clicks", () => {
|
|
170
|
+
const a = createAnchor({ href: "/foo" });
|
|
171
|
+
|
|
172
|
+
const ev = click(a);
|
|
173
|
+
|
|
174
|
+
expect(provider.pushCalls).toHaveLength(1);
|
|
175
|
+
expect(provider.pushCalls[0].url).toBe("/foo");
|
|
176
|
+
expect(ev.defaultPrevented).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("preserves query and hash when intercepting", () => {
|
|
180
|
+
const a = createAnchor({ href: "/foo?x=1#bar" });
|
|
181
|
+
|
|
182
|
+
click(a);
|
|
183
|
+
|
|
184
|
+
expect(provider.pushCalls[0].url).toBe("/foo?x=1#bar");
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("ignores cmd-click (metaKey)", () => {
|
|
188
|
+
const a = createAnchor({ href: "/foo" });
|
|
189
|
+
|
|
190
|
+
const ev = click(a, { metaKey: true });
|
|
191
|
+
|
|
192
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
193
|
+
expect(ev.defaultPrevented).toBe(false);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("ignores ctrl-click", () => {
|
|
197
|
+
const a = createAnchor({ href: "/foo" });
|
|
198
|
+
|
|
199
|
+
click(a, { ctrlKey: true });
|
|
200
|
+
|
|
201
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("ignores shift-click", () => {
|
|
205
|
+
const a = createAnchor({ href: "/foo" });
|
|
206
|
+
|
|
207
|
+
click(a, { shiftKey: true });
|
|
208
|
+
|
|
209
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("ignores alt-click", () => {
|
|
213
|
+
const a = createAnchor({ href: "/foo" });
|
|
214
|
+
|
|
215
|
+
click(a, { altKey: true });
|
|
216
|
+
|
|
217
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("ignores non-primary mouse buttons", () => {
|
|
221
|
+
const a = createAnchor({ href: "/foo" });
|
|
222
|
+
|
|
223
|
+
click(a, { button: 1 });
|
|
224
|
+
|
|
225
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it("ignores anchors with target='_blank'", () => {
|
|
229
|
+
const a = createAnchor({ href: "/foo", target: "_blank" });
|
|
230
|
+
|
|
231
|
+
click(a);
|
|
232
|
+
|
|
233
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("ignores anchors with download attribute", () => {
|
|
237
|
+
const a = createAnchor({ href: "/foo", download: "" });
|
|
238
|
+
|
|
239
|
+
click(a);
|
|
240
|
+
|
|
241
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("ignores anchors with data-no-router attribute", () => {
|
|
245
|
+
const a = createAnchor({ href: "/foo", "data-no-router": "" });
|
|
246
|
+
|
|
247
|
+
click(a);
|
|
248
|
+
|
|
249
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("ignores mailto: hrefs", () => {
|
|
253
|
+
const a = createAnchor({ href: "mailto:foo@bar.com" });
|
|
254
|
+
|
|
255
|
+
click(a);
|
|
256
|
+
|
|
257
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("ignores tel: hrefs", () => {
|
|
261
|
+
const a = createAnchor({ href: "tel:+15555555" });
|
|
262
|
+
|
|
263
|
+
click(a);
|
|
264
|
+
|
|
265
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("ignores hrefs to external origins", () => {
|
|
269
|
+
const a = createAnchor({ href: "https://example.com/foo" });
|
|
270
|
+
|
|
271
|
+
click(a);
|
|
272
|
+
|
|
273
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("ignores hash-only #section hrefs", () => {
|
|
277
|
+
const a = createAnchor({ href: "#section" });
|
|
278
|
+
|
|
279
|
+
click(a);
|
|
280
|
+
|
|
281
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("intercepts when click target is nested inside the anchor", () => {
|
|
285
|
+
const span = document.createElement("span");
|
|
286
|
+
span.textContent = "inner";
|
|
287
|
+
createAnchor({ href: "/foo" }, span);
|
|
288
|
+
|
|
289
|
+
click(span);
|
|
290
|
+
|
|
291
|
+
expect(provider.pushCalls).toHaveLength(1);
|
|
292
|
+
expect(provider.pushCalls[0].url).toBe("/foo");
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("skips when defaultPrevented is already true", () => {
|
|
296
|
+
const a = createAnchor({ href: "/foo" });
|
|
297
|
+
a.addEventListener("click", (ev) => ev.preventDefault());
|
|
298
|
+
|
|
299
|
+
click(a);
|
|
300
|
+
|
|
301
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it("respects interceptAnchorClicks=false at runtime", () => {
|
|
305
|
+
alepha.store.set(reactBrowserOptions.key, {
|
|
306
|
+
...alepha.store.get(reactBrowserOptions.key)!,
|
|
307
|
+
interceptAnchorClicks: false,
|
|
308
|
+
});
|
|
309
|
+
const a = createAnchor({ href: "/foo" });
|
|
310
|
+
|
|
311
|
+
click(a);
|
|
312
|
+
|
|
313
|
+
expect(provider.pushCalls).toHaveLength(0);
|
|
314
|
+
});
|
|
315
|
+
});
|
|
105
316
|
});
|
|
@@ -50,16 +50,6 @@ import { ReactPageService } from "../services/ReactPageService.ts";
|
|
|
50
50
|
* - Hierarchical error handling (child → parent)
|
|
51
51
|
* - HTTP status code handling (404, 401, etc.)
|
|
52
52
|
*
|
|
53
|
-
* **Page Animations**
|
|
54
|
-
* - CSS-based enter/exit animations
|
|
55
|
-
* - Dynamic animations based on page state
|
|
56
|
-
* - Custom timing and easing functions
|
|
57
|
-
*
|
|
58
|
-
* **Lifecycle Management**
|
|
59
|
-
* - Server response hooks for headers and status codes
|
|
60
|
-
* - Page leave handlers for cleanup (browser only)
|
|
61
|
-
* - Permission-based access control
|
|
62
|
-
*
|
|
63
53
|
* @example Simple page with data fetching
|
|
64
54
|
* ```typescript
|
|
65
55
|
* const userProfile = $page({
|
|
@@ -202,13 +192,46 @@ export interface PagePrimitiveOptions<
|
|
|
202
192
|
lazy?: () => Promise<{ default: FC<TProps & TPropsParent> }>;
|
|
203
193
|
|
|
204
194
|
/**
|
|
205
|
-
* Attach child pages to create nested routes
|
|
206
|
-
*
|
|
195
|
+
* Attach child pages to create nested routes, adopting them as children of
|
|
196
|
+
* this page.
|
|
197
|
+
*
|
|
198
|
+
* Use this when you want a parent to own children it cannot modify — most
|
|
199
|
+
* notably pages that come from an injected router in another package, whose
|
|
200
|
+
* `$page` definitions are frozen and cannot declare `parent` themselves.
|
|
201
|
+
*
|
|
202
|
+
* ```ts
|
|
203
|
+
* layout = $page({
|
|
204
|
+
* path: "/app",
|
|
205
|
+
* children: () => [
|
|
206
|
+
* this.productRouter.catalogPage, // from $inject(ProductRouter)
|
|
207
|
+
* this.productRouter.checkoutPage,
|
|
208
|
+
* ],
|
|
209
|
+
* });
|
|
210
|
+
* ```
|
|
211
|
+
*
|
|
212
|
+
* Use a thunk (`() => [...]`) when the children are defined later in the
|
|
213
|
+
* same class.
|
|
214
|
+
*
|
|
215
|
+
* **Declare each edge from one side only.** If a child already sets
|
|
216
|
+
* `parent: thisPage`, do NOT also add it to `children` — the link is
|
|
217
|
+
* already established, and declaring it on both sides creates a TypeScript
|
|
218
|
+
* circular dependency between the two class fields (each references the
|
|
219
|
+
* other before it is initialised).
|
|
207
220
|
*/
|
|
208
221
|
children?: Array<PagePrimitive> | (() => Array<PagePrimitive>);
|
|
209
222
|
|
|
210
223
|
/**
|
|
211
224
|
* Define a parent page for nested routing.
|
|
225
|
+
*
|
|
226
|
+
* Use this when you own the child page and can edit its definition — it is
|
|
227
|
+
* the simplest way to nest routes and reads top-down. For pages you do NOT
|
|
228
|
+
* own (e.g. pages exposed by an injected router from another package), let
|
|
229
|
+
* the parent adopt them via its `children` option instead.
|
|
230
|
+
*
|
|
231
|
+
* **Declare each edge from one side only.** If you set `parent` here, do
|
|
232
|
+
* NOT also add this page to the parent's `children` array — the link is
|
|
233
|
+
* already established, and declaring it on both sides creates a TypeScript
|
|
234
|
+
* circular dependency between the two class fields.
|
|
212
235
|
*/
|
|
213
236
|
parent?: PagePrimitive<PageConfigSchema, TPropsParent, any>;
|
|
214
237
|
|
|
@@ -28,9 +28,20 @@ export const reactBrowserOptions = $atom({
|
|
|
28
28
|
name: "alepha.react.browser.options",
|
|
29
29
|
schema: t.object({
|
|
30
30
|
scrollRestoration: t.enum(["top", "manual"]), // TODO: must be per page?
|
|
31
|
+
/**
|
|
32
|
+
* Intercept clicks on plain `<a href="/...">` anchors and route them
|
|
33
|
+
* through the SPA router, so authors don't need `<Link>` everywhere
|
|
34
|
+
* (notably for SSR/Markdown HTML rendered as raw markup).
|
|
35
|
+
*
|
|
36
|
+
* Skips: modifier keys, non-primary mouse buttons, `target` other than
|
|
37
|
+
* `_self`, `download`, `data-no-router`, non-http(s) schemes, hash-only
|
|
38
|
+
* hrefs, external origins, and clicks already `defaultPrevented`.
|
|
39
|
+
*/
|
|
40
|
+
interceptAnchorClicks: t.boolean({ default: true }),
|
|
31
41
|
}),
|
|
32
42
|
default: {
|
|
33
43
|
scrollRestoration: "top" as const,
|
|
44
|
+
interceptAnchorClicks: true,
|
|
34
45
|
},
|
|
35
46
|
});
|
|
36
47
|
|
|
@@ -325,8 +336,70 @@ export class ReactBrowserProvider {
|
|
|
325
336
|
|
|
326
337
|
this.render();
|
|
327
338
|
});
|
|
339
|
+
|
|
340
|
+
this.attachAnchorInterceptor();
|
|
328
341
|
},
|
|
329
342
|
});
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Attach a delegated click listener that routes plain `<a href="/...">`
|
|
346
|
+
* clicks through the SPA router. Returns a detach function (used in tests).
|
|
347
|
+
*
|
|
348
|
+
* Bails out on modifier keys, non-primary mouse buttons, `target`, `download`,
|
|
349
|
+
* `data-no-router`, hash-only/external/non-http hrefs, and already-prevented
|
|
350
|
+
* events. Honors the runtime `interceptAnchorClicks` flag.
|
|
351
|
+
*/
|
|
352
|
+
protected attachAnchorInterceptor(): () => void {
|
|
353
|
+
const onClick = (ev: MouseEvent) => {
|
|
354
|
+
if (!this.options.interceptAnchorClicks) return;
|
|
355
|
+
if (ev.defaultPrevented) return;
|
|
356
|
+
if (ev.button !== 0) return;
|
|
357
|
+
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
358
|
+
|
|
359
|
+
const node = ev.target as Element | null;
|
|
360
|
+
const a = node?.closest?.("a");
|
|
361
|
+
if (!a) return;
|
|
362
|
+
|
|
363
|
+
if (a.hasAttribute("download")) return;
|
|
364
|
+
if (a.hasAttribute("data-no-router")) return;
|
|
365
|
+
|
|
366
|
+
const target = a.getAttribute("target");
|
|
367
|
+
if (target && target !== "_self") return;
|
|
368
|
+
|
|
369
|
+
const href = a.getAttribute("href");
|
|
370
|
+
if (!href) return;
|
|
371
|
+
if (href.startsWith("#")) return;
|
|
372
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) {
|
|
373
|
+
// absolute scheme: only intercept if it points at our own origin
|
|
374
|
+
let url: URL;
|
|
375
|
+
try {
|
|
376
|
+
url = new URL(href);
|
|
377
|
+
} catch {
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (url.origin !== this.location.origin) return;
|
|
381
|
+
ev.preventDefault();
|
|
382
|
+
const path = url.pathname + url.search + url.hash;
|
|
383
|
+
this.push(this.stripBase(path)).catch((e) => this.log.error(e));
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ev.preventDefault();
|
|
388
|
+
const url = new URL(href, this.location.href);
|
|
389
|
+
const path = url.pathname + url.search + url.hash;
|
|
390
|
+
this.push(this.stripBase(path)).catch((e) => this.log.error(e));
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
this.document.addEventListener("click", onClick);
|
|
394
|
+
return () => this.document.removeEventListener("click", onClick);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
protected stripBase(path: string): string {
|
|
398
|
+
if (this.base && path.startsWith(this.base)) {
|
|
399
|
+
return path.slice(this.base.length) || "/";
|
|
400
|
+
}
|
|
401
|
+
return path;
|
|
402
|
+
}
|
|
330
403
|
}
|
|
331
404
|
|
|
332
405
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
@@ -52,7 +52,7 @@ export class ReactBrowserRouterProvider extends RouterProvider<BrowserRoute> {
|
|
|
52
52
|
previous: PreviousLayerData[] = [],
|
|
53
53
|
meta = {},
|
|
54
54
|
isStale: () => boolean = () => false,
|
|
55
|
-
): Promise<string |
|
|
55
|
+
): Promise<string | undefined> {
|
|
56
56
|
const { pathname, search } = url;
|
|
57
57
|
|
|
58
58
|
const entry: Partial<ReactRouterState> = {
|
|
@@ -65,7 +65,7 @@ export class ReactPreloadProvider {
|
|
|
65
65
|
handler: ({ response }) => {
|
|
66
66
|
// Only add to HTML responses (SSR pages)
|
|
67
67
|
const contentType = response.headers["content-type"];
|
|
68
|
-
if (!contentType
|
|
68
|
+
if (!contentType?.includes("text/html")) {
|
|
69
69
|
return;
|
|
70
70
|
}
|
|
71
71
|
|