Skip to content

[codex] Fix wrapped use server exports#731

Draft
southpolesteve wants to merge 1 commit intomainfrom
codex/issue-722-next-safe-action
Draft

[codex] Fix wrapped use server exports#731
southpolesteve wants to merge 1 commit intomainfrom
codex/issue-722-next-safe-action

Conversation

@southpolesteve
Copy link
Copy Markdown
Collaborator

Summary

Fix file-level "use server" exports that wrap an inline async action, such as next-safe-action's actionClient.action(async () => ...) pattern.

Root Cause

@vitejs/plugin-rsc validates file-level "use server" exports syntactically and rejects wrapped exports whose initializer is a call expression instead of a direct async function expression. That causes vinext to fail with unsupported non async function for valid Next.js-compatible patterns.

Changes

  • add a pre-transform plugin that rewrites wrapped file-level "use server" exports into local bindings plus export { ... }
  • register that plugin ahead of @vitejs/plugin-rsc
  • add a regression test covering the next-safe-action shape and the upstream strict proxy transform behavior

Validation

  • pnpm test tests/use-server-wrapped-exports.test.ts tests/use-server-closure-collision.test.ts
  • pnpm run check
  • built local vinext, linked it into the reporter's repro app from issue Incompatible with next-safe-action #722, and confirmed GET / changed from 500 unsupported non async function to 200

Closes #722.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 31, 2026

Open in StackBlitz

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

commit: 6df0a2a

@southpolesteve
Copy link
Copy Markdown
Collaborator Author

/bigbonk review

@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

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.

Solid fix. The approach is sound: rewriting wrapped "use server" exports into local bindings + export { ... } before plugin-rsc's strict validation is a clean workaround that preserves semantics for both the client proxy transform and the server action transform. Tests are well-structured — they reproduce the upstream bug, verify the fix, and confirm both proxy and server transforms work on the rewritten output.

A few observations (non-blocking):

Multi-declarator export const rewriting: shouldRewriteNamedDeclaration uses .some(), so if even one declarator in a multi-declarator statement needs rewriting, the entire statement gets rewritten. For example, export const a = async () => {}, b = wrap(async () => {}) rewrites both a and b even though a doesn't need it. This is harmless — plugin-rsc accepts export { a } specifiers for direct async functions just fine — but it's worth noting. In practice, multi-declarator export statements in "use server" files are extremely rare.

containsInlineAsyncFunction is deliberately broad: It recurses into the entire initializer subtree, so something like export const config = { handler: async () => {} } would also trigger a rewrite. This is fine — in a "use server" file, non-function exports are invalid anyway, and the rewrite is harmless for plugin-rsc's downstream handling.

Test coverage gaps (consider as follow-ups):

  • No test for export default wrap(async () => {}) (the ExportDefaultDeclaration path at line 67-72). The default export rewrite generates a synthetic local name __vinext_server_default__ and the uniqueness logic, but neither path is exercised by the current test suite.
  • No test for files that contain "use server" as a string value (not a directive) to confirm the fast-path bail-out works correctly.
  • No test for TypeScript-specific patterns like export const action: ServerAction = wrap(async () => {}) with type annotations (though parseAst handles these fine).

The plugin ordering (wrapped exports → closure collision → plugin-rsc) is correct — the two fix plugins address orthogonal concerns (file-level directive vs inline directive) and don't interfere.

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Mar 31, 2026

UnknownError: Your AI Gateway has authentication active, but you didn't provide a valid apiKey

github run

@ask-bonk
Copy link
Copy Markdown
Contributor

ask-bonk bot commented Mar 31, 2026

@southpolesteve Bonk workflow failed. Check the logs for details.

View workflow run · To retry, trigger Bonk again.

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.

Incompatible with next-safe-action

1 participant