Skip to content

fix(pages): interpolate same-segment rewrite params - #2030

Merged
james-elicx merged 2 commits into
mainfrom
codex/fix-pages-rewrites-same-segment-20260615
Jun 15, 2026
Merged

fix(pages): interpolate same-segment rewrite params#2030
james-elicx merged 2 commits into
mainfrom
codex/fix-pages-rewrites-same-segment-20260615

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

  • interpolate dynamic route parameters for query-only same-segment navigation while preserving the visible rewritten URL
  • preserve locale, basePath, encoding, cross-domain locale targets, history mode, and router events
  • match Next.js href-interpolation-failed behavior for missing required parameters
  • keep hash-only navigation out of route interpolation

Stack

This PR is stacked on #2028 (codex/fix-pages-rewrites-navigation-20260615) because it relies on that PR's visible-URL and history handling.

Next.js parity

Fixes the remaining three same-segment assertions in test/e2e/use-router-with-rewrites/use-router-with-rewrites.test.ts.

Validation

  • pinned Next.js v16.2.6 exact assertions: 3/3 passed
  • focused router/Link tests: 43 passed
  • vinext build passed
  • repeated independent review; no actionable findings remain

Cache Components, PPR, and resume behavior are out of scope.

@pkg-pr-new

pkg-pr-new Bot commented Jun 15, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2030
npm i https://pkg.pr.new/vinext@2030

commit: 59e988a

@github-actions

Copy link
Copy Markdown
Contributor
Example Preview Production Original
app-router-cloudflare preview production
pages-router-cloudflare preview production
app-router-playground preview production original
realworld-api-rest preview production
nextra-docs-template preview production
benchmarks preview production
hackernews preview production original
workers-cache preview production
web preview production

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

@james-elicx Bonk workflow was cancelled.

View workflow run · To retry, trigger Bonk again.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

1 similar comment
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: same-segment rewrite param interpolation

I focused this pass on correctness and dev/prod parity, and on whether the new code matches Next.js's documented interpolateAs / href-interpolation-failed behavior. The core interpolation logic in router.ts looks correct and the parity against Next.js is good (the throw fires before routeChangeStart, exactly as Next.js does in router.js:994-10041017; optional catch-all handling, encoding, and param-omission all line up with interpolate-as.js).

However, there is one blocking regression.

🔴 Blocking: 6 existing tests in tests/link.test.ts now fail

navigatePagesRouterLink now unconditionally writes _vinextInterpolateDynamicRoute into the router options object, even when it is false:

// packages/vinext/src/client/pages-router-link-navigation.ts
const routerOptions: PagesRouterLinkTransitionOptions = {
  scroll,
  locale,
  _vinextInterpolateDynamicRoute: interpolateDynamicRoute, // ← always present
};
if (shallow !== undefined) routerOptions.shallow = shallow;

Six pre-existing assertions (not touched by this PR) check the exact shape of the options object passed to router.push/router.replace, e.g. tests/link.test.ts:807:

expect(push).toHaveBeenCalledWith("/", undefined, { scroll: true, locale: false });

These now receive { scroll, locale, _vinextInterpolateDynamicRoute: false } and fail. vp test run tests/link.test.ts reports 6 failed | 115 passed, so the Vitest required check will go red in CI:

  • passes locale=false through the Pages Router Link handoff
  • passes explicit locale through the Pages Router Link handoff
  • forwards shallow=true through the Pages Router Link handoff (push)
  • forwards shallow=true through the Pages Router Link handoff (replace)
  • forwards shallow=false explicitly when the default is used
  • omits shallow from router options when the caller does not pass it

Fix: mirror the existing conditional-assignment pattern used for shallow so the flag is only added when truthy:

const routerOptions: PagesRouterLinkTransitionOptions = { scroll, locale };
if (shallow !== undefined) routerOptions.shallow = shallow;
if (interpolateDynamicRoute) routerOptions._vinextInterpolateDynamicRoute = true;

This keeps the options object shape unchanged for the common (non-interpolating) path, so the existing assertions pass and performNavigation's options?._vinextInterpolateDynamicRoute === true check still behaves identically. (Alternatively, update the six assertions — but the conditional assignment is the lower-risk change and matches how shallow is already handled.)

Non-blocking notes

  • The new shims.test.ts cases (required/optional catch-all, missing-param throw, hash-only no-fetch, cross-origin domain-locale) all pass and cover the important branches well.
  • The missing-param check (value === undefined || "" || empty array) is slightly stricter than Next.js's !query[param], but only diverges on falsy-but-present values that don't occur for route params in practice. Fine to leave as-is.

Once the link.test.ts regression is resolved and Vitest is green, this looks good.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2030.

Summary

I reviewed PR #2030 (fix(pages): interpolate same-segment rewrite params) for issues, focusing on correctness and Next.js parity. The core interpolation logic in router.ts is sound and matches Next.js behavior well, but I found one blocking regression:

🔴 Blocking: 6 pre-existing tests in tests/link.test.ts fail

navigatePagesRouterLink (packages/vinext/src/client/pages-router-link-navigation.ts:68-72) now unconditionally writes _vinextInterpolateDynamicRoute into the router options object — even when false. Six existing assertions (not touched by this PR) check the exact options-object shape and now break. I verified with vp test run tests/link.test.ts6 failed | 115 passed, which means the required Vitest CI check will go red.

The fix is to follow the existing shallow pattern and only add the flag when truthy:

const routerOptions: PagesRouterLinkTransitionOptions = { scroll, locale };
if (shallow !== undefined) routerOptions.shallow = shallow;
if (interpolateDynamicRoute) routerOptions._vinextInterpolateDynamicRoute = true;

What's good

  • The interpolation throw fires before routeChangeStart, matching Next.js's exact ordering.
  • Optional/required catch-all handling, param encoding, and param omission align with Next.js interpolate-as.js.
  • The new shims.test.ts cases all pass and cover the important branches.

I noted one minor non-blocking divergence (the missing-param emptiness check is slightly stricter than Next.js's !query[param], but harmless in practice).

github run

@james-elicx
james-elicx force-pushed the codex/fix-pages-rewrites-same-segment-20260615 branch from 8136a21 to 4e49c9d Compare June 15, 2026 10:09
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: same-segment rewrite param interpolation

I re-reviewed PR #2030 against the latest head (4e49c9d), focusing on correctness, dev/prod parity, and whether the previously-reported blocking regression was resolved.

✅ Prior blocking issue is fixed

The earlier review flagged that navigatePagesRouterLink unconditionally wrote _vinextInterpolateDynamicRoute: false into the router options object, breaking 6 pre-existing tests/link.test.ts assertions. The follow-up commit (4e49c9d) resolves this by switching to the conditional-assignment pattern used for shallow:

// packages/vinext/src/client/pages-router-link-navigation.ts:68-73
const routerOptions: PagesRouterLinkTransitionOptions = { scroll, locale };
if (interpolateDynamicRoute) routerOptions._vinextInterpolateDynamicRoute = true;
if (shallow !== undefined) routerOptions.shallow = shallow;

I verified locally:

  • vp test run tests/link.test.ts tests/shims.test.ts tests/link-navigation.test.ts1293 passed
  • vp check on the three changed source files → no format/lint/type errors

What's correct

  • Interpolation throw orderinginterpolateCurrentDynamicRoute throws HrefInterpolationError synchronously inside performNavigation before routeChangeStart fires, matching Next.js's href-interpolation-failed ordering. The new shims.test.ts case asserts the throw happens with no pushState.
  • Param-format handlingroutePatternParts normalizes Next.js bracket format to :param; the missing-param filter correctly includes required (:id) and required catch-all (:slug+, stripping +) while excluding optional catch-all (:slug*). Verified against fillRoutePatternSegments so the two never disagree.
  • Encoding & param omission — params are encodeURIComponent-ed and route params are deleted from the resulting search string, matching Next.js interpolate-as.js.
  • Locale / basePath / hash-only — visible-path locale is stripped before matching and re-applied to the target; hash-only UrlObject navigation is excluded from interpolation (covered by the new push/replace hash-only tests that assert no fetch).
  • Cross-origin guard — for cross-domain-locale targets the function returns resolved unchanged (no path interpolation into a foreign origin), and the test asserts the rewritten path is preserved. This is a deliberate, documented choice.

One thing worth a second look (non-blocking)

The Link click handler was refactored from a single try/catch wrapping the whole navigation to navigatePagesRouterLinkWithFallback, whose fallback only runs if loadRouter() throws. The actual router.push/router.replace await (pages-router-link-navigation.ts:106) is no longer inside a fallback try/catch.

This is intentional and correct for the HrefInterpolationError path (Next.js throws there too, it does not hard-navigate). But it also means any other rejection from router.push/router.replace now propagates as an unhandled rejection out of the async onClick instead of triggering the old hard-navigation fallback. In practice performNavigation resolves to false rather than rejecting for normal failures, so this is unlikely to regress real navigations — but it is a behavioral narrowing of the fallback that's worth a conscious confirmation against Next.js semantics. Not blocking.

Verdict

The blocking regression is resolved, all targeted Vitest files are green, and the interpolation logic lines up with Next.js parity. LGTM.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2030.

Summary

I re-reviewed PR #2030 (fix(pages): interpolate same-segment rewrite params) against the latest head commit 4e49c9d.

Key finding: the prior blocking regression is fixed

The earlier /bigbonk review flagged 6 failing tests/link.test.ts assertions caused by _vinextInterpolateDynamicRoute: false being written unconditionally into the router options object. The follow-up commit 4e49c9d resolved this with the conditional-assignment pattern (pages-router-link-navigation.ts:72), mirroring how shallow is handled.

I verified locally:

  • vp test run tests/link.test.ts tests/shims.test.ts tests/link-navigation.test.ts1293 passed
  • vp check on all three changed source files → no format/lint/type errors

What's correct

  • Interpolation throws HrefInterpolationError before routeChangeStart, matching Next.js ordering
  • Required vs. optional catch-all param handling aligns with fillRoutePatternSegments
  • Encoding, param omission, locale/basePath handling, and hash-only exclusion all check out
  • Cross-domain-locale targets deliberately skip path interpolation (tested)

One non-blocking note

The Link handler was refactored so the fallback now only fires on loadRouter() failure, not on router.push/replace rejection. This is intentional and correct for the interpolation-error path, but slightly narrows the old hard-navigation fallback — worth a conscious confirmation but not blocking.

Verdict: LGTM. No actionable blocking issues remain.

github run

@james-elicx
james-elicx marked this pull request as ready for review June 15, 2026 10:25
Base automatically changed from codex/fix-pages-rewrites-navigation-20260615 to main June 15, 2026 21:34
@james-elicx
james-elicx force-pushed the codex/fix-pages-rewrites-same-segment-20260615 branch from 4e49c9d to 59e988a Compare June 15, 2026 21:45
@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx
james-elicx enabled auto-merge (squash) June 15, 2026 21:47
@james-elicx
james-elicx merged commit b91d5d1 into main Jun 15, 2026
56 checks passed
@james-elicx
james-elicx deleted the codex/fix-pages-rewrites-same-segment-20260615 branch June 15, 2026 21:49

@ask-bonk ask-bonk Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review: same-segment rewrite param interpolation

I reviewed PR #2030 against local head (59e988a / 68a1e79), focusing on correctness, Next.js parity, dev/prod parity, and edge cases beyond what prior /bigbonk passes covered.

Verification

  • vp test run tests/link.test.ts tests/shims.test.ts tests/link-navigation.test.ts1295 passed
  • vp check on the three changed source files → no format/lint/type errors
  • vp run vinext#build → build complete (the virtual:vite-rsc/client-references warning is pre-existing and unrelated)

The blocking regression from the earlier pass (unconditional _vinextInterpolateDynamicRoute: false write breaking 6 link.test.ts assertions) is resolved — pages-router-link-navigation.ts:72 now only writes the flag when truthy, mirroring shallow.

What I checked and confirmed correct

  • No double-encoding. query comes from parseQueryString(target.search), which decodes via URLSearchParams, so the subsequent encodeURIComponent in interpolateCurrentDynamicRoute re-encodes exactly once.
  • Non-route query params are preserved. Only extractRouteParamNames(routePattern) keys are deleted from target.searchParams, so ?id=1&foo=bar on /x/[id] correctly yields /x/1?foo=bar, matching Next.js interpolate-as.js.
  • Cross-origin guard is correct. Relative resolved parses to http://vinext.local origin and is not early-returned; the cross-domain-locale case returns resolved unchanged and hard-navigates (tested, win.location.assign with the foreign origin, no pushState).
  • Catch-all param extraction. The missingParams filter keeps required (:id) and required catch-all (:slug+, stripping +) while excluding optional catch-all (:slug*), and stays consistent with fillRoutePatternSegments' null-on-empty behavior.
  • Hash-only excluded. inheritsCurrentPath deliberately omits the hash branch present in replaceInheritedLocale, so hash-only UrlObject navigation skips interpolation (covered by the new push/replace no-fetch tests).
  • Interpolation throw ordering. HrefInterpolationError is thrown synchronously before routeChangeStart fires, matching Next.js href-interpolation-failed ordering (asserted with no pushState).

On the fallback narrowing (re-confirmed, non-blocking)

The Link handler moved from a try/catch wrapping the whole navigation to navigatePagesRouterLinkWithFallback, whose fallback only fires on loadRouter() failure. I traced every reject path of performNavigation:

  • Genuine navigation failures (network/parse/import/render) are caught inside runNavigateClient (router.ts:2144-2157), which handles the hard-nav fallback itself and resolves performNavigation to false — it does not reject. So those never relied on the Link-level catch.
  • The only rejecting paths are HrefInterpolationError, the synchronous dangerous-scheme assert (already blocked upstream in Link via isDangerous), and the SSR no-router throw.

So the narrowing is benign and the rethrow-not-fallback behavior is intentional and tested (link.test.ts: "rethrows missing-required-param interpolation errors instead of using Link fallback"). This matches Next.js, which rethrows non-cancelled errors from linkClicked.

Minor notes (non-blocking)

  • For a non-catch-all [id] receiving an array of length > 1, fillRoutePatternSegments returns null and interpolateCurrentDynamicRoute silently returns resolved (no interpolation) rather than throwing href-interpolation-failed. This is a rare shape and unlikely to occur for real route params; fine to leave.
  • interpolateDynamicRoute is wired only through <Link> (resolvedHref.startsWith("?")); a direct string router.push("?id=1") won't set the flag and won't interpolate. UrlObject pushes do interpolate without the flag. This is consistent with how the feature is scoped, just worth a mental note.

Verdict

LGTM. The blocking regression is fixed, targeted Vitest files and the build are green, and the interpolation logic matches Next.js parity. No actionable blocking issues found.

@ask-bonk

ask-bonk Bot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2030.

Summary

I reviewed PR #2030 (fix(pages): interpolate same-segment rewrite params) for issues, focusing on correctness, Next.js parity, and edge cases beyond what prior passes covered.

Verdict: LGTM — no actionable blocking issues.

Verification

  • vp test run tests/link.test.ts tests/shims.test.ts tests/link-navigation.test.ts1295 passed
  • vp check on the 3 changed source files → clean
  • vp run vinext#build → succeeds

The previously-flagged blocking regression (unconditional _vinextInterpolateDynamicRoute: false write breaking 6 link.test.ts assertions) is fixed at pages-router-link-navigation.ts:72.

Additional edge cases I scrutinized (all correct)

  • No double-encodingparseQueryString decodes, encodeURIComponent re-encodes once
  • Non-route query params preserved — only route-param keys are deleted from the search string
  • Cross-origin guard — relative paths handled; cross-domain-locale targets hard-navigate without interpolation
  • Catch-all extraction — required vs optional catch-all handling stays consistent with fillRoutePatternSegments
  • Hash-only excluded from interpolation
  • Throw orderingHrefInterpolationError fires before routeChangeStart

On the fallback narrowing (non-blocking, re-confirmed)

I traced every reject path of performNavigation: genuine navigation failures are caught inside runNavigateClient (router.ts:2144-2157) and resolve to false rather than rejecting, so they never relied on the Link-level catch. The only rejecting paths are the interpolation error (intentional, tested), the dangerous-scheme assert (already blocked upstream in Link), and the SSR no-router throw. The change is benign and matches Next.js's rethrow-non-cancelled behavior.

Two minor non-blocking notes were included in the review (array-of-length>1 on a non-catch-all [id] silently no-ops instead of throwing; the interpolateDynamicRoute flag is only wired through <Link> for string hrefs).

github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant