@jsenv/navi 0.16.23 → 0.16.25

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.
@@ -7723,7 +7723,11 @@ const createRoutePattern = (pattern) => {
7723
7723
  }
7724
7724
  }
7725
7725
 
7726
- const parsedPattern = parsePattern(cleanPattern, parameterDefaults);
7726
+ const parsedPattern = parsePattern(
7727
+ cleanPattern,
7728
+ parameterDefaults,
7729
+ connections,
7730
+ );
7727
7731
 
7728
7732
  // Create signalSet to track all signals this pattern depends on
7729
7733
  const signalSet = new Set();
@@ -9334,7 +9338,11 @@ const canParameterReachChildRoute = (
9334
9338
  /**
9335
9339
  * Parse a route pattern string into structured segments
9336
9340
  */
9337
- const parsePattern = (pattern, parameterDefaults = new Map()) => {
9341
+ const parsePattern = (
9342
+ pattern,
9343
+ parameterDefaults = new Map(),
9344
+ connections = [],
9345
+ ) => {
9338
9346
  // Handle root route
9339
9347
  if (pattern === "/") {
9340
9348
  return {
@@ -9403,7 +9411,27 @@ const parsePattern = (pattern, parameterDefaults = new Map()) => {
9403
9411
  if (seg.startsWith(":")) {
9404
9412
  // Parameter segment
9405
9413
  const paramName = seg.slice(1).replace("?", ""); // Remove : and optional ?
9406
- const isOptional = seg.endsWith("?") || parameterDefaults.has(paramName);
9414
+
9415
+ // Check if parameter should be optional:
9416
+ // 1. Explicitly marked with ?
9417
+ // 2. Has a default value
9418
+ // 3. Connected signal has undefined value and no explicit default (allows /map to match /map/:panel)
9419
+ let isOptional = seg.endsWith("?") || parameterDefaults.has(paramName);
9420
+
9421
+ if (!isOptional) {
9422
+ // Check if connected signal has undefined value (making parameter optional for index routes)
9423
+ const connection = connections.find(
9424
+ (conn) => conn.paramName === paramName,
9425
+ );
9426
+ if (
9427
+ connection &&
9428
+ connection.signal &&
9429
+ connection.signal.value === undefined &&
9430
+ !parameterDefaults.has(paramName)
9431
+ ) {
9432
+ isOptional = true;
9433
+ }
9434
+ }
9407
9435
 
9408
9436
  return {
9409
9437
  type: "param",
@@ -9597,14 +9625,18 @@ const matchUrl = (
9597
9625
  // Check for remaining URL segments
9598
9626
  // Patterns with trailing slashes can match additional URL segments (like wildcards)
9599
9627
  // Patterns without trailing slashes should match exactly (unless they're wildcards)
9628
+ // BUT: if pattern has children, it can also match additional segments (hierarchical matching)
9629
+ const hasChildren =
9630
+ patternObj && patternObj.children && patternObj.children.length > 0;
9600
9631
  if (
9601
9632
  !parsedPattern.wildcard &&
9602
9633
  !parsedPattern.trailingSlash &&
9634
+ !hasChildren &&
9603
9635
  urlSegmentIndex < urlSegments.length
9604
9636
  ) {
9605
- return null; // Pattern without trailing slash should not match extra segments
9637
+ return null; // Pattern without trailing slash/wildcard/children should not match extra segments
9606
9638
  }
9607
- // If pattern has trailing slash or wildcard, allow extra segments (no additional check needed)
9639
+ // If pattern has trailing slash, wildcard, or children, allow extra segments
9608
9640
 
9609
9641
  // Add search parameters
9610
9642
  const searchParams = extractSearchParams(urlObj, connections);
@@ -10262,77 +10294,107 @@ const updateRoutes = (
10262
10294
  }
10263
10295
  stateSignal.value = urlParamValue;
10264
10296
  } else {
10265
- // When route doesn't match, check if we're navigating to a parent route
10266
- let parentRouteMatching = false;
10297
+ // Route doesn't match - check if any matching route extracts this parameter
10298
+ let parameterExtractedByMatchingRoute = false;
10299
+ let matchingRouteInSameFamily = false;
10300
+
10267
10301
  for (const otherRoute of routeSet) {
10268
10302
  if (otherRoute === route || !otherRoute.matching) {
10269
10303
  continue;
10270
10304
  }
10271
10305
  const otherRouteProperties = getRoutePrivateProperties(otherRoute);
10306
+ const otherParams = otherRouteProperties.rawParamsSignal.value;
10307
+
10308
+ // Check if this matching route extracts the parameter
10309
+ if (paramName in otherParams) {
10310
+ parameterExtractedByMatchingRoute = true;
10311
+ }
10312
+
10313
+ // Check if this matching route is in the same family using parent-child relationships
10314
+ const thisPatternObj = routePattern;
10272
10315
  const otherPatternObj = otherRouteProperties.routePattern;
10273
10316
 
10274
- // Check if the other route pattern is a parent of this route pattern
10275
- // Using the built relationships in the pattern objects
10276
- let currentParent = routePattern.parent;
10277
- let foundParent = false;
10317
+ // Routes are in same family if they share a hierarchical relationship:
10318
+ // 1. One is parent/ancestor of the other
10319
+ // 2. They share a common parent/ancestor
10320
+ let inSameFamily = false;
10321
+
10322
+ // Check if other route is ancestor of this route
10323
+ let currentParent = thisPatternObj.parent;
10278
10324
  while (currentParent) {
10279
10325
  if (currentParent === otherPatternObj) {
10280
- foundParent = true;
10326
+ inSameFamily = true;
10281
10327
  break;
10282
10328
  }
10283
10329
  currentParent = currentParent.parent;
10284
10330
  }
10285
10331
 
10286
- if (!foundParent) {
10287
- continue;
10332
+ // Check if this route is ancestor of other route
10333
+ if (!inSameFamily) {
10334
+ currentParent = otherPatternObj.parent;
10335
+ while (currentParent) {
10336
+ if (currentParent === thisPatternObj) {
10337
+ inSameFamily = true;
10338
+ break;
10339
+ }
10340
+ currentParent = currentParent.parent;
10341
+ }
10288
10342
  }
10289
10343
 
10290
- // Found a parent route that's matching, but check if there's a more specific
10291
- // sibling route also matching (indicating sibling navigation, not parent navigation)
10292
- let hasMatchingSibling = false;
10293
- for (const siblingCandidateRoute of routeSet) {
10294
- if (
10295
- siblingCandidateRoute === route ||
10296
- siblingCandidateRoute === otherRoute ||
10297
- !siblingCandidateRoute.matching
10298
- ) {
10299
- continue;
10344
+ // Check if they share a common parent (siblings or cousins)
10345
+ if (!inSameFamily) {
10346
+ const thisAncestors = new Set();
10347
+ currentParent = thisPatternObj.parent;
10348
+ while (currentParent) {
10349
+ thisAncestors.add(currentParent);
10350
+ currentParent = currentParent.parent;
10300
10351
  }
10301
10352
 
10302
- const siblingProperties = getRoutePrivateProperties(
10303
- siblingCandidateRoute,
10304
- );
10305
- const siblingPatternObj = siblingProperties.routePattern;
10306
-
10307
- // Check if this is a sibling (shares the same parent)
10308
- if (siblingPatternObj.parent === currentParent) {
10309
- hasMatchingSibling = true;
10310
- break;
10353
+ currentParent = otherPatternObj.parent;
10354
+ while (currentParent) {
10355
+ if (thisAncestors.has(currentParent)) {
10356
+ inSameFamily = true;
10357
+ break;
10358
+ }
10359
+ currentParent = currentParent.parent;
10311
10360
  }
10312
10361
  }
10313
10362
 
10314
- // Only treat as parent navigation if no sibling is matching
10315
- if (!hasMatchingSibling) {
10316
- parentRouteMatching = true;
10317
- break; // Found the parent route, no need to check other routes
10363
+ if (inSameFamily) {
10364
+ matchingRouteInSameFamily = true;
10318
10365
  }
10319
10366
  }
10320
10367
 
10321
- if (parentRouteMatching) {
10322
- // We're navigating to a parent route - clear this signal to reflect the hierarchy
10368
+ // Only reset signal if:
10369
+ // 1. We're navigating within the same route family (not to completely unrelated routes)
10370
+ // 2. AND no matching route extracts this parameter from URL
10371
+ // 3. AND parameter has no default value (making it truly optional)
10372
+ if (matchingRouteInSameFamily && !parameterExtractedByMatchingRoute) {
10323
10373
  const defaultValue = routePattern.parameterDefaults?.get(paramName);
10324
- if (debug) {
10374
+ if (defaultValue === undefined) {
10375
+ // Parameter is not extracted within same family and has no default - reset it
10376
+ if (debug) {
10377
+ console.debug(
10378
+ `[route] Same family navigation, ${paramName} not extracted and has no default: resetting signal`,
10379
+ );
10380
+ }
10381
+ stateSignal.value = undefined;
10382
+ } else if (debug) {
10383
+ // Parameter has a default value - preserve current signal value
10325
10384
  console.debug(
10326
- `[route] Parent route ${parentRouteMatching} matching: clearing ${paramName} signal to default: ${defaultValue}`,
10385
+ `[route] Parameter ${paramName} has default value ${defaultValue}: preserving signal value: ${stateSignal.value}`,
10327
10386
  );
10328
10387
  }
10329
- stateSignal.value = defaultValue;
10330
10388
  } else if (debug) {
10331
- // We're navigating to a different route family - preserve signal for future URL building
10332
- // Keep current signal value unchanged
10333
- console.debug(
10334
- `[route] Different route family: preserving ${paramName} signal value: ${stateSignal.value}`,
10335
- );
10389
+ if (!matchingRouteInSameFamily) {
10390
+ console.debug(
10391
+ `[route] Different route family: preserving ${paramName} signal value: ${stateSignal.value}`,
10392
+ );
10393
+ } else {
10394
+ console.debug(
10395
+ `[route] Parameter ${paramName} extracted by matching route: preserving signal value: ${stateSignal.value}`,
10396
+ );
10397
+ }
10336
10398
  }
10337
10399
  }
10338
10400
  }
@@ -10514,10 +10576,20 @@ const registerRoute = (routePattern) => {
10514
10576
  const rawParamsSignal = signal(ROUTE_NOT_MATCHING_PARAMS);
10515
10577
  const paramsSignal = computed(() => {
10516
10578
  const rawParams = rawParamsSignal.value;
10517
- // Pattern system handles parameter defaults, routes just work with raw params
10518
- return rawParams || {};
10579
+ const resolvedParams = routePattern.resolveParams(rawParams);
10580
+ return resolvedParams;
10519
10581
  });
10520
10582
  const visitedSignal = signal(false);
10583
+
10584
+ // Keep route.params synchronized with computed paramsSignal
10585
+ // This ensures route.params includes parameters from child routes
10586
+ effect(() => {
10587
+ const computedParams = paramsSignal.value;
10588
+ if (route.params !== computedParams) {
10589
+ route.params = computedParams;
10590
+ }
10591
+ });
10592
+
10521
10593
  for (const { signal: stateSignal, paramName, options = {} } of connections) {
10522
10594
  const { debug } = options;
10523
10595