@jsenv/navi 0.16.7 → 0.16.9

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 = {}) => {
@@ -7835,26 +7891,24 @@ const createRoutePattern = (pattern) => {
7835
7891
  };
7836
7892
  }
7837
7893
 
7838
- // Check for incompatible cases
7839
- if (item.isUserProvided && !matchesChildLiteral) {
7894
+ // Check for generic parameter-literal conflicts
7895
+ if (!matchesChildLiteral) {
7840
7896
  // Check if this is a path parameter from parent pattern
7841
7897
  const isParentPathParam = connections.some(
7842
7898
  (conn) => conn.paramName === paramName,
7843
7899
  );
7844
7900
 
7845
7901
  if (isParentPathParam) {
7846
- // User provided a path param value that doesn't match this child's literals
7847
- return { isCompatible: false };
7848
- }
7849
- }
7902
+ // Parameter value (from user or signal) doesn't match this child's literals
7903
+ // Check if child has any literal segments that would conflict with this parameter
7904
+ const hasConflictingLiteral = childParsedPattern.segments.some(
7905
+ (segment) =>
7906
+ segment.type === "literal" && segment.value !== paramValue,
7907
+ );
7850
7908
 
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 };
7909
+ if (hasConflictingLiteral) {
7910
+ return { isCompatible: false };
7911
+ }
7858
7912
  }
7859
7913
  }
7860
7914
 
@@ -8001,7 +8055,9 @@ const createRoutePattern = (pattern) => {
8001
8055
  const finalParams = removeDefaultValues(resolvedParams);
8002
8056
  const currentUrl = buildUrlFromPattern(
8003
8057
  parsedPattern,
8004
- finalParams);
8058
+ finalParams,
8059
+ pattern,
8060
+ );
8005
8061
  if (currentUrl.length < childUrl.length) {
8006
8062
  return currentUrl;
8007
8063
  }
@@ -8105,7 +8161,7 @@ const createRoutePattern = (pattern) => {
8105
8161
  filteredPattern.trailingSlash = false;
8106
8162
  }
8107
8163
 
8108
- return buildUrlFromPattern(filteredPattern, finalParams);
8164
+ return buildUrlFromPattern(filteredPattern, finalParams, pattern);
8109
8165
  };
8110
8166
 
8111
8167
  /**
@@ -8160,6 +8216,40 @@ const createRoutePattern = (pattern) => {
8160
8216
  );
8161
8217
 
8162
8218
  if (optimizedParentUrl) {
8219
+ // Before returning optimized parent URL, check if we need to inherit parameters
8220
+ // from our ancestors that the parent route might not inherit on its own
8221
+ const parentFinalParams = { ...resolvedParams };
8222
+
8223
+ // Remove params that belong to current route (they're at defaults anyway)
8224
+ for (const conn of connections) {
8225
+ delete parentFinalParams[conn.paramName];
8226
+ }
8227
+
8228
+ // Inherit from all ancestor routes, not just immediate parent
8229
+ inheritParentParameters(parentFinalParams, relationships);
8230
+
8231
+ // If we inherited any parameters, add them to the parent URL
8232
+ const extraParamEntries = Object.entries(parentFinalParams).filter(
8233
+ ([key, value]) => {
8234
+ // Only include params not handled by parent route
8235
+ const isParentParam = parentPatternObj.connections.some(
8236
+ (conn) => conn.paramName === key,
8237
+ );
8238
+ return !isParentParam && value !== undefined;
8239
+ },
8240
+ );
8241
+
8242
+ if (extraParamEntries.length > 0) {
8243
+ const queryString = buildQueryString(
8244
+ Object.fromEntries(extraParamEntries),
8245
+ );
8246
+ return (
8247
+ optimizedParentUrl +
8248
+ (optimizedParentUrl.includes("?") ? "&" : "?") +
8249
+ queryString
8250
+ );
8251
+ }
8252
+
8163
8253
  return optimizedParentUrl;
8164
8254
  }
8165
8255
  }
@@ -8591,58 +8681,98 @@ const extractSearchParams = (urlObj, connections = []) => {
8591
8681
  };
8592
8682
 
8593
8683
  /**
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
8684
+ * Build query parameters respecting hierarchical order from ancestor patterns
8597
8685
  */
8598
- const detectParentParameterInheritance = (
8599
- paramName,
8600
- paramValue,
8686
+ const buildHierarchicalQueryParams = (
8601
8687
  parsedPattern,
8602
- pathParamNames,
8603
- queryParamNames,
8688
+ params,
8689
+ originalPattern,
8604
8690
  ) => {
8605
- // Parameter doesn't belong to current route
8606
- const isExtraParam =
8607
- !pathParamNames.has(paramName) && !queryParamNames.has(paramName);
8691
+ const queryParams = {};
8692
+ const processedParams = new Set();
8693
+
8694
+ // Get relationships for this pattern
8695
+ const relationships = patternRelationships.get(originalPattern);
8696
+ const parentPatterns = relationships?.parentPatterns || [];
8697
+
8698
+ // Step 1: Add query parameters from ancestor patterns (oldest to newest)
8699
+ // This ensures ancestor parameters come first in their declaration order
8700
+ const ancestorPatterns = parentPatterns; // Process in order: root ancestor first, then immediate parent
8701
+
8702
+ for (const ancestorPatternObj of ancestorPatterns) {
8703
+ if (ancestorPatternObj.pattern?.queryParams) {
8704
+
8705
+ for (const queryParam of ancestorPatternObj.pattern.queryParams) {
8706
+ const paramName = queryParam.name;
8707
+ if (
8708
+ params[paramName] !== undefined &&
8709
+ !processedParams.has(paramName)
8710
+ ) {
8711
+ queryParams[paramName] = params[paramName];
8712
+ processedParams.add(paramName);
8713
+ }
8714
+ }
8715
+ }
8716
+ }
8717
+
8718
+ // Step 2: Add query parameters from current pattern
8719
+ if (parsedPattern.queryParams) {
8720
+
8721
+ for (const queryParam of parsedPattern.queryParams) {
8722
+ const paramName = queryParam.name;
8723
+ if (params[paramName] !== undefined && !processedParams.has(paramName)) {
8724
+ queryParams[paramName] = params[paramName];
8725
+ processedParams.add(paramName);
8726
+ }
8727
+ }
8728
+ }
8608
8729
 
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",
8730
+ // Step 3: Add remaining parameters (extra params) alphabetically
8731
+ const extraParams = [];
8732
+
8733
+ // Get all path parameter names to exclude them
8734
+ const pathParamNames = new Set(
8735
+ parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
8612
8736
  );
8613
8737
 
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);
8738
+ for (const [key, value] of Object.entries(params)) {
8739
+ if (
8740
+ !pathParamNames.has(key) &&
8741
+ !processedParams.has(key) &&
8742
+ value !== undefined
8743
+ ) {
8744
+ extraParams.push([key, value]);
8745
+ }
8746
+ }
8623
8747
 
8624
- return {
8625
- isParentInheritance:
8626
- isExtraParam && hasLiteralSegments && looksLikeParentParam,
8627
- isExtraParam,
8628
- hasLiteralSegments,
8629
- looksLikeParentParam,
8630
- };
8748
+ // Sort extra params alphabetically for consistent order
8749
+ extraParams.sort(([a], [b]) => a.localeCompare(b));
8750
+
8751
+ // Add sorted extra params
8752
+ for (const [key, value] of extraParams) {
8753
+ queryParams[key] = value;
8754
+ }
8755
+
8756
+ return queryParams;
8631
8757
  };
8632
8758
 
8633
8759
  /**
8634
8760
  * Build a URL from a pattern and parameters
8635
8761
  */
8636
- const buildUrlFromPattern = (parsedPattern, params = {}) => {
8762
+ const buildUrlFromPattern = (
8763
+ parsedPattern,
8764
+ params = {},
8765
+ originalPattern = null,
8766
+ ) => {
8637
8767
  if (parsedPattern.segments.length === 0) {
8638
8768
  // Root route
8639
- const searchParams = new URLSearchParams();
8769
+ const queryParams = {};
8640
8770
  for (const [key, value] of Object.entries(params)) {
8641
8771
  if (value !== undefined) {
8642
- searchParams.set(key, value);
8772
+ queryParams[key] = value;
8643
8773
  }
8644
8774
  }
8645
- const search = searchParams.toString();
8775
+ const search = buildQueryString(queryParams);
8646
8776
  return `/${search ? `?${search}` : ""}`;
8647
8777
  }
8648
8778
 
@@ -8656,7 +8786,7 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
8656
8786
 
8657
8787
  // If value is provided, include it
8658
8788
  if (value !== undefined) {
8659
- segments.push(encodeURIComponent(value));
8789
+ segments.push(encodeParamValue(value, false)); // Named parameters encode slashes
8660
8790
  } else if (!patternSeg.optional) {
8661
8791
  // For required parameters without values, keep the placeholder
8662
8792
  segments.push(`:${patternSeg.name}`);
@@ -8710,84 +8840,14 @@ const buildUrlFromPattern = (parsedPattern, params = {}) => {
8710
8840
  path = path.slice(0, -1);
8711
8841
  }
8712
8842
 
8713
- // Add search parameters
8714
- const pathParamNames = new Set(
8715
- parsedPattern.segments.filter((s) => s.type === "param").map((s) => s.name),
8843
+ // Build query parameters respecting hierarchical order
8844
+ const queryParams = buildHierarchicalQueryParams(
8845
+ parsedPattern,
8846
+ params,
8847
+ originalPattern,
8716
8848
  );
8717
8849
 
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();
8850
+ const search = buildQueryString(queryParams);
8791
8851
 
8792
8852
  // No longer handle trailing slash inheritance here
8793
8853
 
@@ -18714,14 +18774,6 @@ const RouteLink = ({
18714
18774
  });
18715
18775
  };
18716
18776
 
18717
- const rawUrlPartSymbol = Symbol("raw_url_part");
18718
- const rawUrlPart = (value) => {
18719
- return {
18720
- [rawUrlPartSymbol]: true,
18721
- value,
18722
- };
18723
- };
18724
-
18725
18777
  installImportMetaCss(import.meta);Object.assign(PSEUDO_CLASSES, {
18726
18778
  ":-navi-tab-selected": {
18727
18779
  attribute: "data-tab-selected"