@jsenv/navi 0.16.7 → 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.
@@ -7552,6 +7552,62 @@ const useUITransitionContentId = value => {
7552
7552
  */
7553
7553
 
7554
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
+
7555
7611
  // Base URL management
7556
7612
  let baseFileUrl;
7557
7613
  let baseUrl;
@@ -7668,7 +7724,7 @@ const createRoutePattern = (pattern) => {
7668
7724
  };
7669
7725
 
7670
7726
  const buildUrl = (params = {}) => {
7671
- return buildUrlFromPattern(parsedPattern, params);
7727
+ return buildUrlFromPattern(parsedPattern, params, pattern);
7672
7728
  };
7673
7729
 
7674
7730
  const resolveParams = (providedParams = {}) => {
@@ -8001,7 +8057,9 @@ const createRoutePattern = (pattern) => {
8001
8057
  const finalParams = removeDefaultValues(resolvedParams);
8002
8058
  const currentUrl = buildUrlFromPattern(
8003
8059
  parsedPattern,
8004
- finalParams);
8060
+ finalParams,
8061
+ pattern,
8062
+ );
8005
8063
  if (currentUrl.length < childUrl.length) {
8006
8064
  return currentUrl;
8007
8065
  }
@@ -8105,7 +8163,7 @@ const createRoutePattern = (pattern) => {
8105
8163
  filteredPattern.trailingSlash = false;
8106
8164
  }
8107
8165
 
8108
- return buildUrlFromPattern(filteredPattern, finalParams);
8166
+ return buildUrlFromPattern(filteredPattern, finalParams, pattern);
8109
8167
  };
8110
8168
 
8111
8169
  /**
@@ -8160,6 +8218,40 @@ const createRoutePattern = (pattern) => {
8160
8218
  );
8161
8219
 
8162
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
+
8163
8255
  return optimizedParentUrl;
8164
8256
  }
8165
8257
  }
@@ -8591,58 +8683,98 @@ const extractSearchParams = (urlObj, connections = []) => {
8591
8683
  };
8592
8684
 
8593
8685
  /**
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
8686
+ * Build query parameters respecting hierarchical order from ancestor patterns
8597
8687
  */
8598
- const detectParentParameterInheritance = (
8599
- paramName,
8600
- paramValue,
8688
+ const buildHierarchicalQueryParams = (
8601
8689
  parsedPattern,
8602
- pathParamNames,
8603
- queryParamNames,
8690
+ params,
8691
+ originalPattern,
8604
8692
  ) => {
8605
- // Parameter doesn't belong to current route
8606
- const isExtraParam =
8607
- !pathParamNames.has(paramName) && !queryParamNames.has(paramName);
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 = [];
8608
8734
 
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",
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),
8612
8738
  );
8613
8739
 
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);
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
+ }
8623
8749
 
8624
- return {
8625
- isParentInheritance:
8626
- isExtraParam && hasLiteralSegments && looksLikeParentParam,
8627
- isExtraParam,
8628
- hasLiteralSegments,
8629
- looksLikeParentParam,
8630
- };
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;
8631
8759
  };
8632
8760
 
8633
8761
  /**
8634
8762
  * Build a URL from a pattern and parameters
8635
8763
  */
8636
- const buildUrlFromPattern = (parsedPattern, params = {}) => {
8764
+ const buildUrlFromPattern = (
8765
+ parsedPattern,
8766
+ params = {},
8767
+ originalPattern = null,
8768
+ ) => {
8637
8769
  if (parsedPattern.segments.length === 0) {
8638
8770
  // Root route
8639
- const searchParams = new URLSearchParams();
8771
+ const queryParams = {};
8640
8772
  for (const [key, value] of Object.entries(params)) {
8641
8773
  if (value !== undefined) {
8642
- searchParams.set(key, value);
8774
+ queryParams[key] = value;
8643
8775
  }
8644
8776
  }
8645
- const search = searchParams.toString();
8777
+ const search = buildQueryString(queryParams);
8646
8778
  return `/${search ? `?${search}` : ""}`;
8647
8779
  }
8648
8780
 
@@ -8656,7 +8788,7 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
8656
8788
 
8657
8789
  // If value is provided, include it
8658
8790
  if (value !== undefined) {
8659
- segments.push(encodeURIComponent(value));
8791
+ segments.push(encodeParamValue(value, false)); // Named parameters encode slashes
8660
8792
  } else if (!patternSeg.optional) {
8661
8793
  // For required parameters without values, keep the placeholder
8662
8794
  segments.push(`:${patternSeg.name}`);
@@ -8710,84 +8842,14 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
8710
8842
  path = path.slice(0, -1);
8711
8843
  }
8712
8844
 
8713
- // Add search parameters
8714
- const pathParamNames = new Set(
8715
- parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
8845
+ // Build query parameters respecting hierarchical order
8846
+ const queryParams = buildHierarchicalQueryParams(
8847
+ parsedPattern,
8848
+ params,
8849
+ originalPattern,
8716
8850
  );
8717
8851
 
8718
- // Add query parameters defined in the pattern first
8719
- const queryParamNames = new Set();
8720
- const searchParams = new URLSearchParams();
8721
-
8722
- // Handle pattern-defined query parameters (from ?tab, &lon, etc.)
8723
- if (parsedPattern.queryParams) {
8724
- for (const queryParam of parsedPattern.queryParams) {
8725
- const paramName = queryParam.name;
8726
- queryParamNames.add(paramName);
8727
-
8728
- const value = params[paramName];
8729
- if (value !== undefined) {
8730
- searchParams.set(paramName, value);
8731
- }
8732
- // If no value provided, don't add the parameter to keep URLs clean
8733
- }
8734
- }
8735
-
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
-
8740
- for (const [key, value] of Object.entries(params)) {
8741
- if (
8742
- !pathParamNames.has(key) &&
8743
- !queryParamNames.has(key) &&
8744
- value !== undefined
8745
- ) {
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
8779
- }
8780
- }
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
-
8790
- const search = searchParams.toString();
8852
+ const search = buildQueryString(queryParams);
8791
8853
 
8792
8854
  // No longer handle trailing slash inheritance here
8793
8855
 
@@ -18714,14 +18776,6 @@ const RouteLink = ({
18714
18776
  });
18715
18777
  };
18716
18778
 
18717
- const rawUrlPartSymbol = Symbol("raw_url_part");
18718
- const rawUrlPart = (value) => {
18719
- return {
18720
- [rawUrlPartSymbol]: true,
18721
- value,
18722
- };
18723
- };
18724
-
18725
18779
  installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
18726
18780
  ":-navi-tab-selected": {
18727
18781
  attribute: "data-tab-selected"