@jsenv/navi 0.16.40 → 0.16.42
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/jsenv_navi.js +183 -170
- package/dist/jsenv_navi.js.map +3 -3
- package/package.json +4 -4
package/dist/jsenv_navi.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { installImportMetaCss } from "./jsenv_navi_side_effects.js";
|
|
2
2
|
import { useErrorBoundary, useLayoutEffect, useEffect, useRef, useState, useCallback, useContext, useMemo, useImperativeHandle, useId } from "preact/hooks";
|
|
3
3
|
import { jsxs, jsx, Fragment } from "preact/jsx-runtime";
|
|
4
|
-
import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, resolveCSSSize, findBefore, findAfter, createValueEffect,
|
|
4
|
+
import { createIterableWeakSet, mergeOneStyle, stringifyStyle, createPubSub, mergeTwoStyles, normalizeStyles, createGroupTransitionController, getElementSignature, getBorderRadius, preventIntermediateScrollbar, createOpacityTransition, resolveCSSSize, findBefore, findAfter, createValueEffect, getVisuallyVisibleInfo, getFirstVisuallyVisibleAncestor, allowWheelThrough, resolveCSSColor, createStyleController, visibleRectEffect, pickPositionRelativeTo, getBorderSizes, getPaddingSizes, hasCSSSizeUnit, activeElementSignal, canInterceptKeys, pickLightOrDark, resolveColorLuminance, initFocusGroup, dragAfterThreshold, getScrollContainer, stickyAsRelativeCoords, createDragToMoveGestureController, getDropTargetInfo, setStyles, useActiveElement, elementIsFocusable } from "@jsenv/dom";
|
|
5
5
|
import { prefixFirstAndIndentRemainingLines } from "@jsenv/humanize";
|
|
6
6
|
import { effect, signal, computed, batch, useSignal } from "@preact/signals";
|
|
7
7
|
import { createValidity } from "@jsenv/validity";
|
|
@@ -10145,6 +10145,107 @@ const resolveRouteUrl = (relativeUrl) => {
|
|
|
10145
10145
|
*/
|
|
10146
10146
|
|
|
10147
10147
|
|
|
10148
|
+
/**
|
|
10149
|
+
* Set up all application routes with reactive state management.
|
|
10150
|
+
*
|
|
10151
|
+
* Creates route objects that automatically sync with the current URL and provide
|
|
10152
|
+
* reactive signals for building dynamic UIs. Each route tracks its matching state,
|
|
10153
|
+
* extracted parameters, and computed URLs.
|
|
10154
|
+
*
|
|
10155
|
+
* @example
|
|
10156
|
+
* ```js
|
|
10157
|
+
* import { setupRoutes, stateSignal } from "@jsenv/navi";
|
|
10158
|
+
*
|
|
10159
|
+
* const settingsTabSignal = stateSignal('general', { type: "string", oneOf: ['general', 'overview'] });
|
|
10160
|
+
*
|
|
10161
|
+
* let { USER_PROFILE } = setupRoutes({
|
|
10162
|
+
* HOME: "/",
|
|
10163
|
+
* SETTINGS: "/settings/:tab=${settingsTabSignal}/",
|
|
10164
|
+
* });
|
|
10165
|
+
*
|
|
10166
|
+
* USER_PROFILE.matching // boolean
|
|
10167
|
+
* USER_PROFILE.matchingSignal.value // reactive signal
|
|
10168
|
+
* settingsTabSignal.value = 'overview'; // updates URL automatically
|
|
10169
|
+
* ```
|
|
10170
|
+
*
|
|
10171
|
+
* ⚠️ HOT RELOAD: Use 'let' instead of 'const' when destructuring:
|
|
10172
|
+
* ```js
|
|
10173
|
+
* // ❌ const { HOME, USER_PROFILE } = setupRoutes({...})
|
|
10174
|
+
* // ✅ let { HOME, USER_PROFILE } = setupRoutes({...})
|
|
10175
|
+
* ```
|
|
10176
|
+
*
|
|
10177
|
+
* @param {Object} routeDefinition - Object mapping route names to URL patterns
|
|
10178
|
+
* @param {string} routeDefinition[key] - URL pattern with optional parameters
|
|
10179
|
+
* @returns {Object} Object with route names as keys and route objects as values
|
|
10180
|
+
* @returns {Object.<string, {
|
|
10181
|
+
* pattern: string,
|
|
10182
|
+
* matching: boolean,
|
|
10183
|
+
* params: Object,
|
|
10184
|
+
* url: string,
|
|
10185
|
+
* relativeUrl: string,
|
|
10186
|
+
* matchingSignal: import("@preact/signals").Signal<boolean>,
|
|
10187
|
+
* paramsSignal: import("@preact/signals").Signal<Object>,
|
|
10188
|
+
* urlSignal: import("@preact/signals").Signal<string>,
|
|
10189
|
+
* navTo: (params?: Object) => Promise<void>,
|
|
10190
|
+
* redirectTo: (params?: Object) => Promise<void>,
|
|
10191
|
+
* replaceParams: (params: Object) => Promise<void>,
|
|
10192
|
+
* buildUrl: (params?: Object) => string,
|
|
10193
|
+
* buildRelativeUrl: (params?: Object) => string,
|
|
10194
|
+
* }>} Route objects with reactive state and navigation methods
|
|
10195
|
+
*
|
|
10196
|
+
* All routes MUST be created at once because any url can be accessed
|
|
10197
|
+
* at any given time (url can be shared, reloaded, etc..)
|
|
10198
|
+
*/
|
|
10199
|
+
|
|
10200
|
+
const setupRoutes = (routeDefinition) => {
|
|
10201
|
+
// Prevent calling setupRoutes when routes already exist - enforce clean setup
|
|
10202
|
+
if (routeSet.size > 0) {
|
|
10203
|
+
throw new Error(
|
|
10204
|
+
"Routes already exist. Call clearAllRoutes() first to clean up existing routes before creating new ones. This prevents cross-test pollution and ensures clean state.",
|
|
10205
|
+
);
|
|
10206
|
+
}
|
|
10207
|
+
// PHASE 1: Setup patterns with unified objects (includes all relationships and signal connections)
|
|
10208
|
+
const routePatterns = setupPatterns(routeDefinition);
|
|
10209
|
+
|
|
10210
|
+
// PHASE 2: Create routes using the unified pattern objects
|
|
10211
|
+
const routes = {};
|
|
10212
|
+
for (const key of Object.keys(routeDefinition)) {
|
|
10213
|
+
const routePattern = routePatterns[key];
|
|
10214
|
+
const route = registerRoute(routePattern);
|
|
10215
|
+
routes[key] = route;
|
|
10216
|
+
}
|
|
10217
|
+
onRouteDefined();
|
|
10218
|
+
|
|
10219
|
+
return routes;
|
|
10220
|
+
};
|
|
10221
|
+
|
|
10222
|
+
const useRouteStatus = (route) => {
|
|
10223
|
+
const { urlSignal, matchingSignal, paramsSignal, visitedSignal } = route;
|
|
10224
|
+
const url = urlSignal.value;
|
|
10225
|
+
const matching = matchingSignal.value;
|
|
10226
|
+
const params = paramsSignal.value;
|
|
10227
|
+
const visited = visitedSignal.value;
|
|
10228
|
+
|
|
10229
|
+
return {
|
|
10230
|
+
url,
|
|
10231
|
+
matching,
|
|
10232
|
+
params,
|
|
10233
|
+
visited,
|
|
10234
|
+
};
|
|
10235
|
+
};
|
|
10236
|
+
|
|
10237
|
+
// for unit tests
|
|
10238
|
+
const clearAllRoutes = () => {
|
|
10239
|
+
for (const [, routePrivateProperties] of routePrivatePropertiesMap) {
|
|
10240
|
+
routePrivateProperties.cleanup();
|
|
10241
|
+
}
|
|
10242
|
+
routeSet.clear();
|
|
10243
|
+
routePrivatePropertiesMap.clear();
|
|
10244
|
+
// Pattern registry is now local to setupPatterns, no global cleanup needed
|
|
10245
|
+
// Don't clear signal registry here - let tests manage it explicitly
|
|
10246
|
+
// This prevents clearing signals that are still being used across multiple route registrations
|
|
10247
|
+
};
|
|
10248
|
+
|
|
10148
10249
|
// Flag to prevent signal-to-URL synchronization during URL-to-signal synchronization
|
|
10149
10250
|
let isUpdatingRoutesFromUrl = false;
|
|
10150
10251
|
|
|
@@ -10159,9 +10260,11 @@ const ROUTE_DEACTIVATION_STRATEGY = "abort"; // 'abort', 'keep-loading'
|
|
|
10159
10260
|
const ROUTE_NOT_MATCHING_PARAMS = {};
|
|
10160
10261
|
|
|
10161
10262
|
const routeSet = new Set();
|
|
10162
|
-
// Store previous route states to detect changes
|
|
10163
10263
|
const routePrivatePropertiesMap = new Map();
|
|
10164
|
-
|
|
10264
|
+
const getRoutePrivateProperties = (route) => {
|
|
10265
|
+
return routePrivatePropertiesMap.get(route);
|
|
10266
|
+
};
|
|
10267
|
+
// Store previous route states to detect changes
|
|
10165
10268
|
const routePreviousStateMap = new WeakMap();
|
|
10166
10269
|
// Store abort controllers per action to control their lifecycle based on route state
|
|
10167
10270
|
const actionAbortControllerWeakMap = new WeakMap();
|
|
@@ -10261,8 +10364,8 @@ const updateRoutes = (
|
|
|
10261
10364
|
|
|
10262
10365
|
for (const [paramName, connection] of connectionMap) {
|
|
10263
10366
|
const { signal: paramSignal, debug } = connection;
|
|
10264
|
-
const
|
|
10265
|
-
const urlParamValue =
|
|
10367
|
+
const rawParams = route.rawParamsSignal.value;
|
|
10368
|
+
const urlParamValue = rawParams[paramName];
|
|
10266
10369
|
|
|
10267
10370
|
if (!newMatching) {
|
|
10268
10371
|
// Route doesn't match - check if any matching route extracts this parameter
|
|
@@ -10273,17 +10376,18 @@ const updateRoutes = (
|
|
|
10273
10376
|
if (otherRoute === route || !otherRoute.matching) {
|
|
10274
10377
|
continue;
|
|
10275
10378
|
}
|
|
10276
|
-
const
|
|
10277
|
-
const
|
|
10379
|
+
const otherRawParams = otherRoute.rawParamsSignal.value;
|
|
10380
|
+
const otherRoutePrivateProperties =
|
|
10381
|
+
getRoutePrivateProperties(otherRoute);
|
|
10278
10382
|
|
|
10279
10383
|
// Check if this matching route extracts the parameter
|
|
10280
|
-
if (paramName in
|
|
10384
|
+
if (paramName in otherRawParams) {
|
|
10281
10385
|
parameterExtractedByMatchingRoute = true;
|
|
10282
10386
|
}
|
|
10283
10387
|
|
|
10284
10388
|
// Check if this matching route is in the same family using parent-child relationships
|
|
10285
10389
|
const thisPatternObj = routePattern;
|
|
10286
|
-
const otherPatternObj =
|
|
10390
|
+
const otherPatternObj = otherRoutePrivateProperties.routePattern;
|
|
10287
10391
|
|
|
10288
10392
|
// Routes are in same family if they share a hierarchical relationship:
|
|
10289
10393
|
// 1. One is parent/ancestor of the other
|
|
@@ -10500,23 +10604,12 @@ const updateRoutes = (
|
|
|
10500
10604
|
};
|
|
10501
10605
|
};
|
|
10502
10606
|
|
|
10503
|
-
const getRoutePrivateProperties = (route) => {
|
|
10504
|
-
return routePrivatePropertiesMap.get(route);
|
|
10505
|
-
};
|
|
10506
|
-
|
|
10507
10607
|
const registerRoute = (routePattern) => {
|
|
10508
10608
|
const urlPatternRaw = routePattern.originalPattern;
|
|
10509
10609
|
const { cleanPattern, connectionMap } = routePattern;
|
|
10510
|
-
|
|
10511
|
-
const cleanupCallbackSet = new Set();
|
|
10512
|
-
const cleanup = () => {
|
|
10513
|
-
for (const cleanupCallback of cleanupCallbackSet) {
|
|
10514
|
-
cleanupCallback();
|
|
10515
|
-
}
|
|
10516
|
-
cleanupCallbackSet.clear();
|
|
10517
|
-
};
|
|
10518
10610
|
const [publishStatus, subscribeStatus] = createPubSub();
|
|
10519
10611
|
|
|
10612
|
+
// prepare route object
|
|
10520
10613
|
const route = {
|
|
10521
10614
|
urlPattern: cleanPattern,
|
|
10522
10615
|
pattern: cleanPattern,
|
|
@@ -10528,74 +10621,78 @@ const registerRoute = (routePattern) => {
|
|
|
10528
10621
|
relativeUrl: null,
|
|
10529
10622
|
url: null,
|
|
10530
10623
|
action: null,
|
|
10531
|
-
|
|
10624
|
+
matchingSignal: null,
|
|
10625
|
+
paramsSignal: null,
|
|
10626
|
+
urlSignal: null,
|
|
10627
|
+
replaceParams: undefined,
|
|
10628
|
+
subscribeStatus,
|
|
10532
10629
|
toString: () => {
|
|
10533
10630
|
return `route "${cleanPattern}"`;
|
|
10534
10631
|
},
|
|
10535
|
-
replaceParams: undefined,
|
|
10536
|
-
subscribeStatus,
|
|
10537
10632
|
};
|
|
10538
10633
|
routeSet.add(route);
|
|
10539
10634
|
const routePrivateProperties = {
|
|
10540
10635
|
routePattern,
|
|
10541
10636
|
originalPattern: urlPatternRaw,
|
|
10542
10637
|
pattern: cleanPattern,
|
|
10543
|
-
|
|
10544
|
-
|
|
10545
|
-
rawParamsSignal: null,
|
|
10546
|
-
visitedSignal: null,
|
|
10547
|
-
relativeUrlSignal: null,
|
|
10548
|
-
urlSignal: null,
|
|
10549
|
-
updateStatus: ({ matching, params, visited }) => {
|
|
10550
|
-
let someChange = false;
|
|
10551
|
-
matchingSignal.value = matching;
|
|
10552
|
-
|
|
10553
|
-
if (route.matching !== matching) {
|
|
10554
|
-
route.matching = matching;
|
|
10555
|
-
someChange = true;
|
|
10556
|
-
}
|
|
10557
|
-
visitedSignal.value = visited;
|
|
10558
|
-
if (route.visited !== visited) {
|
|
10559
|
-
route.visited = visited;
|
|
10560
|
-
someChange = true;
|
|
10561
|
-
}
|
|
10562
|
-
// Store raw params (from URL) - paramsSignal will reactively compute merged params
|
|
10563
|
-
rawParamsSignal.value = params;
|
|
10564
|
-
// Get merged params for comparison (computed signal will handle the merging)
|
|
10565
|
-
const mergedParams = paramsSignal.value;
|
|
10566
|
-
if (route.params !== mergedParams) {
|
|
10567
|
-
route.params = mergedParams;
|
|
10568
|
-
someChange = true;
|
|
10569
|
-
}
|
|
10570
|
-
if (someChange) {
|
|
10571
|
-
publishStatus({
|
|
10572
|
-
matching,
|
|
10573
|
-
params: mergedParams,
|
|
10574
|
-
visited,
|
|
10575
|
-
});
|
|
10576
|
-
}
|
|
10577
|
-
},
|
|
10638
|
+
updateStatus: null,
|
|
10639
|
+
cleanup: null,
|
|
10578
10640
|
};
|
|
10579
10641
|
routePrivatePropertiesMap.set(route, routePrivateProperties);
|
|
10642
|
+
const cleanupCallbackSet = new Set();
|
|
10643
|
+
routePrivateProperties.cleanup = () => {
|
|
10644
|
+
for (const cleanupCallback of cleanupCallbackSet) {
|
|
10645
|
+
cleanupCallback();
|
|
10646
|
+
}
|
|
10647
|
+
cleanupCallbackSet.clear();
|
|
10648
|
+
};
|
|
10649
|
+
routePrivateProperties.updateStatus = ({ matching, params, visited }) => {
|
|
10650
|
+
let someChange = false;
|
|
10651
|
+
route.matchingSignal.value = matching;
|
|
10652
|
+
|
|
10653
|
+
if (route.matching !== matching) {
|
|
10654
|
+
route.matching = matching;
|
|
10655
|
+
someChange = true;
|
|
10656
|
+
}
|
|
10657
|
+
route.visitedSignal.value = visited;
|
|
10658
|
+
if (route.visited !== visited) {
|
|
10659
|
+
route.visited = visited;
|
|
10660
|
+
someChange = true;
|
|
10661
|
+
}
|
|
10662
|
+
// Store raw params (from URL) - paramsSignal will reactively compute merged params
|
|
10663
|
+
route.rawParamsSignal.value = params;
|
|
10664
|
+
// Get merged params for comparison (computed signal will handle the merging)
|
|
10665
|
+
const mergedParams = route.paramsSignal.value;
|
|
10666
|
+
if (route.params !== mergedParams) {
|
|
10667
|
+
route.params = mergedParams;
|
|
10668
|
+
someChange = true;
|
|
10669
|
+
}
|
|
10670
|
+
if (someChange) {
|
|
10671
|
+
publishStatus({
|
|
10672
|
+
matching,
|
|
10673
|
+
params: mergedParams,
|
|
10674
|
+
visited,
|
|
10675
|
+
});
|
|
10676
|
+
}
|
|
10677
|
+
};
|
|
10580
10678
|
|
|
10581
|
-
|
|
10582
|
-
|
|
10583
|
-
|
|
10584
|
-
|
|
10679
|
+
// populate route object
|
|
10680
|
+
route.matchingSignal = signal(false);
|
|
10681
|
+
route.rawParamsSignal = signal(ROUTE_NOT_MATCHING_PARAMS);
|
|
10682
|
+
route.paramsSignal = computed(() => {
|
|
10683
|
+
const rawParams = route.rawParamsSignal.value;
|
|
10585
10684
|
const resolvedParams = routePattern.resolveParams(rawParams);
|
|
10586
10685
|
return resolvedParams;
|
|
10587
10686
|
});
|
|
10588
|
-
|
|
10589
|
-
|
|
10687
|
+
route.visitedSignal = signal(false);
|
|
10590
10688
|
// Keep route.params synchronized with computed paramsSignal
|
|
10591
10689
|
// This ensures route.params includes parameters from child routes
|
|
10592
10690
|
effect(() => {
|
|
10593
|
-
const computedParams = paramsSignal.value;
|
|
10691
|
+
const computedParams = route.paramsSignal.value;
|
|
10594
10692
|
if (route.params !== computedParams) {
|
|
10595
10693
|
route.params = computedParams;
|
|
10596
10694
|
}
|
|
10597
10695
|
});
|
|
10598
|
-
|
|
10599
10696
|
for (const [paramName, connection] of connectionMap) {
|
|
10600
10697
|
const { signal: paramSignal, debug } = connection;
|
|
10601
10698
|
|
|
@@ -10610,9 +10707,9 @@ const registerRoute = (routePattern) => {
|
|
|
10610
10707
|
// eslint-disable-next-line no-loop-func
|
|
10611
10708
|
effect(() => {
|
|
10612
10709
|
const value = paramSignal.value;
|
|
10613
|
-
const
|
|
10614
|
-
const urlParamValue =
|
|
10615
|
-
const matching = matchingSignal.value;
|
|
10710
|
+
const rawParams = route.rawParamsSignal.value;
|
|
10711
|
+
const urlParamValue = rawParams[paramName];
|
|
10712
|
+
const matching = route.matchingSignal.value;
|
|
10616
10713
|
|
|
10617
10714
|
// Signal returned to default - clean up URL by removing the parameter
|
|
10618
10715
|
// Skip cleanup during URL-to-signal synchronization to prevent recursion
|
|
@@ -10662,7 +10759,6 @@ const registerRoute = (routePattern) => {
|
|
|
10662
10759
|
route.replaceParams({ [paramName]: value });
|
|
10663
10760
|
});
|
|
10664
10761
|
}
|
|
10665
|
-
|
|
10666
10762
|
route.navTo = (params) => {
|
|
10667
10763
|
if (!integration) {
|
|
10668
10764
|
return Promise.resolve();
|
|
@@ -10679,7 +10775,7 @@ const registerRoute = (routePattern) => {
|
|
|
10679
10775
|
});
|
|
10680
10776
|
};
|
|
10681
10777
|
route.replaceParams = (newParams) => {
|
|
10682
|
-
const matching = matchingSignal.peek();
|
|
10778
|
+
const matching = route.matchingSignal.peek();
|
|
10683
10779
|
if (!matching) {
|
|
10684
10780
|
console.warn(
|
|
10685
10781
|
`Cannot replace params on route ${route} because it is not matching the current URL.`,
|
|
@@ -10695,16 +10791,14 @@ const registerRoute = (routePattern) => {
|
|
|
10695
10791
|
if (matchingRoute.action) {
|
|
10696
10792
|
const matchingRoutePrivateProperties =
|
|
10697
10793
|
getRoutePrivateProperties(matchingRoute);
|
|
10698
|
-
|
|
10699
|
-
|
|
10700
|
-
|
|
10701
|
-
|
|
10702
|
-
|
|
10703
|
-
|
|
10704
|
-
|
|
10705
|
-
|
|
10706
|
-
matchingRoute.action.replaceParams(updatedActionParams);
|
|
10707
|
-
}
|
|
10794
|
+
const { routePattern: matchingRoutePattern } =
|
|
10795
|
+
matchingRoutePrivateProperties;
|
|
10796
|
+
const currentResolvedParams = matchingRoutePattern.resolveParams();
|
|
10797
|
+
const updatedActionParams = {
|
|
10798
|
+
...currentResolvedParams,
|
|
10799
|
+
...newParams,
|
|
10800
|
+
};
|
|
10801
|
+
matchingRoute.action.replaceParams(updatedActionParams);
|
|
10708
10802
|
}
|
|
10709
10803
|
}
|
|
10710
10804
|
|
|
@@ -10753,20 +10847,20 @@ const registerRoute = (routePattern) => {
|
|
|
10753
10847
|
};
|
|
10754
10848
|
|
|
10755
10849
|
// relativeUrl/url
|
|
10756
|
-
|
|
10757
|
-
const rawParams = rawParamsSignal.value;
|
|
10850
|
+
route.relativeUrlSignal = computed(() => {
|
|
10851
|
+
const rawParams = route.rawParamsSignal.value;
|
|
10758
10852
|
const relativeUrl = route.buildRelativeUrl(rawParams);
|
|
10759
10853
|
return relativeUrl;
|
|
10760
10854
|
});
|
|
10761
|
-
|
|
10855
|
+
route.urlSignal = computed(() => {
|
|
10762
10856
|
const routeUrl = route.buildUrl();
|
|
10763
10857
|
return routeUrl;
|
|
10764
10858
|
});
|
|
10765
10859
|
const disposeRelativeUrlEffect = effect(() => {
|
|
10766
|
-
route.relativeUrl = relativeUrlSignal.value;
|
|
10860
|
+
route.relativeUrl = route.relativeUrlSignal.value;
|
|
10767
10861
|
});
|
|
10768
10862
|
const disposeUrlEffect = effect(() => {
|
|
10769
|
-
route.url = urlSignal.value;
|
|
10863
|
+
route.url = route.urlSignal.value;
|
|
10770
10864
|
});
|
|
10771
10865
|
cleanupCallbackSet.add(disposeRelativeUrlEffect);
|
|
10772
10866
|
cleanupCallbackSet.add(disposeUrlEffect);
|
|
@@ -10779,7 +10873,7 @@ const registerRoute = (routePattern) => {
|
|
|
10779
10873
|
if (mutableIdKeys.length) {
|
|
10780
10874
|
const mutableIdKey = mutableIdKeys[0];
|
|
10781
10875
|
const mutableIdValueSignal = computed(() => {
|
|
10782
|
-
const params = paramsSignal.value;
|
|
10876
|
+
const params = route.paramsSignal.value;
|
|
10783
10877
|
const mutableIdValue = params[mutableIdKey];
|
|
10784
10878
|
return mutableIdValue;
|
|
10785
10879
|
});
|
|
@@ -10799,45 +10893,14 @@ const registerRoute = (routePattern) => {
|
|
|
10799
10893
|
}
|
|
10800
10894
|
}
|
|
10801
10895
|
|
|
10802
|
-
const actionBoundToThisRoute = action.bindParams(paramsSignal);
|
|
10896
|
+
const actionBoundToThisRoute = action.bindParams(route.paramsSignal);
|
|
10803
10897
|
route.action = actionBoundToThisRoute;
|
|
10804
10898
|
return actionBoundToThisRoute;
|
|
10805
10899
|
};
|
|
10806
10900
|
|
|
10807
|
-
// Store private properties for internal access
|
|
10808
|
-
routePrivateProperties.matchingSignal = matchingSignal;
|
|
10809
|
-
routePrivateProperties.paramsSignal = paramsSignal;
|
|
10810
|
-
routePrivateProperties.rawParamsSignal = rawParamsSignal;
|
|
10811
|
-
routePrivateProperties.visitedSignal = visitedSignal;
|
|
10812
|
-
routePrivateProperties.relativeUrlSignal = relativeUrlSignal;
|
|
10813
|
-
routePrivateProperties.urlSignal = urlSignal;
|
|
10814
|
-
routePrivateProperties.cleanupCallbackSet = cleanupCallbackSet;
|
|
10815
|
-
|
|
10816
10901
|
return route;
|
|
10817
10902
|
};
|
|
10818
10903
|
|
|
10819
|
-
const useRouteStatus = (route) => {
|
|
10820
|
-
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
10821
|
-
if (!routePrivateProperties) {
|
|
10822
|
-
throw new Error(`Cannot find route private properties for ${route}`);
|
|
10823
|
-
}
|
|
10824
|
-
|
|
10825
|
-
const { urlSignal, matchingSignal, paramsSignal, visitedSignal } =
|
|
10826
|
-
routePrivateProperties;
|
|
10827
|
-
|
|
10828
|
-
const url = urlSignal.value;
|
|
10829
|
-
const matching = matchingSignal.value;
|
|
10830
|
-
const params = paramsSignal.value;
|
|
10831
|
-
const visited = visitedSignal.value;
|
|
10832
|
-
|
|
10833
|
-
return {
|
|
10834
|
-
url,
|
|
10835
|
-
matching,
|
|
10836
|
-
params,
|
|
10837
|
-
visited,
|
|
10838
|
-
};
|
|
10839
|
-
};
|
|
10840
|
-
|
|
10841
10904
|
let integration;
|
|
10842
10905
|
const setRouteIntegration = (integrationInterface) => {
|
|
10843
10906
|
integration = integrationInterface;
|
|
@@ -10846,56 +10909,6 @@ let onRouteDefined = () => {};
|
|
|
10846
10909
|
const setOnRouteDefined = (v) => {
|
|
10847
10910
|
onRouteDefined = v;
|
|
10848
10911
|
};
|
|
10849
|
-
/**
|
|
10850
|
-
* Define all routes for the application.
|
|
10851
|
-
*
|
|
10852
|
-
* ⚠️ HOT RELOAD WARNING: When destructuring the returned routes, use 'let' instead of 'const'
|
|
10853
|
-
* to allow hot reload to update the route references:
|
|
10854
|
-
*
|
|
10855
|
-
* ❌ const [ROLE_ROUTE, DATABASE_ROUTE] = defineRoutes({...})
|
|
10856
|
-
* ✅ let [ROLE_ROUTE, DATABASE_ROUTE] = defineRoutes({...})
|
|
10857
|
-
*
|
|
10858
|
-
* @param {Object} routeDefinition - Object mapping URL patterns to actions
|
|
10859
|
-
* @returns {Array} Array of route objects in the same order as the keys
|
|
10860
|
-
*/
|
|
10861
|
-
// All routes MUST be created at once because any url can be accessed
|
|
10862
|
-
// at any given time (url can be shared, reloaded, etc..)
|
|
10863
|
-
// Later I'll consider adding ability to have dynamic import into the mix
|
|
10864
|
-
// (An async function returning an action)
|
|
10865
|
-
|
|
10866
|
-
const setupRoutes = (routeDefinition) => {
|
|
10867
|
-
// Prevent calling setupRoutes when routes already exist - enforce clean setup
|
|
10868
|
-
if (routeSet.size > 0) {
|
|
10869
|
-
throw new Error(
|
|
10870
|
-
"Routes already exist. Call clearAllRoutes() first to clean up existing routes before creating new ones. This prevents cross-test pollution and ensures clean state.",
|
|
10871
|
-
);
|
|
10872
|
-
}
|
|
10873
|
-
// PHASE 1: Setup patterns with unified objects (includes all relationships and signal connections)
|
|
10874
|
-
const routePatterns = setupPatterns(routeDefinition);
|
|
10875
|
-
|
|
10876
|
-
// PHASE 2: Create routes using the unified pattern objects
|
|
10877
|
-
const routes = {};
|
|
10878
|
-
for (const key of Object.keys(routeDefinition)) {
|
|
10879
|
-
const routePattern = routePatterns[key];
|
|
10880
|
-
const route = registerRoute(routePattern);
|
|
10881
|
-
routes[key] = route;
|
|
10882
|
-
}
|
|
10883
|
-
onRouteDefined();
|
|
10884
|
-
|
|
10885
|
-
return routes;
|
|
10886
|
-
};
|
|
10887
|
-
|
|
10888
|
-
// for unit tests
|
|
10889
|
-
const clearAllRoutes = () => {
|
|
10890
|
-
for (const route of routeSet) {
|
|
10891
|
-
route.cleanup();
|
|
10892
|
-
}
|
|
10893
|
-
routeSet.clear();
|
|
10894
|
-
routePrivatePropertiesMap.clear();
|
|
10895
|
-
// Pattern registry is now local to setupPatterns, no global cleanup needed
|
|
10896
|
-
// Don't clear signal registry here - let tests manage it explicitly
|
|
10897
|
-
// This prevents clearing signals that are still being used across multiple route registrations
|
|
10898
|
-
};
|
|
10899
10912
|
|
|
10900
10913
|
const arraySignal = (initialValue = []) => {
|
|
10901
10914
|
const theSignal = signal(initialValue);
|