@rangojs/router 0.0.0-experimental.76 → 0.0.0-experimental.77

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.
@@ -1864,7 +1864,7 @@ import { resolve } from "node:path";
1864
1864
  // package.json
1865
1865
  var package_default = {
1866
1866
  name: "@rangojs/router",
1867
- version: "0.0.0-experimental.76",
1867
+ version: "0.0.0-experimental.77",
1868
1868
  description: "Django-inspired RSC router with composable URL patterns",
1869
1869
  keywords: [
1870
1870
  "react",
@@ -3580,11 +3580,19 @@ function substituteRouteParams(pattern, params, encode = encodeURIComponent) {
3580
3580
  let hadOmittedOptional = false;
3581
3581
  for (const [key, value] of Object.entries(params)) {
3582
3582
  const escaped = escapeRegExp2(key);
3583
- result = result.replace(
3584
- new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
3585
- encode(value)
3586
- );
3587
- result = result.replace(`*${key}`, encode(value));
3583
+ if (value === "") {
3584
+ result = result.replace(
3585
+ new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
3586
+ ""
3587
+ );
3588
+ result = result.replace(`*${key}`, "");
3589
+ } else {
3590
+ result = result.replace(
3591
+ new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
3592
+ encode(value)
3593
+ );
3594
+ result = result.replace(`*${key}`, encode(value));
3595
+ }
3588
3596
  }
3589
3597
  result = result.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?\?/g, () => {
3590
3598
  hadOmittedOptional = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rangojs/router",
3
- "version": "0.0.0-experimental.76",
3
+ "version": "0.0.0-experimental.77",
4
4
  "description": "Django-inspired RSC router with composable URL patterns",
5
5
  "keywords": [
6
6
  "react",
package/src/reverse.ts CHANGED
@@ -311,7 +311,10 @@ export function createReverse<TRoutes extends Record<string, string>>(
311
311
  /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
312
312
  (_, key, _constraint, optional) => {
313
313
  const value = params[key];
314
- if (value === undefined) {
314
+ // Empty string is treated as omitted — the trie matcher fills
315
+ // unmatched optional params with "" (not undefined), so reverse
316
+ // must collapse those segments instead of leaving empty slots.
317
+ if (value === undefined || value === "") {
315
318
  hadOmittedOptional = true;
316
319
  return "";
317
320
  }
@@ -174,7 +174,10 @@ export function createReverseFunction(
174
174
  /:([a-zA-Z_][a-zA-Z0-9_]*)(\([^)]*\))?(\?)/g,
175
175
  (_, key) => {
176
176
  const value = effectiveParams[key];
177
- if (value === undefined) {
177
+ // Empty string is treated as omitted — the trie matcher fills
178
+ // unmatched optional params with "" (not undefined), so reverse
179
+ // must collapse those segments instead of leaving empty slots.
180
+ if (value === undefined || value === "") {
178
181
  hadOmittedOptional = true;
179
182
  return "";
180
183
  }
@@ -41,14 +41,28 @@ export function substituteRouteParams(
41
41
  let result = pattern;
42
42
  let hadOmittedOptional = false;
43
43
 
44
- // First pass: substitute provided params
44
+ // First pass: substitute provided params.
45
+ // Empty string on an optional placeholder is treated as omitted (the trie
46
+ // matcher fills unmatched optionals with "" — letting the second pass
47
+ // strip them keeps slash cleanup consistent). Empty string on required
48
+ // `:key` or wildcard `*key` still substitutes, matching prior behaviour.
45
49
  for (const [key, value] of Object.entries(params)) {
46
50
  const escaped = escapeRegExp(key);
47
- result = result.replace(
48
- new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
49
- encode(value),
50
- );
51
- result = result.replace(`*${key}`, encode(value));
51
+ if (value === "") {
52
+ // Only replace required placeholders (negative lookahead for `?`);
53
+ // leave `:key?` for the second pass.
54
+ result = result.replace(
55
+ new RegExp(`:${escaped}(\\([^)]*\\))?(?!\\?)`),
56
+ "",
57
+ );
58
+ result = result.replace(`*${key}`, "");
59
+ } else {
60
+ result = result.replace(
61
+ new RegExp(`:${escaped}(\\([^)]*\\))?\\??`),
62
+ encode(value),
63
+ );
64
+ result = result.replace(`*${key}`, encode(value));
65
+ }
52
66
  }
53
67
 
54
68
  // Second pass: strip remaining optional param placeholders not in params