Skip to content

fix(form): match Next.js navigation parity#2245

Open
james-elicx wants to merge 4 commits into
mainfrom
codex/fix-next-form-parity
Open

fix(form): match Next.js navigation parity#2245
james-elicx wants to merge 4 commits into
mainfrom
codex/fix-next-form-parity

Conversation

@james-elicx

Copy link
Copy Markdown
Member

Summary

Match next/form behavior from Next.js v16.2.6 while preserving the existing basePath and viewport-prefetch work.

Fixes three Next.js E2E parity failure themes:

  1. Action URL handling — ordinary HTTP(S), same-origin absolute, external absolute, protocol-relative, and relative actions remain valid form markup.
  2. Navigation semantics — external actions and external submitter formAction overrides fall through to native hard navigation instead of being blocked; same-origin and relative actions continue to soft-navigate.
  3. Production form prefetch — form viewport prefetch seeds the optimistic loading shell, so a blocked navigation RSC request still shows the prefetched loading.tsx; prefetch={false} and development do not prefetch.

Also verifies basePath is applied exactly once and submitter formAction is used verbatim.

Validation

Targeted only, one worker where applicable:

  • vp test run tests/shims.test.ts -t "next/form shim" --maxWorkers=1 — 8 passed
  • vp check packages/vinext/src/shims/form.tsx tests/shims.test.ts playwright.config.ts tests/e2e/next-form-parity tests/fixtures/next-form-parity — passed
  • PLAYWRIGHT_PROJECT=next-form-parity-dev vp exec playwright test tests/e2e/next-form-parity --workers=1 — 10 passed, 1 production-only skipped
  • PLAYWRIGHT_PROJECT=next-form-parity-prod vp exec playwright test tests/e2e/next-form-parity --workers=1 — 10 passed, 1 development-only skipped

No broad test suites were run.

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@pkg-pr-new

pkg-pr-new Bot commented Jun 21, 2026

Copy link
Copy Markdown

Open in StackBlitz

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

commit: e48f6fc

@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

@github-actions

github-actions Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared e48f6fc against base cf9d601 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 132.8 KB 132.8 KB ⚫ -0.0%
Client entry size (gzip) vinext 118.7 KB 118.7 KB ⚫ 0.0%
Dev server cold start vinext 2.44 s 2.46 s ⚫ +1.0%
Production build time vinext 3.08 s 3.09 s ⚫ +0.5%
RSC entry closure size (gzip) vinext 92.2 KB 92.2 KB ⚫ 0.0%
Server bundle size (gzip) vinext 166.9 KB 166.9 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

@ask-bonk

ask-bonk Bot commented Jun 21, 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

@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.

/bigbonk review — fix(form): match Next.js navigation parity

Reviewed the form-shim rework against Next.js v16.2.6 source (packages/next/src/client/app-dir/form.tsx). The action-URL classification, external/protocol-relative hard-navigation fallthrough, and verbatim submitter formAction handling all match upstream, and the production prefetch loading-shell change looks consistent with how <Link> seeds optimistic shells. One blocking issue.

Blocking: dangerous-scheme (javascript:/data:/vbscript:) form actions are no longer blocked (parity + security regression)

This PR removes isSafeAction (which delegated to isDangerousScheme) from both the form-level action gate and the submitter effectiveAction gate in packages/vinext/src/shims/form.tsx. Removing the external-origin and protocol-relative blocking is correct — Next.js intentionally hard-navigates to those. But removing the dangerous-scheme block diverges from Next.js and from the rest of this repo.

In Next.js, onFormSubmit ends with router[method](targetHref, { scroll }) (form.tsx:230). The App Router router.push/replace block javascript: URLs and throw "Next.js has blocked a javascript: URL as a security precaution." (app-router-instance.ts:332,389). So a javascript: form action is blocked by Next.js via the router guard.

vinext's form does not route through the guarded router — it calls navigateClientSide(url, ...) directly (form.tsx:429). navigateClientSide has no assertSafeNavigationUrl/isDangerousScheme check (only _appRouter.push/replace do, navigation.ts:1789/1807). Trace for action="javascript:alert(1)" after this PR:

  1. e.preventDefault() runs (form.tsx:422)
  2. createFormSubmitDestinationUrlnew URL("javascript:alert(1)", href) preserves the javascript: scheme
  3. navigateClientSideisExternalUrl true → toSameOriginAppPath returns null → hardNavigateTowindow.location.assign("javascript:...")

So vinext silently hard-navigates instead of blocking + emitting the standard console.error. Same path applies to data: and vbscript: (the repo intentionally extends the block to those — see shims/url-safety.ts).

This contradicts the repo's existing parity, which is explicitly tested in tests/router-javascript-urls.test.ts (push/replace/prefetch + Pages Router all throw BLOCK_MESSAGE) and tests/url-safety.test.ts. The form shim on main upheld this via isSafeAction; this PR drops it with no replacement on the navigation path.

Fix options:

  • Have the form submit through the guarded router methods (matches Next.js exactly, which calls router[method]), or
  • Re-add a dangerous-scheme guard in the submit path (keep isDangerousScheme(effectiveAction)reportBlockedDangerousNavigation() + bail), while still allowing external/protocol-relative to hard-navigate.

Either way, please add a test covering a javascript: (and ideally data:) form action and submitter formAction, asserting it does not hard-navigate and emits the block message. The new tests/e2e/next-form-parity/form-actions.spec.ts covers external/protocol-relative but has no dangerous-scheme case, so this regression is currently untested.

Non-blocking notes

  • isPrefetchableAction now resolves actionHref against window.location.href and gates prefetch to same-origin http(s). Good — but it references window unguarded; it's only called inside the production useEffect that already returns in non-browser/dev, so it's fine in practice. Worth a one-line comment that it's browser-only.
  • Prefetch options flip to cacheForNavigation: false + optimisticRouteShell: true with renderMode: APP_RSC_RENDER_MODE_PREFETCH_LOADING_SHELL. This matches the loading-shell seeding intent; the new shims.test.ts assertion only string-matches the source, so consider a behavioral test that the prefetched shell is actually consumed on the blocked-RSC navigation (the prod-only E2E may already cover this).

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2245.

Summary

I reviewed the fix(form): match Next.js navigation parity changes against Next.js v16.2.6 source. Most of the rework is correct — external/protocol-relative hard-navigation fallthrough and verbatim submitter formAction handling match upstream app-dir/form.tsx, and the production prefetch loading-shell change is consistent.

One blocking issue found:

The PR removes isSafeAction (which used isDangerousScheme) entirely. Dropping the external-origin/protocol-relative blocking is correct, but it also drops dangerous-scheme blocking (javascript:/data:/vbscript:), which is both a Next.js parity regression and a security weakening:

  • Next.js blocks javascript: form actions because onFormSubmit calls router[method](), and the App Router router throws "Next.js has blocked a javascript: URL...".
  • vinext's form calls navigateClientSide() directly, bypassing the assertSafeNavigationUrl/isDangerousScheme guard that only _appRouter.push/replace apply. So a javascript: action now reaches window.location.assign("javascript:...") instead of being blocked.
  • This contradicts the repo's own extensively-tested parity (tests/router-javascript-urls.test.ts, tests/url-safety.test.ts), and the new E2E spec covers external/protocol-relative but has no dangerous-scheme case, so the regression is untested.

I recommended either routing the form submit through the guarded router methods (exact Next.js match) or re-adding a dangerous-scheme guard in the submit path, plus a test for javascript:/data: form actions and submitter formAction overrides. I also left two non-blocking notes about the browser-only isPrefetchableAction and the source-string-only prefetch assertion in shims.test.ts.

github run

@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.

/bigbonk re-review — fix(form): match Next.js navigation parity

Re-reviewed after b573f1b ("fix(form): block dangerous navigation schemes"), which addresses my prior blocking note.

Resolved: dangerous-scheme blocking restored ✅

b573f1b re-adds the guard via assertSafeNavigationUrl(url) in the App Router submit path (form.tsx:431), running it before navigateClientSide. This correctly blocks javascript:/data:/vbscript: (isDangerousScheme) while still letting external/protocol-relative URLs fall through to hard navigation — both parity goals preserved. The synchronous handleSubmit now lets the throw bubble into React's event runtime → console.error, matching the Next.js app-dir/javascript-urls assertion. Unit coverage for javascript:/data:/vbscript: form-level actions and data:/vbscript: submitter overrides is good.

Blocking: the new javascript-submitter-override E2E test contradicts the code path

tests/e2e/next-form-parity/form-actions.spec.ts:128-154 adds a javascript submitter formAction overrides case asserting (a) the block message appears in console.error, (b) the URL is unchanged, and (c) __VINEXT_FORM_JAVASCRIPT_SUBMITTER__ stays undefined. The fixture (app/forms/actions/page.tsx:69-76) uses formAction="javascript:globalThis.__VINEXT_FORM_JAVASCRIPT_SUBMITTER__=true".

But a javascript: submitter formAction never reaches assertSafeNavigationUrl. It is caught earlier by hasReactClientActionAttributes (form.tsx:406, regex /\s*javascript:/i) and bails silently at line 407before e.preventDefault() at line 423. So:

  • No block message is emitted (assertSafeNavigationUrl never runs) → assertion (a) fails.
  • preventDefault() is never called, so in a real browser the native form submission proceeds to formAction="javascript:..." and executes the script, setting the global → assertion (c) fails too.

This is internally inconsistent with the unit test the same commit keeps green: form.test.ts:549 (bails for React client-action submitters) explicitly asserts that a javascript: submitter formAction produces no error, no preventDefault(), and no navigation. And the new unit loop at form.test.ts:323 deliberately tests only ["data", "vbscript"] submitter overrides — javascript: is excluded precisely because it takes the silent client-action path. The E2E test asserts the opposite of the unit tests for the identical input.

Two things to reconcile:

  1. The E2E test as written should fail. Please run PLAYWRIGHT_PROJECT=next-form-parity-dev and ...-prod against tests/e2e/next-form-parity/form-actions.spec.ts — the PR's validation numbers predate b573f1b, so these new dangerous-scheme E2E cases (added in that commit) appear not to have been exercised. If they were green, I'd want to understand why, because the trace above says the marker should be set and no block message emitted.

  2. Decide the intended behavior and make unit + E2E agree. In real Next.js, formAction="javascript:..." is a React-generated client action reference that React intercepts (the hasReactClientActionAttributes bail mirrors form-shared.tsx). A raw author-written javascript: formAction like this fixture is not a React action, so the native browser submit will execute it. If the goal is to actually block a raw javascript: submitter override (defense-in-depth), the silent return at form.tsx:407 needs to e.preventDefault() + emit the block message instead of relying on assertSafeNavigationUrl (which is unreachable for this input). If the intent is to keep matching Next.js's client-action bail, then the javascript-submitter-override E2E case should be dropped/rewritten to assert the silent-bail behavior the unit test already encodes — not the block message.

Non-blocking

  • isPrefetchableAction (form.tsx:68-78) references window unguarded; it's only invoked inside the prod useEffect that already returns in dev/non-browser, so it's safe in practice. A one-line "browser-only" comment would prevent future misuse.

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2245.

Summary

I re-reviewed the form-shim parity work after commit b573f1b ("fix(form): block dangerous navigation schemes"), which addresses my prior blocking note.

Resolved: The dangerous-scheme regression is fixed. b573f1b re-adds assertSafeNavigationUrl(url) in the App Router submit path before navigateClientSide, correctly blocking javascript:/data:/vbscript: while still letting external/protocol-relative actions hard-navigate. The synchronous handleSubmit lets the throw bubble into React → console.error, matching Next.js.

One new blocking issue found: The new javascript-submitter-override E2E case (form-actions.spec.ts:128-154) contradicts the actual code path. A javascript: submitter formAction is caught by hasReactClientActionAttributes (form.tsx:406) and bails silently before preventDefault() — it never reaches assertSafeNavigationUrl. So:

  • No block message is emitted (E2E assertion fails).
  • preventDefault() is never called, so the native browser submit executes the javascript: URL, setting the marker (E2E assertion fails).

This is internally inconsistent with the unit tests the same commit keeps green: form.test.ts:549 asserts a javascript: submitter override produces no error, no preventDefault, no navigation, and the new unit loop at form.test.ts:323 deliberately excludes javascript: from submitter-override cases. The E2E asserts the opposite for the identical input. The PR's validation numbers predate b573f1b, so these new E2E cases appear unexercised.

I recommended running the next-form-parity E2E projects against the new spec and reconciling the intended behavior so unit and E2E agree — either block raw javascript: submitter overrides explicitly (add preventDefault + block message at the bail) or rewrite the E2E case to match the silent client-action bail the unit test encodes. Also left one non-blocking note about the browser-only isPrefetchableAction.

github run

@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.

/bigbonk re-review — fix(form): match Next.js navigation parity

Re-reviewed after 472dd7a ("fix(form): address Pages navigation review"), which lands on top of the dangerous-scheme fix from b573f1b.

Both prior blocking issues are resolved ✅

  1. Dangerous-scheme blocking restored. assertSafeNavigationUrl(url) now runs in both submit paths before navigation: App Router at form.tsx:432 (before navigateClientSide) and Pages Router at form.tsx:440 (before the async import("./router.js") IIFE). Traced the fixture cases end to end:

    • action="javascript:..."withBasePath leaves the scheme intact (isAbsoluteOrProtocolRelativeUrl true) → createFormSubmitDestinationUrlnew URL("javascript:...", href) preserves javascript:assertSafeNavigationUrl throws + console.error. ✓
    • formAction="data:..." / vbscript: submitter overrides: opaque URLs keep their scheme even after searchParams.append (verified vbscript:dangerous?q=react still matches isDangerousScheme's prefix regex). ✓
    • The throw is synchronous inside handleSubmit, so it bubbles into React's event runtime → console.error, matching the Next.js app-dir/javascript-urls assertion.
  2. The contradictory javascript-submitter-override E2E case is gone. 472dd7a removed both the E2E case (form-actions.spec.ts) and the fixture <Form> (actions/page.tsx) that asserted a block message for a raw javascript: submitter formAction — which would actually hit the silent hasReactClientActionAttributes client-action bail at form.tsx:407 (no preventDefault, native submit executes the URL). The remaining dangerous E2E cases (javascript: form action, data: submitter override) take paths that do reach assertSafeNavigationUrl, so unit (form.test.ts:704-730) and E2E now agree.

Verified locally

  • vp test run tests/form.test.ts — 38 passed
  • vp test run tests/shims.test.ts -t "next/form shim" — 8 passed
  • vp check on all changed source/test/config/fixture files — clean (format, lint, types)
  • External / protocol-relative actions correctly hard-navigate: navigateClientSideisExternalUrltoSameOriginAppPath returns null → hardNavigateTo (navigation.ts:1608-1621), and the dangerous guard runs first so it can't be bypassed.
  • Pages Router refactor is sound: assertSafeNavigationUrl is hoisted out of the async IIFE, and the import-failure fallback now returns before the Router push/replace branch, so a failed import can't double-navigate. The new unit loop asserts no pushState/replaceState/dispatch on a blocked scheme.
  • Playwright ports de-duped (catch-error → 4193, next-form-parity-dev → 4192); no port collisions, and every baseURL matches its server port. CI matrix adds both next-form-parity-dev/-prod projects.

Non-blocking notes

  1. next-form-parity fixture has no package.json (the only prod fixture without one). This forces the bespoke start-prod.sh workaround — symlinking node_modules from app-basic, writing dist/package.json with {"type":"module"}, and cp-ing index.mjsindex.js / ssr/index.mjsssr/index.js because prod-server.ts resolves dist/server/index.js (prod-server.ts:962) but the build emits .mjs without "type":"module". Every other prod fixture (e.g. global-not-found-basic) ships its own package.json with "type":"module" and just runs cli.js start directly. Adding a minimal package.json ({"name":"next-form-parity-fixture","private":true,"type":"module"}) would let you delete the .mjs.js copy hack and the dist/package.json write, making the fixture consistent with the rest of the suite and less likely to break if the build output naming changes.

  2. shims.test.ts prefetch assertion is still source-string-only (expect(source).toContain("renderMode: APP_RSC_RENDER_MODE_PREFETCH_LOADING_SHELL") etc.). The behavioral coverage that the prefetched loading shell is actually consumed lives in the prod-only form-prefetch.spec.ts, which is the right place — just noting the unit assertion is brittle to refactors (renaming the option or reformatting the call would fail it without a behavior change). Fine as-is.

  3. isPrefetchableAction (form.tsx:68-79) references window unguarded; now documented with a "Browser-only" comment and only invoked inside the prod useEffect that returns early in dev/non-browser. Good.

LGTM once the two prior blocking issues are confirmed green in CI (the next-form-parity-dev/-prod E2E projects, which now run there). The fixture-package.json cleanup is optional but recommended.

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2245.

Summary

I re-reviewed the form-shim parity work after the latest commit 472dd7a ("fix(form): address Pages navigation review"), which sits on top of b573f1b (dangerous-scheme fix).

Both prior blocking issues are now resolved:

  1. Dangerous-scheme blocking restoredassertSafeNavigationUrl(url) runs before navigation in both the App Router (form.tsx:432) and Pages Router (form.tsx:440) submit paths, throwing synchronously so React reports it via console.error. I traced each fixture case (javascript: action, data:/vbscript: submitter overrides) and confirmed the scheme survives withBasePathcreateFormSubmitDestinationUrlisDangerousScheme.

  2. The contradictory javascript-submitter-override E2E case was removed — both the spec case and the fixture form were deleted, so unit and E2E tests no longer assert opposite behaviors for the same input.

Verified locally: form unit tests (38 passed), form shim tests (8 passed), vp check clean on all changed files, external/protocol-relative hard-navigation intact, Pages Router IIFE refactor sound (no double-navigate on import failure), and Playwright ports de-duped with no collisions.

Non-blocking findings:

  • The next-form-parity fixture is the only prod fixture lacking a package.json, which forces the bespoke start-prod.sh .mjs.js copy hack. Adding a minimal package.json with "type":"module" would eliminate the workaround and match the rest of the suite (recommended cleanup).
  • The shims.test.ts prefetch assertion is source-string-only (brittle but acceptable since behavior is covered by the prod E2E).
  • isPrefetchableAction's browser-only window use is now documented.

No new blocking issues. LGTM pending the next-form-parity-dev/-prod E2E projects going green in CI (now wired into the matrix).

github run

@james-elicx

Copy link
Copy Markdown
Member Author

/bigbonk review for issues

@james-elicx james-elicx marked this pull request as ready for review June 21, 2026 22:17

@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.

/bigbonk re-review — fix(form): match Next.js navigation parity

Re-reviewed after the new commit e48f6fc ("fix(form): preserve native and hard navigation fallbacks"), which landed after the prior LGTM. It makes two behavioral changes; I verified both against Next.js v16.2.6 source and the test suite.

e48f6fc changes are correct ✅

  1. e.preventDefault() moved to after createFormSubmitDestinationUrl() (form.tsx:424-425). This matches upstream app-dir/form.tsx exactly: Next.js calls createFormSubmitDestinationUrl(action, formElement) first, then event.preventDefault() under the comment "Finally, no more reasons for bailing out." So if URL construction throws (e.g. action="http://["), preventDefault is never reached and the browser performs its native submission — correct progressive-enhancement parity. The new Pages Router unit cases (form.test.ts:743-763) assert exactly this: a malformed form action / submitter formAction throws, preventDefault is not called, and no assign/replace/pushState/replaceState fires. The dangerous-scheme ordering is unaffected: for javascript:/data:/vbscript: the URL constructs fine, so preventDefault runs and then assertSafeNavigationUrl throws — native submit is correctly suppressed (verified new URL("data:…"/"javascript:…", base) preserves the dangerous protocol).

  2. Pages Router import-failure fallback now hard-navigates instead of faking history (form.tsx:441-461). The old catch did history.pushState + a synthetic popstate + scrollTo; the new one does window.location.assign/replace(url). This is the safer behavior — publishing a URL via history alone without actually loading the destination leaves the page stale. I confirmed this catch won't cause spurious hard reloads on normal navigation failures: Router.push/replace resolve to "failed"/"cancelled" internally (runNavigateClient at router.ts:2178-2194 catches and returns rather than rejecting, falling back to window.location.href itself), so the form's catch only fires on genuinely thrown setup/import failures. The new parameterized test (form.test.ts:697-725) forces Router.push/replace to throw and asserts location.assign/location.replace is called with the resolved URL and that no pushState/replaceState leaks.

Both prior blocking issues remain resolved ✅

  • Dangerous-scheme blocking still runs before navigation in both paths (form.tsx:432 App Router, form.tsx:440 Pages Router). The javascript: form action and data: submitter override in the fixture (actions/page.tsx:55-68) both reach assertSafeNavigationUrl (the data: override is not caught by the javascript:-only hasReactClientActionAttributes regex), matching the unit tests.
  • The contradictory javascript: submitter-override E2E case stays removed, so unit + E2E agree.

Verified locally

  • vp test run tests/form.test.ts42 passed (was 38; +4 from e48f6fc)
  • vp test run tests/shims.test.ts -t "next/form shim"8 passed
  • vp check packages/vinext/src/shims/form.tsx tests/form.test.ts — clean (format, lint, types)
  • Playwright ports: no collisions — catch-error → 4193, pages-scroll-restoration owns the freed 4185, next-form-parity-dev → 4192, next-form-parity-prod → 4191; every baseURL matches its server port. CI matrix adds both next-form-parity-{dev,prod} projects.
  • form-prefetch.spec.ts header constants match source: x-vinext-rsc-render-mode (Playwright lowercases the X-Vinext-Rsc-Render-Mode header) and value prefetch-loading-shell (APP_RSC_RENDER_MODE_PREFETCH_LOADING_SHELL).

Non-blocking notes (carried forward / new)

  1. App Router malformed-URL native fallback is untested. The new e48f6fc unit cases for the malformed-URL → no-preventDefault fallback only cover the Pages Router branch (form.test.ts:743-763). The App Router branch shares the same createFormSubmitDestinationUrlpreventDefault ordering so it's structurally covered, but a symmetric App Router unit case (throw before preventDefault, no navigateClientSide) would lock in the parity and guard against a future refactor that special-cases one branch. Minor.

  2. next-form-parity fixture still has no package.json (the only prod fixture without one), forcing the bespoke start-prod.sh dist/package.json write + .mjs.js copy hack. Confirmed every other prod fixture (e.g. global-not-found-basic) ships a package.json with "type":"module" and runs cli.js start directly. Adding {"name":"next-form-parity-fixture","private":true,"type":"module"} would delete the workaround and make the fixture consistent. Recommended cleanup, not blocking.

  3. shims.test.ts prefetch assertion is source-string-only — brittle to refactors but fine, since the behavior is covered by the prod-only form-prefetch.spec.ts.

No new blocking issues. The post-LGTM commit e48f6fc is a correct parity improvement. LGTM pending the next-form-parity-dev/-prod E2E projects going green in CI.

@ask-bonk

ask-bonk Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review posted to PR #2245.

Summary

I re-reviewed the form-shim parity work after the new commit e48f6fc ("fix(form): preserve native and hard navigation fallbacks"), which landed after the prior LGTM and was not covered by any earlier review.

e48f6fc's two changes are correct:

  1. e.preventDefault() moved after createFormSubmitDestinationUrl() — matches Next.js v16.2.6 app-dir/form.tsx exactly (it constructs the URL, then calls preventDefault under "Finally, no more reasons for bailing out"). On a malformed action, the throw now precedes preventDefault, so the browser's native submit proceeds (correct progressive enhancement). Dangerous-scheme ordering is unaffected — those URLs construct fine, preventDefault runs, then assertSafeNavigationUrl throws.

  2. Pages Router import-failure fallback now window.location.assign/replace instead of faking history with pushState+popstate. I confirmed Router.push/replace resolve (not reject) on normal navigation failures via runNavigateClient's internal catch, so the form's catch only triggers on genuine setup/import failures — no spurious hard reloads.

Both prior blocking issues remain resolved, and I verified locally:

  • form unit tests 42 passed (up from 38), form shim tests 8 passed, vp check clean
  • No Playwright port collisions; CI matrix wires both next-form-parity-{dev,prod} projects
  • Prefetch spec header constants match source

No new blocking issues. Non-blocking notes: App Router malformed-URL fallback lacks a symmetric unit test, the fixture still lacks a package.json (forcing the start-prod.sh hack), and the shims.test.ts prefetch assertion is source-string-only. LGTM pending the new E2E projects going green in CI.

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