From 2b9e36af8b9611a51c5236961696cc1bb8e6492c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:14:22 +0900 Subject: [PATCH 01/13] feat(rsc): support inline directive methods Co-authored-by: OpenCode --- .../custom-server-function-plugin.ts | 12 ++- .../callable-cache-plugin.ts | 11 ++- .../examples/use-cache/vite.config.ts | 10 +- .../src/transforms/directive-function.test.ts | 78 +++++++++++++++ .../src/transforms/directive-function.ts | 99 +++++++++++++++++++ .../src/transforms/fixtures/hoist/methods.js | 26 +++++ .../fixtures/hoist/methods.js.snap.encode.js | 40 ++++++++ .../fixtures/hoist/methods.js.snap.js | 38 +++++++ .../fixtures/source-map/hoist/methods.js | 17 ++++ .../source-map/hoist/methods.js.map.snap.md | 29 ++++++ .../source-map/hoist/methods.js.snap.md | 56 +++++++++++ .../plugin-rsc/src/transforms/hoist.test.ts | 30 ++++++ packages/plugin-rsc/src/transforms/hoist.ts | 63 +++++++++++- packages/plugin-rsc/src/transforms/index.ts | 1 + .../src/transforms/server-action.test.ts | 27 +++++ .../src/transforms/server-action.ts | 11 ++- 16 files changed, 536 insertions(+), 12 deletions(-) create mode 100644 packages/plugin-rsc/src/transforms/directive-function.test.ts create mode 100644 packages/plugin-rsc/src/transforms/directive-function.ts create mode 100644 packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js create mode 100644 packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js create mode 100644 packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js create mode 100644 packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js create mode 100644 packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md create mode 100644 packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md diff --git a/packages/plugin-rsc/examples/custom-server-function/custom-server-function-plugin.ts b/packages/plugin-rsc/examples/custom-server-function/custom-server-function-plugin.ts index bfa6823d2..cec1e0b8b 100644 --- a/packages/plugin-rsc/examples/custom-server-function/custom-server-function-plugin.ts +++ b/packages/plugin-rsc/examples/custom-server-function/custom-server-function-plugin.ts @@ -4,6 +4,7 @@ import { transformDirectiveProxyExport, transformHoistInlineDirective, transformModuleExportEffect, + validateDirectiveFunction, } from '@vitejs/plugin-rsc/transforms' import { parseAstAsync, type Plugin } from 'vite' @@ -39,12 +40,17 @@ export function customServerFunctionPlugin(): Plugin { const result = hasDirective(ast.body, directive) ? transformModuleExportEffect(code, ast, { rejectNonAsyncFunction: true, - generate: ({ binding, exportName }) => - runtime(binding, exportName), + generate: ({ binding, exportName, meta }) => { + validateDirectiveFunction(meta.valueNode, directive) + return runtime(binding, exportName) + }, }) : transformHoistInlineDirective(code, ast, { directive, - runtime, + runtime: (value, name, meta) => { + validateDirectiveFunction(meta.valueNode, directive) + return runtime(value, name) + }, rejectNonAsyncFunction: true, }) if (!result.output.hasChanged()) { diff --git a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts index 5825c3a5a..c82d2b372 100644 --- a/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts +++ b/packages/plugin-rsc/examples/use-cache-callable/callable-cache-plugin.ts @@ -5,6 +5,7 @@ import { transformDirectiveProxyExport, transformHoistInlineDirective, transformWrapExport, + validateDirectiveFunction, } from '@vitejs/plugin-rsc/transforms' import { parseAstAsync, type Plugin } from 'vite' import type { CacheWrapperOptions } from './src/framework/use-cache-runtime' @@ -40,10 +41,13 @@ export function callableCachePlugin(): Plugin { `$$cacheWrapper(${value}, ${JSON.stringify(options)}),` + `${JSON.stringify(reference.referenceKey)},` + `${JSON.stringify(name)})` + const wrap = (value: string, name: string, meta: ModuleExportMeta) => { + validateDirectiveFunction(meta.valueNode, directive) + return runtime(value, name, getCacheWrapperOptions(meta)) + } const result = hasDirective(ast.body, directive) ? transformWrapExport(code, ast, { - runtime: (value, name, meta) => - runtime(value, name, getCacheWrapperOptions(meta)), + runtime: wrap, // Next.js calls rejecting primitive literals while permitting // objects and arrays arbitrary, but keeps the latter for metadata // and viewport exports. @@ -57,8 +61,7 @@ export function callableCachePlugin(): Plugin { directive, rejectNonAsyncFunction: true, hoistRuntime: true, - runtime: (value, name, meta) => - runtime(value, name, getCacheWrapperOptions(meta)), + runtime: wrap, encode: (value) => `$$encryptCacheCaptures(${value})`, // The cache runtime replaces the envelope with decoded captures // before invoking this private implementation. diff --git a/packages/plugin-rsc/examples/use-cache/vite.config.ts b/packages/plugin-rsc/examples/use-cache/vite.config.ts index 29954254b..09359b4b7 100644 --- a/packages/plugin-rsc/examples/use-cache/vite.config.ts +++ b/packages/plugin-rsc/examples/use-cache/vite.config.ts @@ -1,6 +1,9 @@ import react from '@vitejs/plugin-react' import rsc from '@vitejs/plugin-rsc' -import { transformHoistInlineDirective } from '@vitejs/plugin-rsc/transforms' +import { + transformHoistInlineDirective, + validateDirectiveFunction, +} from '@vitejs/plugin-rsc/transforms' import { defineConfig, parseAstAsync, type Plugin } from 'vite' export default defineConfig({ @@ -25,7 +28,10 @@ function vitePluginUseCache(): Plugin[] { if (!code.includes('use cache')) return const ast = await parseAstAsync(code) const result = transformHoistInlineDirective(code, ast, { - runtime: (value) => `__vite_rsc_cache(${value})`, + runtime: (value, _name, meta) => { + validateDirectiveFunction(meta.valueNode, 'use cache') + return `__vite_rsc_cache(${value})` + }, directive: 'use cache', rejectNonAsyncFunction: true, noExport: true, diff --git a/packages/plugin-rsc/src/transforms/directive-function.test.ts b/packages/plugin-rsc/src/transforms/directive-function.test.ts new file mode 100644 index 000000000..e37cdf282 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/directive-function.test.ts @@ -0,0 +1,78 @@ +import type { Node, Program } from 'estree' +import { parseAstAsync } from 'vite' +import { describe, expect, test } from 'vitest' +import { validateDirectiveFunction } from './directive-function' + +async function getFunction(input: string): Promise { + const ast = (await parseAstAsync(input)) as unknown as Program + const statement = ast.body[0]! + if (statement.type === 'FunctionDeclaration') return statement + if (statement.type === 'ClassDeclaration') { + const member = statement.body.body[0]! + if (member.type === 'MethodDefinition') return member.value + } + throw new Error('unsupported test input') +} + +describe(validateDirectiveFunction, () => { + test.each([ + [`async function action() { this.value }`, 'this'], + [`async function action() { arguments }`, 'arguments'], + [`async function action(value = arguments) {}`, 'arguments'], + [`class A extends B { static async action() { super.action() } }`, 'super'], + ])('rejects %s', async (input, expression) => { + const node = await getFunction(input) + expect(() => validateDirectiveFunction(node, 'use server')).toThrow( + `"use server" functions cannot use "${expression}".`, + ) + }) + + test('follows arrow lexical bindings', async () => { + const node = await getFunction(` +async function action() { + const nested = () => arguments +} +`) + expect(() => validateDirectiveFunction(node, 'use server')).toThrow( + /arguments/, + ) + }) + + test('stops at nested normal functions', async () => { + const node = await getFunction(` +async function action() { + function nested() { + this.value + arguments + const arrow = () => this.value + } +} +`) + expect(() => validateDirectiveFunction(node, 'use server')).not.toThrow() + }) + + test('ignores syntax-only arguments identifiers and jsxDEV metadata', async () => { + const node = await getFunction(` +async function action() { + const value = { arguments: 1 } + return _jsxDEV(Component, {}, undefined, false, undefined, this) +} +`) + expect(() => validateDirectiveFunction(node, 'use server')).not.toThrow() + }) + + test('ignores unknown and non-function values', async () => { + const ast = (await parseAstAsync( + 'export default action', + )) as unknown as Program + const statement = ast.body[0]! + expect(statement.type).toBe('ExportDefaultDeclaration') + if (statement.type !== 'ExportDefaultDeclaration') return + expect(() => + validateDirectiveFunction(statement.declaration, 'use server'), + ).not.toThrow() + expect(() => + validateDirectiveFunction(undefined, 'use server'), + ).not.toThrow() + }) +}) diff --git a/packages/plugin-rsc/src/transforms/directive-function.ts b/packages/plugin-rsc/src/transforms/directive-function.ts new file mode 100644 index 000000000..c39a35c25 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/directive-function.ts @@ -0,0 +1,99 @@ +import type { + ArrowFunctionExpression, + FunctionDeclaration, + FunctionExpression, + Identifier, + Node, +} from 'estree' +import { walk } from 'estree-walker' + +export type DirectiveFunction = + | ArrowFunctionExpression + | FunctionDeclaration + | FunctionExpression + +/** + * Applies the function restrictions used by Next.js Server Functions. + * Unknown and non-function values are ignored so transform callbacks can pass + * `meta.valueNode` directly. + */ +export function validateDirectiveFunction( + valueNode: { type: string } | null | undefined, + directive: string, +): void { + if (!valueNode) return + const node = valueNode as Node + if (!isFunction(node)) return + + walk(node, { + enter(child, parent) { + if ( + child !== node && + (child.type === 'FunctionDeclaration' || + child.type === 'FunctionExpression') + ) { + this.skip() + return + } + + const expression = + child.type === 'ThisExpression' && !isJsxDevSourceThis(child, parent) + ? 'this' + : child.type === 'Super' + ? 'super' + : child.type === 'Identifier' && + child.name === 'arguments' && + isReferenceIdentifier(child, parent) + ? 'arguments' + : undefined + if (expression) { + throw Object.assign( + new Error( + `${JSON.stringify(directive)} functions cannot use ${JSON.stringify(expression)}.`, + ), + { pos: child.start }, + ) + } + }, + }) +} + +function isFunction(node: Node): node is DirectiveFunction { + return ( + node.type === 'ArrowFunctionExpression' || + node.type === 'FunctionDeclaration' || + node.type === 'FunctionExpression' + ) +} + +function isJsxDevSourceThis(node: Node, parent: Node | null): boolean { + return ( + node.type === 'ThisExpression' && + parent?.type === 'CallExpression' && + parent.arguments.at(-1) === node && + parent.callee.type === 'Identifier' && + /(?:^|_)jsxDEV$/.test(parent.callee.name) + ) +} + +function isReferenceIdentifier(node: Identifier, parent: Node | null): boolean { + if (!parent) return true + if ( + parent.type === 'MemberExpression' && + parent.property === node && + !parent.computed + ) { + return false + } + if ( + (parent.type === 'Property' || + parent.type === 'MethodDefinition' || + parent.type === 'PropertyDefinition') && + parent.key === node && + !parent.computed && + !(parent.type === 'Property' && parent.shorthand) + ) { + return false + } + return true +} diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js new file mode 100644 index 000000000..39dec58d4 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -0,0 +1,26 @@ +const key = 'computed' + +export function createObject(value) { + return { + async action() { + 'use server' + return value + }, + async [key]() { + 'use server' + return value + 1 + }, + } +} + +export class Actions { + static async action() { + 'use server' + return 1 + } + + static async ['computed']() { + 'use server' + return 2 + } +} diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js new file mode 100644 index 000000000..c6d229a07 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -0,0 +1,40 @@ +const key = 'computed' + +export function createObject(value) { + return { + action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, __enc([value])), + [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, __enc([value])), + } +} + +export class Actions { + static action = /* #__PURE__ */ $$register($$hoist_2_action, "", "$$hoist_2_action"); + + static ['computed'] = /* #__PURE__ */ $$register($$hoist_3_computed, "", "$$hoist_3_computed"); +} + +;export async function $$hoist_0_action($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + }; +/* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); + +;export async function $$hoist_1_key($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_1_key, "name", { value: "key" }); + +;export async function $$hoist_2_action() { + 'use server' + return 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_2_action, "name", { value: "action" }); + +;export async function $$hoist_3_computed() { + 'use server' + return 2 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_3_computed, "name", { value: "computed" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js new file mode 100644 index 000000000..dcef8d979 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -0,0 +1,38 @@ +const key = 'computed' + +export function createObject(value) { + return { + action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, value), + [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, value), + } +} + +export class Actions { + static action = /* #__PURE__ */ $$register($$hoist_2_action, "", "$$hoist_2_action"); + + static ['computed'] = /* #__PURE__ */ $$register($$hoist_3_computed, "", "$$hoist_3_computed"); +} + +;export async function $$hoist_0_action(value) { + 'use server' + return value + }; +/* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); + +;export async function $$hoist_1_key(value) { + 'use server' + return value + 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_1_key, "name", { value: "key" }); + +;export async function $$hoist_2_action() { + 'use server' + return 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_2_action, "name", { value: "action" }); + +;export async function $$hoist_3_computed() { + 'use server' + return 2 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_3_computed, "name", { value: "computed" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js new file mode 100644 index 000000000..2e3b9b52f --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js @@ -0,0 +1,17 @@ +const key = 'computed' + +export function createObject(value) { + return { + async [key]() { + 'use server' + return value + }, + } +} + +export class Actions { + static async action() { + 'use server' + return 1 + } +} diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md new file mode 100644 index 000000000..6cb4a7539 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md @@ -0,0 +1,29 @@ +## hoist + +```txt +(0:0) "const key = 'computed'\n" --> (0:0) "const key = 'computed'\n" +(2:0) "export function createObject(value) {\n" --> (2:0) "export function createObject(value) {\n" +(3:0) " return {\n" --> (3:0) " return {\n" +(4:0) " async [key]() {\n" --> (4:0) " [key]: /* #__PURE__ */ registerServerReference($$hoist_0_key, \"$$hoist_0_key\").bind(null, encrypt([value]))" +(7:5) ",\n" --> (4:111) ",\n" +(8:0) " }\n" --> (5:0) " }\n" +(9:0) "}\n" --> (6:0) "}\n" +(11:0) "export class Actions {\n" --> (8:0) "export class Actions {\n" +(12:0) " static async action() {\n" --> (9:0) " static action = /* #__PURE__ */ registerServerReference($$hoist_1_action, \"$$hoist_1_action\");\n" +(16:0) "}\n" --> (10:0) "}\n" +(4:15) "() {\n" --> (11:0) "\n" +(4:15) "() " --> (12:0) ";export async function $$hoist_0_key($$hoist_encoded) " +(4:18) "{\n" --> (12:54) "{\n" +(5:0) " 'use server'\n" --> (13:0) " const [value] = await decrypt($$hoist_encoded);\n" +(5:6) "'use server'\n" --> (14:0) "'use server'\n" +(6:0) " return value\n" --> (15:0) " return value\n" +(7:0) " },\n" --> (16:0) " };\n" +[unmapped] --> (17:0) "/* #__PURE__ */ Object.defineProperty($$hoist_0_key, \"name\", { value: \"key\" });\n" +(12:21) "() {\n" --> (18:0) "\n" +(12:21) "() " --> (19:0) ";export async function $$hoist_1_action() " +(12:24) "{\n" --> (19:42) "{\n" +(13:0) " 'use server'\n" --> (20:0) " 'use server'\n" +(14:0) " return 1\n" --> (21:0) " return 1\n" +(15:0) " }\n" --> (22:0) " };\n" +[unmapped] --> (23:0) "/* #__PURE__ */ Object.defineProperty($$hoist_1_action, \"name\", { value: \"action\" });\n" +``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md new file mode 100644 index 000000000..916c36d8b --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md @@ -0,0 +1,56 @@ +## Input + +```js +const key = 'computed' + +export function createObject(value) { + return { + async [key]() { + 'use server' + return value + }, + } +} + +export class Actions { + static async action() { + 'use server' + return 1 + } +} +``` + +## hoist + +**Status:** transformed + +**References:** $$hoist_0_key, $$hoist_1_action + +[Source map visualization](https://evanw.github.io/source-map-visualization/#NzExAGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2tleV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfa2V5LCAiJCRob2lzdF8wX2tleSIpLmJpbmQobnVsbCwgZW5jcnlwdChbdmFsdWVdKSksCiAgfQp9CgpleHBvcnQgY2xhc3MgQWN0aW9ucyB7CiAgc3RhdGljIGFjdGlvbiA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzFfYWN0aW9uLCAiJCRob2lzdF8xX2FjdGlvbiIpOwp9Cgo7ZXhwb3J0IGFzeW5jIGZ1bmN0aW9uICQkaG9pc3RfMF9rZXkoJCRob2lzdF9lbmNvZGVkKSB7CiAgICAgIGNvbnN0IFt2YWx1ZV0gPSBhd2FpdCBkZWNyeXB0KCQkaG9pc3RfZW5jb2RlZCk7Cid1c2Ugc2VydmVyJwogICAgICByZXR1cm4gdmFsdWUKICAgIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8wX2tleSwgIm5hbWUiLCB7IHZhbHVlOiAia2V5IiB9KTsKCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8xX2FjdGlvbigpIHsKICAgICd1c2Ugc2VydmVyJwogICAgcmV0dXJuIDEKICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMV9hY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFjdGlvbiIgfSk7CjgxMQB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImNvbnN0IGtleSA9ICdjb21wdXRlZCdcblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9iamVjdCh2YWx1ZSkge1xuICByZXR1cm4ge1xuICAgIGFzeW5jIFtrZXldKCkge1xuICAgICAgJ3VzZSBzZXJ2ZXInXG4gICAgICByZXR1cm4gdmFsdWVcbiAgICB9LFxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBY3Rpb25zIHtcbiAgc3RhdGljIGFzeW5jIGFjdGlvbigpIHtcbiAgICAndXNlIHNlcnZlcidcbiAgICByZXR1cm4gMVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFROztBQUVyQixNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1QsQ0FBQyxDQUFDLENBQUMsQ0FBQywyR0FHQztBQUNMLENBQUMsQ0FBQztBQUNGOztBQUVBLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDO0FBQ3JCLENBQUMsQ0FBQztBQUlGO0FBWmU7QUFBQSxzREFBRztBQUNsQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNO0FBQ2pCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUNiLENBQUMsQ0FBQyxDQUFDLENBQUM7O0FBS2lCO0FBQUEsMENBQUc7QUFDeEIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNO0FBQ2YsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDWCxDQUFDLENBQUM7OyJ9) + +```js +const key = 'computed' + +export function createObject(value) { + return { + [key]: /* #__PURE__ */ registerServerReference($$hoist_0_key, "$$hoist_0_key").bind(null, encrypt([value])), + } +} + +export class Actions { + static action = /* #__PURE__ */ registerServerReference($$hoist_1_action, "$$hoist_1_action"); +} + +;export async function $$hoist_0_key($$hoist_encoded) { + const [value] = await decrypt($$hoist_encoded); +'use server' + return value + }; +/* #__PURE__ */ Object.defineProperty($$hoist_0_key, "name", { value: "key" }); + +;export async function $$hoist_1_action() { + 'use server' + return 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_1_action, "name", { value: "action" }); +``` diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index 019a2ca48..c9df836d1 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -196,6 +196,36 @@ async function action() { expect(await testTransformNames(input)).toEqual(['$$hoist_0_action']) }) + it('preserves arbitrary computed method keys', async () => { + const input = ` +const key = getKey(); +const object = { + async [key]() { + "use server"; + }, +}; +class Actions { + static async [key]() { + "use server"; + } +} +` + const transformed = await testTransform(input) + expect(transformed).toContain('[key]: /* #__PURE__ */') + expect(transformed).toContain('static [key] = /* #__PURE__ */') + }) + + it('rejects unsupported method forms', async () => { + for (const input of [ + `class Actions { async action() { "use server" } }`, + `class Actions { static async #action() { "use server" } }`, + `const actions = { get action() { "use server" } }`, + `class Actions { static set action(value) { "use server" } }`, + ]) { + await expect(testTransform(input)).rejects.toThrow(/not allowed/) + } + }) + it('finds directives only in directive-capable bodies', async () => { const input = ` { diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index cca3d6b7b..a33ec6be3 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -166,12 +166,58 @@ export function transformHoistInlineDirective( ) } + const isObjectMethod = + node.type === 'FunctionExpression' && + parent?.type === 'Property' && + parent.value === node && + (parent.method || parent.kind !== 'init') + const isClassMethod = + node.type === 'FunctionExpression' && + parent?.type === 'MethodDefinition' + if (isClassMethod && !parent.static) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(match[0])} class instance methods.`, + ), + { pos: parent.start }, + ) + } + if (isClassMethod && parent.key.type === 'PrivateIdentifier') { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(match[0])} private class methods.`, + ), + { pos: parent.start }, + ) + } + if ( + (isObjectMethod && parent.kind !== 'init') || + (isClassMethod && parent.kind !== 'method') + ) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(match[0])} getters or setters.`, + ), + { pos: parent.start }, + ) + } + // Capture the source-level name so the hoisted function can preserve it // with Object.defineProperty below. Anonymous functions get a stable // fallback for registration and diagnostics. const declName = node.type === 'FunctionDeclaration' && node.id.name + const methodName = + (isObjectMethod || isClassMethod) && + (parent.key.type === 'Identifier' || parent.key.type === 'Literal') + ? String( + parent.key.type === 'Identifier' + ? parent.key.name + : parent.key.value, + ) + : undefined const originalName = declName || + methodName || (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.id.name) || @@ -246,7 +292,22 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (declName) { + if (isObjectMethod) { + const key = input.slice(parent.key.start, parent.key.end) + output.update( + parent.start, + node.start, + `${parent.computed ? `[${key}]` : key}: `, + ) + } else if (isClassMethod) { + const key = input.slice(parent.key.start, parent.key.end) + output.update( + parent.start, + node.start, + `static ${parent.computed ? `[${key}]` : key} = `, + ) + newCode += ';' + } else if (declName) { // A function declaration becomes a const declaration. For a default // export, retain the export as a separate statement after that const. newCode = `const ${declName} = ${newCode};` diff --git a/packages/plugin-rsc/src/transforms/index.ts b/packages/plugin-rsc/src/transforms/index.ts index ab49332a2..976af770c 100644 --- a/packages/plugin-rsc/src/transforms/index.ts +++ b/packages/plugin-rsc/src/transforms/index.ts @@ -6,3 +6,4 @@ export * from './proxy-export' export * from './utils' export * from './server-action' export * from './expand-export-all' +export * from './directive-function' diff --git a/packages/plugin-rsc/src/transforms/server-action.test.ts b/packages/plugin-rsc/src/transforms/server-action.test.ts index ff6b343cb..1f0708f78 100644 --- a/packages/plugin-rsc/src/transforms/server-action.test.ts +++ b/packages/plugin-rsc/src/transforms/server-action.test.ts @@ -30,3 +30,30 @@ test('normalizes inline server reference names', async () => { referenceNames: ['$$hoist_0_anonymous_server_function'], }) }) + +test.each([ + `'use server'; export async function action() { arguments }`, + `export function App() { return async function action() { 'use server'; this.value } }`, +])('validates server functions: %s', async (input) => { + await expect(transform(input)).rejects.toThrow(/cannot use/) +}) + +test('does not apply a file directive to non-exported functions', async () => { + await expect( + transform(` +'use server' +async function helper() { arguments } +export async function action() {} +`), + ).resolves.toBeDefined() +}) + +test('leaves unresolved local exports for local binding resolution', async () => { + await expect( + transform(` +'use server' +async function action() { arguments } +export { action } +`), + ).resolves.toBeDefined() +}) diff --git a/packages/plugin-rsc/src/transforms/server-action.ts b/packages/plugin-rsc/src/transforms/server-action.ts index a2c4074f1..610978c20 100644 --- a/packages/plugin-rsc/src/transforms/server-action.ts +++ b/packages/plugin-rsc/src/transforms/server-action.ts @@ -1,5 +1,6 @@ import type MagicString from 'magic-string' import type { ESTree } from 'vite' +import { validateDirectiveFunction } from './directive-function' import { transformHoistInlineDirective } from './hoist' import { transformModuleExportEffect } from './module-export-effect' import { hasDirective } from './utils' @@ -32,14 +33,20 @@ export function transformServerActionServer( if (hasDirective(ast.body, 'use server')) { const result = transformModuleExportEffect(input, ast, { rejectNonAsyncFunction: options.rejectNonAsyncFunction, - generate: ({ binding, exportName }) => - options.runtime(binding, exportName), + generate: ({ binding, exportName, meta }) => { + validateDirectiveFunction(meta.valueNode, 'use server') + return options.runtime(binding, exportName) + }, }) return { ...result, exportNames: result.referenceNames } } const result = transformHoistInlineDirective(input, ast, { ...options, directive: 'use server', + runtime: (value, name, meta) => { + validateDirectiveFunction(meta.valueNode, 'use server') + return options.runtime(value, name) + }, }) return { ...result, referenceNames: result.names } } From ac815e791edd1d67ae7f587d22e5780203e1d30d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:20:23 +0900 Subject: [PATCH 02/13] fix(rsc): preserve __proto__ directive methods Co-authored-by: OpenCode --- .../src/transforms/fixtures/hoist/methods.js | 4 ++++ .../fixtures/hoist/methods.js.snap.encode.js | 20 +++++++++++++------ .../fixtures/hoist/methods.js.snap.js | 19 ++++++++++++------ .../plugin-rsc/src/transforms/hoist.test.ts | 11 ++++++++++ packages/plugin-rsc/src/transforms/hoist.ts | 6 +++++- 5 files changed, 47 insertions(+), 13 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js index 39dec58d4..a5ae03bf7 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -10,6 +10,10 @@ export function createObject(value) { 'use server' return value + 1 }, + async __proto__() { + 'use server' + return value + 2 + }, } } diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js index c6d229a07..f1b01d2a2 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -4,13 +4,14 @@ export function createObject(value) { return { action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, __enc([value])), [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, __enc([value])), + ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, __enc([value])), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_2_action, "", "$$hoist_2_action"); + static action = /* #__PURE__ */ $$register($$hoist_3_action, "", "$$hoist_3_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_3_computed, "", "$$hoist_3_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_4_computed, "", "$$hoist_4_computed"); } ;export async function $$hoist_0_action($$hoist_encoded) { @@ -27,14 +28,21 @@ export class Actions { }; /* #__PURE__ */ Object.defineProperty($$hoist_1_key, "name", { value: "key" }); -;export async function $$hoist_2_action() { +;export async function $$hoist_2___proto__($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + 2 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_2___proto__, "name", { value: "__proto__" }); + +;export async function $$hoist_3_action() { 'use server' return 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_2_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_3_action, "name", { value: "action" }); -;export async function $$hoist_3_computed() { +;export async function $$hoist_4_computed() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_4_computed, "name", { value: "computed" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js index dcef8d979..8aed2ea01 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -4,13 +4,14 @@ export function createObject(value) { return { action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, value), [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, value), + ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, value), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_2_action, "", "$$hoist_2_action"); + static action = /* #__PURE__ */ $$register($$hoist_3_action, "", "$$hoist_3_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_3_computed, "", "$$hoist_3_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_4_computed, "", "$$hoist_4_computed"); } ;export async function $$hoist_0_action(value) { @@ -25,14 +26,20 @@ export class Actions { }; /* #__PURE__ */ Object.defineProperty($$hoist_1_key, "name", { value: "key" }); -;export async function $$hoist_2_action() { +;export async function $$hoist_2___proto__(value) { + 'use server' + return value + 2 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_2___proto__, "name", { value: "__proto__" }); + +;export async function $$hoist_3_action() { 'use server' return 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_2_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_3_action, "name", { value: "action" }); -;export async function $$hoist_3_computed() { +;export async function $$hoist_4_computed() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_4_computed, "name", { value: "computed" }); diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index c9df836d1..a1e42b1ac 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -215,6 +215,17 @@ class Actions { expect(transformed).toContain('static [key] = /* #__PURE__ */') }) + it('preserves __proto__ as an own object property', async () => { + const transformed = await testTransform(` +const object = { + async __proto__() { + "use server"; + }, +}; +`) + expect(transformed).toContain('["__proto__"]: /* #__PURE__ */') + }) + it('rejects unsupported method forms', async () => { for (const input of [ `class Actions { async action() { "use server" } }`, diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index a33ec6be3..0da7f8eb8 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -294,10 +294,14 @@ export function transformHoistInlineDirective( } if (isObjectMethod) { const key = input.slice(parent.key.start, parent.key.end) + const isProto = + (parent.key.type === 'Identifier' && + parent.key.name === '__proto__') || + (parent.key.type === 'Literal' && parent.key.value === '__proto__') output.update( parent.start, node.start, - `${parent.computed ? `[${key}]` : key}: `, + `${isProto ? '["__proto__"]' : parent.computed ? `[${key}]` : key}: `, ) } else if (isClassMethod) { const key = input.slice(parent.key.start, parent.key.end) From e539a430ed1c0cd393ce53035ed6bf9358aec269 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:20:53 +0900 Subject: [PATCH 03/13] fix(rsc): guard generated method names Co-authored-by: OpenCode --- .../src/transforms/fixtures/hoist/methods.js | 8 ++++++ .../fixtures/hoist/methods.js.snap.encode.js | 28 +++++++++++++++---- .../fixtures/hoist/methods.js.snap.js | 26 +++++++++++++---- packages/plugin-rsc/src/transforms/hoist.ts | 5 +++- 4 files changed, 54 insertions(+), 13 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js index a5ae03bf7..659f6c67a 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -14,6 +14,14 @@ export function createObject(value) { 'use server' return value + 2 }, + async 'foo-bar'() { + 'use server' + return value + 3 + }, + async 1.5() { + 'use server' + return value + 4 + }, } } diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js index f1b01d2a2..4c2334b8f 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -5,13 +5,15 @@ export function createObject(value) { action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, __enc([value])), [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, __enc([value])), ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, __enc([value])), + 'foo-bar': /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, __enc([value])), + 1.5: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, __enc([value])), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_3_action, "", "$$hoist_3_action"); + static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_4_computed, "", "$$hoist_4_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); } ;export async function $$hoist_0_action($$hoist_encoded) { @@ -35,14 +37,28 @@ export class Actions { }; /* #__PURE__ */ Object.defineProperty($$hoist_2___proto__, "name", { value: "__proto__" }); -;export async function $$hoist_3_action() { +;export async function $$hoist_3_anonymous_server_function($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + 3 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "foo-bar" }); + +;export async function $$hoist_4_anonymous_server_function($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + 4 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "1.5" }); + +;export async function $$hoist_5_action() { 'use server' return 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_5_action, "name", { value: "action" }); -;export async function $$hoist_4_computed() { +;export async function $$hoist_6_computed() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_4_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js index 8aed2ea01..7761a6693 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -5,13 +5,15 @@ export function createObject(value) { action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, value), [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, value), ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, value), + 'foo-bar': /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, value), + 1.5: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, value), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_3_action, "", "$$hoist_3_action"); + static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_4_computed, "", "$$hoist_4_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); } ;export async function $$hoist_0_action(value) { @@ -32,14 +34,26 @@ export class Actions { }; /* #__PURE__ */ Object.defineProperty($$hoist_2___proto__, "name", { value: "__proto__" }); -;export async function $$hoist_3_action() { +;export async function $$hoist_3_anonymous_server_function(value) { + 'use server' + return value + 3 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "foo-bar" }); + +;export async function $$hoist_4_anonymous_server_function(value) { + 'use server' + return value + 4 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "1.5" }); + +;export async function $$hoist_5_action() { 'use server' return 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_5_action, "name", { value: "action" }); -;export async function $$hoist_4_computed() { +;export async function $$hoist_6_computed() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_4_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 0da7f8eb8..ec12f1d40 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -222,6 +222,9 @@ export function transformHoistInlineDirective( parent.id.type === 'Identifier' && parent.id.name) || 'anonymous_server_function' + const generatedName = /^[$A-Z_a-z][$\w]*$/.test(originalName) + ? originalName + : 'anonymous_server_function' // Convert closure captures into leading parameters of the hoisted // function. At the original call site, registration below binds the @@ -249,7 +252,7 @@ export function transformHoistInlineDirective( // Rewrite and hoist the original function range into its module-level form. // These edits must happen before `.move()` (hoist) so they travel with the range. const newName = - `$$hoist_${names.length}` + (originalName ? `_${originalName}` : '') + `$$hoist_${names.length}` + (generatedName ? `_${generatedName}` : '') names.push(newName) // Hoisted runtimes need two module bindings: a private function for the // original body and a canonical binding for the runtime result. The From 68930e2f756279999b4b2828a786e5e99742e0f0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:21:11 +0900 Subject: [PATCH 04/13] fix(rsc): lower static constructor methods safely Co-authored-by: OpenCode --- .../plugin-rsc/src/transforms/fixtures/hoist/methods.js | 5 +++++ .../transforms/fixtures/hoist/methods.js.snap.encode.js | 8 ++++++++ .../src/transforms/fixtures/hoist/methods.js.snap.js | 8 ++++++++ packages/plugin-rsc/src/transforms/hoist.ts | 8 +++++++- 4 files changed, 28 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js index 659f6c67a..0c595ada5 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -35,4 +35,9 @@ export class Actions { 'use server' return 2 } + + static async constructor() { + 'use server' + return 3 + } } diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js index 4c2334b8f..24963818d 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -14,6 +14,8 @@ export class Actions { static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); + + static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); } ;export async function $$hoist_0_action($$hoist_encoded) { @@ -62,3 +64,9 @@ export class Actions { return 2 }; /* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); + +;export async function $$hoist_7_constructor() { + 'use server' + return 3 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js index 7761a6693..0f53d9f74 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -14,6 +14,8 @@ export class Actions { static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); + + static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); } ;export async function $$hoist_0_action(value) { @@ -57,3 +59,9 @@ export class Actions { return 2 }; /* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); + +;export async function $$hoist_7_constructor() { + 'use server' + return 3 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index ec12f1d40..468737cf4 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -308,10 +308,16 @@ export function transformHoistInlineDirective( ) } else if (isClassMethod) { const key = input.slice(parent.key.start, parent.key.end) + const isConstructor = + !parent.computed && + ((parent.key.type === 'Identifier' && + parent.key.name === 'constructor') || + (parent.key.type === 'Literal' && + parent.key.value === 'constructor')) output.update( parent.start, node.start, - `static ${parent.computed ? `[${key}]` : key} = `, + `static ${isConstructor ? '["constructor"]' : parent.computed ? `[${key}]` : key} = `, ) newCode += ';' } else if (declName) { From caf4ef5f7c9aad4d04bd7f2aa1c1ff8dafe224d8 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:22:33 +0900 Subject: [PATCH 05/13] fix(rsc): preserve computed method key mappings Co-authored-by: OpenCode --- .../fixtures/source-map/hoist/methods.js | 2 +- .../source-map/hoist/methods.js.map.snap.md | 16 +++---- .../source-map/hoist/methods.js.snap.md | 12 +++--- packages/plugin-rsc/src/transforms/hoist.ts | 42 +++++++++++++------ 4 files changed, 46 insertions(+), 26 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js index 2e3b9b52f..cce986ee4 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js @@ -2,7 +2,7 @@ const key = 'computed' export function createObject(value) { return { - async [key]() { + async [getKey(key, value)]() { 'use server' return value }, diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md index 6cb4a7539..36e1e565b 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md @@ -4,21 +4,23 @@ (0:0) "const key = 'computed'\n" --> (0:0) "const key = 'computed'\n" (2:0) "export function createObject(value) {\n" --> (2:0) "export function createObject(value) {\n" (3:0) " return {\n" --> (3:0) " return {\n" -(4:0) " async [key]() {\n" --> (4:0) " [key]: /* #__PURE__ */ registerServerReference($$hoist_0_key, \"$$hoist_0_key\").bind(null, encrypt([value]))" -(7:5) ",\n" --> (4:111) ",\n" +(4:0) " async [" --> (4:0) " [" +(4:11) "getKey(key, value)]() {\n" --> (4:5) "getKey(key, value)]: /* #__PURE__ */ registerServerReference($$hoist_0_anonymous_server_function, \"$$hoist_0_anonymous_server_function\").bind(null, encrypt([value]))" +(7:5) ",\n" --> (4:170) ",\n" (8:0) " }\n" --> (5:0) " }\n" (9:0) "}\n" --> (6:0) "}\n" (11:0) "export class Actions {\n" --> (8:0) "export class Actions {\n" -(12:0) " static async action() {\n" --> (9:0) " static action = /* #__PURE__ */ registerServerReference($$hoist_1_action, \"$$hoist_1_action\");\n" +(12:0) " static async " --> (9:0) " static " +(12:15) "action() {\n" --> (9:9) "action = /* #__PURE__ */ registerServerReference($$hoist_1_action, \"$$hoist_1_action\");\n" (16:0) "}\n" --> (10:0) "}\n" -(4:15) "() {\n" --> (11:0) "\n" -(4:15) "() " --> (12:0) ";export async function $$hoist_0_key($$hoist_encoded) " -(4:18) "{\n" --> (12:54) "{\n" +(4:30) "() {\n" --> (11:0) "\n" +(4:30) "() " --> (12:0) ";export async function $$hoist_0_anonymous_server_function($$hoist_encoded) " +(4:33) "{\n" --> (12:76) "{\n" (5:0) " 'use server'\n" --> (13:0) " const [value] = await decrypt($$hoist_encoded);\n" (5:6) "'use server'\n" --> (14:0) "'use server'\n" (6:0) " return value\n" --> (15:0) " return value\n" (7:0) " },\n" --> (16:0) " };\n" -[unmapped] --> (17:0) "/* #__PURE__ */ Object.defineProperty($$hoist_0_key, \"name\", { value: \"key\" });\n" +[unmapped] --> (17:0) "/* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, \"name\", { value: \"anonymous_server_function\" });\n" (12:21) "() {\n" --> (18:0) "\n" (12:21) "() " --> (19:0) ";export async function $$hoist_1_action() " (12:24) "{\n" --> (19:42) "{\n" diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md index 916c36d8b..f0298c826 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md @@ -5,7 +5,7 @@ const key = 'computed' export function createObject(value) { return { - async [key]() { + async [getKey(key, value)]() { 'use server' return value }, @@ -24,16 +24,16 @@ export class Actions { **Status:** transformed -**References:** $$hoist_0_key, $$hoist_1_action +**References:** $$hoist_0_anonymous_server_function, $$hoist_1_action -[Source map visualization](https://evanw.github.io/source-map-visualization/#NzExAGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2tleV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfa2V5LCAiJCRob2lzdF8wX2tleSIpLmJpbmQobnVsbCwgZW5jcnlwdChbdmFsdWVdKSksCiAgfQp9CgpleHBvcnQgY2xhc3MgQWN0aW9ucyB7CiAgc3RhdGljIGFjdGlvbiA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzFfYWN0aW9uLCAiJCRob2lzdF8xX2FjdGlvbiIpOwp9Cgo7ZXhwb3J0IGFzeW5jIGZ1bmN0aW9uICQkaG9pc3RfMF9rZXkoJCRob2lzdF9lbmNvZGVkKSB7CiAgICAgIGNvbnN0IFt2YWx1ZV0gPSBhd2FpdCBkZWNyeXB0KCQkaG9pc3RfZW5jb2RlZCk7Cid1c2Ugc2VydmVyJwogICAgICByZXR1cm4gdmFsdWUKICAgIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8wX2tleSwgIm5hbWUiLCB7IHZhbHVlOiAia2V5IiB9KTsKCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8xX2FjdGlvbigpIHsKICAgICd1c2Ugc2VydmVyJwogICAgcmV0dXJuIDEKICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMV9hY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFjdGlvbiIgfSk7CjgxMQB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImNvbnN0IGtleSA9ICdjb21wdXRlZCdcblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9iamVjdCh2YWx1ZSkge1xuICByZXR1cm4ge1xuICAgIGFzeW5jIFtrZXldKCkge1xuICAgICAgJ3VzZSBzZXJ2ZXInXG4gICAgICByZXR1cm4gdmFsdWVcbiAgICB9LFxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBY3Rpb25zIHtcbiAgc3RhdGljIGFzeW5jIGFjdGlvbigpIHtcbiAgICAndXNlIHNlcnZlcidcbiAgICByZXR1cm4gMVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFROztBQUVyQixNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1QsQ0FBQyxDQUFDLENBQUMsQ0FBQywyR0FHQztBQUNMLENBQUMsQ0FBQztBQUNGOztBQUVBLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDO0FBQ3JCLENBQUMsQ0FBQztBQUlGO0FBWmU7QUFBQSxzREFBRztBQUNsQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNO0FBQ2pCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUNiLENBQUMsQ0FBQyxDQUFDLENBQUM7O0FBS2lCO0FBQUEsMENBQUc7QUFDeEIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxNQUFNO0FBQ2YsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDWCxDQUFDLENBQUM7OyJ9) +[Source map visualization](https://evanw.github.io/source-map-visualization/#ODM2AGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgYWN0aW9uID0gLyogI19fUFVSRV9fICovIHJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKCQkaG9pc3RfMV9hY3Rpb24sICIkJGhvaXN0XzFfYWN0aW9uIik7Cn0KCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8wX2Fub255bW91c19zZXJ2ZXJfZnVuY3Rpb24oJCRob2lzdF9lbmNvZGVkKSB7CiAgICAgIGNvbnN0IFt2YWx1ZV0gPSBhd2FpdCBkZWNyeXB0KCQkaG9pc3RfZW5jb2RlZCk7Cid1c2Ugc2VydmVyJwogICAgICByZXR1cm4gdmFsdWUKICAgIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8wX2Fub255bW91c19zZXJ2ZXJfZnVuY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFub255bW91c19zZXJ2ZXJfZnVuY3Rpb24iIH0pOwoKO2V4cG9ydCBhc3luYyBmdW5jdGlvbiAkJGhvaXN0XzFfYWN0aW9uKCkgewogICAgJ3VzZSBzZXJ2ZXInCiAgICByZXR1cm4gMQogIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8xX2FjdGlvbiwgIm5hbWUiLCB7IHZhbHVlOiAiYWN0aW9uIiB9KTsKODczAHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3Qga2V5ID0gJ2NvbXB1dGVkJ1xuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlT2JqZWN0KHZhbHVlKSB7XG4gIHJldHVybiB7XG4gICAgYXN5bmMgW2dldEtleShrZXksIHZhbHVlKV0oKSB7XG4gICAgICAndXNlIHNlcnZlcidcbiAgICAgIHJldHVybiB2YWx1ZVxuICAgIH0sXG4gIH1cbn1cblxuZXhwb3J0IGNsYXNzIEFjdGlvbnMge1xuICBzdGF0aWMgYXN5bmMgYWN0aW9uKCkge1xuICAgICd1c2Ugc2VydmVyJ1xuICAgIHJldHVybiAxXG4gIH1cbn1cbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVE7O0FBRXJCLE1BQU0sQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3BDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDVCxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQU8sTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxtSkFHeEI7QUFDTCxDQUFDLENBQUM7QUFDRjs7QUFFQSxNQUFNLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQztBQUNyQixDQUFDLENBQUMsT0FBYTtBQUlmO0FBWjhCO0FBQUEsNEVBQUc7QUFDakMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNqQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDYixDQUFDLENBQUMsQ0FBQyxDQUFDOztBQUtpQjtBQUFBLDBDQUFHO0FBQ3hCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNmLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1gsQ0FBQyxDQUFDOzsifQ==) ```js const key = 'computed' export function createObject(value) { return { - [key]: /* #__PURE__ */ registerServerReference($$hoist_0_key, "$$hoist_0_key").bind(null, encrypt([value])), + [getKey(key, value)]: /* #__PURE__ */ registerServerReference($$hoist_0_anonymous_server_function, "$$hoist_0_anonymous_server_function").bind(null, encrypt([value])), } } @@ -41,12 +41,12 @@ export class Actions { static action = /* #__PURE__ */ registerServerReference($$hoist_1_action, "$$hoist_1_action"); } -;export async function $$hoist_0_key($$hoist_encoded) { +;export async function $$hoist_0_anonymous_server_function($$hoist_encoded) { const [value] = await decrypt($$hoist_encoded); 'use server' return value }; -/* #__PURE__ */ Object.defineProperty($$hoist_0_key, "name", { value: "key" }); +/* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_1_action() { 'use server' diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 468737cf4..93219aac7 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -296,29 +296,47 @@ export function transformHoistInlineDirective( newCode = `${newCode}.bind(null, ${bindArgs})` } if (isObjectMethod) { - const key = input.slice(parent.key.start, parent.key.end) const isProto = (parent.key.type === 'Identifier' && parent.key.name === '__proto__') || (parent.key.type === 'Literal' && parent.key.value === '__proto__') - output.update( - parent.start, - node.start, - `${isProto ? '["__proto__"]' : parent.computed ? `[${key}]` : key}: `, - ) + if (isProto) { + output.update(parent.start, node.start, '["__proto__"]: ') + } else { + output.update( + parent.start, + parent.key.start, + parent.computed ? '[' : '', + ) + const suffix = parent.computed ? ']: ' : ': ' + if (parent.key.end === node.start) { + output.appendLeft(node.start, suffix) + } else { + output.update(parent.key.end, node.start, suffix) + } + } } else if (isClassMethod) { - const key = input.slice(parent.key.start, parent.key.end) const isConstructor = !parent.computed && ((parent.key.type === 'Identifier' && parent.key.name === 'constructor') || (parent.key.type === 'Literal' && parent.key.value === 'constructor')) - output.update( - parent.start, - node.start, - `static ${isConstructor ? '["constructor"]' : parent.computed ? `[${key}]` : key} = `, - ) + if (isConstructor) { + output.update(parent.start, node.start, 'static ["constructor"] = ') + } else { + output.update( + parent.start, + parent.key.start, + parent.computed ? 'static [' : 'static ', + ) + const suffix = parent.computed ? '] = ' : ' = ' + if (parent.key.end === node.start) { + output.appendLeft(node.start, suffix) + } else { + output.update(parent.key.end, node.start, suffix) + } + } newCode += ';' } else if (declName) { // A function declaration becomes a const declaration. For a default From dc29e54ebdd2fe787a4c2978c8b64136364322fb Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:23:06 +0900 Subject: [PATCH 06/13] fix(rsc): respect nested class directive boundaries Co-authored-by: OpenCode --- .../src/transforms/directive-function.test.ts | 13 +++++++++++++ .../plugin-rsc/src/transforms/directive-function.ts | 4 +++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/transforms/directive-function.test.ts b/packages/plugin-rsc/src/transforms/directive-function.test.ts index e37cdf282..6399e93ee 100644 --- a/packages/plugin-rsc/src/transforms/directive-function.test.ts +++ b/packages/plugin-rsc/src/transforms/directive-function.test.ts @@ -75,4 +75,17 @@ async function action() { validateDirectiveFunction(undefined, 'use server'), ).not.toThrow() }) + + test('stops at nested classes', async () => { + const node = await getFunction(` +async function action() { + class Nested extends Base { + value = this.value + method() { return super.method() } + static { this.initialize() } + } +} +`) + expect(() => validateDirectiveFunction(node, 'use server')).not.toThrow() + }) }) diff --git a/packages/plugin-rsc/src/transforms/directive-function.ts b/packages/plugin-rsc/src/transforms/directive-function.ts index c39a35c25..350490f4a 100644 --- a/packages/plugin-rsc/src/transforms/directive-function.ts +++ b/packages/plugin-rsc/src/transforms/directive-function.ts @@ -30,7 +30,9 @@ export function validateDirectiveFunction( if ( child !== node && (child.type === 'FunctionDeclaration' || - child.type === 'FunctionExpression') + child.type === 'FunctionExpression' || + child.type === 'ClassDeclaration' || + child.type === 'ClassExpression') ) { this.skip() return From bd96a384fa616baecdc697c4f4a58a438996ba47 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:23:27 +0900 Subject: [PATCH 07/13] fix(rsc): narrow jsxDEV this exemption Co-authored-by: OpenCode --- .../plugin-rsc/src/transforms/directive-function.test.ts | 9 +++++++++ packages/plugin-rsc/src/transforms/directive-function.ts | 5 +++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/directive-function.test.ts b/packages/plugin-rsc/src/transforms/directive-function.test.ts index 6399e93ee..3ae4ff823 100644 --- a/packages/plugin-rsc/src/transforms/directive-function.test.ts +++ b/packages/plugin-rsc/src/transforms/directive-function.test.ts @@ -76,6 +76,15 @@ async function action() { ).not.toThrow() }) + test.each([ + `custom_jsxDEV(this)`, + `_jsxDEV(this)`, + `_jsxDEV(Component, {}, undefined, false, this, undefined)`, + ])('does not exempt user this expressions: %s', async (expression) => { + const node = await getFunction(`async function action() { ${expression} }`) + expect(() => validateDirectiveFunction(node, 'use server')).toThrow(/this/) + }) + test('stops at nested classes', async () => { const node = await getFunction(` async function action() { diff --git a/packages/plugin-rsc/src/transforms/directive-function.ts b/packages/plugin-rsc/src/transforms/directive-function.ts index 350490f4a..de199be86 100644 --- a/packages/plugin-rsc/src/transforms/directive-function.ts +++ b/packages/plugin-rsc/src/transforms/directive-function.ts @@ -72,9 +72,10 @@ function isJsxDevSourceThis(node: Node, parent: Node | null): boolean { return ( node.type === 'ThisExpression' && parent?.type === 'CallExpression' && - parent.arguments.at(-1) === node && + parent.arguments.length === 6 && + parent.arguments[5] === node && parent.callee.type === 'Identifier' && - /(?:^|_)jsxDEV$/.test(parent.callee.name) + (parent.callee.name === 'jsxDEV' || parent.callee.name === '_jsxDEV') ) } From 0e0488ae879430eb9e004a02cdc2fecf26a96798 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:23:46 +0900 Subject: [PATCH 08/13] fix(rsc): ignore arguments labels Co-authored-by: OpenCode --- .../src/transforms/directive-function.test.ts | 11 +++++++++++ .../plugin-rsc/src/transforms/directive-function.ts | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/packages/plugin-rsc/src/transforms/directive-function.test.ts b/packages/plugin-rsc/src/transforms/directive-function.test.ts index 3ae4ff823..b66ffa3c9 100644 --- a/packages/plugin-rsc/src/transforms/directive-function.test.ts +++ b/packages/plugin-rsc/src/transforms/directive-function.test.ts @@ -76,6 +76,17 @@ async function action() { ).not.toThrow() }) + test('ignores arguments labels', async () => { + const node = await getFunction(` +async function action() { + arguments: while (true) { + break arguments + } +} +`) + expect(() => validateDirectiveFunction(node, 'use server')).not.toThrow() + }) + test.each([ `custom_jsxDEV(this)`, `_jsxDEV(this)`, diff --git a/packages/plugin-rsc/src/transforms/directive-function.ts b/packages/plugin-rsc/src/transforms/directive-function.ts index de199be86..524adfe41 100644 --- a/packages/plugin-rsc/src/transforms/directive-function.ts +++ b/packages/plugin-rsc/src/transforms/directive-function.ts @@ -98,5 +98,13 @@ function isReferenceIdentifier(node: Identifier, parent: Node | null): boolean { ) { return false } + if ( + (parent.type === 'LabeledStatement' || + parent.type === 'BreakStatement' || + parent.type === 'ContinueStatement') && + parent.label === node + ) { + return false + } return true } From bb22b37f73c58ef26f709baa4590c3af7d3b6fc9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:24:04 +0900 Subject: [PATCH 09/13] test(rsc): cover static method captures Co-authored-by: OpenCode --- .../src/transforms/fixtures/hoist/methods.js | 9 +++++++++ .../fixtures/hoist/methods.js.snap.encode.js | 13 +++++++++++++ .../transforms/fixtures/hoist/methods.js.snap.js | 12 ++++++++++++ 3 files changed, 34 insertions(+) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js index 0c595ada5..2cdd7b513 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -41,3 +41,12 @@ export class Actions { return 3 } } + +export function createActions(value) { + return class Actions { + static async action() { + 'use server' + return value + } + } +} diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js index 24963818d..16e9eb33a 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -18,6 +18,12 @@ export class Actions { static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); } +export function createActions(value) { + return class Actions { + static action = /* #__PURE__ */ $$register($$hoist_8_action, "", "$$hoist_8_action").bind(null, __enc([value])); + } +} + ;export async function $$hoist_0_action($$hoist_encoded) { const [value] = __dec($$hoist_encoded); 'use server' @@ -70,3 +76,10 @@ export class Actions { return 3 }; /* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); + +;export async function $$hoist_8_action($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + }; +/* #__PURE__ */ Object.defineProperty($$hoist_8_action, "name", { value: "action" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js index 0f53d9f74..3c536d0d8 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -18,6 +18,12 @@ export class Actions { static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); } +export function createActions(value) { + return class Actions { + static action = /* #__PURE__ */ $$register($$hoist_8_action, "", "$$hoist_8_action").bind(null, value); + } +} + ;export async function $$hoist_0_action(value) { 'use server' return value @@ -65,3 +71,9 @@ export class Actions { return 3 }; /* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); + +;export async function $$hoist_8_action(value) { + 'use server' + return value + }; +/* #__PURE__ */ Object.defineProperty($$hoist_8_action, "name", { value: "action" }); From 8733ef1f1ddc08b903762f47e129595807652ee0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:27:29 +0900 Subject: [PATCH 10/13] fix(rsc): preserve computed __proto__ keys Co-authored-by: OpenCode --- packages/plugin-rsc/src/transforms/hoist.test.ts | 12 ++++++++++++ packages/plugin-rsc/src/transforms/hoist.ts | 6 ++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index a1e42b1ac..4c380c184 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -226,6 +226,18 @@ const object = { expect(transformed).toContain('["__proto__"]: /* #__PURE__ */') }) + it('preserves computed __proto__ expressions', async () => { + const transformed = await testTransform(` +const __proto__ = getKey(); +const object = { + async [__proto__]() { + "use server"; + }, +}; +`) + expect(transformed).toContain('[__proto__]: /* #__PURE__ */') + }) + it('rejects unsupported method forms', async () => { for (const input of [ `class Actions { async action() { "use server" } }`, diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 93219aac7..5f159b644 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -297,9 +297,11 @@ export function transformHoistInlineDirective( } if (isObjectMethod) { const isProto = - (parent.key.type === 'Identifier' && + !parent.computed && + ((parent.key.type === 'Identifier' && parent.key.name === '__proto__') || - (parent.key.type === 'Literal' && parent.key.value === '__proto__') + (parent.key.type === 'Literal' && + parent.key.value === '__proto__')) if (isProto) { output.update(parent.start, node.start, '["__proto__"]: ') } else { From 4a968cf35a1598910399ce6f6a56e5b174dfee30 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:27:51 +0900 Subject: [PATCH 11/13] fix(rsc): avoid inferred computed method names Co-authored-by: OpenCode --- .../src/transforms/fixtures/hoist/methods.js.snap.encode.js | 6 +++--- .../src/transforms/fixtures/hoist/methods.js.snap.js | 6 +++--- packages/plugin-rsc/src/transforms/hoist.ts | 3 ++- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js index 16e9eb33a..5f18db568 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -3,7 +3,7 @@ const key = 'computed' export function createObject(value) { return { action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, __enc([value])), - [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, __enc([value])), + [key]: /* #__PURE__ */ $$register($$hoist_1_anonymous_server_function, "", "$$hoist_1_anonymous_server_function").bind(null, __enc([value])), ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, __enc([value])), 'foo-bar': /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, __enc([value])), 1.5: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, __enc([value])), @@ -31,12 +31,12 @@ export function createActions(value) { }; /* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); -;export async function $$hoist_1_key($$hoist_encoded) { +;export async function $$hoist_1_anonymous_server_function($$hoist_encoded) { const [value] = __dec($$hoist_encoded); 'use server' return value + 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_1_key, "name", { value: "key" }); +/* #__PURE__ */ Object.defineProperty($$hoist_1_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_2___proto__($$hoist_encoded) { const [value] = __dec($$hoist_encoded); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js index 3c536d0d8..ef06d89f0 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -3,7 +3,7 @@ const key = 'computed' export function createObject(value) { return { action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, value), - [key]: /* #__PURE__ */ $$register($$hoist_1_key, "", "$$hoist_1_key").bind(null, value), + [key]: /* #__PURE__ */ $$register($$hoist_1_anonymous_server_function, "", "$$hoist_1_anonymous_server_function").bind(null, value), ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, value), 'foo-bar': /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, value), 1.5: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, value), @@ -30,11 +30,11 @@ export function createActions(value) { }; /* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); -;export async function $$hoist_1_key(value) { +;export async function $$hoist_1_anonymous_server_function(value) { 'use server' return value + 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_1_key, "name", { value: "key" }); +/* #__PURE__ */ Object.defineProperty($$hoist_1_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_2___proto__(value) { 'use server' diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 5f159b644..a73461482 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -208,7 +208,8 @@ export function transformHoistInlineDirective( const declName = node.type === 'FunctionDeclaration' && node.id.name const methodName = (isObjectMethod || isClassMethod) && - (parent.key.type === 'Identifier' || parent.key.type === 'Literal') + (parent.key.type === 'Literal' || + (!parent.computed && parent.key.type === 'Identifier')) ? String( parent.key.type === 'Identifier' ? parent.key.name From 0db3859d038e1f27da27b5e667f02e911e13d21b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:31:44 +0900 Subject: [PATCH 12/13] fix(rsc): prioritize unsupported method diagnostics Co-authored-by: OpenCode --- packages/plugin-rsc/src/transforms/hoist.test.ts | 10 ++++++++++ packages/plugin-rsc/src/transforms/hoist.ts | 16 ++++++++-------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index 4c380c184..4e339d7cb 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -83,6 +83,7 @@ describe(transformHoistInlineDirective, () => { encode?: boolean noExport?: boolean directive?: string | RegExp + rejectNonAsyncFunction?: boolean }, ) { const ast = await parseAstAsync(input) @@ -98,6 +99,7 @@ describe(transformHoistInlineDirective, () => { encode: options?.encode ? (v) => `__enc(${v})` : undefined, decode: options?.encode ? (v) => `__dec(${v})` : undefined, noExport: options?.noExport, + rejectNonAsyncFunction: options?.rejectNonAsyncFunction, }) if (!output.hasChanged()) { return @@ -249,6 +251,14 @@ const object = { } }) + it('reports unsupported methods before async policy', async () => { + await expect( + testTransform(`const actions = { get action() { "use server" } }`, { + rejectNonAsyncFunction: true, + }), + ).rejects.toThrow(/getters or setters/) + }) + it('finds directives only in directive-capable bodies', async () => { const input = ` { diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index a73461482..26300a0cb 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -157,14 +157,6 @@ export function transformHoistInlineDirective( // directive. Other function shapes cannot contain directive prologues. const match = matchDirective(node.body.body, directive)?.match if (!match) return - if (!node.async && rejectNonAsyncFunction) { - throw Object.assign( - new Error(`"${directive}" doesn't allow non async function`), - { - pos: node.start, - }, - ) - } const isObjectMethod = node.type === 'FunctionExpression' && @@ -201,6 +193,14 @@ export function transformHoistInlineDirective( { pos: parent.start }, ) } + if (!node.async && rejectNonAsyncFunction) { + throw Object.assign( + new Error(`"${directive}" doesn't allow non async function`), + { + pos: node.start, + }, + ) + } // Capture the source-level name so the hoisted function can preserve it // with Object.defineProperty below. Anonymous functions get a stable From 5ee499b4ea0168809f748fa3d3a0a9d8580e4df0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:39:07 +0900 Subject: [PATCH 13/13] refactor(rsc): split inline directive method support Co-authored-by: OpenCode --- .../src/transforms/fixtures/hoist/methods.js | 52 ---------- .../fixtures/hoist/methods.js.snap.encode.js | 85 ---------------- .../fixtures/hoist/methods.js.snap.js | 79 --------------- .../fixtures/source-map/hoist/methods.js | 17 ---- .../source-map/hoist/methods.js.map.snap.md | 31 ------ .../source-map/hoist/methods.js.snap.md | 56 ----------- .../plugin-rsc/src/transforms/hoist.test.ts | 63 ------------ packages/plugin-rsc/src/transforms/hoist.ts | 99 +------------------ 8 files changed, 2 insertions(+), 480 deletions(-) delete mode 100644 packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js delete mode 100644 packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js delete mode 100644 packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js delete mode 100644 packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js delete mode 100644 packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md delete mode 100644 packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js deleted file mode 100644 index 2cdd7b513..000000000 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ /dev/null @@ -1,52 +0,0 @@ -const key = 'computed' - -export function createObject(value) { - return { - async action() { - 'use server' - return value - }, - async [key]() { - 'use server' - return value + 1 - }, - async __proto__() { - 'use server' - return value + 2 - }, - async 'foo-bar'() { - 'use server' - return value + 3 - }, - async 1.5() { - 'use server' - return value + 4 - }, - } -} - -export class Actions { - static async action() { - 'use server' - return 1 - } - - static async ['computed']() { - 'use server' - return 2 - } - - static async constructor() { - 'use server' - return 3 - } -} - -export function createActions(value) { - return class Actions { - static async action() { - 'use server' - return value - } - } -} diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js deleted file mode 100644 index 5f18db568..000000000 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js +++ /dev/null @@ -1,85 +0,0 @@ -const key = 'computed' - -export function createObject(value) { - return { - action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, __enc([value])), - [key]: /* #__PURE__ */ $$register($$hoist_1_anonymous_server_function, "", "$$hoist_1_anonymous_server_function").bind(null, __enc([value])), - ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, __enc([value])), - 'foo-bar': /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, __enc([value])), - 1.5: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, __enc([value])), - } -} - -export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); - - static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); - - static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); -} - -export function createActions(value) { - return class Actions { - static action = /* #__PURE__ */ $$register($$hoist_8_action, "", "$$hoist_8_action").bind(null, __enc([value])); - } -} - -;export async function $$hoist_0_action($$hoist_encoded) { - const [value] = __dec($$hoist_encoded); -'use server' - return value - }; -/* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); - -;export async function $$hoist_1_anonymous_server_function($$hoist_encoded) { - const [value] = __dec($$hoist_encoded); -'use server' - return value + 1 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_1_anonymous_server_function, "name", { value: "anonymous_server_function" }); - -;export async function $$hoist_2___proto__($$hoist_encoded) { - const [value] = __dec($$hoist_encoded); -'use server' - return value + 2 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_2___proto__, "name", { value: "__proto__" }); - -;export async function $$hoist_3_anonymous_server_function($$hoist_encoded) { - const [value] = __dec($$hoist_encoded); -'use server' - return value + 3 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "foo-bar" }); - -;export async function $$hoist_4_anonymous_server_function($$hoist_encoded) { - const [value] = __dec($$hoist_encoded); -'use server' - return value + 4 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "1.5" }); - -;export async function $$hoist_5_action() { - 'use server' - return 1 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_5_action, "name", { value: "action" }); - -;export async function $$hoist_6_computed() { - 'use server' - return 2 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); - -;export async function $$hoist_7_constructor() { - 'use server' - return 3 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); - -;export async function $$hoist_8_action($$hoist_encoded) { - const [value] = __dec($$hoist_encoded); -'use server' - return value - }; -/* #__PURE__ */ Object.defineProperty($$hoist_8_action, "name", { value: "action" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js deleted file mode 100644 index ef06d89f0..000000000 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js +++ /dev/null @@ -1,79 +0,0 @@ -const key = 'computed' - -export function createObject(value) { - return { - action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, value), - [key]: /* #__PURE__ */ $$register($$hoist_1_anonymous_server_function, "", "$$hoist_1_anonymous_server_function").bind(null, value), - ["__proto__"]: /* #__PURE__ */ $$register($$hoist_2___proto__, "", "$$hoist_2___proto__").bind(null, value), - 'foo-bar': /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, value), - 1.5: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, value), - } -} - -export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); - - static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); - - static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); -} - -export function createActions(value) { - return class Actions { - static action = /* #__PURE__ */ $$register($$hoist_8_action, "", "$$hoist_8_action").bind(null, value); - } -} - -;export async function $$hoist_0_action(value) { - 'use server' - return value - }; -/* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); - -;export async function $$hoist_1_anonymous_server_function(value) { - 'use server' - return value + 1 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_1_anonymous_server_function, "name", { value: "anonymous_server_function" }); - -;export async function $$hoist_2___proto__(value) { - 'use server' - return value + 2 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_2___proto__, "name", { value: "__proto__" }); - -;export async function $$hoist_3_anonymous_server_function(value) { - 'use server' - return value + 3 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "foo-bar" }); - -;export async function $$hoist_4_anonymous_server_function(value) { - 'use server' - return value + 4 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "1.5" }); - -;export async function $$hoist_5_action() { - 'use server' - return 1 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_5_action, "name", { value: "action" }); - -;export async function $$hoist_6_computed() { - 'use server' - return 2 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); - -;export async function $$hoist_7_constructor() { - 'use server' - return 3 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); - -;export async function $$hoist_8_action(value) { - 'use server' - return value - }; -/* #__PURE__ */ Object.defineProperty($$hoist_8_action, "name", { value: "action" }); diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js deleted file mode 100644 index cce986ee4..000000000 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js +++ /dev/null @@ -1,17 +0,0 @@ -const key = 'computed' - -export function createObject(value) { - return { - async [getKey(key, value)]() { - 'use server' - return value - }, - } -} - -export class Actions { - static async action() { - 'use server' - return 1 - } -} diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md deleted file mode 100644 index 36e1e565b..000000000 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md +++ /dev/null @@ -1,31 +0,0 @@ -## hoist - -```txt -(0:0) "const key = 'computed'\n" --> (0:0) "const key = 'computed'\n" -(2:0) "export function createObject(value) {\n" --> (2:0) "export function createObject(value) {\n" -(3:0) " return {\n" --> (3:0) " return {\n" -(4:0) " async [" --> (4:0) " [" -(4:11) "getKey(key, value)]() {\n" --> (4:5) "getKey(key, value)]: /* #__PURE__ */ registerServerReference($$hoist_0_anonymous_server_function, \"$$hoist_0_anonymous_server_function\").bind(null, encrypt([value]))" -(7:5) ",\n" --> (4:170) ",\n" -(8:0) " }\n" --> (5:0) " }\n" -(9:0) "}\n" --> (6:0) "}\n" -(11:0) "export class Actions {\n" --> (8:0) "export class Actions {\n" -(12:0) " static async " --> (9:0) " static " -(12:15) "action() {\n" --> (9:9) "action = /* #__PURE__ */ registerServerReference($$hoist_1_action, \"$$hoist_1_action\");\n" -(16:0) "}\n" --> (10:0) "}\n" -(4:30) "() {\n" --> (11:0) "\n" -(4:30) "() " --> (12:0) ";export async function $$hoist_0_anonymous_server_function($$hoist_encoded) " -(4:33) "{\n" --> (12:76) "{\n" -(5:0) " 'use server'\n" --> (13:0) " const [value] = await decrypt($$hoist_encoded);\n" -(5:6) "'use server'\n" --> (14:0) "'use server'\n" -(6:0) " return value\n" --> (15:0) " return value\n" -(7:0) " },\n" --> (16:0) " };\n" -[unmapped] --> (17:0) "/* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, \"name\", { value: \"anonymous_server_function\" });\n" -(12:21) "() {\n" --> (18:0) "\n" -(12:21) "() " --> (19:0) ";export async function $$hoist_1_action() " -(12:24) "{\n" --> (19:42) "{\n" -(13:0) " 'use server'\n" --> (20:0) " 'use server'\n" -(14:0) " return 1\n" --> (21:0) " return 1\n" -(15:0) " }\n" --> (22:0) " };\n" -[unmapped] --> (23:0) "/* #__PURE__ */ Object.defineProperty($$hoist_1_action, \"name\", { value: \"action\" });\n" -``` diff --git a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md deleted file mode 100644 index f0298c826..000000000 --- a/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.snap.md +++ /dev/null @@ -1,56 +0,0 @@ -## Input - -```js -const key = 'computed' - -export function createObject(value) { - return { - async [getKey(key, value)]() { - 'use server' - return value - }, - } -} - -export class Actions { - static async action() { - 'use server' - return 1 - } -} -``` - -## hoist - -**Status:** transformed - -**References:** $$hoist_0_anonymous_server_function, $$hoist_1_action - -[Source map visualization](https://evanw.github.io/source-map-visualization/#ODM2AGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgYWN0aW9uID0gLyogI19fUFVSRV9fICovIHJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKCQkaG9pc3RfMV9hY3Rpb24sICIkJGhvaXN0XzFfYWN0aW9uIik7Cn0KCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8wX2Fub255bW91c19zZXJ2ZXJfZnVuY3Rpb24oJCRob2lzdF9lbmNvZGVkKSB7CiAgICAgIGNvbnN0IFt2YWx1ZV0gPSBhd2FpdCBkZWNyeXB0KCQkaG9pc3RfZW5jb2RlZCk7Cid1c2Ugc2VydmVyJwogICAgICByZXR1cm4gdmFsdWUKICAgIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8wX2Fub255bW91c19zZXJ2ZXJfZnVuY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFub255bW91c19zZXJ2ZXJfZnVuY3Rpb24iIH0pOwoKO2V4cG9ydCBhc3luYyBmdW5jdGlvbiAkJGhvaXN0XzFfYWN0aW9uKCkgewogICAgJ3VzZSBzZXJ2ZXInCiAgICByZXR1cm4gMQogIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8xX2FjdGlvbiwgIm5hbWUiLCB7IHZhbHVlOiAiYWN0aW9uIiB9KTsKODczAHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3Qga2V5ID0gJ2NvbXB1dGVkJ1xuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlT2JqZWN0KHZhbHVlKSB7XG4gIHJldHVybiB7XG4gICAgYXN5bmMgW2dldEtleShrZXksIHZhbHVlKV0oKSB7XG4gICAgICAndXNlIHNlcnZlcidcbiAgICAgIHJldHVybiB2YWx1ZVxuICAgIH0sXG4gIH1cbn1cblxuZXhwb3J0IGNsYXNzIEFjdGlvbnMge1xuICBzdGF0aWMgYXN5bmMgYWN0aW9uKCkge1xuICAgICd1c2Ugc2VydmVyJ1xuICAgIHJldHVybiAxXG4gIH1cbn1cbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVE7O0FBRXJCLE1BQU0sQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3BDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDVCxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQU8sTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxtSkFHeEI7QUFDTCxDQUFDLENBQUM7QUFDRjs7QUFFQSxNQUFNLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQztBQUNyQixDQUFDLENBQUMsT0FBYTtBQUlmO0FBWjhCO0FBQUEsNEVBQUc7QUFDakMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNqQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDYixDQUFDLENBQUMsQ0FBQyxDQUFDOztBQUtpQjtBQUFBLDBDQUFHO0FBQ3hCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNmLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1gsQ0FBQyxDQUFDOzsifQ==) - -```js -const key = 'computed' - -export function createObject(value) { - return { - [getKey(key, value)]: /* #__PURE__ */ registerServerReference($$hoist_0_anonymous_server_function, "$$hoist_0_anonymous_server_function").bind(null, encrypt([value])), - } -} - -export class Actions { - static action = /* #__PURE__ */ registerServerReference($$hoist_1_action, "$$hoist_1_action"); -} - -;export async function $$hoist_0_anonymous_server_function($$hoist_encoded) { - const [value] = await decrypt($$hoist_encoded); -'use server' - return value - }; -/* #__PURE__ */ Object.defineProperty($$hoist_0_anonymous_server_function, "name", { value: "anonymous_server_function" }); - -;export async function $$hoist_1_action() { - 'use server' - return 1 - }; -/* #__PURE__ */ Object.defineProperty($$hoist_1_action, "name", { value: "action" }); -``` diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index 4e339d7cb..019a2ca48 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -83,7 +83,6 @@ describe(transformHoistInlineDirective, () => { encode?: boolean noExport?: boolean directive?: string | RegExp - rejectNonAsyncFunction?: boolean }, ) { const ast = await parseAstAsync(input) @@ -99,7 +98,6 @@ describe(transformHoistInlineDirective, () => { encode: options?.encode ? (v) => `__enc(${v})` : undefined, decode: options?.encode ? (v) => `__dec(${v})` : undefined, noExport: options?.noExport, - rejectNonAsyncFunction: options?.rejectNonAsyncFunction, }) if (!output.hasChanged()) { return @@ -198,67 +196,6 @@ async function action() { expect(await testTransformNames(input)).toEqual(['$$hoist_0_action']) }) - it('preserves arbitrary computed method keys', async () => { - const input = ` -const key = getKey(); -const object = { - async [key]() { - "use server"; - }, -}; -class Actions { - static async [key]() { - "use server"; - } -} -` - const transformed = await testTransform(input) - expect(transformed).toContain('[key]: /* #__PURE__ */') - expect(transformed).toContain('static [key] = /* #__PURE__ */') - }) - - it('preserves __proto__ as an own object property', async () => { - const transformed = await testTransform(` -const object = { - async __proto__() { - "use server"; - }, -}; -`) - expect(transformed).toContain('["__proto__"]: /* #__PURE__ */') - }) - - it('preserves computed __proto__ expressions', async () => { - const transformed = await testTransform(` -const __proto__ = getKey(); -const object = { - async [__proto__]() { - "use server"; - }, -}; -`) - expect(transformed).toContain('[__proto__]: /* #__PURE__ */') - }) - - it('rejects unsupported method forms', async () => { - for (const input of [ - `class Actions { async action() { "use server" } }`, - `class Actions { static async #action() { "use server" } }`, - `const actions = { get action() { "use server" } }`, - `class Actions { static set action(value) { "use server" } }`, - ]) { - await expect(testTransform(input)).rejects.toThrow(/not allowed/) - } - }) - - it('reports unsupported methods before async policy', async () => { - await expect( - testTransform(`const actions = { get action() { "use server" } }`, { - rejectNonAsyncFunction: true, - }), - ).rejects.toThrow(/getters or setters/) - }) - it('finds directives only in directive-capable bodies', async () => { const input = ` { diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 26300a0cb..cca3d6b7b 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -157,42 +157,6 @@ export function transformHoistInlineDirective( // directive. Other function shapes cannot contain directive prologues. const match = matchDirective(node.body.body, directive)?.match if (!match) return - - const isObjectMethod = - node.type === 'FunctionExpression' && - parent?.type === 'Property' && - parent.value === node && - (parent.method || parent.kind !== 'init') - const isClassMethod = - node.type === 'FunctionExpression' && - parent?.type === 'MethodDefinition' - if (isClassMethod && !parent.static) { - throw Object.assign( - new Error( - `It is not allowed to define inline ${JSON.stringify(match[0])} class instance methods.`, - ), - { pos: parent.start }, - ) - } - if (isClassMethod && parent.key.type === 'PrivateIdentifier') { - throw Object.assign( - new Error( - `It is not allowed to define inline ${JSON.stringify(match[0])} private class methods.`, - ), - { pos: parent.start }, - ) - } - if ( - (isObjectMethod && parent.kind !== 'init') || - (isClassMethod && parent.kind !== 'method') - ) { - throw Object.assign( - new Error( - `It is not allowed to define inline ${JSON.stringify(match[0])} getters or setters.`, - ), - { pos: parent.start }, - ) - } if (!node.async && rejectNonAsyncFunction) { throw Object.assign( new Error(`"${directive}" doesn't allow non async function`), @@ -206,26 +170,12 @@ export function transformHoistInlineDirective( // with Object.defineProperty below. Anonymous functions get a stable // fallback for registration and diagnostics. const declName = node.type === 'FunctionDeclaration' && node.id.name - const methodName = - (isObjectMethod || isClassMethod) && - (parent.key.type === 'Literal' || - (!parent.computed && parent.key.type === 'Identifier')) - ? String( - parent.key.type === 'Identifier' - ? parent.key.name - : parent.key.value, - ) - : undefined const originalName = declName || - methodName || (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.id.name) || 'anonymous_server_function' - const generatedName = /^[$A-Z_a-z][$\w]*$/.test(originalName) - ? originalName - : 'anonymous_server_function' // Convert closure captures into leading parameters of the hoisted // function. At the original call site, registration below binds the @@ -253,7 +203,7 @@ export function transformHoistInlineDirective( // Rewrite and hoist the original function range into its module-level form. // These edits must happen before `.move()` (hoist) so they travel with the range. const newName = - `$$hoist_${names.length}` + (generatedName ? `_${generatedName}` : '') + `$$hoist_${names.length}` + (originalName ? `_${originalName}` : '') names.push(newName) // Hoisted runtimes need two module bindings: a private function for the // original body and a canonical binding for the runtime result. The @@ -296,52 +246,7 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (isObjectMethod) { - const isProto = - !parent.computed && - ((parent.key.type === 'Identifier' && - parent.key.name === '__proto__') || - (parent.key.type === 'Literal' && - parent.key.value === '__proto__')) - if (isProto) { - output.update(parent.start, node.start, '["__proto__"]: ') - } else { - output.update( - parent.start, - parent.key.start, - parent.computed ? '[' : '', - ) - const suffix = parent.computed ? ']: ' : ': ' - if (parent.key.end === node.start) { - output.appendLeft(node.start, suffix) - } else { - output.update(parent.key.end, node.start, suffix) - } - } - } else if (isClassMethod) { - const isConstructor = - !parent.computed && - ((parent.key.type === 'Identifier' && - parent.key.name === 'constructor') || - (parent.key.type === 'Literal' && - parent.key.value === 'constructor')) - if (isConstructor) { - output.update(parent.start, node.start, 'static ["constructor"] = ') - } else { - output.update( - parent.start, - parent.key.start, - parent.computed ? 'static [' : 'static ', - ) - const suffix = parent.computed ? '] = ' : ' = ' - if (parent.key.end === node.start) { - output.appendLeft(node.start, suffix) - } else { - output.update(parent.key.end, node.start, suffix) - } - } - newCode += ';' - } else if (declName) { + if (declName) { // A function declaration becomes a const declaration. For a default // export, retain the export as a separate statement after that const. newCode = `const ${declName} = ${newCode};`