Skip to content

fix(pages-router): keep 'use server' files outside app/ as normal modules (#1476)#1679

Open
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1476-pages-form-action-literal-v2
Open

fix(pages-router): keep 'use server' files outside app/ as normal modules (#1476)#1679
james-elicx wants to merge 1 commit into
mainfrom
fix/issue-1476-pages-form-action-literal-v2

Conversation

@james-elicx
Copy link
Copy Markdown
Member

Summary

  • Server Actions are an App Router-only feature. When a Pages Router page imports a file containing "use server", @vitejs/plugin-rsc's rsc:use-server transform rewrites the exports into server-reference proxies. Calling such a function from getServerSideProps then throws "Server Functions cannot be called during initial render."
  • Add vinext:strip-non-app-use-server (an enforce: "pre" transform) that strips the "use server" directive prologue from any file outside the app/ directory before plugin-rsc sees it. Files inside app/ are unaffected, so existing server-action behaviour is preserved.
  • This mirrors Next.js's SWC pipeline, where the server-actions transform only runs for the App Router layer (packages/next/src/build/swc/options.ts isAppRouterPagesLayer).

Fixes #1476.

Test plan

  • New regression test in tests/app-router.test.ts reproduces the failure on main and passes with the fix:
    • pnpm test tests/app-router.test.ts -t "use server"
  • Full tests/app-router.test.ts suite still green (333 tests, including the existing server-action coverage).
  • Full tests/pages-router.test.ts suite still green (265 tests).
  • tests/form.test.ts, tests/app-rsc-handler.test.ts, tests/app-server-action-execution.test.ts still green.
  • pnpm run check (format, lint, types) green.
  • CI runs the full Vitest + Playwright matrices.

Ported from Next.js: test/e2e/app-dir/action-in-pages-router/action-in-pages-router.test.ts.

…ules (#1476)

Server Actions are an App Router-only feature. When a Pages Router page
imported a file containing the `"use server"` directive,
`@vitejs/plugin-rsc`'s `rsc:use-server` transform rewrote the exports
into server-reference proxies. Calling such a function from
`getServerSideProps` then threw "Server Functions cannot be called
during initial render."

Add a pre-transform that strips the `"use server"` directive prologue
from any file outside the `app/` directory before plugin-rsc sees it,
mirroring Next.js's SWC pipeline (the server-actions pass only runs for
the App Router layer). Files inside `app/` are unaffected, so existing
server-action behaviour is preserved.

Regression test ported from Next.js
`test/e2e/app-dir/action-in-pages-router/`.
@james-elicx
Copy link
Copy Markdown
Member Author

/bigbonk review

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 28, 2026

Open in StackBlitz

npm i https://pkg.pr.new/vinext@1679

commit: 9cfed98

@github-actions
Copy link
Copy Markdown

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
web preview production

Copy link
Copy Markdown
Contributor

@ask-bonk ask-bonk Bot left a comment

Choose a reason for hiding this comment

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

Clean fix — the approach is correct, the implementation is careful, and the test coverage is sufficient for the stated problem.

What works well:

  • Correct scoping. Stripping the file-level directive prologue (not inline "use server" inside function bodies) is exactly right — inline directives inside function bodies within pages code are already harmless because they don't trigger @vitejs/plugin-rsc's module-level transform gate.

  • Proper plugin ordering. enforce: "pre" ensures this runs before the RSC plugin's rsc:use-server transform. The cheap code.includes("use server") pre-check avoids cost on the vast majority of files.

  • Robust directive parsing. The stripUseServerDirective function correctly handles BOM, hashbang, comments, whitespace, and other directives preceding "use server" (e.g. "use strict"). Follows ECMA-262 Directive Prologue grammar faithfully.

  • Timing safety. appDir is assigned in config() → the strip plugin reads it in configResolved() → Vite guarantees all config() hooks complete before any configResolved() fires. The closure-based getAppDir/hasAppDir callbacks are the same pattern used by createMiddlewareServerOnlyPlugin.

  • Good test. The fixture mirrors the Next.js test (test/e2e/app-dir/action-in-pages-router/) well, and the test assertion checks both the presence of the test element and the real return value ("action:foo"), which would fail if the export were still a server-reference proxy.

One thing to be aware of (not blocking):

There's an inherent trade-off in the path-based scoping: if a file with "use server" lives in a shared directory (e.g. lib/) and is imported by both a pages route and an app route, the directive gets stripped for both importers since Vite's module graph is shared. The app route consumer silently loses the server action. This matches Next.js behavior (SWC's isAppRouterPagesLayer gate is per-compilation-layer, and Next.js has separate bundler pipelines for pages vs app), but is worth noting for anyone who later tries to share "use server" files across routers. Not actionable now — just documenting the design constraint.

LGTM.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk Bot commented May 28, 2026

Review posted and approved on PR #1679.

Summary: This is a clean, well-scoped fix. The plugin strips file-level "use server" directives from modules outside the app/ directory so that @vitejs/plugin-rsc doesn't rewrite them into server-reference proxies when they're imported by Pages Router code. The implementation follows existing plugin patterns, has correct timing guarantees, robust directive parsing, and a good regression test ported from the Next.js test suite. Approved with a non-blocking note about the inherent design constraint when sharing "use server" files across both routers (which matches Next.js behavior).

github run

@james-elicx james-elicx marked this pull request as ready for review May 28, 2026 17:25
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.

Pages Router: literal <form action='action:...'> string is stripped instead of rendered as-is

1 participant