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.
Files changed (232) hide show
  1. package/dist/api/files/index.js +2 -1
  2. package/dist/api/files/index.js.map +1 -1
  3. package/dist/api/jobs/index.browser.js +64 -148
  4. package/dist/api/jobs/index.browser.js.map +1 -1
  5. package/dist/api/jobs/index.d.ts +371 -573
  6. package/dist/api/jobs/index.d.ts.map +1 -1
  7. package/dist/api/jobs/index.js +605 -1012
  8. package/dist/api/jobs/index.js.map +1 -1
  9. package/dist/api/notifications/index.d.ts +78 -17
  10. package/dist/api/notifications/index.d.ts.map +1 -1
  11. package/dist/api/notifications/index.js +90 -23
  12. package/dist/api/notifications/index.js.map +1 -1
  13. package/dist/api/payments/index.d.ts +2 -1
  14. package/dist/api/payments/index.d.ts.map +1 -1
  15. package/dist/api/payments/index.js +4 -2
  16. package/dist/api/payments/index.js.map +1 -1
  17. package/dist/api/users/index.d.ts +34 -31
  18. package/dist/api/users/index.d.ts.map +1 -1
  19. package/dist/api/users/index.js +13 -7
  20. package/dist/api/users/index.js.map +1 -1
  21. package/dist/api/verifications/index.js +2 -1
  22. package/dist/api/verifications/index.js.map +1 -1
  23. package/dist/cli/core/index.d.ts +8 -34
  24. package/dist/cli/core/index.d.ts.map +1 -1
  25. package/dist/cli/core/index.js +43 -232
  26. package/dist/cli/core/index.js.map +1 -1
  27. package/dist/cli/platform/index.d.ts +36 -11
  28. package/dist/cli/platform/index.d.ts.map +1 -1
  29. package/dist/cli/platform/index.js +93 -27
  30. package/dist/cli/platform/index.js.map +1 -1
  31. package/dist/command/index.d.ts +1 -1
  32. package/dist/core/index.browser.js +6 -0
  33. package/dist/core/index.browser.js.map +1 -1
  34. package/dist/core/index.d.ts +6 -0
  35. package/dist/core/index.d.ts.map +1 -1
  36. package/dist/core/index.js +6 -0
  37. package/dist/core/index.js.map +1 -1
  38. package/dist/core/index.native.js +6 -0
  39. package/dist/core/index.native.js.map +1 -1
  40. package/dist/core/index.workerd.js +6 -0
  41. package/dist/core/index.workerd.js.map +1 -1
  42. package/dist/react/form/index.d.ts +60 -1
  43. package/dist/react/form/index.d.ts.map +1 -1
  44. package/dist/react/form/index.js +86 -1
  45. package/dist/react/form/index.js.map +1 -1
  46. package/dist/react/head/index.browser.js +16 -1
  47. package/dist/react/head/index.browser.js.map +1 -1
  48. package/dist/react/head/index.d.ts +6 -0
  49. package/dist/react/head/index.d.ts.map +1 -1
  50. package/dist/react/head/index.js +16 -1
  51. package/dist/react/head/index.js.map +1 -1
  52. package/dist/react/router/index.browser.js +0 -10
  53. package/dist/react/router/index.browser.js.map +1 -1
  54. package/dist/react/router/index.d.ts +35 -12
  55. package/dist/react/router/index.d.ts.map +1 -1
  56. package/dist/react/router/index.js +0 -10
  57. package/dist/react/router/index.js.map +1 -1
  58. package/dist/react/ui/index.d.ts +124 -0
  59. package/dist/react/ui/index.d.ts.map +1 -0
  60. package/dist/react/ui/index.js +206 -0
  61. package/dist/react/ui/index.js.map +1 -0
  62. package/dist/router/index.d.ts +13 -13
  63. package/dist/router/index.d.ts.map +1 -1
  64. package/dist/router/index.js +45 -32
  65. package/dist/router/index.js.map +1 -1
  66. package/dist/system/index.d.ts.map +1 -1
  67. package/dist/system/index.js +1 -0
  68. package/dist/system/index.js.map +1 -1
  69. package/dist/topic/core/index.js +1 -1
  70. package/dist/topic/core/index.js.map +1 -1
  71. package/package.json +6 -23
  72. package/src/api/files/jobs/FileJobs.ts +2 -1
  73. package/src/api/jobs/__tests__/$job.spec.ts +316 -2867
  74. package/src/api/jobs/controllers/AdminJobController.ts +29 -138
  75. package/src/api/jobs/entities/jobExecutionEntity.ts +27 -19
  76. package/src/api/jobs/index.browser.ts +5 -7
  77. package/src/api/jobs/index.ts +23 -51
  78. package/src/api/jobs/primitives/$job.ts +66 -58
  79. package/src/api/jobs/providers/JobProvider.ts +561 -566
  80. package/src/api/jobs/providers/JobQueueProvider.ts +18 -19
  81. package/src/api/jobs/schemas/jobConfigAtom.ts +20 -23
  82. package/src/api/jobs/schemas/jobExecutionQuerySchema.ts +3 -27
  83. package/src/api/jobs/schemas/jobExecutionResourceSchema.ts +5 -7
  84. package/src/api/jobs/schemas/jobRegistrationSchema.ts +7 -4
  85. package/src/api/jobs/schemas/triggerJobSchema.ts +0 -1
  86. package/src/api/jobs/services/JobService.ts +90 -483
  87. package/src/api/notifications/controllers/AdminNotificationController.ts +19 -12
  88. package/src/api/notifications/index.ts +7 -4
  89. package/src/api/notifications/jobs/NotificationJobs.ts +83 -12
  90. package/src/api/payments/services/PaymentService.ts +4 -2
  91. package/src/api/users/__tests__/UserJobs.spec.ts +10 -49
  92. package/src/api/users/audits/UserAudits.ts +3 -1
  93. package/src/api/users/buckets/UserBuckets.ts +2 -1
  94. package/src/api/users/index.ts +1 -4
  95. package/src/api/users/jobs/UserJobs.ts +5 -4
  96. package/src/api/verifications/jobs/VerificationJobs.ts +2 -1
  97. package/src/cli/core/__tests__/init.spec.ts +1 -1
  98. package/src/cli/core/commands/init.ts +0 -12
  99. package/src/cli/core/services/PackageManagerUtils.ts +2 -9
  100. package/src/cli/core/services/ProjectScaffolder.ts +17 -65
  101. package/src/cli/core/templates/agentMd.ts +2 -8
  102. package/src/cli/core/templates/apiIndexTs.ts +4 -18
  103. package/src/cli/core/templates/mainCss.ts +1 -36
  104. package/src/cli/core/templates/vitestConfigTs.ts +17 -0
  105. package/src/cli/core/templates/webAppRouterTs.ts +2 -85
  106. package/src/cli/platform/__tests__/CloudflareAdapter.spec.ts +22 -71
  107. package/src/cli/platform/adapters/CloudflareAdapter.ts +12 -11
  108. package/src/cli/platform/atoms/platformOptions.ts +9 -0
  109. package/src/cli/platform/schemas/cloudflare.ts +3 -2
  110. package/src/cli/platform/services/CloudflareApi.ts +164 -25
  111. package/src/cli/platform/services/WranglerApi.ts +0 -17
  112. package/src/core/Alepha.ts +9 -0
  113. package/src/react/form/index.ts +2 -0
  114. package/src/react/form/services/parseField.ts +163 -0
  115. package/src/react/form/services/prettyName.ts +19 -0
  116. package/src/react/head/providers/BrowserHeadProvider.ts +31 -10
  117. package/src/react/router/primitives/$page.ts +35 -12
  118. package/src/react/ui/atoms/uiAtom.ts +28 -0
  119. package/src/react/ui/components/ColorScheme.tsx +36 -0
  120. package/src/react/ui/hooks/useColorMode.ts +49 -0
  121. package/src/react/ui/hooks/useSidebarState.ts +26 -0
  122. package/src/react/ui/hooks/useTheme.ts +22 -0
  123. package/src/react/ui/index.ts +35 -0
  124. package/src/react/ui/services/UiPersistence.ts +41 -0
  125. package/src/router/TemplatedPathParser.ts +50 -51
  126. package/src/router/__tests__/RouterProvider.spec.ts +62 -0
  127. package/src/router/__tests__/TemplatedPathParser.spec.ts +18 -0
  128. package/src/router/providers/RouterProvider.ts +10 -5
  129. package/src/system/providers/NodeShellProvider.ts +1 -0
  130. package/src/topic/core/providers/TopicProvider.ts +1 -1
  131. package/dist/api/invitations/index.d.ts +0 -790
  132. package/dist/api/invitations/index.d.ts.map +0 -1
  133. package/dist/api/invitations/index.js +0 -662
  134. package/dist/api/invitations/index.js.map +0 -1
  135. package/dist/api/issues/index.d.ts +0 -810
  136. package/dist/api/issues/index.d.ts.map +0 -1
  137. package/dist/api/issues/index.js +0 -444
  138. package/dist/api/issues/index.js.map +0 -1
  139. package/dist/api/subscriptions/index.d.ts +0 -1692
  140. package/dist/api/subscriptions/index.d.ts.map +0 -1
  141. package/dist/api/subscriptions/index.js +0 -1867
  142. package/dist/api/subscriptions/index.js.map +0 -1
  143. package/dist/api/workflows/index.browser.js +0 -246
  144. package/dist/api/workflows/index.browser.js.map +0 -1
  145. package/dist/api/workflows/index.d.ts +0 -1618
  146. package/dist/api/workflows/index.d.ts.map +0 -1
  147. package/dist/api/workflows/index.js +0 -1495
  148. package/dist/api/workflows/index.js.map +0 -1
  149. package/src/api/invitations/__tests__/InvitationService.spec.ts +0 -439
  150. package/src/api/invitations/controllers/AdminInvitationController.ts +0 -86
  151. package/src/api/invitations/controllers/InvitationController.ts +0 -84
  152. package/src/api/invitations/entities/invitations.ts +0 -33
  153. package/src/api/invitations/index.ts +0 -58
  154. package/src/api/invitations/jobs/InvitationJobs.ts +0 -37
  155. package/src/api/invitations/providers/InvitationProvider.ts +0 -45
  156. package/src/api/invitations/schemas/createInvitationSchema.ts +0 -12
  157. package/src/api/invitations/schemas/invitationConfigAtom.ts +0 -20
  158. package/src/api/invitations/schemas/invitationQuerySchema.ts +0 -15
  159. package/src/api/invitations/schemas/invitationResourceSchema.ts +0 -6
  160. package/src/api/invitations/schemas/invitationWithResourceInfoSchema.ts +0 -22
  161. package/src/api/invitations/schemas/myInvitationsQuerySchema.ts +0 -10
  162. package/src/api/invitations/services/InvitationService.ts +0 -556
  163. package/src/api/issues/__tests__/IssueService.spec.ts +0 -263
  164. package/src/api/issues/controllers/AdminIssueController.ts +0 -149
  165. package/src/api/issues/controllers/IssueController.ts +0 -44
  166. package/src/api/issues/entities/issues.ts +0 -49
  167. package/src/api/issues/index.ts +0 -50
  168. package/src/api/issues/schemas/createIssueSchema.ts +0 -13
  169. package/src/api/issues/schemas/issueConfigAtom.ts +0 -13
  170. package/src/api/issues/schemas/issueQuerySchema.ts +0 -18
  171. package/src/api/issues/schemas/issueResourceSchema.ts +0 -6
  172. package/src/api/issues/schemas/myIssueQuerySchema.ts +0 -10
  173. package/src/api/issues/schemas/updateIssueSchema.ts +0 -13
  174. package/src/api/issues/services/IssueService.ts +0 -264
  175. package/src/api/jobs/__tests__/$job-middleware.spec.ts +0 -126
  176. package/src/api/jobs/__tests__/JobService.spec.ts +0 -31
  177. package/src/api/jobs/entities/jobExecutionLogEntity.ts +0 -13
  178. package/src/api/jobs/schemas/jobActivitySchema.ts +0 -15
  179. package/src/api/jobs/schemas/jobCronInfoSchema.ts +0 -22
  180. package/src/api/jobs/schemas/jobExecutionDetailResourceSchema.ts +0 -20
  181. package/src/api/jobs/schemas/jobFailureSchema.ts +0 -9
  182. package/src/api/jobs/schemas/jobQueueDepthSchema.ts +0 -14
  183. package/src/api/jobs/schemas/jobStatsSchema.ts +0 -14
  184. package/src/api/jobs/services/JobService-tests.ts +0 -157
  185. package/src/api/subscriptions/__tests__/BillingService.spec.ts +0 -218
  186. package/src/api/subscriptions/__tests__/SubscriptionService.spec.ts +0 -278
  187. package/src/api/subscriptions/controllers/AdminSubscriptionController.ts +0 -212
  188. package/src/api/subscriptions/controllers/SubscriptionController.ts +0 -189
  189. package/src/api/subscriptions/entities/subscriptionEvents.ts +0 -54
  190. package/src/api/subscriptions/entities/subscriptions.ts +0 -68
  191. package/src/api/subscriptions/index.ts +0 -133
  192. package/src/api/subscriptions/jobs/SubscriptionJobs.ts +0 -382
  193. package/src/api/subscriptions/middleware/$requireLimit.ts +0 -50
  194. package/src/api/subscriptions/middleware/$requirePlan.ts +0 -49
  195. package/src/api/subscriptions/notifications/SubscriptionNotifications.ts +0 -110
  196. package/src/api/subscriptions/schemas/cancelSubscriptionSchema.ts +0 -8
  197. package/src/api/subscriptions/schemas/changePlanSchema.ts +0 -9
  198. package/src/api/subscriptions/schemas/createSubscriptionSchema.ts +0 -11
  199. package/src/api/subscriptions/schemas/entitlementsSchema.ts +0 -21
  200. package/src/api/subscriptions/schemas/mrrSchema.ts +0 -13
  201. package/src/api/subscriptions/schemas/planDefinitionSchema.ts +0 -71
  202. package/src/api/subscriptions/schemas/planResourceSchema.ts +0 -25
  203. package/src/api/subscriptions/schemas/subscriptionEventResourceSchema.ts +0 -8
  204. package/src/api/subscriptions/schemas/subscriptionQuerySchema.ts +0 -19
  205. package/src/api/subscriptions/schemas/subscriptionResourceSchema.ts +0 -6
  206. package/src/api/subscriptions/schemas/subscriptionSettingsSchema.ts +0 -32
  207. package/src/api/subscriptions/schemas/subscriptionStatsSchema.ts +0 -23
  208. package/src/api/subscriptions/services/BillingService.ts +0 -437
  209. package/src/api/subscriptions/services/SubscriptionConfig.ts +0 -56
  210. package/src/api/subscriptions/services/SubscriptionService.ts +0 -867
  211. package/src/api/subscriptions/services/UsageService.ts +0 -118
  212. package/src/api/workflows/__tests__/$workflow.spec.ts +0 -616
  213. package/src/api/workflows/controllers/AdminWorkflowController.ts +0 -191
  214. package/src/api/workflows/entities/workflowExecutions.ts +0 -74
  215. package/src/api/workflows/entities/workflowStepExecutions.ts +0 -74
  216. package/src/api/workflows/entities/workflowStepLogs.ts +0 -13
  217. package/src/api/workflows/index.browser.ts +0 -22
  218. package/src/api/workflows/index.ts +0 -115
  219. package/src/api/workflows/jobs/WorkflowJobs.ts +0 -77
  220. package/src/api/workflows/primitives/$workflow.ts +0 -202
  221. package/src/api/workflows/providers/WorkflowProvider.ts +0 -1284
  222. package/src/api/workflows/schemas/workflowActivitySchema.ts +0 -15
  223. package/src/api/workflows/schemas/workflowConfigAtom.ts +0 -51
  224. package/src/api/workflows/schemas/workflowExecutionDetailSchema.ts +0 -18
  225. package/src/api/workflows/schemas/workflowExecutionQuerySchema.ts +0 -26
  226. package/src/api/workflows/schemas/workflowExecutionResourceSchema.ts +0 -30
  227. package/src/api/workflows/schemas/workflowRegistrationSchema.ts +0 -26
  228. package/src/api/workflows/schemas/workflowStatsSchema.ts +0 -16
  229. package/src/api/workflows/schemas/workflowStepExecutionResourceSchema.ts +0 -15
  230. package/src/api/workflows/services/WorkflowService.ts +0 -382
  231. package/src/cli/core/templates/apiAppSecurityTs.ts +0 -43
  232. 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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"}
@@ -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
- protected readonly template: string;
71
- protected readonly separator: string;
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. Returns an empty object when the template has
89
- * no parameters.
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;EAER,KAAA,CAAM,IAAA,WAAe,UAAA,CAAW,CAAA;EAAA,UAS7B,IAAA,CAAK,IAAA;EAAA,UAML,IAAA,CAAK,KAAA,EAAO,CAAA;EAAA,UA8DZ,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/BsC;;;;;;;EAwCtC,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;;;;AAvLX;;;;;;;;;;;;;;;;;;;;;cCoBa,mBAAA;EAAA,0BACe,WAAA,EAAW,MAAA;EAAA,mBAElB,QAAA;EAAA,mBACA,SAAA;cAEP,QAAA,UAAkB,SAAA;EDzBJ;;;EAAA,ICiCtB,SAAA,CAAA;ED9BM;;;EAAA,ICqCN,UAAA,CAAA;EDnCG;;;;EC2CP,WAAA,CAAY,MAAA,EAAQ,MAAA;EDlCL;;;;;EC8Cf,OAAA,CAAQ,IAAA,WAAe,MAAA;EDsBmB;;;;ECoB1C,WAAA,CAAY,QAAA;EDwBQ;;;;EChBpB,SAAA,CAAU,IAAA;AAAA"}
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"}
@@ -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
- if (this.cache.has(path)) return this.cache.get(path);
9
- const result = this.mapParams(this.createRouteMatch(path));
10
- this.cache.set(path, result);
11
- return result;
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
- * Returns true if the template contains at least one `{param}` placeholder.
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(/\{([^}]+)\}/g, (_, name) => params[name] ?? `{${name}}`);
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. Returns an empty object when the template has
161
- * no parameters.
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
- const names = this.paramNames;
165
- if (names.length === 0) return {};
166
- const escapedSeparator = this.separator.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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 < names.length; i++) result[names[i]] = match[i + 1];
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(/\{[^}]+\}/g, wildcard);
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 = sep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
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 };
@@ -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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"}