alepha 0.20.1 → 0.20.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/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 +371 -573
- 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/notifications/index.d.ts +78 -17
- 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/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 +34 -31
- package/dist/api/users/index.d.ts.map +1 -1
- package/dist/api/users/index.js +13 -7
- package/dist/api/users/index.js.map +1 -1
- package/dist/api/verifications/index.js +2 -1
- package/dist/api/verifications/index.js.map +1 -1
- package/dist/cli/core/index.d.ts +8 -34
- package/dist/cli/core/index.d.ts.map +1 -1
- package/dist/cli/core/index.js +43 -232
- package/dist/cli/core/index.js.map +1 -1
- package/dist/cli/platform/index.d.ts +36 -11
- package/dist/cli/platform/index.d.ts.map +1 -1
- package/dist/cli/platform/index.js +93 -27
- package/dist/cli/platform/index.js.map +1 -1
- package/dist/command/index.d.ts +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 +6 -0
- 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/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/router/index.browser.js +0 -10
- package/dist/react/router/index.browser.js.map +1 -1
- package/dist/react/router/index.d.ts +35 -12
- package/dist/react/router/index.d.ts.map +1 -1
- package/dist/react/router/index.js +0 -10
- package/dist/react/router/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 +206 -0
- package/dist/react/ui/index.js.map +1 -0
- 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/system/index.d.ts.map +1 -1
- package/dist/system/index.js +1 -0
- package/dist/system/index.js.map +1 -1
- package/dist/topic/core/index.js +1 -1
- package/dist/topic/core/index.js.map +1 -1
- package/package.json +6 -23
- package/src/api/files/jobs/FileJobs.ts +2 -1
- package/src/api/jobs/__tests__/$job.spec.ts +316 -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/verifications/jobs/VerificationJobs.ts +2 -1
- package/src/cli/core/__tests__/init.spec.ts +1 -1
- package/src/cli/core/commands/init.ts +0 -12
- package/src/cli/core/services/PackageManagerUtils.ts +2 -9
- package/src/cli/core/services/ProjectScaffolder.ts +17 -65
- package/src/cli/core/templates/agentMd.ts +2 -8
- package/src/cli/core/templates/apiIndexTs.ts +4 -18
- package/src/cli/core/templates/mainCss.ts +1 -36
- package/src/cli/core/templates/vitestConfigTs.ts +17 -0
- package/src/cli/core/templates/webAppRouterTs.ts +2 -85
- package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
- 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/core/Alepha.ts +9 -0
- 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/router/primitives/$page.ts +35 -12
- 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/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/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
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import * as _$alepha from "alepha";
|
|
2
|
+
import { Static } from "alepha";
|
|
3
|
+
import * as _$alepha_react_head0 from "alepha/react/head";
|
|
4
|
+
import * as _$alepha_server_cookies0 from "alepha/server/cookies";
|
|
5
|
+
import * as _$typebox from "typebox";
|
|
6
|
+
|
|
7
|
+
//#region ../../src/react/ui/atoms/uiAtom.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* Persisted UI state — color mode, theme palette, sidebar collapsed state, etc.
|
|
10
|
+
*
|
|
11
|
+
* The atom is bound to a single `alepha-ui` cookie via {@link UiPersistence},
|
|
12
|
+
* so values survive page reloads and are available during SSR.
|
|
13
|
+
*/
|
|
14
|
+
declare const uiAtom: _$alepha.Atom<_$alepha.TObject<{
|
|
15
|
+
/** Color mode preference. `"system"` follows the OS-level setting. */mode: _$alepha.TUnsafe<"light" | "dark" | "system">; /** Theme palette name. UI consumers map this to a CSS class on the root. */
|
|
16
|
+
theme: _$alepha.TString; /** Sidebar UI state. */
|
|
17
|
+
sidebar: _$alepha.TObject<{
|
|
18
|
+
collapsed: _$alepha.TBoolean;
|
|
19
|
+
}>;
|
|
20
|
+
}>, "alepha.react.ui">;
|
|
21
|
+
type UiState = Static<typeof uiAtom.schema>;
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region ../../src/react/ui/components/ColorScheme.d.ts
|
|
24
|
+
/**
|
|
25
|
+
* Applies `class="dark"` and an optional theme palette class
|
|
26
|
+
* (`theme-<name>`) to the document root, syncing whenever the underlying
|
|
27
|
+
* atom mutates.
|
|
28
|
+
*
|
|
29
|
+
* Mount once near the root of your tree (typically inside the layout).
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* <ColorScheme />
|
|
33
|
+
*/
|
|
34
|
+
declare function ColorScheme(): null;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region ../../src/react/ui/hooks/useColorMode.d.ts
|
|
37
|
+
type ColorMode = "light" | "dark" | "system";
|
|
38
|
+
type ResolvedColorMode = "light" | "dark";
|
|
39
|
+
/**
|
|
40
|
+
* Read and update the user's color-mode preference. `"system"` resolves to
|
|
41
|
+
* the OS preference and updates live as the OS toggles between light/dark.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* const { mode, setMode, resolved } = useColorMode();
|
|
45
|
+
* setMode("dark");
|
|
46
|
+
* document.documentElement.classList.toggle("dark", resolved === "dark");
|
|
47
|
+
*/
|
|
48
|
+
declare const useColorMode: () => {
|
|
49
|
+
mode: ColorMode;
|
|
50
|
+
resolved: ResolvedColorMode;
|
|
51
|
+
setMode: (next: ColorMode) => void;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region ../../src/react/ui/hooks/useSidebarState.d.ts
|
|
55
|
+
/**
|
|
56
|
+
* Read and update the sidebar collapsed state. The value is persisted via the
|
|
57
|
+
* `alepha-ui` cookie so it survives reloads and is available during SSR — no
|
|
58
|
+
* flash of expanded-then-collapsed when the user prefers a collapsed shell.
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* const { collapsed, setCollapsed, toggle } = useSidebarState();
|
|
62
|
+
*/
|
|
63
|
+
declare const useSidebarState: () => {
|
|
64
|
+
collapsed: boolean;
|
|
65
|
+
setCollapsed: (next: boolean) => void;
|
|
66
|
+
toggle: () => void;
|
|
67
|
+
};
|
|
68
|
+
//#endregion
|
|
69
|
+
//#region ../../src/react/ui/hooks/useTheme.d.ts
|
|
70
|
+
/**
|
|
71
|
+
* Read and update the active theme palette name. UI consumers typically map
|
|
72
|
+
* the value to a class on the document root (e.g. `theme-claude`).
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* const { theme, setTheme } = useTheme();
|
|
76
|
+
* setTheme("claude");
|
|
77
|
+
*/
|
|
78
|
+
declare const useTheme: () => {
|
|
79
|
+
theme: string;
|
|
80
|
+
setTheme: (next: string) => void;
|
|
81
|
+
};
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region ../../src/react/ui/services/UiPersistence.d.ts
|
|
84
|
+
/**
|
|
85
|
+
* Binds the {@link uiAtom} to an `alepha-ui` cookie + injects an inline
|
|
86
|
+
* boot script into the SSR head to prevent FOUC on first paint.
|
|
87
|
+
*
|
|
88
|
+
* Reading flow: on app boot the cookie is parsed and pushed into the atom
|
|
89
|
+
* (via the `key` option on `$cookie`). Writing flow: every time the atom
|
|
90
|
+
* mutates, the cookie is rewritten — a single `useStore(uiAtom)` call is
|
|
91
|
+
* enough to persist UI preferences across reloads.
|
|
92
|
+
*
|
|
93
|
+
* Persists for 365 days; SameSite=lax so it travels on top-level navigation
|
|
94
|
+
* but not on cross-origin requests.
|
|
95
|
+
*/
|
|
96
|
+
declare class UiPersistence {
|
|
97
|
+
ui: _$alepha_server_cookies0.AbstractCookiePrimitive<_$typebox.TObject<{
|
|
98
|
+
mode: _$typebox.TUnsafe<"light" | "dark" | "system">;
|
|
99
|
+
theme: _$typebox.TString;
|
|
100
|
+
sidebar: _$typebox.TObject<{
|
|
101
|
+
collapsed: _$typebox.TBoolean;
|
|
102
|
+
}>;
|
|
103
|
+
}>>;
|
|
104
|
+
head: _$alepha_react_head0.HeadPrimitive;
|
|
105
|
+
}
|
|
106
|
+
//#endregion
|
|
107
|
+
//#region ../../src/react/ui/index.d.ts
|
|
108
|
+
declare module "alepha" {
|
|
109
|
+
interface State {
|
|
110
|
+
"alepha.react.ui": UiState;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Persisted UI state: color mode, theme palette, sidebar collapsed state.
|
|
115
|
+
*
|
|
116
|
+
* Backed by an `alepha-ui` cookie so preferences survive reloads and are
|
|
117
|
+
* available during SSR (no flash of wrong theme).
|
|
118
|
+
*
|
|
119
|
+
* @module alepha.react.ui
|
|
120
|
+
*/
|
|
121
|
+
declare const AlephaReactUi: _$alepha.Service<_$alepha.Module>;
|
|
122
|
+
//#endregion
|
|
123
|
+
export { AlephaReactUi, ColorMode, ColorScheme, ResolvedColorMode, UiPersistence, UiState, uiAtom, useColorMode, useSidebarState, useTheme };
|
|
124
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/react/ui/atoms/uiAtom.ts","../../../src/react/ui/components/ColorScheme.tsx","../../../src/react/ui/hooks/useColorMode.ts","../../../src/react/ui/hooks/useSidebarState.ts","../../../src/react/ui/hooks/useTheme.ts","../../../src/react/ui/services/UiPersistence.ts","../../../src/react/ui/index.ts"],"mappings":";;;;;;;;;;;;;cAQa,MAAA,EAAM,QAAA,CAAA,IAAA,UAAA,OAAA;EAiBjB,4EAAA,QAAA,CAAA,OAAA,+BAAA;;;;;;KAEU,OAAA,GAAU,MAAA,QAAc,MAAA,CAAO,MAAA;;;;;;;;;;AAnB3C;;;iBCMgB,WAAA,CAAA;;;KCVJ,SAAA;AAAA,KACA,iBAAA;;;;;;AFGZ;;;;cEQa,YAAA;;;kBAQO,SAAA;AAAA;;;;;;;;;;AFhBpB;cGGa,eAAA;;;;;;;;;;;;;;AHHb;cIGa,QAAA;;;;;;;;;;;;;AJHb;;;;;cKoBa,aAAA;EACX,EAAA,EAAE,wBAAA,CAAA,uBAAA,WAAA,OAAA;UADsB,SAAA,CAAA,OAAA;;;;;;EASxB,IAAA,EARE,oBAAA,CAQE,aAAA;AAAA;;;;YCrBa,KAAA;IACf,iBAAA,EAAmB,OAAA;EAAA;AAAA;;;;;;;;;cAcV,aAAA,EAAa,QAAA,CAAA,OAAA,CAGxB,QAAA,CAHwB,MAAA"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { $atom, $module, t } from "alepha";
|
|
2
|
+
import { $head } from "alepha/react/head";
|
|
3
|
+
import { $cookie } from "alepha/server/cookies";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { useStore } from "alepha/react";
|
|
6
|
+
//#region ../../src/react/ui/atoms/uiAtom.ts
|
|
7
|
+
/**
|
|
8
|
+
* Persisted UI state — color mode, theme palette, sidebar collapsed state, etc.
|
|
9
|
+
*
|
|
10
|
+
* The atom is bound to a single `alepha-ui` cookie via {@link UiPersistence},
|
|
11
|
+
* so values survive page reloads and are available during SSR.
|
|
12
|
+
*/
|
|
13
|
+
const uiAtom = $atom({
|
|
14
|
+
name: "alepha.react.ui",
|
|
15
|
+
schema: t.object({
|
|
16
|
+
mode: t.enum([
|
|
17
|
+
"light",
|
|
18
|
+
"dark",
|
|
19
|
+
"system"
|
|
20
|
+
]),
|
|
21
|
+
theme: t.string(),
|
|
22
|
+
sidebar: t.object({ collapsed: t.boolean() })
|
|
23
|
+
}),
|
|
24
|
+
default: {
|
|
25
|
+
mode: "system",
|
|
26
|
+
theme: "default",
|
|
27
|
+
sidebar: { collapsed: false }
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region ../../src/react/ui/services/UiPersistence.ts
|
|
32
|
+
/**
|
|
33
|
+
* Inline `<script>` rendered by SSR into the document `<head>`. Runs
|
|
34
|
+
* synchronously before any CSS or React hydration: reads the `alepha-ui`
|
|
35
|
+
* cookie, resolves `mode === "system"` via `prefers-color-scheme`, and
|
|
36
|
+
* applies `class="dark"` (and optional `theme-<name>`) on `<html>`.
|
|
37
|
+
*
|
|
38
|
+
* This is what kills the flash-of-wrong-theme (FOUC) you'd otherwise get
|
|
39
|
+
* with React-effect-based theming. Failures are swallowed silently — at
|
|
40
|
+
* worst the page paints in light mode for one frame.
|
|
41
|
+
*/
|
|
42
|
+
const colorSchemeBoot = `(function(){try{var m=document.cookie.match(/(?:^|;\\s*)alepha-ui=([^;]+)/);var s=m?JSON.parse(decodeURIComponent(m[1])):{};var mode=s.mode||"system";var dark=mode==="dark"||(mode==="system"&&window.matchMedia&&window.matchMedia("(prefers-color-scheme: dark)").matches);var r=document.documentElement;if(dark)r.classList.add("dark");if(s.theme&&s.theme!=="default")r.classList.add("theme-"+s.theme);}catch(e){}})();`;
|
|
43
|
+
/**
|
|
44
|
+
* Binds the {@link uiAtom} to an `alepha-ui` cookie + injects an inline
|
|
45
|
+
* boot script into the SSR head to prevent FOUC on first paint.
|
|
46
|
+
*
|
|
47
|
+
* Reading flow: on app boot the cookie is parsed and pushed into the atom
|
|
48
|
+
* (via the `key` option on `$cookie`). Writing flow: every time the atom
|
|
49
|
+
* mutates, the cookie is rewritten — a single `useStore(uiAtom)` call is
|
|
50
|
+
* enough to persist UI preferences across reloads.
|
|
51
|
+
*
|
|
52
|
+
* Persists for 365 days; SameSite=lax so it travels on top-level navigation
|
|
53
|
+
* but not on cross-origin requests.
|
|
54
|
+
*/
|
|
55
|
+
var UiPersistence = class {
|
|
56
|
+
ui = $cookie({
|
|
57
|
+
name: "alepha-ui",
|
|
58
|
+
key: uiAtom.key,
|
|
59
|
+
schema: uiAtom.schema,
|
|
60
|
+
ttl: [365, "days"],
|
|
61
|
+
sameSite: "lax"
|
|
62
|
+
});
|
|
63
|
+
head = $head({ script: [{ content: colorSchemeBoot }] });
|
|
64
|
+
};
|
|
65
|
+
//#endregion
|
|
66
|
+
//#region ../../src/react/ui/hooks/useColorMode.ts
|
|
67
|
+
/**
|
|
68
|
+
* Read and update the user's color-mode preference. `"system"` resolves to
|
|
69
|
+
* the OS preference and updates live as the OS toggles between light/dark.
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* const { mode, setMode, resolved } = useColorMode();
|
|
73
|
+
* setMode("dark");
|
|
74
|
+
* document.documentElement.classList.toggle("dark", resolved === "dark");
|
|
75
|
+
*/
|
|
76
|
+
const useColorMode = () => {
|
|
77
|
+
const [state, set] = useStore(uiAtom);
|
|
78
|
+
const mode = state?.mode ?? "system";
|
|
79
|
+
return {
|
|
80
|
+
mode,
|
|
81
|
+
resolved: useResolvedColorMode(mode),
|
|
82
|
+
setMode: (next) => {
|
|
83
|
+
set({
|
|
84
|
+
...state ?? uiAtom.options.default,
|
|
85
|
+
mode: next
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
};
|
|
90
|
+
const useResolvedColorMode = (mode) => {
|
|
91
|
+
const [systemDark, setSystemDark] = useState(() => {
|
|
92
|
+
if (typeof window === "undefined") return false;
|
|
93
|
+
return window.matchMedia?.("(prefers-color-scheme: dark)").matches ?? false;
|
|
94
|
+
});
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
if (typeof window === "undefined") return;
|
|
97
|
+
const mq = window.matchMedia?.("(prefers-color-scheme: dark)");
|
|
98
|
+
if (!mq) return;
|
|
99
|
+
const onChange = (ev) => setSystemDark(ev.matches);
|
|
100
|
+
mq.addEventListener("change", onChange);
|
|
101
|
+
return () => mq.removeEventListener("change", onChange);
|
|
102
|
+
}, []);
|
|
103
|
+
if (mode === "dark") return "dark";
|
|
104
|
+
if (mode === "light") return "light";
|
|
105
|
+
return systemDark ? "dark" : "light";
|
|
106
|
+
};
|
|
107
|
+
//#endregion
|
|
108
|
+
//#region ../../src/react/ui/hooks/useTheme.ts
|
|
109
|
+
/**
|
|
110
|
+
* Read and update the active theme palette name. UI consumers typically map
|
|
111
|
+
* the value to a class on the document root (e.g. `theme-claude`).
|
|
112
|
+
*
|
|
113
|
+
* @example
|
|
114
|
+
* const { theme, setTheme } = useTheme();
|
|
115
|
+
* setTheme("claude");
|
|
116
|
+
*/
|
|
117
|
+
const useTheme = () => {
|
|
118
|
+
const [state, set] = useStore(uiAtom);
|
|
119
|
+
return {
|
|
120
|
+
theme: state?.theme ?? "default",
|
|
121
|
+
setTheme: (next) => {
|
|
122
|
+
set({
|
|
123
|
+
...state ?? uiAtom.options.default,
|
|
124
|
+
theme: next
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
//#endregion
|
|
130
|
+
//#region ../../src/react/ui/components/ColorScheme.tsx
|
|
131
|
+
/**
|
|
132
|
+
* Applies `class="dark"` and an optional theme palette class
|
|
133
|
+
* (`theme-<name>`) to the document root, syncing whenever the underlying
|
|
134
|
+
* atom mutates.
|
|
135
|
+
*
|
|
136
|
+
* Mount once near the root of your tree (typically inside the layout).
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* <ColorScheme />
|
|
140
|
+
*/
|
|
141
|
+
function ColorScheme() {
|
|
142
|
+
const { resolved } = useColorMode();
|
|
143
|
+
const { theme } = useTheme();
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (typeof document === "undefined") return;
|
|
146
|
+
document.documentElement.classList.toggle("dark", resolved === "dark");
|
|
147
|
+
}, [resolved]);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (typeof document === "undefined") return;
|
|
150
|
+
const root = document.documentElement;
|
|
151
|
+
const previous = [];
|
|
152
|
+
root.classList.forEach((cls) => {
|
|
153
|
+
if (cls.startsWith("theme-")) previous.push(cls);
|
|
154
|
+
});
|
|
155
|
+
for (const cls of previous) root.classList.remove(cls);
|
|
156
|
+
if (theme && theme !== "default") root.classList.add(`theme-${theme}`);
|
|
157
|
+
}, [theme]);
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
//#endregion
|
|
161
|
+
//#region ../../src/react/ui/hooks/useSidebarState.ts
|
|
162
|
+
/**
|
|
163
|
+
* Read and update the sidebar collapsed state. The value is persisted via the
|
|
164
|
+
* `alepha-ui` cookie so it survives reloads and is available during SSR — no
|
|
165
|
+
* flash of expanded-then-collapsed when the user prefers a collapsed shell.
|
|
166
|
+
*
|
|
167
|
+
* @example
|
|
168
|
+
* const { collapsed, setCollapsed, toggle } = useSidebarState();
|
|
169
|
+
*/
|
|
170
|
+
const useSidebarState = () => {
|
|
171
|
+
const [state, set] = useStore(uiAtom);
|
|
172
|
+
const collapsed = state?.sidebar.collapsed ?? false;
|
|
173
|
+
const setCollapsed = (next) => {
|
|
174
|
+
const base = state ?? uiAtom.options.default;
|
|
175
|
+
set({
|
|
176
|
+
...base,
|
|
177
|
+
sidebar: {
|
|
178
|
+
...base.sidebar,
|
|
179
|
+
collapsed: next
|
|
180
|
+
}
|
|
181
|
+
});
|
|
182
|
+
};
|
|
183
|
+
return {
|
|
184
|
+
collapsed,
|
|
185
|
+
setCollapsed,
|
|
186
|
+
toggle: () => setCollapsed(!collapsed)
|
|
187
|
+
};
|
|
188
|
+
};
|
|
189
|
+
//#endregion
|
|
190
|
+
//#region ../../src/react/ui/index.ts
|
|
191
|
+
/**
|
|
192
|
+
* Persisted UI state: color mode, theme palette, sidebar collapsed state.
|
|
193
|
+
*
|
|
194
|
+
* Backed by an `alepha-ui` cookie so preferences survive reloads and are
|
|
195
|
+
* available during SSR (no flash of wrong theme).
|
|
196
|
+
*
|
|
197
|
+
* @module alepha.react.ui
|
|
198
|
+
*/
|
|
199
|
+
const AlephaReactUi = $module({
|
|
200
|
+
name: "alepha.react.ui",
|
|
201
|
+
services: [UiPersistence]
|
|
202
|
+
});
|
|
203
|
+
//#endregion
|
|
204
|
+
export { AlephaReactUi, ColorScheme, UiPersistence, uiAtom, useColorMode, useSidebarState, useTheme };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/react/ui/atoms/uiAtom.ts","../../../src/react/ui/services/UiPersistence.ts","../../../src/react/ui/hooks/useColorMode.ts","../../../src/react/ui/hooks/useTheme.ts","../../../src/react/ui/components/ColorScheme.tsx","../../../src/react/ui/hooks/useSidebarState.ts","../../../src/react/ui/index.ts"],"sourcesContent":["import { $atom, type Static, t } from \"alepha\";\n\n/**\n * Persisted UI state — color mode, theme palette, sidebar collapsed state, etc.\n *\n * The atom is bound to a single `alepha-ui` cookie via {@link UiPersistence},\n * so values survive page reloads and are available during SSR.\n */\nexport const uiAtom = $atom({\n name: \"alepha.react.ui\",\n schema: t.object({\n /** Color mode preference. `\"system\"` follows the OS-level setting. */\n mode: t.enum([\"light\", \"dark\", \"system\"]),\n /** Theme palette name. UI consumers map this to a CSS class on the root. */\n theme: t.string(),\n /** Sidebar UI state. */\n sidebar: t.object({\n collapsed: t.boolean(),\n }),\n }),\n default: {\n mode: \"system\",\n theme: \"default\",\n sidebar: { collapsed: false },\n },\n});\n\nexport type UiState = Static<typeof uiAtom.schema>;\n","import { $head } from \"alepha/react/head\";\nimport { $cookie } from \"alepha/server/cookies\";\nimport { uiAtom } from \"../atoms/uiAtom.ts\";\n\n/**\n * Inline `<script>` rendered by SSR into the document `<head>`. Runs\n * synchronously before any CSS or React hydration: reads the `alepha-ui`\n * cookie, resolves `mode === \"system\"` via `prefers-color-scheme`, and\n * applies `class=\"dark\"` (and optional `theme-<name>`) on `<html>`.\n *\n * This is what kills the flash-of-wrong-theme (FOUC) you'd otherwise get\n * with React-effect-based theming. Failures are swallowed silently — at\n * worst the page paints in light mode for one frame.\n */\nconst colorSchemeBoot = `(function(){try{var m=document.cookie.match(/(?:^|;\\\\s*)alepha-ui=([^;]+)/);var s=m?JSON.parse(decodeURIComponent(m[1])):{};var mode=s.mode||\"system\";var dark=mode===\"dark\"||(mode===\"system\"&&window.matchMedia&&window.matchMedia(\"(prefers-color-scheme: dark)\").matches);var r=document.documentElement;if(dark)r.classList.add(\"dark\");if(s.theme&&s.theme!==\"default\")r.classList.add(\"theme-\"+s.theme);}catch(e){}})();`;\n\n/**\n * Binds the {@link uiAtom} to an `alepha-ui` cookie + injects an inline\n * boot script into the SSR head to prevent FOUC on first paint.\n *\n * Reading flow: on app boot the cookie is parsed and pushed into the atom\n * (via the `key` option on `$cookie`). Writing flow: every time the atom\n * mutates, the cookie is rewritten — a single `useStore(uiAtom)` call is\n * enough to persist UI preferences across reloads.\n *\n * Persists for 365 days; SameSite=lax so it travels on top-level navigation\n * but not on cross-origin requests.\n */\nexport class UiPersistence {\n ui = $cookie({\n name: \"alepha-ui\",\n key: uiAtom.key,\n schema: uiAtom.schema,\n ttl: [365, \"days\"],\n sameSite: \"lax\",\n });\n\n head = $head({\n script: [{ content: colorSchemeBoot }],\n });\n}\n","import { useStore } from \"alepha/react\";\nimport { useEffect, useState } from \"react\";\nimport { uiAtom } from \"../atoms/uiAtom.ts\";\n\nexport type ColorMode = \"light\" | \"dark\" | \"system\";\nexport type ResolvedColorMode = \"light\" | \"dark\";\n\n/**\n * Read and update the user's color-mode preference. `\"system\"` resolves to\n * the OS preference and updates live as the OS toggles between light/dark.\n *\n * @example\n * const { mode, setMode, resolved } = useColorMode();\n * setMode(\"dark\");\n * document.documentElement.classList.toggle(\"dark\", resolved === \"dark\");\n */\nexport const useColorMode = () => {\n const [state, set] = useStore(uiAtom);\n const mode = (state?.mode ?? \"system\") as ColorMode;\n const resolved = useResolvedColorMode(mode);\n\n return {\n mode,\n resolved,\n setMode: (next: ColorMode) => {\n set({ ...(state ?? uiAtom.options.default!), mode: next });\n },\n };\n};\n\nconst useResolvedColorMode = (mode: ColorMode): ResolvedColorMode => {\n const [systemDark, setSystemDark] = useState<boolean>(() => {\n if (typeof window === \"undefined\") return false;\n return window.matchMedia?.(\"(prefers-color-scheme: dark)\").matches ?? false;\n });\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const mq = window.matchMedia?.(\"(prefers-color-scheme: dark)\");\n if (!mq) return;\n const onChange = (ev: MediaQueryListEvent) => setSystemDark(ev.matches);\n mq.addEventListener(\"change\", onChange);\n return () => mq.removeEventListener(\"change\", onChange);\n }, []);\n\n if (mode === \"dark\") return \"dark\";\n if (mode === \"light\") return \"light\";\n return systemDark ? \"dark\" : \"light\";\n};\n","import { useStore } from \"alepha/react\";\nimport { uiAtom } from \"../atoms/uiAtom.ts\";\n\n/**\n * Read and update the active theme palette name. UI consumers typically map\n * the value to a class on the document root (e.g. `theme-claude`).\n *\n * @example\n * const { theme, setTheme } = useTheme();\n * setTheme(\"claude\");\n */\nexport const useTheme = () => {\n const [state, set] = useStore(uiAtom);\n const theme = state?.theme ?? \"default\";\n\n return {\n theme,\n setTheme: (next: string) => {\n set({ ...(state ?? uiAtom.options.default!), theme: next });\n },\n };\n};\n","import { useEffect } from \"react\";\nimport { useColorMode } from \"../hooks/useColorMode.ts\";\nimport { useTheme } from \"../hooks/useTheme.ts\";\n\n/**\n * Applies `class=\"dark\"` and an optional theme palette class\n * (`theme-<name>`) to the document root, syncing whenever the underlying\n * atom mutates.\n *\n * Mount once near the root of your tree (typically inside the layout).\n *\n * @example\n * <ColorScheme />\n */\nexport function ColorScheme() {\n const { resolved } = useColorMode();\n const { theme } = useTheme();\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n document.documentElement.classList.toggle(\"dark\", resolved === \"dark\");\n }, [resolved]);\n\n useEffect(() => {\n if (typeof document === \"undefined\") return;\n const root = document.documentElement;\n const previous: string[] = [];\n root.classList.forEach((cls) => {\n if (cls.startsWith(\"theme-\")) previous.push(cls);\n });\n for (const cls of previous) root.classList.remove(cls);\n if (theme && theme !== \"default\") root.classList.add(`theme-${theme}`);\n }, [theme]);\n\n return null;\n}\n","import { useStore } from \"alepha/react\";\nimport { uiAtom } from \"../atoms/uiAtom.ts\";\n\n/**\n * Read and update the sidebar collapsed state. The value is persisted via the\n * `alepha-ui` cookie so it survives reloads and is available during SSR — no\n * flash of expanded-then-collapsed when the user prefers a collapsed shell.\n *\n * @example\n * const { collapsed, setCollapsed, toggle } = useSidebarState();\n */\nexport const useSidebarState = () => {\n const [state, set] = useStore(uiAtom);\n const collapsed = state?.sidebar.collapsed ?? false;\n\n const setCollapsed = (next: boolean) => {\n const base = state ?? uiAtom.options.default!;\n set({ ...base, sidebar: { ...base.sidebar, collapsed: next } });\n };\n\n return {\n collapsed,\n setCollapsed,\n toggle: () => setCollapsed(!collapsed),\n };\n};\n","import { $module } from \"alepha\";\nimport type { UiState } from \"./atoms/uiAtom.ts\";\nimport { UiPersistence } from \"./services/UiPersistence.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\nexport * from \"./atoms/uiAtom.ts\";\nexport * from \"./components/ColorScheme.tsx\";\nexport * from \"./hooks/useColorMode.ts\";\nexport * from \"./hooks/useSidebarState.ts\";\nexport * from \"./hooks/useTheme.ts\";\nexport * from \"./services/UiPersistence.ts\";\n\n// ---------------------------------------------------------------------------------------------------------------------\n\ndeclare module \"alepha\" {\n export interface State {\n \"alepha.react.ui\": UiState;\n }\n}\n\n// ---------------------------------------------------------------------------------------------------------------------\n\n/**\n * Persisted UI state: color mode, theme palette, sidebar collapsed state.\n *\n * Backed by an `alepha-ui` cookie so preferences survive reloads and are\n * available during SSR (no flash of wrong theme).\n *\n * @module alepha.react.ui\n */\nexport const AlephaReactUi = $module({\n name: \"alepha.react.ui\",\n services: [UiPersistence],\n});\n"],"mappings":"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"}
|
package/dist/router/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ declare abstract class RouterProvider<T extends Route = Route> {
|
|
|
3
3
|
protected routePathRegex: RegExp;
|
|
4
4
|
protected tree: Tree<T>;
|
|
5
5
|
protected cache: Map<string, RouteMatch<T>>;
|
|
6
|
+
protected maxCacheSize: number;
|
|
6
7
|
match(path: string): RouteMatch<T>;
|
|
7
8
|
protected test(path: string): void;
|
|
8
9
|
protected push(route: T): void;
|
|
@@ -67,17 +68,12 @@ interface Tree<T extends Route> {
|
|
|
67
68
|
*/
|
|
68
69
|
declare class TemplatedPathParser {
|
|
69
70
|
protected static readonly PARAM_REGEX: RegExp;
|
|
70
|
-
|
|
71
|
-
|
|
71
|
+
readonly template: string;
|
|
72
|
+
readonly separator: string;
|
|
73
|
+
readonly paramNames: readonly string[];
|
|
74
|
+
readonly hasParams: boolean;
|
|
75
|
+
protected readonly extractRegex: RegExp | null;
|
|
72
76
|
constructor(template: string, separator?: string);
|
|
73
|
-
/**
|
|
74
|
-
* Returns true if the template contains at least one `{param}` placeholder.
|
|
75
|
-
*/
|
|
76
|
-
get hasParams(): boolean;
|
|
77
|
-
/**
|
|
78
|
-
* Returns an ordered list of parameter names found in the template.
|
|
79
|
-
*/
|
|
80
|
-
get paramNames(): string[];
|
|
81
77
|
/**
|
|
82
78
|
* Replaces each `{param}` in the template with the corresponding value
|
|
83
79
|
* from the provided params record.
|
|
@@ -85,10 +81,12 @@ declare class TemplatedPathParser {
|
|
|
85
81
|
interpolate(params: Record<string, string>): string;
|
|
86
82
|
/**
|
|
87
83
|
* Extracts parameter values from a concrete path by matching it against
|
|
88
|
-
* the template structure.
|
|
89
|
-
*
|
|
84
|
+
* the template structure.
|
|
85
|
+
*
|
|
86
|
+
* Returns `null` when the path does not match the template.
|
|
87
|
+
* Returns `{}` when the template has no parameters and the path matches.
|
|
90
88
|
*/
|
|
91
|
-
extract(path: string): Record<string, string
|
|
89
|
+
extract(path: string): Record<string, string> | null;
|
|
92
90
|
/**
|
|
93
91
|
* Replaces each `{param}` placeholder in the template with the given
|
|
94
92
|
* wildcard string. Defaults to `"+"` (MQTT-style).
|
|
@@ -99,6 +97,8 @@ declare class TemplatedPathParser {
|
|
|
99
97
|
* trailing separator (unless the path is just the separator itself).
|
|
100
98
|
*/
|
|
101
99
|
normalize(path: string): string;
|
|
100
|
+
protected buildExtractRegex(): RegExp;
|
|
101
|
+
protected escapeRegex(s: string): string;
|
|
102
102
|
}
|
|
103
103
|
//#endregion
|
|
104
104
|
export { Route, RouteMatch, RouterProvider, TemplatedPathParser, Tree };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/router/providers/RouterProvider.ts","../../src/router/TemplatedPathParser.ts"],"mappings":";uBAEsB,cAAA,WAAyB,KAAA,GAAQ,KAAA;EAAA,UAC3C,cAAA,EAAgB,MAAA;EAAA,UAEhB,IAAA,EAAM,IAAA,CAAK,CAAA;EAAA,UACX,KAAA,EAAK,GAAA,SAAA,UAAA,CAAA,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/router/providers/RouterProvider.ts","../../src/router/TemplatedPathParser.ts"],"mappings":";uBAEsB,cAAA,WAAyB,KAAA,GAAQ,KAAA;EAAA,UAC3C,cAAA,EAAgB,MAAA;EAAA,UAEhB,IAAA,EAAM,IAAA,CAAK,CAAA;EAAA,UACX,KAAA,EAAK,GAAA,SAAA,UAAA,CAAA,CAAA;EAAA,UACL,YAAA;EAEH,KAAA,CAAM,IAAA,WAAe,UAAA,CAAW,CAAA;EAAA,UAY7B,IAAA,CAAK,IAAA;EAAA,UAML,IAAA,CAAK,KAAA,EAAO,CAAA;EAAA,UA+DZ,gBAAA,CAAiB,IAAA,WAAe,UAAA,CAAW,CAAA;EAAA,UA4C3C,SAAA,CAAU,KAAA,EAAO,UAAA,CAAW,CAAA,IAAK,UAAA,CAAW,CAAA;EAAA,UAa5C,WAAA,CAAY,IAAA;AAAA;AAAA,UAYP,UAAA,WAAqB,KAAA;EACpC,KAAA,GAAQ,CAAA;EACR,MAAA,GAAS,MAAA;AAAA;AAAA,UAGM,KAAA;EACf,IAAA;EA/B2B;;;;;;;EAwC3B,SAAA,GAAY,MAAA;AAAA;AAAA,UAGG,IAAA,WAAe,KAAA;EAC9B,KAAA,GAAQ,CAAA;EACR,QAAA;IAAA,CACG,GAAA,WAAc,IAAA,CAAK,CAAA;EAAA;EAEtB,KAAA;IACE,KAAA,GAAQ,CAAA;IACR,IAAA;IACA,QAAA;MAAA,CACG,GAAA,WAAc,IAAA,CAAK,CAAA;IAAA;EAAA;EAGxB,QAAA;IACE,KAAA,EAAO,CAAA;EAAA;AAAA;;;;AA5LX;;;;;;;;;;;;;;;;;;;;;cCsBa,mBAAA;EAAA,0BACe,WAAA,EAAW,MAAA;EAAA,SAErB,QAAA;EAAA,SACA,SAAA;EAAA,SACA,UAAA;EAAA,SACA,SAAA;EAAA,mBACG,YAAA,EAAc,MAAA;cAErB,QAAA,UAAkB,SAAA;ED5BT;;;;EC+CrB,WAAA,CAAY,MAAA,EAAQ,MAAA;ED7CV;;;;;;;EC2DV,OAAA,CAAQ,IAAA,WAAe,MAAA;EDvCD;;;;EC4DtB,WAAA,CAAY,QAAA;EDGyC;;;;ECKrD,SAAA,CAAU,IAAA;EAAA,UAaA,iBAAA,CAAA,GAAqB,MAAA;EAAA,UAerB,WAAA,CAAY,CAAA;AAAA"}
|
package/dist/router/index.js
CHANGED
|
@@ -4,11 +4,21 @@ var RouterProvider = class {
|
|
|
4
4
|
routePathRegex = /^\/[A-Za-z0-9._~!$&%'()*+,;=:@{}?/-]*$/;
|
|
5
5
|
tree = { children: {} };
|
|
6
6
|
cache = /* @__PURE__ */ new Map();
|
|
7
|
+
maxCacheSize = 1e4;
|
|
7
8
|
match(path) {
|
|
8
|
-
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const pathname = path.split("?", 1)[0];
|
|
10
|
+
const hit = this.cache.get(pathname);
|
|
11
|
+
if (hit) return {
|
|
12
|
+
route: hit.route,
|
|
13
|
+
params: { ...hit.params }
|
|
14
|
+
};
|
|
15
|
+
const result = this.mapParams(this.createRouteMatch(pathname));
|
|
16
|
+
if (this.cache.size >= this.maxCacheSize) this.cache.clear();
|
|
17
|
+
this.cache.set(pathname, result);
|
|
18
|
+
return {
|
|
19
|
+
route: result.route,
|
|
20
|
+
params: { ...result.params }
|
|
21
|
+
};
|
|
12
22
|
}
|
|
13
23
|
test(path) {
|
|
14
24
|
if (!this.routePathRegex.test(path)) throw new AlephaError(`Route '${path}' is not valid`);
|
|
@@ -16,6 +26,7 @@ var RouterProvider = class {
|
|
|
16
26
|
push(route) {
|
|
17
27
|
const path = route.path.replaceAll("//", "/");
|
|
18
28
|
this.test(path);
|
|
29
|
+
this.cache.clear();
|
|
19
30
|
const parts = this.createParts(path);
|
|
20
31
|
let cursor = this.tree;
|
|
21
32
|
for (let i = 0; i < parts.length; i++) {
|
|
@@ -128,50 +139,41 @@ var RouterProvider = class {
|
|
|
128
139
|
* parser.wildcardize("*"); // "cache:*:*"
|
|
129
140
|
* ```
|
|
130
141
|
*/
|
|
131
|
-
var TemplatedPathParser = class {
|
|
142
|
+
var TemplatedPathParser = class TemplatedPathParser {
|
|
132
143
|
static PARAM_REGEX = /\{([^}]+)\}/g;
|
|
133
144
|
template;
|
|
134
145
|
separator;
|
|
146
|
+
paramNames;
|
|
147
|
+
hasParams;
|
|
148
|
+
extractRegex;
|
|
135
149
|
constructor(template, separator = "/") {
|
|
150
|
+
if (separator.length !== 1) throw new AlephaError(`TemplatedPathParser separator must be a single character, got '${separator}'`);
|
|
136
151
|
this.template = template;
|
|
137
152
|
this.separator = separator;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
*/
|
|
142
|
-
get hasParams() {
|
|
143
|
-
return /\{[^}]+\}/.test(this.template);
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
* Returns an ordered list of parameter names found in the template.
|
|
147
|
-
*/
|
|
148
|
-
get paramNames() {
|
|
149
|
-
return [...this.template.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]);
|
|
153
|
+
this.paramNames = [...template.matchAll(TemplatedPathParser.PARAM_REGEX)].map((m) => m[1]);
|
|
154
|
+
this.hasParams = this.paramNames.length > 0;
|
|
155
|
+
this.extractRegex = this.hasParams ? this.buildExtractRegex() : null;
|
|
150
156
|
}
|
|
151
157
|
/**
|
|
152
158
|
* Replaces each `{param}` in the template with the corresponding value
|
|
153
159
|
* from the provided params record.
|
|
154
160
|
*/
|
|
155
161
|
interpolate(params) {
|
|
156
|
-
return this.template.replace(
|
|
162
|
+
return this.template.replace(TemplatedPathParser.PARAM_REGEX, (_, name) => params[name] ?? `{${name}}`);
|
|
157
163
|
}
|
|
158
164
|
/**
|
|
159
165
|
* Extracts parameter values from a concrete path by matching it against
|
|
160
|
-
* the template structure.
|
|
161
|
-
*
|
|
166
|
+
* the template structure.
|
|
167
|
+
*
|
|
168
|
+
* Returns `null` when the path does not match the template.
|
|
169
|
+
* Returns `{}` when the template has no parameters and the path matches.
|
|
162
170
|
*/
|
|
163
171
|
extract(path) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const regexSource = this.template.replace(/[.*+?^${}()|[\]\\]/g, (char) => {
|
|
168
|
-
if (char === "{" || char === "}") return char;
|
|
169
|
-
return `\\${char}`;
|
|
170
|
-
}).replace(/\{([^}]+)\}/g, `([^${escapedSeparator}]+)`);
|
|
171
|
-
const match = new RegExp(`^${regexSource}$`).exec(path);
|
|
172
|
-
if (!match) return {};
|
|
172
|
+
if (!this.extractRegex) return path === this.template ? {} : null;
|
|
173
|
+
const match = this.extractRegex.exec(path);
|
|
174
|
+
if (!match) return null;
|
|
173
175
|
const result = {};
|
|
174
|
-
for (let i = 0; i <
|
|
176
|
+
for (let i = 0; i < this.paramNames.length; i++) result[this.paramNames[i]] = match[i + 1];
|
|
175
177
|
return result;
|
|
176
178
|
}
|
|
177
179
|
/**
|
|
@@ -179,7 +181,7 @@ var TemplatedPathParser = class {
|
|
|
179
181
|
* wildcard string. Defaults to `"+"` (MQTT-style).
|
|
180
182
|
*/
|
|
181
183
|
wildcardize(wildcard = "+") {
|
|
182
|
-
return this.template.replace(
|
|
184
|
+
return this.template.replace(TemplatedPathParser.PARAM_REGEX, wildcard);
|
|
183
185
|
}
|
|
184
186
|
/**
|
|
185
187
|
* Normalises a path by collapsing repeated separators and stripping a
|
|
@@ -187,11 +189,22 @@ var TemplatedPathParser = class {
|
|
|
187
189
|
*/
|
|
188
190
|
normalize(path) {
|
|
189
191
|
const sep = this.separator;
|
|
190
|
-
const escapedSep =
|
|
192
|
+
const escapedSep = this.escapeRegex(sep);
|
|
191
193
|
let result = path.replace(new RegExp(`${escapedSep}{2,}`, "g"), sep);
|
|
192
194
|
if (result.endsWith(sep) && result.length > sep.length) result = result.slice(0, -sep.length);
|
|
193
195
|
return result;
|
|
194
196
|
}
|
|
197
|
+
buildExtractRegex() {
|
|
198
|
+
const escapedSeparator = this.escapeRegex(this.separator);
|
|
199
|
+
const regexSource = this.template.replace(/[.*+?^${}()|[\]\\]/g, (char) => {
|
|
200
|
+
if (char === "{" || char === "}") return char;
|
|
201
|
+
return `\\${char}`;
|
|
202
|
+
}).replace(/\{[^}]+\}/g, `([^${escapedSeparator}]+)`);
|
|
203
|
+
return new RegExp(`^${regexSource}$`);
|
|
204
|
+
}
|
|
205
|
+
escapeRegex(s) {
|
|
206
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
207
|
+
}
|
|
195
208
|
};
|
|
196
209
|
//#endregion
|
|
197
210
|
export { RouterProvider, TemplatedPathParser };
|
package/dist/router/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/router/providers/RouterProvider.ts","../../src/router/TemplatedPathParser.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport abstract class RouterProvider<T extends Route = Route> {\n protected routePathRegex: RegExp = /^\\/[A-Za-z0-9._~!$&%'()*+,;=:@{}?/-]*$/;\n\n protected tree: Tree<T> = { children: {} };\n protected cache = new Map<string, RouteMatch<T>>();\n\n public match(path: string): RouteMatch<T> {\n if (this.cache.has(path)) {\n return this.cache.get(path)!;\n }\n const result = this.mapParams(this.createRouteMatch(path));\n this.cache.set(path, result);\n return result;\n }\n\n protected test(path: string): void {\n if (!this.routePathRegex.test(path)) {\n throw new AlephaError(`Route '${path}' is not valid`);\n }\n }\n\n protected push(route: T): void {\n const path = route.path.replaceAll(\"//\", \"/\");\n\n this.test(path);\n\n const parts = this.createParts(path);\n\n let cursor = this.tree;\n for (let i = 0; i < parts.length; i++) {\n const isLast = i === parts.length - 1;\n let part = parts[i].toLowerCase(); // url is case-insensitive\n if (part === \"*\" && isLast) {\n cursor.wildcard = { route };\n break;\n }\n\n if (part.includes(\"*\")) {\n throw new AlephaError(`Route '${path}' has an invalid wildcard syntax`);\n }\n\n if (part.includes(\"{\") || part.includes(\"}\")) {\n if (part.startsWith(\"{\") && part.endsWith(\"}\")) {\n part = `:${part.slice(1, -1)}`; // convert {param} to :param\n } else {\n throw new AlephaError(`Route '${path}' has an invalid param syntax`);\n }\n }\n\n if (part.startsWith(\":\")) {\n const name = parts[i].slice(1).replaceAll(\"}\", \"\");\n if (!name) {\n throw new AlephaError(`Route '${path}' has an empty param name`);\n }\n if (!cursor.param) {\n cursor.param = { name, children: {} };\n } else if (cursor.param.name !== name) {\n // damn, 2 url params with different names\n // got this case with /customers/:id and /customers/:userId/payments\n route.mapParams ??= {};\n route.mapParams[cursor.param.name] = name;\n }\n\n if (isLast) {\n cursor.param.route = route;\n }\n\n cursor = cursor.param;\n continue;\n }\n\n if (!cursor.children[part]) {\n cursor.children[part] = { children: {} };\n }\n\n if (isLast) {\n cursor.children[part].route = route;\n }\n\n cursor = cursor.children[part];\n }\n }\n\n protected createRouteMatch(path: string): RouteMatch<T> {\n if (path[0] !== \"/\") {\n throw new AlephaError(`Path '${path}' must start with \"/\"`);\n }\n\n const parts = this.createParts(path);\n\n let cursor = this.tree;\n let wildcard: { route: T } | undefined;\n const params: Record<string, string> = {};\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i].toLowerCase(); // url is case-insensitive\n if (cursor.children[part]) {\n if (cursor.wildcard) {\n wildcard = cursor.wildcard;\n }\n cursor = cursor.children[part];\n } else if (cursor.param) {\n if (cursor.wildcard) {\n wildcard = cursor.wildcard;\n }\n params[cursor.param.name] = parts[i];\n cursor = cursor.param;\n } else if (cursor.wildcard) {\n params[\"*\"] = parts.slice(i).join(\"/\");\n return { route: cursor.wildcard.route, params };\n } else {\n return { route: wildcard?.route, params };\n }\n }\n\n if (!cursor?.route) {\n // when \"/a/*\" - trigger if \"/a\"\n if (cursor.wildcard) {\n return { route: cursor.wildcard.route, params };\n }\n // return deep wildcard or nothing\n return { route: wildcard?.route, params };\n }\n\n return { route: cursor.route, params };\n }\n\n protected mapParams(match: RouteMatch<T>): RouteMatch<T> {\n if (match.route?.mapParams && match.params) {\n for (const [key, value] of Object.entries(match.route.mapParams)) {\n if (match.params[key]) {\n match.params[value] = match.params[key];\n delete match.params[key];\n }\n }\n }\n\n return match;\n }\n\n protected createParts(path: string): string[] {\n let pathname = path.split(\"?\")[0].replaceAll(\"//\", \"/\");\n\n // remove trailing slash\n if (pathname.endsWith(\"/\") && pathname.length > 1) {\n pathname = pathname.slice(0, -1);\n }\n\n return pathname.split(\"/\").slice(1);\n }\n}\n\nexport interface RouteMatch<T extends Route> {\n route?: T;\n params?: Record<string, string>;\n}\n\nexport interface Route {\n path: string;\n\n /**\n * Rename a param in the route.\n * This is automatically filled when you have scenarios like:\n * `/customers/:id` and `/customers/:userId/payments`\n *\n * In this case, `:id` will be renamed to `:userId` in the second route.\n */\n mapParams?: Record<string, string>;\n}\n\nexport interface Tree<T extends Route> {\n route?: T;\n children: {\n [key: string]: Tree<T>;\n };\n param?: {\n route?: T;\n name: string;\n children: {\n [key: string]: Tree<T>;\n };\n };\n wildcard?: {\n route: T;\n };\n}\n","/**\n * Parses and manipulates templated paths with `{param}` placeholders.\n *\n * Used by both RouterProvider (HTTP routes) and TopicProvider (pub/sub topics)\n * to handle parameterized path templates in a unified way.\n *\n * @example\n * ```ts\n * const parser = new TemplatedPathParser(\"/users/{userId}/posts/{postId}\");\n * parser.interpolate({ userId: \"7\", postId: \"42\" }); // \"/users/7/posts/42\"\n * parser.extract(\"/users/7/posts/42\"); // { userId: \"7\", postId: \"42\" }\n * parser.wildcardize(\"+\"); // \"/users/+/posts/+\"\n * ```\n *\n * @example\n * ```ts\n * // Redis-style colon-separated keys\n * const parser = new TemplatedPathParser(\"cache:{namespace}:{key}\", \":\");\n * parser.interpolate({ namespace: \"users\", key: \"42\" }); // \"cache:users:42\"\n * parser.wildcardize(\"*\"); // \"cache:*:*\"\n * ```\n */\nexport class TemplatedPathParser {\n protected static readonly PARAM_REGEX = /\\{([^}]+)\\}/g;\n\n protected readonly template: string;\n protected readonly separator: string;\n\n constructor(template: string, separator = \"/\") {\n this.template = template;\n this.separator = separator;\n }\n\n /**\n * Returns true if the template contains at least one `{param}` placeholder.\n */\n get hasParams(): boolean {\n return /\\{[^}]+\\}/.test(this.template);\n }\n\n /**\n * Returns an ordered list of parameter names found in the template.\n */\n get paramNames(): string[] {\n return [...this.template.matchAll(/\\{([^}]+)\\}/g)].map((m) => m[1]);\n }\n\n /**\n * Replaces each `{param}` in the template with the corresponding value\n * from the provided params record.\n */\n interpolate(params: Record<string, string>): string {\n return this.template.replace(\n /\\{([^}]+)\\}/g,\n (_, name: string) => params[name] ?? `{${name}}`,\n );\n }\n\n /**\n * Extracts parameter values from a concrete path by matching it against\n * the template structure. Returns an empty object when the template has\n * no parameters.\n */\n extract(path: string): Record<string, string> {\n const names = this.paramNames;\n if (names.length === 0) {\n return {};\n }\n\n const escapedSeparator = this.separator.replace(\n /[.*+?^${}()|[\\]\\\\]/g,\n \"\\\\$&\",\n );\n\n // Build a regex from the template: escape literal parts, replace {param} with a capture group\n const regexSource = this.template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n // Allow { and } through so we can then replace them as param groups\n if (char === \"{\" || char === \"}\") {\n return char;\n }\n return `\\\\${char}`;\n })\n // After escaping literal chars, replace {name} patterns with capture groups.\n // The separator is already escaped above, so we match anything that is not the separator.\n .replace(/\\{([^}]+)\\}/g, `([^${escapedSeparator}]+)`);\n\n const regex = new RegExp(`^${regexSource}$`);\n const match = regex.exec(path);\n\n if (!match) {\n return {};\n }\n\n const result: Record<string, string> = {};\n for (let i = 0; i < names.length; i++) {\n result[names[i]] = match[i + 1];\n }\n return result;\n }\n\n /**\n * Replaces each `{param}` placeholder in the template with the given\n * wildcard string. Defaults to `\"+\"` (MQTT-style).\n */\n wildcardize(wildcard = \"+\"): string {\n return this.template.replace(/\\{[^}]+\\}/g, wildcard);\n }\n\n /**\n * Normalises a path by collapsing repeated separators and stripping a\n * trailing separator (unless the path is just the separator itself).\n */\n normalize(path: string): string {\n const sep = this.separator;\n const escapedSep = sep.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n // Replace consecutive separators with a single one\n let result = path.replace(new RegExp(`${escapedSep}{2,}`, \"g\"), sep);\n\n // Strip trailing separator, but preserve a lone separator\n if (result.endsWith(sep) && result.length > sep.length) {\n result = result.slice(0, -sep.length);\n }\n\n return result;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/router/providers/RouterProvider.ts","../../src/router/TemplatedPathParser.ts"],"sourcesContent":["import { AlephaError } from \"alepha\";\n\nexport abstract class RouterProvider<T extends Route = Route> {\n protected routePathRegex: RegExp = /^\\/[A-Za-z0-9._~!$&%'()*+,;=:@{}?/-]*$/;\n\n protected tree: Tree<T> = { children: {} };\n protected cache = new Map<string, RouteMatch<T>>();\n protected maxCacheSize = 10_000;\n\n public match(path: string): RouteMatch<T> {\n const pathname = path.split(\"?\", 1)[0];\n const hit = this.cache.get(pathname);\n if (hit) {\n return { route: hit.route, params: { ...hit.params } };\n }\n const result = this.mapParams(this.createRouteMatch(pathname));\n if (this.cache.size >= this.maxCacheSize) this.cache.clear();\n this.cache.set(pathname, result);\n return { route: result.route, params: { ...result.params } };\n }\n\n protected test(path: string): void {\n if (!this.routePathRegex.test(path)) {\n throw new AlephaError(`Route '${path}' is not valid`);\n }\n }\n\n protected push(route: T): void {\n const path = route.path.replaceAll(\"//\", \"/\");\n\n this.test(path);\n this.cache.clear();\n\n const parts = this.createParts(path);\n\n let cursor = this.tree;\n for (let i = 0; i < parts.length; i++) {\n const isLast = i === parts.length - 1;\n let part = parts[i].toLowerCase(); // url is case-insensitive\n if (part === \"*\" && isLast) {\n cursor.wildcard = { route };\n break;\n }\n\n if (part.includes(\"*\")) {\n throw new AlephaError(`Route '${path}' has an invalid wildcard syntax`);\n }\n\n if (part.includes(\"{\") || part.includes(\"}\")) {\n if (part.startsWith(\"{\") && part.endsWith(\"}\")) {\n part = `:${part.slice(1, -1)}`; // convert {param} to :param\n } else {\n throw new AlephaError(`Route '${path}' has an invalid param syntax`);\n }\n }\n\n if (part.startsWith(\":\")) {\n const name = parts[i].slice(1).replaceAll(\"}\", \"\");\n if (!name) {\n throw new AlephaError(`Route '${path}' has an empty param name`);\n }\n if (!cursor.param) {\n cursor.param = { name, children: {} };\n } else if (cursor.param.name !== name) {\n // damn, 2 url params with different names\n // got this case with /customers/:id and /customers/:userId/payments\n route.mapParams ??= {};\n route.mapParams[cursor.param.name] = name;\n }\n\n if (isLast) {\n cursor.param.route = route;\n }\n\n cursor = cursor.param;\n continue;\n }\n\n if (!cursor.children[part]) {\n cursor.children[part] = { children: {} };\n }\n\n if (isLast) {\n cursor.children[part].route = route;\n }\n\n cursor = cursor.children[part];\n }\n }\n\n protected createRouteMatch(path: string): RouteMatch<T> {\n if (path[0] !== \"/\") {\n throw new AlephaError(`Path '${path}' must start with \"/\"`);\n }\n\n const parts = this.createParts(path);\n\n let cursor = this.tree;\n let wildcard: { route: T } | undefined;\n const params: Record<string, string> = {};\n\n for (let i = 0; i < parts.length; i++) {\n const part = parts[i].toLowerCase(); // url is case-insensitive\n if (cursor.children[part]) {\n if (cursor.wildcard) {\n wildcard = cursor.wildcard;\n }\n cursor = cursor.children[part];\n } else if (cursor.param) {\n if (cursor.wildcard) {\n wildcard = cursor.wildcard;\n }\n params[cursor.param.name] = parts[i];\n cursor = cursor.param;\n } else if (cursor.wildcard) {\n params[\"*\"] = parts.slice(i).join(\"/\");\n return { route: cursor.wildcard.route, params };\n } else {\n return { route: wildcard?.route, params };\n }\n }\n\n if (!cursor?.route) {\n // when \"/a/*\" - trigger if \"/a\"\n if (cursor.wildcard) {\n return { route: cursor.wildcard.route, params };\n }\n // return deep wildcard or nothing\n return { route: wildcard?.route, params };\n }\n\n return { route: cursor.route, params };\n }\n\n protected mapParams(match: RouteMatch<T>): RouteMatch<T> {\n if (match.route?.mapParams && match.params) {\n for (const [key, value] of Object.entries(match.route.mapParams)) {\n if (match.params[key]) {\n match.params[value] = match.params[key];\n delete match.params[key];\n }\n }\n }\n\n return match;\n }\n\n protected createParts(path: string): string[] {\n let pathname = path.split(\"?\")[0].replaceAll(\"//\", \"/\");\n\n // remove trailing slash\n if (pathname.endsWith(\"/\") && pathname.length > 1) {\n pathname = pathname.slice(0, -1);\n }\n\n return pathname.split(\"/\").slice(1);\n }\n}\n\nexport interface RouteMatch<T extends Route> {\n route?: T;\n params?: Record<string, string>;\n}\n\nexport interface Route {\n path: string;\n\n /**\n * Rename a param in the route.\n * This is automatically filled when you have scenarios like:\n * `/customers/:id` and `/customers/:userId/payments`\n *\n * In this case, `:id` will be renamed to `:userId` in the second route.\n */\n mapParams?: Record<string, string>;\n}\n\nexport interface Tree<T extends Route> {\n route?: T;\n children: {\n [key: string]: Tree<T>;\n };\n param?: {\n route?: T;\n name: string;\n children: {\n [key: string]: Tree<T>;\n };\n };\n wildcard?: {\n route: T;\n };\n}\n","import { AlephaError } from \"alepha\";\n\n/**\n * Parses and manipulates templated paths with `{param}` placeholders.\n *\n * Used by both RouterProvider (HTTP routes) and TopicProvider (pub/sub topics)\n * to handle parameterized path templates in a unified way.\n *\n * @example\n * ```ts\n * const parser = new TemplatedPathParser(\"/users/{userId}/posts/{postId}\");\n * parser.interpolate({ userId: \"7\", postId: \"42\" }); // \"/users/7/posts/42\"\n * parser.extract(\"/users/7/posts/42\"); // { userId: \"7\", postId: \"42\" }\n * parser.wildcardize(\"+\"); // \"/users/+/posts/+\"\n * ```\n *\n * @example\n * ```ts\n * // Redis-style colon-separated keys\n * const parser = new TemplatedPathParser(\"cache:{namespace}:{key}\", \":\");\n * parser.interpolate({ namespace: \"users\", key: \"42\" }); // \"cache:users:42\"\n * parser.wildcardize(\"*\"); // \"cache:*:*\"\n * ```\n */\nexport class TemplatedPathParser {\n protected static readonly PARAM_REGEX = /\\{([^}]+)\\}/g;\n\n public readonly template: string;\n public readonly separator: string;\n public readonly paramNames: readonly string[];\n public readonly hasParams: boolean;\n protected readonly extractRegex: RegExp | null;\n\n constructor(template: string, separator = \"/\") {\n if (separator.length !== 1) {\n throw new AlephaError(\n `TemplatedPathParser separator must be a single character, got '${separator}'`,\n );\n }\n this.template = template;\n this.separator = separator;\n this.paramNames = [\n ...template.matchAll(TemplatedPathParser.PARAM_REGEX),\n ].map((m) => m[1]);\n this.hasParams = this.paramNames.length > 0;\n this.extractRegex = this.hasParams ? this.buildExtractRegex() : null;\n }\n\n /**\n * Replaces each `{param}` in the template with the corresponding value\n * from the provided params record.\n */\n interpolate(params: Record<string, string>): string {\n return this.template.replace(\n TemplatedPathParser.PARAM_REGEX,\n (_, name: string) => params[name] ?? `{${name}}`,\n );\n }\n\n /**\n * Extracts parameter values from a concrete path by matching it against\n * the template structure.\n *\n * Returns `null` when the path does not match the template.\n * Returns `{}` when the template has no parameters and the path matches.\n */\n extract(path: string): Record<string, string> | null {\n if (!this.extractRegex) {\n return path === this.template ? {} : null;\n }\n\n const match = this.extractRegex.exec(path);\n if (!match) {\n return null;\n }\n\n const result: Record<string, string> = {};\n for (let i = 0; i < this.paramNames.length; i++) {\n result[this.paramNames[i]] = match[i + 1];\n }\n return result;\n }\n\n /**\n * Replaces each `{param}` placeholder in the template with the given\n * wildcard string. Defaults to `\"+\"` (MQTT-style).\n */\n wildcardize(wildcard = \"+\"): string {\n return this.template.replace(TemplatedPathParser.PARAM_REGEX, wildcard);\n }\n\n /**\n * Normalises a path by collapsing repeated separators and stripping a\n * trailing separator (unless the path is just the separator itself).\n */\n normalize(path: string): string {\n const sep = this.separator;\n const escapedSep = this.escapeRegex(sep);\n\n let result = path.replace(new RegExp(`${escapedSep}{2,}`, \"g\"), sep);\n\n if (result.endsWith(sep) && result.length > sep.length) {\n result = result.slice(0, -sep.length);\n }\n\n return result;\n }\n\n protected buildExtractRegex(): RegExp {\n const escapedSeparator = this.escapeRegex(this.separator);\n\n const regexSource = this.template\n .replace(/[.*+?^${}()|[\\]\\\\]/g, (char) => {\n if (char === \"{\" || char === \"}\") {\n return char;\n }\n return `\\\\${char}`;\n })\n .replace(/\\{[^}]+\\}/g, `([^${escapedSeparator}]+)`);\n\n return new RegExp(`^${regexSource}$`);\n }\n\n protected escapeRegex(s: string): string {\n return s.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n }\n}\n"],"mappings":"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"}
|