@jsenv/navi 0.16.6 → 0.16.7
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 +691 -175
- package/dist/jsenv_navi.js.map +6 -6
- package/package.json +1 -1
package/dist/jsenv_navi.js
CHANGED
|
@@ -2459,8 +2459,9 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2459
2459
|
// Convert numeric IDs to strings for consistency
|
|
2460
2460
|
const signalIdString = String(signalId);
|
|
2461
2461
|
if (globalSignalRegistry.has(signalIdString)) {
|
|
2462
|
+
const conflictInfo = globalSignalRegistry.get(signalIdString);
|
|
2462
2463
|
throw new Error(
|
|
2463
|
-
`Signal ID conflict: A signal with ID "${signalIdString}" already exists`,
|
|
2464
|
+
`Signal ID conflict: A signal with ID "${signalIdString}" already exists (existing default: ${conflictInfo.options.defaultValue})`,
|
|
2464
2465
|
);
|
|
2465
2466
|
}
|
|
2466
2467
|
|
|
@@ -2475,7 +2476,7 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2475
2476
|
if (valueFromLocalStorage !== undefined) {
|
|
2476
2477
|
if (debug) {
|
|
2477
2478
|
console.debug(
|
|
2478
|
-
`[stateSignal] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2479
|
+
`[stateSignal:${signalIdString}] using value from localStorage "${localStorageKey}"=${valueFromLocalStorage}`,
|
|
2479
2480
|
);
|
|
2480
2481
|
}
|
|
2481
2482
|
return valueFromLocalStorage;
|
|
@@ -2485,20 +2486,34 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2485
2486
|
if (sourceValue !== undefined) {
|
|
2486
2487
|
if (debug) {
|
|
2487
2488
|
console.debug(
|
|
2488
|
-
`[stateSignal] using value from source signal=${sourceValue}`,
|
|
2489
|
+
`[stateSignal:${signalIdString}] using value from source signal=${sourceValue}`,
|
|
2489
2490
|
);
|
|
2490
2491
|
}
|
|
2491
2492
|
return sourceValue;
|
|
2492
2493
|
}
|
|
2493
2494
|
}
|
|
2494
2495
|
if (debug) {
|
|
2495
|
-
console.debug(
|
|
2496
|
+
console.debug(
|
|
2497
|
+
`[stateSignal:${signalIdString}] using default value=${defaultValue}`,
|
|
2498
|
+
);
|
|
2496
2499
|
}
|
|
2497
2500
|
return defaultValue;
|
|
2498
2501
|
};
|
|
2499
2502
|
|
|
2500
2503
|
const advancedSignal = signal(getFallbackValue());
|
|
2501
2504
|
|
|
2505
|
+
if (debug) {
|
|
2506
|
+
console.debug(
|
|
2507
|
+
`[stateSignal:${signalIdString}] created with initial value=${advancedSignal.peek()}`,
|
|
2508
|
+
{
|
|
2509
|
+
defaultValue,
|
|
2510
|
+
hasSourceSignal: Boolean(sourceSignal),
|
|
2511
|
+
persists,
|
|
2512
|
+
localStorageKey: persists ? localStorageKey : undefined,
|
|
2513
|
+
},
|
|
2514
|
+
);
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2502
2517
|
// Set signal ID and create meaningful string representation
|
|
2503
2518
|
advancedSignal.__signalId = signalIdString;
|
|
2504
2519
|
advancedSignal.toString = () => `{navi_state_signal:${signalIdString}}`;
|
|
@@ -2562,10 +2577,11 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2562
2577
|
// we don't have anything in the source signal, keep current value
|
|
2563
2578
|
if (debug) {
|
|
2564
2579
|
console.debug(
|
|
2565
|
-
`[stateSignal] source signal is undefined, keeping current value`,
|
|
2580
|
+
`[stateSignal:${signalIdString}] source signal is undefined, keeping current value=${advancedSignal.peek()}`,
|
|
2566
2581
|
{
|
|
2567
2582
|
sourcePreviousValue,
|
|
2568
2583
|
sourceValue,
|
|
2584
|
+
currentValue: advancedSignal.peek(),
|
|
2569
2585
|
},
|
|
2570
2586
|
);
|
|
2571
2587
|
}
|
|
@@ -2574,10 +2590,14 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2574
2590
|
}
|
|
2575
2591
|
// the case we want to support: source signal value changes -> override current value
|
|
2576
2592
|
if (debug) {
|
|
2577
|
-
console.debug(
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2593
|
+
console.debug(
|
|
2594
|
+
`[stateSignal:${signalIdString}] source signal updated, overriding current value`,
|
|
2595
|
+
{
|
|
2596
|
+
sourcePreviousValue,
|
|
2597
|
+
sourceValue,
|
|
2598
|
+
previousValue: advancedSignal.peek(),
|
|
2599
|
+
},
|
|
2600
|
+
);
|
|
2581
2601
|
}
|
|
2582
2602
|
advancedSignal.value = sourceValue;
|
|
2583
2603
|
sourcePreviousValue = sourceValue;
|
|
@@ -2593,14 +2613,14 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2593
2613
|
if (value === undefined || value === null || value === defaultValue) {
|
|
2594
2614
|
if (debug) {
|
|
2595
2615
|
console.debug(
|
|
2596
|
-
`[stateSignal] removing "${localStorageKey}" from localStorage`,
|
|
2616
|
+
`[stateSignal:${signalIdString}] removing "${localStorageKey}" from localStorage (value=${value}, default=${defaultValue})`,
|
|
2597
2617
|
);
|
|
2598
2618
|
}
|
|
2599
2619
|
removeFromLocalStorage();
|
|
2600
2620
|
} else {
|
|
2601
2621
|
if (debug) {
|
|
2602
2622
|
console.debug(
|
|
2603
|
-
`[stateSignal] writing into localStorage "${localStorageKey}"=${value}`,
|
|
2623
|
+
`[stateSignal:${signalIdString}] writing into localStorage "${localStorageKey}"=${value}`,
|
|
2604
2624
|
);
|
|
2605
2625
|
}
|
|
2606
2626
|
writeIntoLocalStorage(value);
|
|
@@ -2611,10 +2631,39 @@ const stateSignal = (defaultValue, options = {}) => {
|
|
|
2611
2631
|
{
|
|
2612
2632
|
effect(() => {
|
|
2613
2633
|
const value = advancedSignal.value;
|
|
2634
|
+
const wasValid = validity.valid;
|
|
2614
2635
|
updateValidity({ oneOf }, validity, value);
|
|
2615
|
-
if (!validity.valid
|
|
2616
|
-
|
|
2617
|
-
|
|
2636
|
+
if (!validity.valid) {
|
|
2637
|
+
if (debug) {
|
|
2638
|
+
console.debug(`[stateSignal:${signalIdString}] validation failed`, {
|
|
2639
|
+
value,
|
|
2640
|
+
oneOf,
|
|
2641
|
+
hasAutoFix: Boolean(autoFix),
|
|
2642
|
+
});
|
|
2643
|
+
}
|
|
2644
|
+
if (autoFix) {
|
|
2645
|
+
const fixedValue = autoFix();
|
|
2646
|
+
if (debug) {
|
|
2647
|
+
console.debug(
|
|
2648
|
+
`[stateSignal:${signalIdString}] auto-fixing invalid value`,
|
|
2649
|
+
{
|
|
2650
|
+
invalidValue: value,
|
|
2651
|
+
fixedValue,
|
|
2652
|
+
},
|
|
2653
|
+
);
|
|
2654
|
+
}
|
|
2655
|
+
advancedSignal.value = fixedValue;
|
|
2656
|
+
return;
|
|
2657
|
+
}
|
|
2658
|
+
} else if (!wasValid && validity.valid) {
|
|
2659
|
+
if (debug) {
|
|
2660
|
+
console.debug(
|
|
2661
|
+
`[stateSignal:${signalIdString}] validation now passes`,
|
|
2662
|
+
{
|
|
2663
|
+
value,
|
|
2664
|
+
},
|
|
2665
|
+
);
|
|
2666
|
+
}
|
|
2618
2667
|
}
|
|
2619
2668
|
});
|
|
2620
2669
|
}
|
|
@@ -7642,213 +7691,409 @@ const createRoutePattern = (pattern) => {
|
|
|
7642
7691
|
* Build the most precise URL by using route relationships from pattern registry.
|
|
7643
7692
|
* Each route is responsible for its own URL generation using its own signals.
|
|
7644
7693
|
*/
|
|
7645
|
-
const buildMostPreciseUrl = (params = {}) => {
|
|
7646
|
-
// Handle parameter resolution internally to preserve user intent detection
|
|
7647
|
-
const resolvedParams = resolveParams(params);
|
|
7648
7694
|
|
|
7649
|
-
|
|
7650
|
-
|
|
7695
|
+
/**
|
|
7696
|
+
* Helper: Filter out default values from parameters for cleaner URLs
|
|
7697
|
+
*/
|
|
7698
|
+
const removeDefaultValues = (params) => {
|
|
7699
|
+
const filtered = { ...params };
|
|
7651
7700
|
|
|
7652
7701
|
for (const connection of connections) {
|
|
7653
7702
|
const { paramName, signal, options } = connection;
|
|
7654
7703
|
const defaultValue = options.defaultValue;
|
|
7655
7704
|
|
|
7656
|
-
if (paramName in
|
|
7657
|
-
|
|
7658
|
-
|
|
7659
|
-
|
|
7660
|
-
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
// Parameter was NOT provided, check signal value
|
|
7665
|
-
else if (signal?.value !== undefined && signal.value !== defaultValue) {
|
|
7666
|
-
// Only include signal value if it's not the default
|
|
7667
|
-
finalParams[paramName] = signal.value;
|
|
7668
|
-
// If signal.value === defaultValue, omit the parameter for shorter URL
|
|
7705
|
+
if (paramName in filtered && filtered[paramName] === defaultValue) {
|
|
7706
|
+
delete filtered[paramName];
|
|
7707
|
+
} else if (
|
|
7708
|
+
!(paramName in filtered) &&
|
|
7709
|
+
signal?.value !== undefined &&
|
|
7710
|
+
signal.value !== defaultValue
|
|
7711
|
+
) {
|
|
7712
|
+
filtered[paramName] = signal.value;
|
|
7669
7713
|
}
|
|
7670
7714
|
}
|
|
7671
7715
|
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
// 1. This route's parameters are all defaults (would be omitted)
|
|
7675
|
-
// 2. A child route has non-default parameters that should be included
|
|
7716
|
+
return filtered;
|
|
7717
|
+
};
|
|
7676
7718
|
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7719
|
+
/**
|
|
7720
|
+
* Helper: Find the best child route that matches current parameters and signals
|
|
7721
|
+
*/
|
|
7722
|
+
const findBestChildRoute = (params, relationships) => {
|
|
7723
|
+
const childPatternObjs = relationships?.childPatterns || [];
|
|
7724
|
+
if (pattern === "/" || !childPatternObjs.length) {
|
|
7725
|
+
return null;
|
|
7726
|
+
}
|
|
7681
7727
|
|
|
7682
|
-
//
|
|
7683
|
-
const
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
if (signal.value !== defaultValue) {
|
|
7689
|
-
signalDerivedParams[paramName] = signal.value;
|
|
7690
|
-
}
|
|
7728
|
+
// Try each child pattern object to find the most specific match
|
|
7729
|
+
for (const childPatternObj of childPatternObjs) {
|
|
7730
|
+
const childRouteCandidate = evaluateChildRoute(childPatternObj, params);
|
|
7731
|
+
|
|
7732
|
+
if (childRouteCandidate) {
|
|
7733
|
+
return childRouteCandidate;
|
|
7691
7734
|
}
|
|
7692
7735
|
}
|
|
7736
|
+
return null;
|
|
7737
|
+
};
|
|
7693
7738
|
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7739
|
+
/**
|
|
7740
|
+
* Helper: Evaluate if a specific child route is suitable for current params/signals
|
|
7741
|
+
*/
|
|
7742
|
+
const evaluateChildRoute = (childPatternObj, params) => {
|
|
7743
|
+
// Step 1: Check parameter compatibility
|
|
7744
|
+
const compatibility = checkChildRouteCompatibility(childPatternObj, params);
|
|
7745
|
+
if (!compatibility.isCompatible) {
|
|
7746
|
+
return null;
|
|
7747
|
+
}
|
|
7748
|
+
|
|
7749
|
+
// Step 2: Determine if child route should be used
|
|
7750
|
+
const shouldUseChild = shouldUseChildRoute(
|
|
7751
|
+
childPatternObj,
|
|
7752
|
+
params,
|
|
7753
|
+
compatibility,
|
|
7754
|
+
);
|
|
7755
|
+
if (!shouldUseChild) {
|
|
7756
|
+
return null;
|
|
7757
|
+
}
|
|
7758
|
+
|
|
7759
|
+
// Step 3: Build child route URL with proper parameter filtering
|
|
7760
|
+
return buildChildRouteUrl(childPatternObj, params);
|
|
7761
|
+
};
|
|
7762
|
+
|
|
7763
|
+
/**
|
|
7764
|
+
* Helper: Check if parameters are compatible with child route
|
|
7765
|
+
*/
|
|
7766
|
+
const checkChildRouteCompatibility = (childPatternObj, params) => {
|
|
7767
|
+
const childParams = {};
|
|
7768
|
+
let isCompatible = true;
|
|
7769
|
+
|
|
7770
|
+
// Check both parent signals AND user-provided params for child route matching
|
|
7771
|
+
const paramsToCheck = [
|
|
7772
|
+
...connections,
|
|
7773
|
+
...Object.entries(params).map(([key, value]) => ({
|
|
7774
|
+
paramName: key,
|
|
7775
|
+
userValue: value,
|
|
7776
|
+
isUserProvided: true,
|
|
7777
|
+
})),
|
|
7778
|
+
];
|
|
7779
|
+
|
|
7780
|
+
for (const item of paramsToCheck) {
|
|
7781
|
+
const result = processParameterForChildRoute(
|
|
7782
|
+
item,
|
|
7783
|
+
childPatternObj.pattern,
|
|
7784
|
+
);
|
|
7785
|
+
|
|
7786
|
+
if (!result.isCompatible) {
|
|
7787
|
+
isCompatible = false;
|
|
7699
7788
|
break;
|
|
7700
7789
|
}
|
|
7790
|
+
|
|
7791
|
+
if (result.shouldInclude) {
|
|
7792
|
+
childParams[result.paramName] = result.paramValue;
|
|
7793
|
+
}
|
|
7701
7794
|
}
|
|
7702
7795
|
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7796
|
+
return { isCompatible, childParams };
|
|
7797
|
+
};
|
|
7798
|
+
|
|
7799
|
+
/**
|
|
7800
|
+
* Helper: Process a single parameter for child route compatibility
|
|
7801
|
+
*/
|
|
7802
|
+
const processParameterForChildRoute = (item, childParsedPattern) => {
|
|
7803
|
+
let paramName;
|
|
7804
|
+
let paramValue;
|
|
7805
|
+
|
|
7806
|
+
if (item.isUserProvided) {
|
|
7807
|
+
paramName = item.paramName;
|
|
7808
|
+
paramValue = item.userValue;
|
|
7809
|
+
} else {
|
|
7810
|
+
const { paramName: name, signal, options } = item;
|
|
7811
|
+
paramName = name;
|
|
7812
|
+
// Only include non-default parent signal values
|
|
7813
|
+
if (
|
|
7814
|
+
signal?.value === undefined ||
|
|
7815
|
+
signal.value === options.defaultValue
|
|
7816
|
+
) {
|
|
7817
|
+
return { isCompatible: true, shouldInclude: false };
|
|
7710
7818
|
}
|
|
7819
|
+
paramValue = signal.value;
|
|
7711
7820
|
}
|
|
7712
7821
|
|
|
7713
|
-
//
|
|
7714
|
-
|
|
7715
|
-
|
|
7822
|
+
// Check if parameter value matches a literal segment in child pattern
|
|
7823
|
+
const matchesChildLiteral = paramMatchesChildLiteral(
|
|
7824
|
+
paramValue,
|
|
7825
|
+
childParsedPattern,
|
|
7826
|
+
);
|
|
7716
7827
|
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
let hasActiveParams = false;
|
|
7727
|
-
const childParams = {};
|
|
7728
|
-
|
|
7729
|
-
// Include parent signal values for child pattern matching
|
|
7730
|
-
// But first check if they're compatible with the child pattern
|
|
7731
|
-
let parentSignalsCompatibleWithChild = true;
|
|
7732
|
-
for (const parentConnection of connections) {
|
|
7733
|
-
const { paramName, signal, options } = parentConnection;
|
|
7734
|
-
// Only include non-default parent signal values
|
|
7735
|
-
if (
|
|
7736
|
-
signal?.value !== undefined &&
|
|
7737
|
-
signal.value !== options.defaultValue
|
|
7738
|
-
) {
|
|
7739
|
-
// Check if child pattern has conflicting literal segments for this parameter
|
|
7740
|
-
const childParsedPattern = childPatternData.parsedPattern;
|
|
7828
|
+
if (matchesChildLiteral) {
|
|
7829
|
+
// Compatible - parameter value matches child literal
|
|
7830
|
+
return {
|
|
7831
|
+
isCompatible: true,
|
|
7832
|
+
shouldInclude: !item.isUserProvided,
|
|
7833
|
+
paramName,
|
|
7834
|
+
paramValue,
|
|
7835
|
+
};
|
|
7836
|
+
}
|
|
7741
7837
|
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7838
|
+
// Check for incompatible cases
|
|
7839
|
+
if (item.isUserProvided && !matchesChildLiteral) {
|
|
7840
|
+
// Check if this is a path parameter from parent pattern
|
|
7841
|
+
const isParentPathParam = connections.some(
|
|
7842
|
+
(conn) => conn.paramName === paramName,
|
|
7843
|
+
);
|
|
7747
7844
|
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
}
|
|
7845
|
+
if (isParentPathParam) {
|
|
7846
|
+
// User provided a path param value that doesn't match this child's literals
|
|
7847
|
+
return { isCompatible: false };
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7754
7850
|
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
break;
|
|
7765
|
-
}
|
|
7766
|
-
}
|
|
7851
|
+
// Special case: section parameter with settings literal
|
|
7852
|
+
if (paramName === "section" && paramValue !== "settings") {
|
|
7853
|
+
const hasSettingsLiteral = childParsedPattern.segments.some(
|
|
7854
|
+
(segment) => segment.type === "literal" && segment.value === "settings",
|
|
7855
|
+
);
|
|
7856
|
+
if (hasSettingsLiteral) {
|
|
7857
|
+
return { isCompatible: false };
|
|
7858
|
+
}
|
|
7859
|
+
}
|
|
7767
7860
|
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7861
|
+
// Compatible but should only include if from signal (not user-provided)
|
|
7862
|
+
return {
|
|
7863
|
+
isCompatible: true,
|
|
7864
|
+
shouldInclude: !item.isUserProvided && !matchesChildLiteral,
|
|
7865
|
+
paramName,
|
|
7866
|
+
paramValue,
|
|
7867
|
+
};
|
|
7868
|
+
};
|
|
7869
|
+
|
|
7870
|
+
/**
|
|
7871
|
+
* Helper: Determine if child route should be used based on active parameters
|
|
7872
|
+
*/
|
|
7873
|
+
const shouldUseChildRoute = (childPatternObj, params, compatibility) => {
|
|
7874
|
+
// Check if child has active non-default signal values
|
|
7875
|
+
let hasActiveParams = false;
|
|
7876
|
+
const childParams = { ...compatibility.childParams };
|
|
7877
|
+
|
|
7878
|
+
for (const connection of childPatternObj.connections) {
|
|
7879
|
+
const { paramName, signal, options } = connection;
|
|
7880
|
+
const defaultValue = options.defaultValue;
|
|
7881
|
+
|
|
7882
|
+
if (signal?.value !== undefined) {
|
|
7883
|
+
childParams[paramName] = signal.value;
|
|
7884
|
+
if (signal.value !== defaultValue) {
|
|
7885
|
+
hasActiveParams = true;
|
|
7771
7886
|
}
|
|
7887
|
+
}
|
|
7888
|
+
}
|
|
7772
7889
|
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7890
|
+
// Check if child pattern can be fully satisfied
|
|
7891
|
+
const initialMergedParams = { ...childParams, ...params };
|
|
7892
|
+
const canBuildChildCompletely = childPatternObj.pattern.segments.every(
|
|
7893
|
+
(segment) => {
|
|
7894
|
+
if (segment.type === "literal") return true;
|
|
7895
|
+
if (segment.type === "param") {
|
|
7896
|
+
return (
|
|
7897
|
+
segment.optional || initialMergedParams[segment.name] !== undefined
|
|
7898
|
+
);
|
|
7776
7899
|
}
|
|
7900
|
+
return true;
|
|
7901
|
+
},
|
|
7902
|
+
);
|
|
7777
7903
|
|
|
7778
|
-
|
|
7779
|
-
for (const connection of childPatternData.connections) {
|
|
7780
|
-
const { paramName, signal, options } = connection;
|
|
7781
|
-
const defaultValue = options.defaultValue;
|
|
7904
|
+
const hasProvidedParams = Object.keys(params).length > 0;
|
|
7782
7905
|
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7906
|
+
// Use child route if:
|
|
7907
|
+
// 1. Child has active non-default parameters, OR
|
|
7908
|
+
// 2. User provided params AND child can be built completely
|
|
7909
|
+
return hasActiveParams || (hasProvidedParams && canBuildChildCompletely);
|
|
7910
|
+
};
|
|
7911
|
+
|
|
7912
|
+
/**
|
|
7913
|
+
* Helper: Build URL for selected child route with proper parameter filtering
|
|
7914
|
+
*/
|
|
7915
|
+
const buildChildRouteUrl = (childPatternObj, params) => {
|
|
7916
|
+
// Start with child signal values
|
|
7917
|
+
const baseParams = {};
|
|
7918
|
+
for (const connection of childPatternObj.connections) {
|
|
7919
|
+
const { paramName, signal, options } = connection;
|
|
7920
|
+
if (
|
|
7921
|
+
signal?.value !== undefined &&
|
|
7922
|
+
signal.value !== options.defaultValue
|
|
7923
|
+
) {
|
|
7924
|
+
baseParams[paramName] = signal.value;
|
|
7925
|
+
}
|
|
7926
|
+
}
|
|
7927
|
+
|
|
7928
|
+
// Apply user params with filtering logic
|
|
7929
|
+
for (const [paramName, userValue] of Object.entries(params)) {
|
|
7930
|
+
const childConnection = childPatternObj.connections.find(
|
|
7931
|
+
(conn) => conn.paramName === paramName,
|
|
7932
|
+
);
|
|
7933
|
+
|
|
7934
|
+
if (childConnection) {
|
|
7935
|
+
const { options } = childConnection;
|
|
7936
|
+
const defaultValue = options.defaultValue;
|
|
7937
|
+
|
|
7938
|
+
// Only include if it's NOT the signal's default value
|
|
7939
|
+
if (userValue !== defaultValue) {
|
|
7940
|
+
baseParams[paramName] = userValue;
|
|
7941
|
+
} else {
|
|
7942
|
+
// User provided the default value - complete omission
|
|
7943
|
+
delete baseParams[paramName];
|
|
7944
|
+
}
|
|
7945
|
+
} else {
|
|
7946
|
+
// Check if param corresponds to a literal segment in child pattern
|
|
7947
|
+
const isConsumedByChildPath = childPatternObj.pattern.segments.some(
|
|
7948
|
+
(segment) =>
|
|
7949
|
+
segment.type === "literal" && segment.value === userValue,
|
|
7950
|
+
);
|
|
7951
|
+
|
|
7952
|
+
if (!isConsumedByChildPath) {
|
|
7953
|
+
// Not consumed by child path, keep it as query param
|
|
7954
|
+
baseParams[paramName] = userValue;
|
|
7789
7955
|
}
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
|
|
7959
|
+
// Build child URL
|
|
7960
|
+
const childUrl = childPatternObj.buildUrl(baseParams);
|
|
7961
|
+
|
|
7962
|
+
if (childUrl && !childUrl.includes(":")) {
|
|
7963
|
+
// Check for parent optimization before returning
|
|
7964
|
+
const optimizedUrl = checkChildParentOptimization(
|
|
7965
|
+
childPatternObj.originalPattern,
|
|
7966
|
+
childUrl,
|
|
7967
|
+
baseParams,
|
|
7968
|
+
);
|
|
7969
|
+
return optimizedUrl || childUrl;
|
|
7970
|
+
}
|
|
7971
|
+
|
|
7972
|
+
return null;
|
|
7973
|
+
};
|
|
7974
|
+
|
|
7975
|
+
/**
|
|
7976
|
+
* Helper: Check if parent route optimization applies to child route
|
|
7977
|
+
*/
|
|
7978
|
+
const checkChildParentOptimization = (childPattern, childUrl, baseParams) => {
|
|
7979
|
+
if (Object.keys(baseParams).length > 0) {
|
|
7980
|
+
return null; // No optimization if parameters exist
|
|
7981
|
+
}
|
|
7790
7982
|
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
7983
|
+
const childRelationships = patternRelationships.get(childPattern);
|
|
7984
|
+
const childParentObjs = childRelationships?.parentPatterns || [];
|
|
7985
|
+
|
|
7986
|
+
for (const childParentObj of childParentObjs) {
|
|
7987
|
+
if (childParentObj.originalPattern === pattern) {
|
|
7988
|
+
// Get the child pattern object from relationships instead of recreating
|
|
7989
|
+
const childPatternObj = childRelationships;
|
|
7990
|
+
|
|
7991
|
+
const allChildParamsAreDefaults = (
|
|
7992
|
+
childPatternObj.connections || []
|
|
7993
|
+
).every((childConnection) => {
|
|
7994
|
+
const { signal, options } = childConnection;
|
|
7995
|
+
return signal?.value === options.defaultValue;
|
|
7996
|
+
});
|
|
7997
|
+
|
|
7998
|
+
if (allChildParamsAreDefaults) {
|
|
7999
|
+
// Build current route URL for comparison
|
|
8000
|
+
const resolvedParams = resolveParams({});
|
|
8001
|
+
const finalParams = removeDefaultValues(resolvedParams);
|
|
8002
|
+
const currentUrl = buildUrlFromPattern(
|
|
8003
|
+
parsedPattern,
|
|
8004
|
+
finalParams);
|
|
8005
|
+
if (currentUrl.length < childUrl.length) {
|
|
8006
|
+
return currentUrl;
|
|
7798
8007
|
}
|
|
7799
8008
|
}
|
|
7800
8009
|
}
|
|
7801
8010
|
}
|
|
8011
|
+
return null;
|
|
8012
|
+
};
|
|
8013
|
+
|
|
8014
|
+
const buildMostPreciseUrl = (params = {}) => {
|
|
8015
|
+
// Step 1: Resolve and clean parameters
|
|
8016
|
+
const resolvedParams = resolveParams(params);
|
|
8017
|
+
|
|
8018
|
+
// Step 2: Check for parent route optimization BEFORE removing defaults
|
|
8019
|
+
// This allows optimization when final effective values match defaults
|
|
8020
|
+
const relationships = patternRelationships.get(pattern);
|
|
8021
|
+
const optimizedUrl = checkParentRouteOptimization(
|
|
8022
|
+
resolvedParams,
|
|
8023
|
+
relationships,
|
|
8024
|
+
);
|
|
8025
|
+
if (optimizedUrl) {
|
|
8026
|
+
return optimizedUrl;
|
|
8027
|
+
}
|
|
7802
8028
|
|
|
7803
|
-
//
|
|
7804
|
-
|
|
7805
|
-
const parentPatterns = relationships?.parentPatterns || [];
|
|
7806
|
-
for (const parentPattern of parentPatterns) {
|
|
7807
|
-
const parentPatternData = getPatternData(parentPattern);
|
|
7808
|
-
if (!parentPatternData) continue;
|
|
8029
|
+
// Step 3: Remove default values for normal URL building
|
|
8030
|
+
let finalParams = removeDefaultValues(resolvedParams);
|
|
7809
8031
|
|
|
8032
|
+
// Step 4: Try to find a more specific child route
|
|
8033
|
+
const childRouteUrl = findBestChildRoute(params, relationships);
|
|
8034
|
+
if (childRouteUrl) {
|
|
8035
|
+
return childRouteUrl;
|
|
8036
|
+
}
|
|
8037
|
+
|
|
8038
|
+
// Step 5: Inherit parameters from parent routes
|
|
8039
|
+
inheritParentParameters(finalParams, relationships);
|
|
8040
|
+
|
|
8041
|
+
// Step 6: Build the current route URL
|
|
8042
|
+
const generatedUrl = buildCurrentRouteUrl(finalParams);
|
|
8043
|
+
|
|
8044
|
+
return generatedUrl;
|
|
8045
|
+
};
|
|
8046
|
+
|
|
8047
|
+
/**
|
|
8048
|
+
* Helper: Inherit query parameters from parent patterns
|
|
8049
|
+
*/
|
|
8050
|
+
const inheritParentParameters = (finalParams, relationships) => {
|
|
8051
|
+
const parentPatternObjs = relationships?.parentPatterns || [];
|
|
8052
|
+
|
|
8053
|
+
for (const parentPatternObj of parentPatternObjs) {
|
|
7810
8054
|
// Check parent's signal connections for non-default values to inherit
|
|
7811
|
-
for (const parentConnection of
|
|
8055
|
+
for (const parentConnection of parentPatternObj.connections) {
|
|
7812
8056
|
const { paramName, signal, options } = parentConnection;
|
|
7813
8057
|
const defaultValue = options.defaultValue;
|
|
7814
8058
|
|
|
7815
|
-
//
|
|
8059
|
+
// Only inherit if we don't have this param and parent has non-default value
|
|
7816
8060
|
if (
|
|
7817
8061
|
!(paramName in finalParams) &&
|
|
7818
8062
|
signal?.value !== undefined &&
|
|
7819
8063
|
signal.value !== defaultValue
|
|
7820
8064
|
) {
|
|
7821
|
-
//
|
|
7822
|
-
// E.g., don't inherit "section=analytics" if our path is "/admin/analytics"
|
|
8065
|
+
// Don't inherit if parameter corresponds to a literal in our path
|
|
7823
8066
|
const shouldInherit = !isParameterRedundantWithLiteralSegments(
|
|
7824
8067
|
parsedPattern,
|
|
7825
|
-
|
|
8068
|
+
parentPatternObj.pattern,
|
|
7826
8069
|
paramName,
|
|
7827
8070
|
signal.value,
|
|
7828
8071
|
);
|
|
7829
8072
|
|
|
7830
8073
|
if (shouldInherit) {
|
|
7831
|
-
// Inherit the parent's signal value
|
|
7832
8074
|
finalParams[paramName] = signal.value;
|
|
7833
8075
|
}
|
|
7834
8076
|
}
|
|
7835
8077
|
}
|
|
7836
8078
|
}
|
|
8079
|
+
};
|
|
7837
8080
|
|
|
8081
|
+
/**
|
|
8082
|
+
* Helper: Build URL for current route with filtered pattern
|
|
8083
|
+
*/
|
|
8084
|
+
const buildCurrentRouteUrl = (finalParams) => {
|
|
7838
8085
|
if (!parsedPattern.segments) {
|
|
7839
8086
|
return "/";
|
|
7840
8087
|
}
|
|
7841
8088
|
|
|
7842
|
-
// Filter out segments
|
|
8089
|
+
// Filter out parameter segments that don't have values
|
|
7843
8090
|
const filteredPattern = {
|
|
7844
8091
|
...parsedPattern,
|
|
7845
8092
|
segments: parsedPattern.segments.filter((segment) => {
|
|
7846
8093
|
if (segment.type === "param") {
|
|
7847
|
-
// Only keep parameter segments if we have a value for them
|
|
7848
8094
|
return segment.name in finalParams;
|
|
7849
8095
|
}
|
|
7850
|
-
//
|
|
7851
|
-
return true;
|
|
8096
|
+
return true; // Keep literal segments
|
|
7852
8097
|
}),
|
|
7853
8098
|
};
|
|
7854
8099
|
|
|
@@ -7863,11 +8108,149 @@ const createRoutePattern = (pattern) => {
|
|
|
7863
8108
|
return buildUrlFromPattern(filteredPattern, finalParams);
|
|
7864
8109
|
};
|
|
7865
8110
|
|
|
8111
|
+
/**
|
|
8112
|
+
* Helper: Check if parent route can provide a shorter equivalent URL
|
|
8113
|
+
*/
|
|
8114
|
+
const checkParentRouteOptimization = (resolvedParams, relationships) => {
|
|
8115
|
+
// Only consider parent optimization for patterns with signal connections
|
|
8116
|
+
if (connections.length === 0) {
|
|
8117
|
+
return null;
|
|
8118
|
+
}
|
|
8119
|
+
|
|
8120
|
+
// Check if all final effective values equal their defaults
|
|
8121
|
+
const allEffectiveValuesAreDefaults = connections.every((conn) => {
|
|
8122
|
+
// Final effective value is what's in resolvedParams (signals + provided params)
|
|
8123
|
+
const effectiveValue =
|
|
8124
|
+
resolvedParams[conn.paramName] ?? conn.options.defaultValue;
|
|
8125
|
+
return effectiveValue === conn.options.defaultValue;
|
|
8126
|
+
});
|
|
8127
|
+
|
|
8128
|
+
// Only optimize if all effective values equal their defaults
|
|
8129
|
+
if (!allEffectiveValuesAreDefaults) {
|
|
8130
|
+
return null;
|
|
8131
|
+
}
|
|
8132
|
+
|
|
8133
|
+
// Check if there are extra parameters not handled by current route's connections
|
|
8134
|
+
const connectionParamNames = new Set(
|
|
8135
|
+
connections.map((conn) => conn.paramName),
|
|
8136
|
+
);
|
|
8137
|
+
const hasExtraParams = Object.keys(resolvedParams).some(
|
|
8138
|
+
(paramName) => !connectionParamNames.has(paramName),
|
|
8139
|
+
);
|
|
8140
|
+
|
|
8141
|
+
// Don't optimize if there are extra parameters that would be lost
|
|
8142
|
+
if (hasExtraParams) {
|
|
8143
|
+
return null;
|
|
8144
|
+
}
|
|
8145
|
+
|
|
8146
|
+
const possibleParentObjs = relationships?.parentPatterns || [];
|
|
8147
|
+
|
|
8148
|
+
for (const parentPatternObj of possibleParentObjs) {
|
|
8149
|
+
// Skip root route and routes without parameters
|
|
8150
|
+
if (
|
|
8151
|
+
parentPatternObj.originalPattern === "/" ||
|
|
8152
|
+
!parentPatternObj.originalPattern.includes(":")
|
|
8153
|
+
) {
|
|
8154
|
+
continue;
|
|
8155
|
+
}
|
|
8156
|
+
|
|
8157
|
+
const optimizedParentUrl = evaluateParentOptimization(
|
|
8158
|
+
parentPatternObj,
|
|
8159
|
+
resolvedParams,
|
|
8160
|
+
);
|
|
8161
|
+
|
|
8162
|
+
if (optimizedParentUrl) {
|
|
8163
|
+
return optimizedParentUrl;
|
|
8164
|
+
}
|
|
8165
|
+
}
|
|
8166
|
+
|
|
8167
|
+
return null;
|
|
8168
|
+
};
|
|
8169
|
+
|
|
8170
|
+
/**
|
|
8171
|
+
* Helper: Evaluate a specific parent pattern for URL optimization
|
|
8172
|
+
*/
|
|
8173
|
+
const evaluateParentOptimization = (parentPatternObj, resolvedParams) => {
|
|
8174
|
+
// Get literal segments from child pattern to map to parent parameters
|
|
8175
|
+
const childLiterals = getPatternLiterals(parsedPattern);
|
|
8176
|
+
|
|
8177
|
+
// Check if parent would also have all default values
|
|
8178
|
+
// For parent optimization, we consider both explicitly provided params and literal segments
|
|
8179
|
+
const allParentParamsAreDefaults = parentPatternObj.connections.every(
|
|
8180
|
+
(parentConnection) => {
|
|
8181
|
+
const paramName = parentConnection.paramName;
|
|
8182
|
+
|
|
8183
|
+
// If explicitly provided in resolved params, use that
|
|
8184
|
+
if (resolvedParams[paramName] !== undefined) {
|
|
8185
|
+
return (
|
|
8186
|
+
resolvedParams[paramName] === parentConnection.options.defaultValue
|
|
8187
|
+
);
|
|
8188
|
+
}
|
|
8189
|
+
|
|
8190
|
+
// Check if this parent parameter corresponds to a literal segment in child
|
|
8191
|
+
// If parent default matches a child literal, consider it as using the default
|
|
8192
|
+
const defaultValue = parentConnection.options.defaultValue;
|
|
8193
|
+
if (childLiterals.includes(defaultValue)) {
|
|
8194
|
+
return true; // Literal segment effectively provides the default value
|
|
8195
|
+
}
|
|
8196
|
+
|
|
8197
|
+
// Otherwise assume parent would use its default for optimization purposes
|
|
8198
|
+
return true;
|
|
8199
|
+
},
|
|
8200
|
+
);
|
|
8201
|
+
|
|
8202
|
+
if (!allParentParamsAreDefaults) {
|
|
8203
|
+
return null; // Can't optimize if parent has non-default values
|
|
8204
|
+
}
|
|
8205
|
+
|
|
8206
|
+
// Check if parent's default values match our literals
|
|
8207
|
+
const parentPointsToCurrentRoute = parentPatternObj.connections.every(
|
|
8208
|
+
(parentConnection) => {
|
|
8209
|
+
const { options } = parentConnection;
|
|
8210
|
+
const defaultValue = options.defaultValue;
|
|
8211
|
+
return childLiterals.includes(defaultValue);
|
|
8212
|
+
},
|
|
8213
|
+
);
|
|
8214
|
+
|
|
8215
|
+
if (parentPointsToCurrentRoute) {
|
|
8216
|
+
// Build parent URL using defaults, not current signal values
|
|
8217
|
+
const parentDefaultParams = {};
|
|
8218
|
+
for (const parentConnection of parentPatternObj.connections) {
|
|
8219
|
+
parentDefaultParams[parentConnection.paramName] =
|
|
8220
|
+
parentConnection.options.defaultValue;
|
|
8221
|
+
}
|
|
8222
|
+
// Build parent URL and check if it can be optimized further
|
|
8223
|
+
let parentUrl = parentPatternObj.buildUrl(parentDefaultParams);
|
|
8224
|
+
|
|
8225
|
+
// Check if parent can optimize itself by removing default parameters
|
|
8226
|
+
if (parentUrl && parentUrl !== "/") {
|
|
8227
|
+
// Check if all parent's default params are actually defaults
|
|
8228
|
+
const parentAllDefaults = parentPatternObj.connections.every((conn) => {
|
|
8229
|
+
const paramValue = parentDefaultParams[conn.paramName];
|
|
8230
|
+
return paramValue === conn.options.defaultValue;
|
|
8231
|
+
});
|
|
8232
|
+
|
|
8233
|
+
if (parentAllDefaults) {
|
|
8234
|
+
// Try to build parent URL without any parameters to see if it's shorter
|
|
8235
|
+
const parentMinimalUrl = parentPatternObj.buildUrl({});
|
|
8236
|
+
if (parentMinimalUrl && parentMinimalUrl.length < parentUrl.length) {
|
|
8237
|
+
parentUrl = parentMinimalUrl;
|
|
8238
|
+
}
|
|
8239
|
+
}
|
|
8240
|
+
|
|
8241
|
+
return parentUrl;
|
|
8242
|
+
}
|
|
8243
|
+
}
|
|
8244
|
+
|
|
8245
|
+
return null;
|
|
8246
|
+
};
|
|
8247
|
+
|
|
7866
8248
|
return {
|
|
7867
8249
|
originalPattern: pattern, // Return the original pattern string
|
|
7868
8250
|
pattern: parsedPattern,
|
|
7869
8251
|
cleanPattern, // Return the clean pattern string
|
|
7870
8252
|
connections, // Return signal connections along with pattern
|
|
8253
|
+
specificity: calculatePatternSpecificity(parsedPattern), // Pre-calculate specificity
|
|
7871
8254
|
applyOn,
|
|
7872
8255
|
buildUrl,
|
|
7873
8256
|
buildMostPreciseUrl,
|
|
@@ -7875,6 +8258,50 @@ const createRoutePattern = (pattern) => {
|
|
|
7875
8258
|
};
|
|
7876
8259
|
};
|
|
7877
8260
|
|
|
8261
|
+
/**
|
|
8262
|
+
* Helper: Extract literal values from pattern segments
|
|
8263
|
+
*/
|
|
8264
|
+
const getPatternLiterals = (pattern) => {
|
|
8265
|
+
return pattern.segments
|
|
8266
|
+
.filter((seg) => seg.type === "literal")
|
|
8267
|
+
.map((seg) => seg.value);
|
|
8268
|
+
};
|
|
8269
|
+
|
|
8270
|
+
/**
|
|
8271
|
+
* Helper: Check if parameter matches any literal in child pattern
|
|
8272
|
+
*/
|
|
8273
|
+
const paramMatchesChildLiteral = (paramValue, childParsedPattern) => {
|
|
8274
|
+
return childParsedPattern.segments.some(
|
|
8275
|
+
(segment) => segment.type === "literal" && segment.value === paramValue,
|
|
8276
|
+
);
|
|
8277
|
+
};
|
|
8278
|
+
|
|
8279
|
+
/**
|
|
8280
|
+
* Calculate pattern specificity score for route matching
|
|
8281
|
+
* Higher score = more specific route
|
|
8282
|
+
*/
|
|
8283
|
+
const calculatePatternSpecificity = (parsedPattern) => {
|
|
8284
|
+
let specificity = 0;
|
|
8285
|
+
|
|
8286
|
+
// Count path segments (ignoring query params for specificity)
|
|
8287
|
+
const pathSegments = parsedPattern.segments || [];
|
|
8288
|
+
|
|
8289
|
+
for (const segment of pathSegments) {
|
|
8290
|
+
if (segment.type === "literal") {
|
|
8291
|
+
// Literal segments are more specific than parameters
|
|
8292
|
+
specificity += 100; // High score for literal segments
|
|
8293
|
+
} else if (segment.type === "param") {
|
|
8294
|
+
// Parameter segments are less specific
|
|
8295
|
+
specificity += 10; // Lower score for parameters
|
|
8296
|
+
}
|
|
8297
|
+
}
|
|
8298
|
+
|
|
8299
|
+
// Add base score for number of path segments (more segments = more specific)
|
|
8300
|
+
specificity += pathSegments.length;
|
|
8301
|
+
|
|
8302
|
+
return specificity;
|
|
8303
|
+
};
|
|
8304
|
+
|
|
7878
8305
|
/**
|
|
7879
8306
|
* Parse a route pattern string into structured segments
|
|
7880
8307
|
*/
|
|
@@ -8163,6 +8590,46 @@ const extractSearchParams = (urlObj, connections = []) => {
|
|
|
8163
8590
|
return params;
|
|
8164
8591
|
};
|
|
8165
8592
|
|
|
8593
|
+
/**
|
|
8594
|
+
* Helper: Check if a parameter represents parent route inheritance
|
|
8595
|
+
* This detects when a parameter doesn't match the current route's parameters
|
|
8596
|
+
* but the route has literal segments that might correspond to parent route parameters
|
|
8597
|
+
*/
|
|
8598
|
+
const detectParentParameterInheritance = (
|
|
8599
|
+
paramName,
|
|
8600
|
+
paramValue,
|
|
8601
|
+
parsedPattern,
|
|
8602
|
+
pathParamNames,
|
|
8603
|
+
queryParamNames,
|
|
8604
|
+
) => {
|
|
8605
|
+
// Parameter doesn't belong to current route
|
|
8606
|
+
const isExtraParam =
|
|
8607
|
+
!pathParamNames.has(paramName) && !queryParamNames.has(paramName);
|
|
8608
|
+
|
|
8609
|
+
// Route has literal segments (suggesting it might be a child of a parameterized parent)
|
|
8610
|
+
const hasLiteralSegments = parsedPattern.segments.some(
|
|
8611
|
+
(s) => s.type === "literal",
|
|
8612
|
+
);
|
|
8613
|
+
|
|
8614
|
+
// Common parent parameter names (heuristic)
|
|
8615
|
+
const commonParentParams = new Set([
|
|
8616
|
+
"section",
|
|
8617
|
+
"category",
|
|
8618
|
+
"type",
|
|
8619
|
+
"area",
|
|
8620
|
+
"zone",
|
|
8621
|
+
]);
|
|
8622
|
+
const looksLikeParentParam = commonParentParams.has(paramName);
|
|
8623
|
+
|
|
8624
|
+
return {
|
|
8625
|
+
isParentInheritance:
|
|
8626
|
+
isExtraParam && hasLiteralSegments && looksLikeParentParam,
|
|
8627
|
+
isExtraParam,
|
|
8628
|
+
hasLiteralSegments,
|
|
8629
|
+
looksLikeParentParam,
|
|
8630
|
+
};
|
|
8631
|
+
};
|
|
8632
|
+
|
|
8166
8633
|
/**
|
|
8167
8634
|
* Build a URL from a pattern and parameters
|
|
8168
8635
|
*/
|
|
@@ -8267,16 +8734,59 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
|
|
|
8267
8734
|
}
|
|
8268
8735
|
|
|
8269
8736
|
// Add remaining parameters as additional query parameters (excluding path and pattern query params)
|
|
8737
|
+
// Handle parameter inheritance and extra parameters
|
|
8738
|
+
const extraParams = [];
|
|
8739
|
+
|
|
8270
8740
|
for (const [key, value] of Object.entries(params)) {
|
|
8271
8741
|
if (
|
|
8272
8742
|
!pathParamNames.has(key) &&
|
|
8273
8743
|
!queryParamNames.has(key) &&
|
|
8274
8744
|
value !== undefined
|
|
8275
8745
|
) {
|
|
8276
|
-
|
|
8746
|
+
// This parameter doesn't match any path or query parameter in this route pattern,
|
|
8747
|
+
// so it will be treated as an extra query parameter.
|
|
8748
|
+
//
|
|
8749
|
+
// COMMON SCENARIOS:
|
|
8750
|
+
// 1. Parent route parameter inheritance: When a child route has literal segments
|
|
8751
|
+
// that correspond to parent route parameters. For example:
|
|
8752
|
+
// - Parent: /admin/:section/
|
|
8753
|
+
// - Child: /admin/settings/:tab (has "settings" as literal)
|
|
8754
|
+
// - Calling child.buildUrl({section: "toto"}) → /admin/settings?section=toto
|
|
8755
|
+
// The "section" param becomes a query param because "settings" is hardcoded.
|
|
8756
|
+
//
|
|
8757
|
+
// 2. Extra state parameters: Completely additional parameters for URL state
|
|
8758
|
+
// - Calling route.buildUrl({filter: "active"}) → /route?filter=active
|
|
8759
|
+
|
|
8760
|
+
// Check if this parameter value is redundant with literal segments in the path
|
|
8761
|
+
// E.g., don't add "section=settings" if path is already "/admin/settings"
|
|
8762
|
+
const isRedundantWithPath = parsedPattern.segments.some(
|
|
8763
|
+
(segment) => segment.type === "literal" && segment.value === value,
|
|
8764
|
+
);
|
|
8765
|
+
|
|
8766
|
+
if (!isRedundantWithPath) {
|
|
8767
|
+
extraParams.push([key, value]);
|
|
8768
|
+
|
|
8769
|
+
// Optional: Detect and log parent parameter inheritance for debugging
|
|
8770
|
+
detectParentParameterInheritance(
|
|
8771
|
+
key,
|
|
8772
|
+
value,
|
|
8773
|
+
parsedPattern,
|
|
8774
|
+
pathParamNames,
|
|
8775
|
+
queryParamNames,
|
|
8776
|
+
);
|
|
8777
|
+
}
|
|
8778
|
+
// Note: Redundant parameters are intentionally omitted for cleaner URLs
|
|
8277
8779
|
}
|
|
8278
8780
|
}
|
|
8279
8781
|
|
|
8782
|
+
// Sort extra params alphabetically for consistent order
|
|
8783
|
+
extraParams.sort(([a], [b]) => a.localeCompare(b));
|
|
8784
|
+
|
|
8785
|
+
// Add sorted extra params to searchParams
|
|
8786
|
+
for (const [key, value] of extraParams) {
|
|
8787
|
+
searchParams.set(key, value);
|
|
8788
|
+
}
|
|
8789
|
+
|
|
8280
8790
|
const search = searchParams.toString();
|
|
8281
8791
|
|
|
8282
8792
|
// No longer handle trailing slash inheritance here
|
|
@@ -8386,7 +8896,9 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8386
8896
|
patternRegistry.clear();
|
|
8387
8897
|
patternRelationships.clear();
|
|
8388
8898
|
|
|
8389
|
-
// Phase 1: Register all patterns
|
|
8899
|
+
// Phase 1: Register all patterns and create pattern objects
|
|
8900
|
+
const patternObjects = new Map(); // pattern string -> pattern object
|
|
8901
|
+
|
|
8390
8902
|
for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) {
|
|
8391
8903
|
const [cleanPattern, connections] = detectSignals(urlPatternRaw);
|
|
8392
8904
|
const parsedPattern = parsePattern(cleanPattern);
|
|
@@ -8402,6 +8914,10 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8402
8914
|
};
|
|
8403
8915
|
|
|
8404
8916
|
patternRegistry.set(urlPatternRaw, patternData);
|
|
8917
|
+
|
|
8918
|
+
// Create the full pattern object for this pattern
|
|
8919
|
+
const patternObj = createRoutePattern(urlPatternRaw);
|
|
8920
|
+
patternObjects.set(urlPatternRaw, patternObj);
|
|
8405
8921
|
}
|
|
8406
8922
|
|
|
8407
8923
|
// Phase 2: Build relationships between all patterns
|
|
@@ -8417,30 +8933,24 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8417
8933
|
|
|
8418
8934
|
// Check if current pattern is a child of other pattern using clean patterns
|
|
8419
8935
|
if (isChildPattern(currentData.cleanPattern, otherData.cleanPattern)) {
|
|
8420
|
-
|
|
8421
|
-
|
|
8936
|
+
// Store pattern objects instead of pattern strings
|
|
8937
|
+
currentData.parentPatterns.push(patternObjects.get(otherPattern));
|
|
8938
|
+
otherData.childPatterns.push(patternObjects.get(currentPattern));
|
|
8422
8939
|
}
|
|
8423
8940
|
}
|
|
8424
8941
|
|
|
8425
|
-
// Store relationships for easy access
|
|
8942
|
+
// Store relationships for easy access with pattern objects
|
|
8426
8943
|
patternRelationships.set(currentPattern, {
|
|
8427
8944
|
pattern: currentData.parsedPattern,
|
|
8428
8945
|
parsedPattern: currentData.parsedPattern,
|
|
8429
8946
|
connections: currentData.connections,
|
|
8430
|
-
childPatterns: currentData.childPatterns, //
|
|
8431
|
-
parentPatterns: currentData.parentPatterns, //
|
|
8947
|
+
childPatterns: currentData.childPatterns, // Now contains pattern objects
|
|
8948
|
+
parentPatterns: currentData.parentPatterns, // Now contains pattern objects
|
|
8432
8949
|
originalPattern: currentPattern,
|
|
8433
8950
|
});
|
|
8434
8951
|
}
|
|
8435
8952
|
};
|
|
8436
8953
|
|
|
8437
|
-
/**
|
|
8438
|
-
* Get pattern data for a registered pattern
|
|
8439
|
-
*/
|
|
8440
|
-
const getPatternData = (urlPatternRaw) => {
|
|
8441
|
-
return patternRegistry.get(urlPatternRaw);
|
|
8442
|
-
};
|
|
8443
|
-
|
|
8444
8954
|
/**
|
|
8445
8955
|
* Clear all registered patterns
|
|
8446
8956
|
*/
|
|
@@ -8687,8 +9197,7 @@ const getRoutePrivateProperties = (route) => {
|
|
|
8687
9197
|
|
|
8688
9198
|
const registerRoute = (routePattern) => {
|
|
8689
9199
|
const urlPatternRaw = routePattern.originalPattern;
|
|
8690
|
-
const
|
|
8691
|
-
const { cleanPattern, connections } = patternData;
|
|
9200
|
+
const { cleanPattern, connections } = routePattern;
|
|
8692
9201
|
|
|
8693
9202
|
const cleanupCallbackSet = new Set();
|
|
8694
9203
|
const cleanup = () => {
|
|
@@ -8710,6 +9219,7 @@ const registerRoute = (routePattern) => {
|
|
|
8710
9219
|
relativeUrl: null,
|
|
8711
9220
|
url: null,
|
|
8712
9221
|
action: null,
|
|
9222
|
+
specificity: routePattern.specificity, // Expose pattern specificity publicly
|
|
8713
9223
|
cleanup,
|
|
8714
9224
|
toString: () => {
|
|
8715
9225
|
return `route "${cleanPattern}"`;
|
|
@@ -8872,16 +9382,22 @@ const registerRoute = (routePattern) => {
|
|
|
8872
9382
|
}
|
|
8873
9383
|
}
|
|
8874
9384
|
|
|
8875
|
-
// Find the most specific route
|
|
9385
|
+
// Find the most specific route using pre-calculated specificity scores
|
|
8876
9386
|
let mostSpecificRoute = route;
|
|
8877
|
-
|
|
9387
|
+
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
9388
|
+
let maxSpecificity = routePrivateProperties?.routePattern?.specificity || 0;
|
|
8878
9389
|
|
|
8879
9390
|
for (const matchingRoute of allMatchingRoutes) {
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
9391
|
+
if (matchingRoute === route) {
|
|
9392
|
+
continue;
|
|
9393
|
+
}
|
|
9394
|
+
const matchingRoutePrivateProperties =
|
|
9395
|
+
getRoutePrivateProperties(matchingRoute);
|
|
9396
|
+
const specificity =
|
|
9397
|
+
matchingRoutePrivateProperties?.routePattern?.specificity || 0;
|
|
9398
|
+
|
|
9399
|
+
if (specificity > maxSpecificity) {
|
|
9400
|
+
maxSpecificity = specificity;
|
|
8885
9401
|
mostSpecificRoute = matchingRoute;
|
|
8886
9402
|
}
|
|
8887
9403
|
}
|
|
@@ -18541,12 +19057,12 @@ const TabRoute = ({
|
|
|
18541
19057
|
expand: true,
|
|
18542
19058
|
discrete: true,
|
|
18543
19059
|
padding: padding,
|
|
19060
|
+
paddingX: paddingX,
|
|
19061
|
+
paddingY: paddingY,
|
|
18544
19062
|
paddingLeft: paddingLeft,
|
|
18545
19063
|
paddingRight: paddingRight,
|
|
18546
19064
|
paddingTop: paddingTop,
|
|
18547
19065
|
paddingBottom: paddingBottom,
|
|
18548
|
-
paddingX: paddingX,
|
|
18549
|
-
paddingY: paddingY,
|
|
18550
19066
|
alignX: alignX,
|
|
18551
19067
|
alignY: alignY,
|
|
18552
19068
|
children: children
|