@jsenv/navi 0.16.6 → 0.16.8
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 +792 -222
- package/dist/jsenv_navi.js.map +6 -8
- 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
|
}
|
|
@@ -7503,6 +7552,62 @@ const useUITransitionContentId = value => {
|
|
|
7503
7552
|
*/
|
|
7504
7553
|
|
|
7505
7554
|
|
|
7555
|
+
// Raw URL part functionality for bypassing encoding
|
|
7556
|
+
const rawUrlPartSymbol = Symbol("raw_url_part");
|
|
7557
|
+
const rawUrlPart = (value) => {
|
|
7558
|
+
return {
|
|
7559
|
+
[rawUrlPartSymbol]: true,
|
|
7560
|
+
value,
|
|
7561
|
+
};
|
|
7562
|
+
};
|
|
7563
|
+
|
|
7564
|
+
/**
|
|
7565
|
+
* Encode parameter values for URL usage, with special handling for raw URL parts.
|
|
7566
|
+
* When a parameter is wrapped with rawUrlPart(), it bypasses encoding and is
|
|
7567
|
+
* inserted as-is into the URL.
|
|
7568
|
+
*/
|
|
7569
|
+
const encodeParamValue = (value, isWildcard = false) => {
|
|
7570
|
+
if (value && value[rawUrlPartSymbol]) {
|
|
7571
|
+
return value.value;
|
|
7572
|
+
}
|
|
7573
|
+
|
|
7574
|
+
if (isWildcard) {
|
|
7575
|
+
// For wildcards, only encode characters that are invalid in URL paths,
|
|
7576
|
+
// but preserve slashes as they are path separators
|
|
7577
|
+
return value
|
|
7578
|
+
? value.replace(/[^a-zA-Z0-9\-._~!$&'()*+,;=:@/]/g, (char) => {
|
|
7579
|
+
return encodeURIComponent(char);
|
|
7580
|
+
})
|
|
7581
|
+
: value;
|
|
7582
|
+
}
|
|
7583
|
+
|
|
7584
|
+
// For named parameters and search params, encode everything including slashes
|
|
7585
|
+
return encodeURIComponent(value);
|
|
7586
|
+
};
|
|
7587
|
+
|
|
7588
|
+
/**
|
|
7589
|
+
* Build query string from parameters, respecting rawUrlPart values
|
|
7590
|
+
*/
|
|
7591
|
+
const buildQueryString = (params) => {
|
|
7592
|
+
const searchParamPairs = [];
|
|
7593
|
+
|
|
7594
|
+
for (const [key, value] of Object.entries(params)) {
|
|
7595
|
+
if (value !== undefined && value !== null) {
|
|
7596
|
+
const encodedKey = encodeURIComponent(key);
|
|
7597
|
+
|
|
7598
|
+
// Handle boolean values - if true, just add the key without value
|
|
7599
|
+
if (value === true || value === "") {
|
|
7600
|
+
searchParamPairs.push(encodedKey);
|
|
7601
|
+
} else {
|
|
7602
|
+
const encodedValue = encodeParamValue(value, false); // Search params encode slashes
|
|
7603
|
+
searchParamPairs.push(`${encodedKey}=${encodedValue}`);
|
|
7604
|
+
}
|
|
7605
|
+
}
|
|
7606
|
+
}
|
|
7607
|
+
|
|
7608
|
+
return searchParamPairs.join("&");
|
|
7609
|
+
};
|
|
7610
|
+
|
|
7506
7611
|
// Base URL management
|
|
7507
7612
|
let baseFileUrl;
|
|
7508
7613
|
let baseUrl;
|
|
@@ -7619,7 +7724,7 @@ const createRoutePattern = (pattern) => {
|
|
|
7619
7724
|
};
|
|
7620
7725
|
|
|
7621
7726
|
const buildUrl = (params = {}) => {
|
|
7622
|
-
return buildUrlFromPattern(parsedPattern, params);
|
|
7727
|
+
return buildUrlFromPattern(parsedPattern, params, pattern);
|
|
7623
7728
|
};
|
|
7624
7729
|
|
|
7625
7730
|
const resolveParams = (providedParams = {}) => {
|
|
@@ -7642,213 +7747,411 @@ const createRoutePattern = (pattern) => {
|
|
|
7642
7747
|
* Build the most precise URL by using route relationships from pattern registry.
|
|
7643
7748
|
* Each route is responsible for its own URL generation using its own signals.
|
|
7644
7749
|
*/
|
|
7645
|
-
const buildMostPreciseUrl = (params = {}) => {
|
|
7646
|
-
// Handle parameter resolution internally to preserve user intent detection
|
|
7647
|
-
const resolvedParams = resolveParams(params);
|
|
7648
7750
|
|
|
7649
|
-
|
|
7650
|
-
|
|
7751
|
+
/**
|
|
7752
|
+
* Helper: Filter out default values from parameters for cleaner URLs
|
|
7753
|
+
*/
|
|
7754
|
+
const removeDefaultValues = (params) => {
|
|
7755
|
+
const filtered = { ...params };
|
|
7651
7756
|
|
|
7652
7757
|
for (const connection of connections) {
|
|
7653
7758
|
const { paramName, signal, options } = connection;
|
|
7654
7759
|
const defaultValue = options.defaultValue;
|
|
7655
7760
|
|
|
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
|
|
7761
|
+
if (paramName in filtered && filtered[paramName] === defaultValue) {
|
|
7762
|
+
delete filtered[paramName];
|
|
7763
|
+
} else if (
|
|
7764
|
+
!(paramName in filtered) &&
|
|
7765
|
+
signal?.value !== undefined &&
|
|
7766
|
+
signal.value !== defaultValue
|
|
7767
|
+
) {
|
|
7768
|
+
filtered[paramName] = signal.value;
|
|
7669
7769
|
}
|
|
7670
7770
|
}
|
|
7671
7771
|
|
|
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
|
|
7772
|
+
return filtered;
|
|
7773
|
+
};
|
|
7676
7774
|
|
|
7677
|
-
|
|
7678
|
-
|
|
7679
|
-
|
|
7680
|
-
|
|
7775
|
+
/**
|
|
7776
|
+
* Helper: Find the best child route that matches current parameters and signals
|
|
7777
|
+
*/
|
|
7778
|
+
const findBestChildRoute = (params, relationships) => {
|
|
7779
|
+
const childPatternObjs = relationships?.childPatterns || [];
|
|
7780
|
+
if (pattern === "/" || !childPatternObjs.length) {
|
|
7781
|
+
return null;
|
|
7782
|
+
}
|
|
7681
7783
|
|
|
7682
|
-
//
|
|
7683
|
-
const
|
|
7684
|
-
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
|
|
7688
|
-
if (signal.value !== defaultValue) {
|
|
7689
|
-
signalDerivedParams[paramName] = signal.value;
|
|
7690
|
-
}
|
|
7784
|
+
// Try each child pattern object to find the most specific match
|
|
7785
|
+
for (const childPatternObj of childPatternObjs) {
|
|
7786
|
+
const childRouteCandidate = evaluateChildRoute(childPatternObj, params);
|
|
7787
|
+
|
|
7788
|
+
if (childRouteCandidate) {
|
|
7789
|
+
return childRouteCandidate;
|
|
7691
7790
|
}
|
|
7692
7791
|
}
|
|
7792
|
+
return null;
|
|
7793
|
+
};
|
|
7693
7794
|
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
|
|
7697
|
-
|
|
7698
|
-
|
|
7795
|
+
/**
|
|
7796
|
+
* Helper: Evaluate if a specific child route is suitable for current params/signals
|
|
7797
|
+
*/
|
|
7798
|
+
const evaluateChildRoute = (childPatternObj, params) => {
|
|
7799
|
+
// Step 1: Check parameter compatibility
|
|
7800
|
+
const compatibility = checkChildRouteCompatibility(childPatternObj, params);
|
|
7801
|
+
if (!compatibility.isCompatible) {
|
|
7802
|
+
return null;
|
|
7803
|
+
}
|
|
7804
|
+
|
|
7805
|
+
// Step 2: Determine if child route should be used
|
|
7806
|
+
const shouldUseChild = shouldUseChildRoute(
|
|
7807
|
+
childPatternObj,
|
|
7808
|
+
params,
|
|
7809
|
+
compatibility,
|
|
7810
|
+
);
|
|
7811
|
+
if (!shouldUseChild) {
|
|
7812
|
+
return null;
|
|
7813
|
+
}
|
|
7814
|
+
|
|
7815
|
+
// Step 3: Build child route URL with proper parameter filtering
|
|
7816
|
+
return buildChildRouteUrl(childPatternObj, params);
|
|
7817
|
+
};
|
|
7818
|
+
|
|
7819
|
+
/**
|
|
7820
|
+
* Helper: Check if parameters are compatible with child route
|
|
7821
|
+
*/
|
|
7822
|
+
const checkChildRouteCompatibility = (childPatternObj, params) => {
|
|
7823
|
+
const childParams = {};
|
|
7824
|
+
let isCompatible = true;
|
|
7825
|
+
|
|
7826
|
+
// Check both parent signals AND user-provided params for child route matching
|
|
7827
|
+
const paramsToCheck = [
|
|
7828
|
+
...connections,
|
|
7829
|
+
...Object.entries(params).map(([key, value]) => ({
|
|
7830
|
+
paramName: key,
|
|
7831
|
+
userValue: value,
|
|
7832
|
+
isUserProvided: true,
|
|
7833
|
+
})),
|
|
7834
|
+
];
|
|
7835
|
+
|
|
7836
|
+
for (const item of paramsToCheck) {
|
|
7837
|
+
const result = processParameterForChildRoute(
|
|
7838
|
+
item,
|
|
7839
|
+
childPatternObj.pattern,
|
|
7840
|
+
);
|
|
7841
|
+
|
|
7842
|
+
if (!result.isCompatible) {
|
|
7843
|
+
isCompatible = false;
|
|
7699
7844
|
break;
|
|
7700
7845
|
}
|
|
7846
|
+
|
|
7847
|
+
if (result.shouldInclude) {
|
|
7848
|
+
childParams[result.paramName] = result.paramValue;
|
|
7849
|
+
}
|
|
7701
7850
|
}
|
|
7702
7851
|
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7852
|
+
return { isCompatible, childParams };
|
|
7853
|
+
};
|
|
7854
|
+
|
|
7855
|
+
/**
|
|
7856
|
+
* Helper: Process a single parameter for child route compatibility
|
|
7857
|
+
*/
|
|
7858
|
+
const processParameterForChildRoute = (item, childParsedPattern) => {
|
|
7859
|
+
let paramName;
|
|
7860
|
+
let paramValue;
|
|
7861
|
+
|
|
7862
|
+
if (item.isUserProvided) {
|
|
7863
|
+
paramName = item.paramName;
|
|
7864
|
+
paramValue = item.userValue;
|
|
7865
|
+
} else {
|
|
7866
|
+
const { paramName: name, signal, options } = item;
|
|
7867
|
+
paramName = name;
|
|
7868
|
+
// Only include non-default parent signal values
|
|
7869
|
+
if (
|
|
7870
|
+
signal?.value === undefined ||
|
|
7871
|
+
signal.value === options.defaultValue
|
|
7872
|
+
) {
|
|
7873
|
+
return { isCompatible: true, shouldInclude: false };
|
|
7710
7874
|
}
|
|
7875
|
+
paramValue = signal.value;
|
|
7711
7876
|
}
|
|
7712
7877
|
|
|
7713
|
-
//
|
|
7714
|
-
|
|
7715
|
-
|
|
7878
|
+
// Check if parameter value matches a literal segment in child pattern
|
|
7879
|
+
const matchesChildLiteral = paramMatchesChildLiteral(
|
|
7880
|
+
paramValue,
|
|
7881
|
+
childParsedPattern,
|
|
7882
|
+
);
|
|
7716
7883
|
|
|
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;
|
|
7884
|
+
if (matchesChildLiteral) {
|
|
7885
|
+
// Compatible - parameter value matches child literal
|
|
7886
|
+
return {
|
|
7887
|
+
isCompatible: true,
|
|
7888
|
+
shouldInclude: !item.isUserProvided,
|
|
7889
|
+
paramName,
|
|
7890
|
+
paramValue,
|
|
7891
|
+
};
|
|
7892
|
+
}
|
|
7741
7893
|
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
|
|
7894
|
+
// Check for incompatible cases
|
|
7895
|
+
if (item.isUserProvided && !matchesChildLiteral) {
|
|
7896
|
+
// Check if this is a path parameter from parent pattern
|
|
7897
|
+
const isParentPathParam = connections.some(
|
|
7898
|
+
(conn) => conn.paramName === paramName,
|
|
7899
|
+
);
|
|
7747
7900
|
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
|
|
7752
|
-
|
|
7753
|
-
}
|
|
7901
|
+
if (isParentPathParam) {
|
|
7902
|
+
// User provided a path param value that doesn't match this child's literals
|
|
7903
|
+
return { isCompatible: false };
|
|
7904
|
+
}
|
|
7905
|
+
}
|
|
7754
7906
|
|
|
7755
|
-
|
|
7756
|
-
|
|
7757
|
-
|
|
7758
|
-
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
7762
|
-
|
|
7763
|
-
|
|
7764
|
-
break;
|
|
7765
|
-
}
|
|
7766
|
-
}
|
|
7907
|
+
// Special case: section parameter with settings literal
|
|
7908
|
+
if (paramName === "section" && paramValue !== "settings") {
|
|
7909
|
+
const hasSettingsLiteral = childParsedPattern.segments.some(
|
|
7910
|
+
(segment) => segment.type === "literal" && segment.value === "settings",
|
|
7911
|
+
);
|
|
7912
|
+
if (hasSettingsLiteral) {
|
|
7913
|
+
return { isCompatible: false };
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7767
7916
|
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7917
|
+
// Compatible but should only include if from signal (not user-provided)
|
|
7918
|
+
return {
|
|
7919
|
+
isCompatible: true,
|
|
7920
|
+
shouldInclude: !item.isUserProvided && !matchesChildLiteral,
|
|
7921
|
+
paramName,
|
|
7922
|
+
paramValue,
|
|
7923
|
+
};
|
|
7924
|
+
};
|
|
7925
|
+
|
|
7926
|
+
/**
|
|
7927
|
+
* Helper: Determine if child route should be used based on active parameters
|
|
7928
|
+
*/
|
|
7929
|
+
const shouldUseChildRoute = (childPatternObj, params, compatibility) => {
|
|
7930
|
+
// Check if child has active non-default signal values
|
|
7931
|
+
let hasActiveParams = false;
|
|
7932
|
+
const childParams = { ...compatibility.childParams };
|
|
7933
|
+
|
|
7934
|
+
for (const connection of childPatternObj.connections) {
|
|
7935
|
+
const { paramName, signal, options } = connection;
|
|
7936
|
+
const defaultValue = options.defaultValue;
|
|
7937
|
+
|
|
7938
|
+
if (signal?.value !== undefined) {
|
|
7939
|
+
childParams[paramName] = signal.value;
|
|
7940
|
+
if (signal.value !== defaultValue) {
|
|
7941
|
+
hasActiveParams = true;
|
|
7771
7942
|
}
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7772
7945
|
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7946
|
+
// Check if child pattern can be fully satisfied
|
|
7947
|
+
const initialMergedParams = { ...childParams, ...params };
|
|
7948
|
+
const canBuildChildCompletely = childPatternObj.pattern.segments.every(
|
|
7949
|
+
(segment) => {
|
|
7950
|
+
if (segment.type === "literal") return true;
|
|
7951
|
+
if (segment.type === "param") {
|
|
7952
|
+
return (
|
|
7953
|
+
segment.optional || initialMergedParams[segment.name] !== undefined
|
|
7954
|
+
);
|
|
7776
7955
|
}
|
|
7956
|
+
return true;
|
|
7957
|
+
},
|
|
7958
|
+
);
|
|
7777
7959
|
|
|
7778
|
-
|
|
7779
|
-
for (const connection of childPatternData.connections) {
|
|
7780
|
-
const { paramName, signal, options } = connection;
|
|
7781
|
-
const defaultValue = options.defaultValue;
|
|
7960
|
+
const hasProvidedParams = Object.keys(params).length > 0;
|
|
7782
7961
|
|
|
7783
|
-
|
|
7784
|
-
|
|
7785
|
-
|
|
7786
|
-
|
|
7787
|
-
|
|
7788
|
-
|
|
7962
|
+
// Use child route if:
|
|
7963
|
+
// 1. Child has active non-default parameters, OR
|
|
7964
|
+
// 2. User provided params AND child can be built completely
|
|
7965
|
+
return hasActiveParams || (hasProvidedParams && canBuildChildCompletely);
|
|
7966
|
+
};
|
|
7967
|
+
|
|
7968
|
+
/**
|
|
7969
|
+
* Helper: Build URL for selected child route with proper parameter filtering
|
|
7970
|
+
*/
|
|
7971
|
+
const buildChildRouteUrl = (childPatternObj, params) => {
|
|
7972
|
+
// Start with child signal values
|
|
7973
|
+
const baseParams = {};
|
|
7974
|
+
for (const connection of childPatternObj.connections) {
|
|
7975
|
+
const { paramName, signal, options } = connection;
|
|
7976
|
+
if (
|
|
7977
|
+
signal?.value !== undefined &&
|
|
7978
|
+
signal.value !== options.defaultValue
|
|
7979
|
+
) {
|
|
7980
|
+
baseParams[paramName] = signal.value;
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
|
|
7984
|
+
// Apply user params with filtering logic
|
|
7985
|
+
for (const [paramName, userValue] of Object.entries(params)) {
|
|
7986
|
+
const childConnection = childPatternObj.connections.find(
|
|
7987
|
+
(conn) => conn.paramName === paramName,
|
|
7988
|
+
);
|
|
7989
|
+
|
|
7990
|
+
if (childConnection) {
|
|
7991
|
+
const { options } = childConnection;
|
|
7992
|
+
const defaultValue = options.defaultValue;
|
|
7993
|
+
|
|
7994
|
+
// Only include if it's NOT the signal's default value
|
|
7995
|
+
if (userValue !== defaultValue) {
|
|
7996
|
+
baseParams[paramName] = userValue;
|
|
7997
|
+
} else {
|
|
7998
|
+
// User provided the default value - complete omission
|
|
7999
|
+
delete baseParams[paramName];
|
|
8000
|
+
}
|
|
8001
|
+
} else {
|
|
8002
|
+
// Check if param corresponds to a literal segment in child pattern
|
|
8003
|
+
const isConsumedByChildPath = childPatternObj.pattern.segments.some(
|
|
8004
|
+
(segment) =>
|
|
8005
|
+
segment.type === "literal" && segment.value === userValue,
|
|
8006
|
+
);
|
|
8007
|
+
|
|
8008
|
+
if (!isConsumedByChildPath) {
|
|
8009
|
+
// Not consumed by child path, keep it as query param
|
|
8010
|
+
baseParams[paramName] = userValue;
|
|
7789
8011
|
}
|
|
8012
|
+
}
|
|
8013
|
+
}
|
|
8014
|
+
|
|
8015
|
+
// Build child URL
|
|
8016
|
+
const childUrl = childPatternObj.buildUrl(baseParams);
|
|
8017
|
+
|
|
8018
|
+
if (childUrl && !childUrl.includes(":")) {
|
|
8019
|
+
// Check for parent optimization before returning
|
|
8020
|
+
const optimizedUrl = checkChildParentOptimization(
|
|
8021
|
+
childPatternObj.originalPattern,
|
|
8022
|
+
childUrl,
|
|
8023
|
+
baseParams,
|
|
8024
|
+
);
|
|
8025
|
+
return optimizedUrl || childUrl;
|
|
8026
|
+
}
|
|
8027
|
+
|
|
8028
|
+
return null;
|
|
8029
|
+
};
|
|
8030
|
+
|
|
8031
|
+
/**
|
|
8032
|
+
* Helper: Check if parent route optimization applies to child route
|
|
8033
|
+
*/
|
|
8034
|
+
const checkChildParentOptimization = (childPattern, childUrl, baseParams) => {
|
|
8035
|
+
if (Object.keys(baseParams).length > 0) {
|
|
8036
|
+
return null; // No optimization if parameters exist
|
|
8037
|
+
}
|
|
8038
|
+
|
|
8039
|
+
const childRelationships = patternRelationships.get(childPattern);
|
|
8040
|
+
const childParentObjs = childRelationships?.parentPatterns || [];
|
|
8041
|
+
|
|
8042
|
+
for (const childParentObj of childParentObjs) {
|
|
8043
|
+
if (childParentObj.originalPattern === pattern) {
|
|
8044
|
+
// Get the child pattern object from relationships instead of recreating
|
|
8045
|
+
const childPatternObj = childRelationships;
|
|
7790
8046
|
|
|
7791
|
-
|
|
7792
|
-
|
|
7793
|
-
|
|
7794
|
-
|
|
7795
|
-
|
|
7796
|
-
|
|
7797
|
-
|
|
8047
|
+
const allChildParamsAreDefaults = (
|
|
8048
|
+
childPatternObj.connections || []
|
|
8049
|
+
).every((childConnection) => {
|
|
8050
|
+
const { signal, options } = childConnection;
|
|
8051
|
+
return signal?.value === options.defaultValue;
|
|
8052
|
+
});
|
|
8053
|
+
|
|
8054
|
+
if (allChildParamsAreDefaults) {
|
|
8055
|
+
// Build current route URL for comparison
|
|
8056
|
+
const resolvedParams = resolveParams({});
|
|
8057
|
+
const finalParams = removeDefaultValues(resolvedParams);
|
|
8058
|
+
const currentUrl = buildUrlFromPattern(
|
|
8059
|
+
parsedPattern,
|
|
8060
|
+
finalParams,
|
|
8061
|
+
pattern,
|
|
8062
|
+
);
|
|
8063
|
+
if (currentUrl.length < childUrl.length) {
|
|
8064
|
+
return currentUrl;
|
|
7798
8065
|
}
|
|
7799
8066
|
}
|
|
7800
8067
|
}
|
|
7801
8068
|
}
|
|
8069
|
+
return null;
|
|
8070
|
+
};
|
|
8071
|
+
|
|
8072
|
+
const buildMostPreciseUrl = (params = {}) => {
|
|
8073
|
+
// Step 1: Resolve and clean parameters
|
|
8074
|
+
const resolvedParams = resolveParams(params);
|
|
8075
|
+
|
|
8076
|
+
// Step 2: Check for parent route optimization BEFORE removing defaults
|
|
8077
|
+
// This allows optimization when final effective values match defaults
|
|
8078
|
+
const relationships = patternRelationships.get(pattern);
|
|
8079
|
+
const optimizedUrl = checkParentRouteOptimization(
|
|
8080
|
+
resolvedParams,
|
|
8081
|
+
relationships,
|
|
8082
|
+
);
|
|
8083
|
+
if (optimizedUrl) {
|
|
8084
|
+
return optimizedUrl;
|
|
8085
|
+
}
|
|
8086
|
+
|
|
8087
|
+
// Step 3: Remove default values for normal URL building
|
|
8088
|
+
let finalParams = removeDefaultValues(resolvedParams);
|
|
8089
|
+
|
|
8090
|
+
// Step 4: Try to find a more specific child route
|
|
8091
|
+
const childRouteUrl = findBestChildRoute(params, relationships);
|
|
8092
|
+
if (childRouteUrl) {
|
|
8093
|
+
return childRouteUrl;
|
|
8094
|
+
}
|
|
7802
8095
|
|
|
7803
|
-
//
|
|
7804
|
-
|
|
7805
|
-
|
|
7806
|
-
|
|
7807
|
-
|
|
7808
|
-
|
|
8096
|
+
// Step 5: Inherit parameters from parent routes
|
|
8097
|
+
inheritParentParameters(finalParams, relationships);
|
|
8098
|
+
|
|
8099
|
+
// Step 6: Build the current route URL
|
|
8100
|
+
const generatedUrl = buildCurrentRouteUrl(finalParams);
|
|
8101
|
+
|
|
8102
|
+
return generatedUrl;
|
|
8103
|
+
};
|
|
8104
|
+
|
|
8105
|
+
/**
|
|
8106
|
+
* Helper: Inherit query parameters from parent patterns
|
|
8107
|
+
*/
|
|
8108
|
+
const inheritParentParameters = (finalParams, relationships) => {
|
|
8109
|
+
const parentPatternObjs = relationships?.parentPatterns || [];
|
|
7809
8110
|
|
|
8111
|
+
for (const parentPatternObj of parentPatternObjs) {
|
|
7810
8112
|
// Check parent's signal connections for non-default values to inherit
|
|
7811
|
-
for (const parentConnection of
|
|
8113
|
+
for (const parentConnection of parentPatternObj.connections) {
|
|
7812
8114
|
const { paramName, signal, options } = parentConnection;
|
|
7813
8115
|
const defaultValue = options.defaultValue;
|
|
7814
8116
|
|
|
7815
|
-
//
|
|
8117
|
+
// Only inherit if we don't have this param and parent has non-default value
|
|
7816
8118
|
if (
|
|
7817
8119
|
!(paramName in finalParams) &&
|
|
7818
8120
|
signal?.value !== undefined &&
|
|
7819
8121
|
signal.value !== defaultValue
|
|
7820
8122
|
) {
|
|
7821
|
-
//
|
|
7822
|
-
// E.g., don't inherit "section=analytics" if our path is "/admin/analytics"
|
|
8123
|
+
// Don't inherit if parameter corresponds to a literal in our path
|
|
7823
8124
|
const shouldInherit = !isParameterRedundantWithLiteralSegments(
|
|
7824
8125
|
parsedPattern,
|
|
7825
|
-
|
|
8126
|
+
parentPatternObj.pattern,
|
|
7826
8127
|
paramName,
|
|
7827
8128
|
signal.value,
|
|
7828
8129
|
);
|
|
7829
8130
|
|
|
7830
8131
|
if (shouldInherit) {
|
|
7831
|
-
// Inherit the parent's signal value
|
|
7832
8132
|
finalParams[paramName] = signal.value;
|
|
7833
8133
|
}
|
|
7834
8134
|
}
|
|
7835
8135
|
}
|
|
7836
8136
|
}
|
|
8137
|
+
};
|
|
7837
8138
|
|
|
8139
|
+
/**
|
|
8140
|
+
* Helper: Build URL for current route with filtered pattern
|
|
8141
|
+
*/
|
|
8142
|
+
const buildCurrentRouteUrl = (finalParams) => {
|
|
7838
8143
|
if (!parsedPattern.segments) {
|
|
7839
8144
|
return "/";
|
|
7840
8145
|
}
|
|
7841
8146
|
|
|
7842
|
-
// Filter out segments
|
|
8147
|
+
// Filter out parameter segments that don't have values
|
|
7843
8148
|
const filteredPattern = {
|
|
7844
8149
|
...parsedPattern,
|
|
7845
8150
|
segments: parsedPattern.segments.filter((segment) => {
|
|
7846
8151
|
if (segment.type === "param") {
|
|
7847
|
-
// Only keep parameter segments if we have a value for them
|
|
7848
8152
|
return segment.name in finalParams;
|
|
7849
8153
|
}
|
|
7850
|
-
//
|
|
7851
|
-
return true;
|
|
8154
|
+
return true; // Keep literal segments
|
|
7852
8155
|
}),
|
|
7853
8156
|
};
|
|
7854
8157
|
|
|
@@ -7860,7 +8163,178 @@ const createRoutePattern = (pattern) => {
|
|
|
7860
8163
|
filteredPattern.trailingSlash = false;
|
|
7861
8164
|
}
|
|
7862
8165
|
|
|
7863
|
-
return buildUrlFromPattern(filteredPattern, finalParams);
|
|
8166
|
+
return buildUrlFromPattern(filteredPattern, finalParams, pattern);
|
|
8167
|
+
};
|
|
8168
|
+
|
|
8169
|
+
/**
|
|
8170
|
+
* Helper: Check if parent route can provide a shorter equivalent URL
|
|
8171
|
+
*/
|
|
8172
|
+
const checkParentRouteOptimization = (resolvedParams, relationships) => {
|
|
8173
|
+
// Only consider parent optimization for patterns with signal connections
|
|
8174
|
+
if (connections.length === 0) {
|
|
8175
|
+
return null;
|
|
8176
|
+
}
|
|
8177
|
+
|
|
8178
|
+
// Check if all final effective values equal their defaults
|
|
8179
|
+
const allEffectiveValuesAreDefaults = connections.every((conn) => {
|
|
8180
|
+
// Final effective value is what's in resolvedParams (signals + provided params)
|
|
8181
|
+
const effectiveValue =
|
|
8182
|
+
resolvedParams[conn.paramName] ?? conn.options.defaultValue;
|
|
8183
|
+
return effectiveValue === conn.options.defaultValue;
|
|
8184
|
+
});
|
|
8185
|
+
|
|
8186
|
+
// Only optimize if all effective values equal their defaults
|
|
8187
|
+
if (!allEffectiveValuesAreDefaults) {
|
|
8188
|
+
return null;
|
|
8189
|
+
}
|
|
8190
|
+
|
|
8191
|
+
// Check if there are extra parameters not handled by current route's connections
|
|
8192
|
+
const connectionParamNames = new Set(
|
|
8193
|
+
connections.map((conn) => conn.paramName),
|
|
8194
|
+
);
|
|
8195
|
+
const hasExtraParams = Object.keys(resolvedParams).some(
|
|
8196
|
+
(paramName) => !connectionParamNames.has(paramName),
|
|
8197
|
+
);
|
|
8198
|
+
|
|
8199
|
+
// Don't optimize if there are extra parameters that would be lost
|
|
8200
|
+
if (hasExtraParams) {
|
|
8201
|
+
return null;
|
|
8202
|
+
}
|
|
8203
|
+
|
|
8204
|
+
const possibleParentObjs = relationships?.parentPatterns || [];
|
|
8205
|
+
|
|
8206
|
+
for (const parentPatternObj of possibleParentObjs) {
|
|
8207
|
+
// Skip root route and routes without parameters
|
|
8208
|
+
if (
|
|
8209
|
+
parentPatternObj.originalPattern === "/" ||
|
|
8210
|
+
!parentPatternObj.originalPattern.includes(":")
|
|
8211
|
+
) {
|
|
8212
|
+
continue;
|
|
8213
|
+
}
|
|
8214
|
+
|
|
8215
|
+
const optimizedParentUrl = evaluateParentOptimization(
|
|
8216
|
+
parentPatternObj,
|
|
8217
|
+
resolvedParams,
|
|
8218
|
+
);
|
|
8219
|
+
|
|
8220
|
+
if (optimizedParentUrl) {
|
|
8221
|
+
// Before returning optimized parent URL, check if we need to inherit parameters
|
|
8222
|
+
// from our ancestors that the parent route might not inherit on its own
|
|
8223
|
+
const parentFinalParams = { ...resolvedParams };
|
|
8224
|
+
|
|
8225
|
+
// Remove params that belong to current route (they're at defaults anyway)
|
|
8226
|
+
for (const conn of connections) {
|
|
8227
|
+
delete parentFinalParams[conn.paramName];
|
|
8228
|
+
}
|
|
8229
|
+
|
|
8230
|
+
// Inherit from all ancestor routes, not just immediate parent
|
|
8231
|
+
inheritParentParameters(parentFinalParams, relationships);
|
|
8232
|
+
|
|
8233
|
+
// If we inherited any parameters, add them to the parent URL
|
|
8234
|
+
const extraParamEntries = Object.entries(parentFinalParams).filter(
|
|
8235
|
+
([key, value]) => {
|
|
8236
|
+
// Only include params not handled by parent route
|
|
8237
|
+
const isParentParam = parentPatternObj.connections.some(
|
|
8238
|
+
(conn) => conn.paramName === key,
|
|
8239
|
+
);
|
|
8240
|
+
return !isParentParam && value !== undefined;
|
|
8241
|
+
},
|
|
8242
|
+
);
|
|
8243
|
+
|
|
8244
|
+
if (extraParamEntries.length > 0) {
|
|
8245
|
+
const queryString = buildQueryString(
|
|
8246
|
+
Object.fromEntries(extraParamEntries),
|
|
8247
|
+
);
|
|
8248
|
+
return (
|
|
8249
|
+
optimizedParentUrl +
|
|
8250
|
+
(optimizedParentUrl.includes("?") ? "&" : "?") +
|
|
8251
|
+
queryString
|
|
8252
|
+
);
|
|
8253
|
+
}
|
|
8254
|
+
|
|
8255
|
+
return optimizedParentUrl;
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
|
|
8259
|
+
return null;
|
|
8260
|
+
};
|
|
8261
|
+
|
|
8262
|
+
/**
|
|
8263
|
+
* Helper: Evaluate a specific parent pattern for URL optimization
|
|
8264
|
+
*/
|
|
8265
|
+
const evaluateParentOptimization = (parentPatternObj, resolvedParams) => {
|
|
8266
|
+
// Get literal segments from child pattern to map to parent parameters
|
|
8267
|
+
const childLiterals = getPatternLiterals(parsedPattern);
|
|
8268
|
+
|
|
8269
|
+
// Check if parent would also have all default values
|
|
8270
|
+
// For parent optimization, we consider both explicitly provided params and literal segments
|
|
8271
|
+
const allParentParamsAreDefaults = parentPatternObj.connections.every(
|
|
8272
|
+
(parentConnection) => {
|
|
8273
|
+
const paramName = parentConnection.paramName;
|
|
8274
|
+
|
|
8275
|
+
// If explicitly provided in resolved params, use that
|
|
8276
|
+
if (resolvedParams[paramName] !== undefined) {
|
|
8277
|
+
return (
|
|
8278
|
+
resolvedParams[paramName] === parentConnection.options.defaultValue
|
|
8279
|
+
);
|
|
8280
|
+
}
|
|
8281
|
+
|
|
8282
|
+
// Check if this parent parameter corresponds to a literal segment in child
|
|
8283
|
+
// If parent default matches a child literal, consider it as using the default
|
|
8284
|
+
const defaultValue = parentConnection.options.defaultValue;
|
|
8285
|
+
if (childLiterals.includes(defaultValue)) {
|
|
8286
|
+
return true; // Literal segment effectively provides the default value
|
|
8287
|
+
}
|
|
8288
|
+
|
|
8289
|
+
// Otherwise assume parent would use its default for optimization purposes
|
|
8290
|
+
return true;
|
|
8291
|
+
},
|
|
8292
|
+
);
|
|
8293
|
+
|
|
8294
|
+
if (!allParentParamsAreDefaults) {
|
|
8295
|
+
return null; // Can't optimize if parent has non-default values
|
|
8296
|
+
}
|
|
8297
|
+
|
|
8298
|
+
// Check if parent's default values match our literals
|
|
8299
|
+
const parentPointsToCurrentRoute = parentPatternObj.connections.every(
|
|
8300
|
+
(parentConnection) => {
|
|
8301
|
+
const { options } = parentConnection;
|
|
8302
|
+
const defaultValue = options.defaultValue;
|
|
8303
|
+
return childLiterals.includes(defaultValue);
|
|
8304
|
+
},
|
|
8305
|
+
);
|
|
8306
|
+
|
|
8307
|
+
if (parentPointsToCurrentRoute) {
|
|
8308
|
+
// Build parent URL using defaults, not current signal values
|
|
8309
|
+
const parentDefaultParams = {};
|
|
8310
|
+
for (const parentConnection of parentPatternObj.connections) {
|
|
8311
|
+
parentDefaultParams[parentConnection.paramName] =
|
|
8312
|
+
parentConnection.options.defaultValue;
|
|
8313
|
+
}
|
|
8314
|
+
// Build parent URL and check if it can be optimized further
|
|
8315
|
+
let parentUrl = parentPatternObj.buildUrl(parentDefaultParams);
|
|
8316
|
+
|
|
8317
|
+
// Check if parent can optimize itself by removing default parameters
|
|
8318
|
+
if (parentUrl && parentUrl !== "/") {
|
|
8319
|
+
// Check if all parent's default params are actually defaults
|
|
8320
|
+
const parentAllDefaults = parentPatternObj.connections.every((conn) => {
|
|
8321
|
+
const paramValue = parentDefaultParams[conn.paramName];
|
|
8322
|
+
return paramValue === conn.options.defaultValue;
|
|
8323
|
+
});
|
|
8324
|
+
|
|
8325
|
+
if (parentAllDefaults) {
|
|
8326
|
+
// Try to build parent URL without any parameters to see if it's shorter
|
|
8327
|
+
const parentMinimalUrl = parentPatternObj.buildUrl({});
|
|
8328
|
+
if (parentMinimalUrl && parentMinimalUrl.length < parentUrl.length) {
|
|
8329
|
+
parentUrl = parentMinimalUrl;
|
|
8330
|
+
}
|
|
8331
|
+
}
|
|
8332
|
+
|
|
8333
|
+
return parentUrl;
|
|
8334
|
+
}
|
|
8335
|
+
}
|
|
8336
|
+
|
|
8337
|
+
return null;
|
|
7864
8338
|
};
|
|
7865
8339
|
|
|
7866
8340
|
return {
|
|
@@ -7868,6 +8342,7 @@ const createRoutePattern = (pattern) => {
|
|
|
7868
8342
|
pattern: parsedPattern,
|
|
7869
8343
|
cleanPattern, // Return the clean pattern string
|
|
7870
8344
|
connections, // Return signal connections along with pattern
|
|
8345
|
+
specificity: calculatePatternSpecificity(parsedPattern), // Pre-calculate specificity
|
|
7871
8346
|
applyOn,
|
|
7872
8347
|
buildUrl,
|
|
7873
8348
|
buildMostPreciseUrl,
|
|
@@ -7875,6 +8350,50 @@ const createRoutePattern = (pattern) => {
|
|
|
7875
8350
|
};
|
|
7876
8351
|
};
|
|
7877
8352
|
|
|
8353
|
+
/**
|
|
8354
|
+
* Helper: Extract literal values from pattern segments
|
|
8355
|
+
*/
|
|
8356
|
+
const getPatternLiterals = (pattern) => {
|
|
8357
|
+
return pattern.segments
|
|
8358
|
+
.filter((seg) => seg.type === "literal")
|
|
8359
|
+
.map((seg) => seg.value);
|
|
8360
|
+
};
|
|
8361
|
+
|
|
8362
|
+
/**
|
|
8363
|
+
* Helper: Check if parameter matches any literal in child pattern
|
|
8364
|
+
*/
|
|
8365
|
+
const paramMatchesChildLiteral = (paramValue, childParsedPattern) => {
|
|
8366
|
+
return childParsedPattern.segments.some(
|
|
8367
|
+
(segment) => segment.type === "literal" && segment.value === paramValue,
|
|
8368
|
+
);
|
|
8369
|
+
};
|
|
8370
|
+
|
|
8371
|
+
/**
|
|
8372
|
+
* Calculate pattern specificity score for route matching
|
|
8373
|
+
* Higher score = more specific route
|
|
8374
|
+
*/
|
|
8375
|
+
const calculatePatternSpecificity = (parsedPattern) => {
|
|
8376
|
+
let specificity = 0;
|
|
8377
|
+
|
|
8378
|
+
// Count path segments (ignoring query params for specificity)
|
|
8379
|
+
const pathSegments = parsedPattern.segments || [];
|
|
8380
|
+
|
|
8381
|
+
for (const segment of pathSegments) {
|
|
8382
|
+
if (segment.type === "literal") {
|
|
8383
|
+
// Literal segments are more specific than parameters
|
|
8384
|
+
specificity += 100; // High score for literal segments
|
|
8385
|
+
} else if (segment.type === "param") {
|
|
8386
|
+
// Parameter segments are less specific
|
|
8387
|
+
specificity += 10; // Lower score for parameters
|
|
8388
|
+
}
|
|
8389
|
+
}
|
|
8390
|
+
|
|
8391
|
+
// Add base score for number of path segments (more segments = more specific)
|
|
8392
|
+
specificity += pathSegments.length;
|
|
8393
|
+
|
|
8394
|
+
return specificity;
|
|
8395
|
+
};
|
|
8396
|
+
|
|
7878
8397
|
/**
|
|
7879
8398
|
* Parse a route pattern string into structured segments
|
|
7880
8399
|
*/
|
|
@@ -8163,19 +8682,99 @@ const extractSearchParams = (urlObj, connections = []) => {
|
|
|
8163
8682
|
return params;
|
|
8164
8683
|
};
|
|
8165
8684
|
|
|
8685
|
+
/**
|
|
8686
|
+
* Build query parameters respecting hierarchical order from ancestor patterns
|
|
8687
|
+
*/
|
|
8688
|
+
const buildHierarchicalQueryParams = (
|
|
8689
|
+
parsedPattern,
|
|
8690
|
+
params,
|
|
8691
|
+
originalPattern,
|
|
8692
|
+
) => {
|
|
8693
|
+
const queryParams = {};
|
|
8694
|
+
const processedParams = new Set();
|
|
8695
|
+
|
|
8696
|
+
// Get relationships for this pattern
|
|
8697
|
+
const relationships = patternRelationships.get(originalPattern);
|
|
8698
|
+
const parentPatterns = relationships?.parentPatterns || [];
|
|
8699
|
+
|
|
8700
|
+
// Step 1: Add query parameters from ancestor patterns (oldest to newest)
|
|
8701
|
+
// This ensures ancestor parameters come first in their declaration order
|
|
8702
|
+
const ancestorPatterns = parentPatterns; // Process in order: root ancestor first, then immediate parent
|
|
8703
|
+
|
|
8704
|
+
for (const ancestorPatternObj of ancestorPatterns) {
|
|
8705
|
+
if (ancestorPatternObj.pattern?.queryParams) {
|
|
8706
|
+
|
|
8707
|
+
for (const queryParam of ancestorPatternObj.pattern.queryParams) {
|
|
8708
|
+
const paramName = queryParam.name;
|
|
8709
|
+
if (
|
|
8710
|
+
params[paramName] !== undefined &&
|
|
8711
|
+
!processedParams.has(paramName)
|
|
8712
|
+
) {
|
|
8713
|
+
queryParams[paramName] = params[paramName];
|
|
8714
|
+
processedParams.add(paramName);
|
|
8715
|
+
}
|
|
8716
|
+
}
|
|
8717
|
+
}
|
|
8718
|
+
}
|
|
8719
|
+
|
|
8720
|
+
// Step 2: Add query parameters from current pattern
|
|
8721
|
+
if (parsedPattern.queryParams) {
|
|
8722
|
+
|
|
8723
|
+
for (const queryParam of parsedPattern.queryParams) {
|
|
8724
|
+
const paramName = queryParam.name;
|
|
8725
|
+
if (params[paramName] !== undefined && !processedParams.has(paramName)) {
|
|
8726
|
+
queryParams[paramName] = params[paramName];
|
|
8727
|
+
processedParams.add(paramName);
|
|
8728
|
+
}
|
|
8729
|
+
}
|
|
8730
|
+
}
|
|
8731
|
+
|
|
8732
|
+
// Step 3: Add remaining parameters (extra params) alphabetically
|
|
8733
|
+
const extraParams = [];
|
|
8734
|
+
|
|
8735
|
+
// Get all path parameter names to exclude them
|
|
8736
|
+
const pathParamNames = new Set(
|
|
8737
|
+
parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
|
|
8738
|
+
);
|
|
8739
|
+
|
|
8740
|
+
for (const [key, value] of Object.entries(params)) {
|
|
8741
|
+
if (
|
|
8742
|
+
!pathParamNames.has(key) &&
|
|
8743
|
+
!processedParams.has(key) &&
|
|
8744
|
+
value !== undefined
|
|
8745
|
+
) {
|
|
8746
|
+
extraParams.push([key, value]);
|
|
8747
|
+
}
|
|
8748
|
+
}
|
|
8749
|
+
|
|
8750
|
+
// Sort extra params alphabetically for consistent order
|
|
8751
|
+
extraParams.sort(([a], [b]) => a.localeCompare(b));
|
|
8752
|
+
|
|
8753
|
+
// Add sorted extra params
|
|
8754
|
+
for (const [key, value] of extraParams) {
|
|
8755
|
+
queryParams[key] = value;
|
|
8756
|
+
}
|
|
8757
|
+
|
|
8758
|
+
return queryParams;
|
|
8759
|
+
};
|
|
8760
|
+
|
|
8166
8761
|
/**
|
|
8167
8762
|
* Build a URL from a pattern and parameters
|
|
8168
8763
|
*/
|
|
8169
|
-
const buildUrlFromPattern = (
|
|
8764
|
+
const buildUrlFromPattern = (
|
|
8765
|
+
parsedPattern,
|
|
8766
|
+
params = {},
|
|
8767
|
+
originalPattern = null,
|
|
8768
|
+
) => {
|
|
8170
8769
|
if (parsedPattern.segments.length === 0) {
|
|
8171
8770
|
// Root route
|
|
8172
|
-
const
|
|
8771
|
+
const queryParams = {};
|
|
8173
8772
|
for (const [key, value] of Object.entries(params)) {
|
|
8174
8773
|
if (value !== undefined) {
|
|
8175
|
-
|
|
8774
|
+
queryParams[key] = value;
|
|
8176
8775
|
}
|
|
8177
8776
|
}
|
|
8178
|
-
const search =
|
|
8777
|
+
const search = buildQueryString(queryParams);
|
|
8179
8778
|
return `/${search ? `?${search}` : ""}`;
|
|
8180
8779
|
}
|
|
8181
8780
|
|
|
@@ -8189,7 +8788,7 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
|
|
|
8189
8788
|
|
|
8190
8789
|
// If value is provided, include it
|
|
8191
8790
|
if (value !== undefined) {
|
|
8192
|
-
segments.push(
|
|
8791
|
+
segments.push(encodeParamValue(value, false)); // Named parameters encode slashes
|
|
8193
8792
|
} else if (!patternSeg.optional) {
|
|
8194
8793
|
// For required parameters without values, keep the placeholder
|
|
8195
8794
|
segments.push(`:${patternSeg.name}`);
|
|
@@ -8243,41 +8842,14 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
|
|
|
8243
8842
|
path = path.slice(0, -1);
|
|
8244
8843
|
}
|
|
8245
8844
|
|
|
8246
|
-
//
|
|
8247
|
-
const
|
|
8248
|
-
parsedPattern
|
|
8845
|
+
// Build query parameters respecting hierarchical order
|
|
8846
|
+
const queryParams = buildHierarchicalQueryParams(
|
|
8847
|
+
parsedPattern,
|
|
8848
|
+
params,
|
|
8849
|
+
originalPattern,
|
|
8249
8850
|
);
|
|
8250
8851
|
|
|
8251
|
-
|
|
8252
|
-
const queryParamNames = new Set();
|
|
8253
|
-
const searchParams = new URLSearchParams();
|
|
8254
|
-
|
|
8255
|
-
// Handle pattern-defined query parameters (from ?tab, &lon, etc.)
|
|
8256
|
-
if (parsedPattern.queryParams) {
|
|
8257
|
-
for (const queryParam of parsedPattern.queryParams) {
|
|
8258
|
-
const paramName = queryParam.name;
|
|
8259
|
-
queryParamNames.add(paramName);
|
|
8260
|
-
|
|
8261
|
-
const value = params[paramName];
|
|
8262
|
-
if (value !== undefined) {
|
|
8263
|
-
searchParams.set(paramName, value);
|
|
8264
|
-
}
|
|
8265
|
-
// If no value provided, don't add the parameter to keep URLs clean
|
|
8266
|
-
}
|
|
8267
|
-
}
|
|
8268
|
-
|
|
8269
|
-
// Add remaining parameters as additional query parameters (excluding path and pattern query params)
|
|
8270
|
-
for (const [key, value] of Object.entries(params)) {
|
|
8271
|
-
if (
|
|
8272
|
-
!pathParamNames.has(key) &&
|
|
8273
|
-
!queryParamNames.has(key) &&
|
|
8274
|
-
value !== undefined
|
|
8275
|
-
) {
|
|
8276
|
-
searchParams.set(key, value);
|
|
8277
|
-
}
|
|
8278
|
-
}
|
|
8279
|
-
|
|
8280
|
-
const search = searchParams.toString();
|
|
8852
|
+
const search = buildQueryString(queryParams);
|
|
8281
8853
|
|
|
8282
8854
|
// No longer handle trailing slash inheritance here
|
|
8283
8855
|
|
|
@@ -8386,7 +8958,9 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8386
8958
|
patternRegistry.clear();
|
|
8387
8959
|
patternRelationships.clear();
|
|
8388
8960
|
|
|
8389
|
-
// Phase 1: Register all patterns
|
|
8961
|
+
// Phase 1: Register all patterns and create pattern objects
|
|
8962
|
+
const patternObjects = new Map(); // pattern string -> pattern object
|
|
8963
|
+
|
|
8390
8964
|
for (const [key, urlPatternRaw] of Object.entries(patternDefinitions)) {
|
|
8391
8965
|
const [cleanPattern, connections] = detectSignals(urlPatternRaw);
|
|
8392
8966
|
const parsedPattern = parsePattern(cleanPattern);
|
|
@@ -8402,6 +8976,10 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8402
8976
|
};
|
|
8403
8977
|
|
|
8404
8978
|
patternRegistry.set(urlPatternRaw, patternData);
|
|
8979
|
+
|
|
8980
|
+
// Create the full pattern object for this pattern
|
|
8981
|
+
const patternObj = createRoutePattern(urlPatternRaw);
|
|
8982
|
+
patternObjects.set(urlPatternRaw, patternObj);
|
|
8405
8983
|
}
|
|
8406
8984
|
|
|
8407
8985
|
// Phase 2: Build relationships between all patterns
|
|
@@ -8417,30 +8995,24 @@ const setupPatterns = (patternDefinitions) => {
|
|
|
8417
8995
|
|
|
8418
8996
|
// Check if current pattern is a child of other pattern using clean patterns
|
|
8419
8997
|
if (isChildPattern(currentData.cleanPattern, otherData.cleanPattern)) {
|
|
8420
|
-
|
|
8421
|
-
|
|
8998
|
+
// Store pattern objects instead of pattern strings
|
|
8999
|
+
currentData.parentPatterns.push(patternObjects.get(otherPattern));
|
|
9000
|
+
otherData.childPatterns.push(patternObjects.get(currentPattern));
|
|
8422
9001
|
}
|
|
8423
9002
|
}
|
|
8424
9003
|
|
|
8425
|
-
// Store relationships for easy access
|
|
9004
|
+
// Store relationships for easy access with pattern objects
|
|
8426
9005
|
patternRelationships.set(currentPattern, {
|
|
8427
9006
|
pattern: currentData.parsedPattern,
|
|
8428
9007
|
parsedPattern: currentData.parsedPattern,
|
|
8429
9008
|
connections: currentData.connections,
|
|
8430
|
-
childPatterns: currentData.childPatterns, //
|
|
8431
|
-
parentPatterns: currentData.parentPatterns, //
|
|
9009
|
+
childPatterns: currentData.childPatterns, // Now contains pattern objects
|
|
9010
|
+
parentPatterns: currentData.parentPatterns, // Now contains pattern objects
|
|
8432
9011
|
originalPattern: currentPattern,
|
|
8433
9012
|
});
|
|
8434
9013
|
}
|
|
8435
9014
|
};
|
|
8436
9015
|
|
|
8437
|
-
/**
|
|
8438
|
-
* Get pattern data for a registered pattern
|
|
8439
|
-
*/
|
|
8440
|
-
const getPatternData = (urlPatternRaw) => {
|
|
8441
|
-
return patternRegistry.get(urlPatternRaw);
|
|
8442
|
-
};
|
|
8443
|
-
|
|
8444
9016
|
/**
|
|
8445
9017
|
* Clear all registered patterns
|
|
8446
9018
|
*/
|
|
@@ -8687,8 +9259,7 @@ const getRoutePrivateProperties = (route) => {
|
|
|
8687
9259
|
|
|
8688
9260
|
const registerRoute = (routePattern) => {
|
|
8689
9261
|
const urlPatternRaw = routePattern.originalPattern;
|
|
8690
|
-
const
|
|
8691
|
-
const { cleanPattern, connections } = patternData;
|
|
9262
|
+
const { cleanPattern, connections } = routePattern;
|
|
8692
9263
|
|
|
8693
9264
|
const cleanupCallbackSet = new Set();
|
|
8694
9265
|
const cleanup = () => {
|
|
@@ -8710,6 +9281,7 @@ const registerRoute = (routePattern) => {
|
|
|
8710
9281
|
relativeUrl: null,
|
|
8711
9282
|
url: null,
|
|
8712
9283
|
action: null,
|
|
9284
|
+
specificity: routePattern.specificity, // Expose pattern specificity publicly
|
|
8713
9285
|
cleanup,
|
|
8714
9286
|
toString: () => {
|
|
8715
9287
|
return `route "${cleanPattern}"`;
|
|
@@ -8872,16 +9444,22 @@ const registerRoute = (routePattern) => {
|
|
|
8872
9444
|
}
|
|
8873
9445
|
}
|
|
8874
9446
|
|
|
8875
|
-
// Find the most specific route
|
|
9447
|
+
// Find the most specific route using pre-calculated specificity scores
|
|
8876
9448
|
let mostSpecificRoute = route;
|
|
8877
|
-
|
|
9449
|
+
const routePrivateProperties = getRoutePrivateProperties(route);
|
|
9450
|
+
let maxSpecificity = routePrivateProperties?.routePattern?.specificity || 0;
|
|
8878
9451
|
|
|
8879
9452
|
for (const matchingRoute of allMatchingRoutes) {
|
|
8880
|
-
|
|
8881
|
-
|
|
8882
|
-
|
|
8883
|
-
|
|
8884
|
-
|
|
9453
|
+
if (matchingRoute === route) {
|
|
9454
|
+
continue;
|
|
9455
|
+
}
|
|
9456
|
+
const matchingRoutePrivateProperties =
|
|
9457
|
+
getRoutePrivateProperties(matchingRoute);
|
|
9458
|
+
const specificity =
|
|
9459
|
+
matchingRoutePrivateProperties?.routePattern?.specificity || 0;
|
|
9460
|
+
|
|
9461
|
+
if (specificity > maxSpecificity) {
|
|
9462
|
+
maxSpecificity = specificity;
|
|
8885
9463
|
mostSpecificRoute = matchingRoute;
|
|
8886
9464
|
}
|
|
8887
9465
|
}
|
|
@@ -18198,14 +18776,6 @@ const RouteLink = ({
|
|
|
18198
18776
|
});
|
|
18199
18777
|
};
|
|
18200
18778
|
|
|
18201
|
-
const rawUrlPartSymbol = Symbol("raw_url_part");
|
|
18202
|
-
const rawUrlPart = (value) => {
|
|
18203
|
-
return {
|
|
18204
|
-
[rawUrlPartSymbol]: true,
|
|
18205
|
-
value,
|
|
18206
|
-
};
|
|
18207
|
-
};
|
|
18208
|
-
|
|
18209
18779
|
installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
|
|
18210
18780
|
":-navi-tab-selected": {
|
|
18211
18781
|
attribute: "data-tab-selected"
|
|
@@ -18541,12 +19111,12 @@ const TabRoute = ({
|
|
|
18541
19111
|
expand: true,
|
|
18542
19112
|
discrete: true,
|
|
18543
19113
|
padding: padding,
|
|
19114
|
+
paddingX: paddingX,
|
|
19115
|
+
paddingY: paddingY,
|
|
18544
19116
|
paddingLeft: paddingLeft,
|
|
18545
19117
|
paddingRight: paddingRight,
|
|
18546
19118
|
paddingTop: paddingTop,
|
|
18547
19119
|
paddingBottom: paddingBottom,
|
|
18548
|
-
paddingX: paddingX,
|
|
18549
|
-
paddingY: paddingY,
|
|
18550
19120
|
alignX: alignX,
|
|
18551
19121
|
alignY: alignY,
|
|
18552
19122
|
children: children
|