From dd0b1cf4d9174d5bf329f0f9cc5967852518686d Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 16:38:21 +0900 Subject: [PATCH 01/20] feat(rsc): support inline directive methods 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, 480 insertions(+), 2 deletions(-) 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/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js new file mode 100644 index 000000000..2cdd7b513 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -0,0 +1,52 @@ +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 new file mode 100644 index 000000000..5f18db568 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -0,0 +1,85 @@ +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 new file mode 100644 index 000000000..ef06d89f0 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -0,0 +1,79 @@ +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 new file mode 100644 index 000000000..cce986ee4 --- /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 [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 new file mode 100644 index 000000000..36e1e565b --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md @@ -0,0 +1,31 @@ +## 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 new file mode 100644 index 000000000..f0298c826 --- /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 [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 019a2ca48..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 @@ -196,6 +198,67 @@ 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 cca3d6b7b..26300a0cb 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -157,6 +157,42 @@ 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`), @@ -170,12 +206,26 @@ 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 @@ -203,7 +253,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 @@ -246,7 +296,52 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (declName) { + 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) { // 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};` From 75099e7eaec2b3f7b318a5c9ee4fcdc71b9922e3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:10:38 +0900 Subject: [PATCH 02/20] test(rsc): refine inline method coverage Co-authored-by: OpenCode --- .../src/transforms/fixtures/hoist/methods.js | 16 +++- .../fixtures/hoist/methods.js.snap.encode.js | 49 ++++++---- .../fixtures/hoist/methods.js.snap.js | 48 ++++++---- .../plugin-rsc/src/transforms/hoist.test.ts | 94 +++++++------------ 4 files changed, 111 insertions(+), 96 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js index 2cdd7b513..ff330a21c 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -1,4 +1,5 @@ const key = 'computed' +const __proto__ = 'computed-proto' export function createObject(value) { return { @@ -14,14 +15,18 @@ export function createObject(value) { 'use server' return value + 2 }, - async 'foo-bar'() { + async [__proto__]() { 'use server' return value + 3 }, - async 1.5() { + async 'foo-bar'() { 'use server' return value + 4 }, + async 1.5() { + 'use server' + return value + 5 + }, } } @@ -36,10 +41,15 @@ export class Actions { return 2 } - static async constructor() { + static async [key]() { 'use server' return 3 } + + static async constructor() { + 'use server' + return 4 + } } export function createActions(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 5f18db568..2751af765 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 @@ -1,26 +1,30 @@ const key = 'computed' +const __proto__ = 'computed-proto' 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])), + [__proto__]: /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, __enc([value])), + 'foo-bar': /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, __enc([value])), + 1.5: /* #__PURE__ */ $$register($$hoist_5_anonymous_server_function, "", "$$hoist_5_anonymous_server_function").bind(null, __enc([value])), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); + static action = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_computed, "", "$$hoist_7_computed"); - static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); + static [key] = /* #__PURE__ */ $$register($$hoist_8_anonymous_server_function, "", "$$hoist_8_anonymous_server_function"); + + static ["constructor"] = /* #__PURE__ */ $$register($$hoist_9_constructor, "", "$$hoist_9_constructor"); } export function createActions(value) { return class Actions { - static action = /* #__PURE__ */ $$register($$hoist_8_action, "", "$$hoist_8_action").bind(null, __enc([value])); + static action = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, __enc([value])); } } @@ -50,36 +54,49 @@ export function createActions(value) { 'use server' return value + 3 }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "foo-bar" }); +/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;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" }); +/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "foo-bar" }); + +;export async function $$hoist_5_anonymous_server_function($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + 5 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_5_anonymous_server_function, "name", { value: "1.5" }); -;export async function $$hoist_5_action() { +;export async function $$hoist_6_action() { 'use server' return 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_5_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_6_action, "name", { value: "action" }); -;export async function $$hoist_6_computed() { +;export async function $$hoist_7_computed() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_7_computed, "name", { value: "computed" }); -;export async function $$hoist_7_constructor() { +;export async function $$hoist_8_anonymous_server_function() { 'use server' return 3 }; -/* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); +/* #__PURE__ */ Object.defineProperty($$hoist_8_anonymous_server_function, "name", { value: "anonymous_server_function" }); + +;export async function $$hoist_9_constructor() { + 'use server' + return 4 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_9_constructor, "name", { value: "constructor" }); -;export async function $$hoist_8_action($$hoist_encoded) { +;export async function $$hoist_10_action($$hoist_encoded) { const [value] = __dec($$hoist_encoded); 'use server' return value }; -/* #__PURE__ */ Object.defineProperty($$hoist_8_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_10_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 ef06d89f0..01fd88d5e 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 @@ -1,26 +1,30 @@ const key = 'computed' +const __proto__ = 'computed-proto' 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), + [__proto__]: /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, value), + 'foo-bar': /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, value), + 1.5: /* #__PURE__ */ $$register($$hoist_5_anonymous_server_function, "", "$$hoist_5_anonymous_server_function").bind(null, value), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_5_action, "", "$$hoist_5_action"); + static action = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_6_computed, "", "$$hoist_6_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_computed, "", "$$hoist_7_computed"); - static ["constructor"] = /* #__PURE__ */ $$register($$hoist_7_constructor, "", "$$hoist_7_constructor"); + static [key] = /* #__PURE__ */ $$register($$hoist_8_anonymous_server_function, "", "$$hoist_8_anonymous_server_function"); + + static ["constructor"] = /* #__PURE__ */ $$register($$hoist_9_constructor, "", "$$hoist_9_constructor"); } export function createActions(value) { return class Actions { - static action = /* #__PURE__ */ $$register($$hoist_8_action, "", "$$hoist_8_action").bind(null, value); + static action = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, value); } } @@ -46,34 +50,46 @@ export function createActions(value) { 'use server' return value + 3 }; -/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "foo-bar" }); +/* #__PURE__ */ Object.defineProperty($$hoist_3_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;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" }); +/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "foo-bar" }); + +;export async function $$hoist_5_anonymous_server_function(value) { + 'use server' + return value + 5 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_5_anonymous_server_function, "name", { value: "1.5" }); -;export async function $$hoist_5_action() { +;export async function $$hoist_6_action() { 'use server' return 1 }; -/* #__PURE__ */ Object.defineProperty($$hoist_5_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_6_action, "name", { value: "action" }); -;export async function $$hoist_6_computed() { +;export async function $$hoist_7_computed() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_6_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_7_computed, "name", { value: "computed" }); -;export async function $$hoist_7_constructor() { +;export async function $$hoist_8_anonymous_server_function() { 'use server' return 3 }; -/* #__PURE__ */ Object.defineProperty($$hoist_7_constructor, "name", { value: "constructor" }); +/* #__PURE__ */ Object.defineProperty($$hoist_8_anonymous_server_function, "name", { value: "anonymous_server_function" }); + +;export async function $$hoist_9_constructor() { + 'use server' + return 4 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_9_constructor, "name", { value: "constructor" }); -;export async function $$hoist_8_action(value) { +;export async function $$hoist_10_action(value) { 'use server' return value }; -/* #__PURE__ */ Object.defineProperty($$hoist_8_action, "name", { value: "action" }); +/* #__PURE__ */ Object.defineProperty($$hoist_10_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..a0448f535 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -1,7 +1,11 @@ import path from 'node:path' import { parseAstAsync } from 'vite' import { describe, expect, it } from 'vitest' -import { findDirectives, transformHoistInlineDirective } from './hoist' +import { + findDirectives, + transformHoistInlineDirective, + type TransformHoistInlineDirectiveOptions, +} from './hoist' describe('fixtures', () => { const fixtures = import.meta.glob( @@ -79,27 +83,25 @@ describe('hoistRuntime fixtures', () => { describe(transformHoistInlineDirective, () => { async function testTransform( input: string, - options?: { - encode?: boolean - noExport?: boolean - directive?: string | RegExp - rejectNonAsyncFunction?: boolean - }, + options?: Omit< + Partial, + 'runtime' | 'encode' | 'decode' + > & { encode?: boolean }, ) { + const { encode, ...transformOptions } = options ?? {} const ast = await parseAstAsync(input) const { output } = transformHoistInlineDirective(input, ast, { + ...transformOptions, runtime: (value, name, meta) => `$$register(${value}, "", ${JSON.stringify(name)}` + `${ - options?.directive instanceof RegExp + transformOptions.directive instanceof RegExp ? `, ${JSON.stringify({ directiveMatch: meta.directiveMatch })}` : '' })`, - directive: options?.directive ?? 'use server', - encode: options?.encode ? (v) => `__enc(${v})` : undefined, - decode: options?.encode ? (v) => `__dec(${v})` : undefined, - noExport: options?.noExport, - rejectNonAsyncFunction: options?.rejectNonAsyncFunction, + directive: transformOptions.directive ?? 'use server', + encode: encode ? (v) => `__enc(${v})` : undefined, + decode: encode ? (v) => `__dec(${v})` : undefined, }) if (!output.hasChanged()) { return @@ -198,57 +200,25 @@ 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 [ + it.each([ + [ `class Actions { async action() { "use server" } }`, + `It is not allowed to define inline "use server" class instance methods.`, + ], + [ `class Actions { static async #action() { "use server" } }`, + `It is not allowed to define inline "use server" private class methods.`, + ], + [ `const actions = { get action() { "use server" } }`, + `It is not allowed to define inline "use server" getters or setters.`, + ], + [ `class Actions { static set action(value) { "use server" } }`, - ]) { - await expect(testTransform(input)).rejects.toThrow(/not allowed/) - } + `It is not allowed to define inline "use server" getters or setters.`, + ], + ])('rejects unsupported method form in %s', async (input, message) => { + await expect(testTransform(input)).rejects.toThrow(message) }) it('reports unsupported methods before async policy', async () => { @@ -256,7 +226,9 @@ const object = { testTransform(`const actions = { get action() { "use server" } }`, { rejectNonAsyncFunction: true, }), - ).rejects.toThrow(/getters or setters/) + ).rejects.toThrow( + `It is not allowed to define inline "use server" getters or setters.`, + ) }) it('finds directives only in directive-capable bodies', async () => { From 72f93431b818fb9cdd9d3d3ab87b0cdf4682977c Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:11:39 +0900 Subject: [PATCH 03/20] nit --- .../plugin-rsc/src/transforms/hoist.test.ts | 69 +++++++++---------- 1 file changed, 34 insertions(+), 35 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index a0448f535..ba8b49de8 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -83,10 +83,9 @@ describe('hoistRuntime fixtures', () => { describe(transformHoistInlineDirective, () => { async function testTransform( input: string, - options?: Omit< - Partial, - 'runtime' | 'encode' | 'decode' - > & { encode?: boolean }, + options?: Omit, 'encode'> & { + encode?: boolean + }, ) { const { encode, ...transformOptions } = options ?? {} const ast = await parseAstAsync(input) @@ -200,37 +199,6 @@ async function action() { expect(await testTransformNames(input)).toEqual(['$$hoist_0_action']) }) - it.each([ - [ - `class Actions { async action() { "use server" } }`, - `It is not allowed to define inline "use server" class instance methods.`, - ], - [ - `class Actions { static async #action() { "use server" } }`, - `It is not allowed to define inline "use server" private class methods.`, - ], - [ - `const actions = { get action() { "use server" } }`, - `It is not allowed to define inline "use server" getters or setters.`, - ], - [ - `class Actions { static set action(value) { "use server" } }`, - `It is not allowed to define inline "use server" getters or setters.`, - ], - ])('rejects unsupported method form in %s', async (input, message) => { - await expect(testTransform(input)).rejects.toThrow(message) - }) - - it('reports unsupported methods before async policy', async () => { - await expect( - testTransform(`const actions = { get action() { "use server" } }`, { - rejectNonAsyncFunction: true, - }), - ).rejects.toThrow( - `It is not allowed to define inline "use server" getters or setters.`, - ) - }) - it('finds directives only in directive-capable bodies', async () => { const input = ` { @@ -336,3 +304,34 @@ export async function test() { `) }) }) + +it.each([ + [ + `class Actions { async action() { "use server" } }`, + `It is not allowed to define inline "use server" class instance methods.`, + ], + [ + `class Actions { static async #action() { "use server" } }`, + `It is not allowed to define inline "use server" private class methods.`, + ], + [ + `const actions = { get action() { "use server" } }`, + `It is not allowed to define inline "use server" getters or setters.`, + ], + [ + `class Actions { static set action(value) { "use server" } }`, + `It is not allowed to define inline "use server" getters or setters.`, + ], +])('rejects unsupported method form in %s', async (input, message) => { + await expect(testTransform(input)).rejects.toThrow(message) +}) + +it('reports unsupported methods before async policy', async () => { + await expect( + testTransform(`const actions = { get action() { "use server" } }`, { + rejectNonAsyncFunction: true, + }), + ).rejects.toThrow( + `It is not allowed to define inline "use server" getters or setters.`, + ) +}) From f8365b0f8b9bea60d7805620194c9b4aebdd008b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:12:49 +0900 Subject: [PATCH 04/20] nit --- .../plugin-rsc/src/transforms/hoist.test.ts | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index ba8b49de8..2cfaf6627 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -7,6 +7,11 @@ import { type TransformHoistInlineDirectiveOptions, } from './hoist' +type TestTransformOptions = Omit< + Partial, + 'encode' +> & { encode?: boolean } + describe('fixtures', () => { const fixtures = import.meta.glob( ['./fixtures/hoist/**/*.js', '!**/*.snap.*'], @@ -17,7 +22,7 @@ describe('fixtures', () => { async function transformFixture( input: string, - options?: { encode?: boolean }, + options?: TestTransformOptions, ) { const ast = await parseAstAsync(input) const { output } = transformHoistInlineDirective(input, ast, { @@ -81,12 +86,7 @@ describe('hoistRuntime fixtures', () => { }) describe(transformHoistInlineDirective, () => { - async function testTransform( - input: string, - options?: Omit, 'encode'> & { - encode?: boolean - }, - ) { + async function testTransform(input: string, options?: TestTransformOptions) { const { encode, ...transformOptions } = options ?? {} const ast = await parseAstAsync(input) const { output } = transformHoistInlineDirective(input, ast, { @@ -303,35 +303,35 @@ export async function test() { " `) }) -}) -it.each([ - [ - `class Actions { async action() { "use server" } }`, - `It is not allowed to define inline "use server" class instance methods.`, - ], - [ - `class Actions { static async #action() { "use server" } }`, - `It is not allowed to define inline "use server" private class methods.`, - ], - [ - `const actions = { get action() { "use server" } }`, - `It is not allowed to define inline "use server" getters or setters.`, - ], - [ - `class Actions { static set action(value) { "use server" } }`, - `It is not allowed to define inline "use server" getters or setters.`, - ], -])('rejects unsupported method form in %s', async (input, message) => { - await expect(testTransform(input)).rejects.toThrow(message) -}) + it.each([ + [ + `class Actions { async action() { "use server" } }`, + `It is not allowed to define inline "use server" class instance methods.`, + ], + [ + `class Actions { static async #action() { "use server" } }`, + `It is not allowed to define inline "use server" private class methods.`, + ], + [ + `const actions = { get action() { "use server" } }`, + `It is not allowed to define inline "use server" getters or setters.`, + ], + [ + `class Actions { static set action(value) { "use server" } }`, + `It is not allowed to define inline "use server" getters or setters.`, + ], + ])('rejects unsupported method form in %s', async (input, message) => { + await expect(testTransform(input)).rejects.toThrow(message) + }) -it('reports unsupported methods before async policy', async () => { - await expect( - testTransform(`const actions = { get action() { "use server" } }`, { - rejectNonAsyncFunction: true, - }), - ).rejects.toThrow( - `It is not allowed to define inline "use server" getters or setters.`, - ) + it('reports unsupported methods before async policy', async () => { + await expect( + testTransform(`const actions = { get action() { "use server" } }`, { + rejectNonAsyncFunction: true, + }), + ).rejects.toThrow( + `It is not allowed to define inline "use server" getters or setters.`, + ) + }) }) From 61460b799409baf1a1adcb5ed92c0c1990d310fc Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:13:24 +0900 Subject: [PATCH 05/20] nit --- packages/plugin-rsc/src/transforms/hoist.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index 2cfaf6627..9c9bf4a16 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -86,19 +86,19 @@ describe('hoistRuntime fixtures', () => { }) describe(transformHoistInlineDirective, () => { - async function testTransform(input: string, options?: TestTransformOptions) { - const { encode, ...transformOptions } = options ?? {} + async function testTransform(input: string, options_?: TestTransformOptions) { + const { encode, ...options } = options_ ?? {} const ast = await parseAstAsync(input) const { output } = transformHoistInlineDirective(input, ast, { - ...transformOptions, + ...options, runtime: (value, name, meta) => `$$register(${value}, "", ${JSON.stringify(name)}` + `${ - transformOptions.directive instanceof RegExp + options.directive instanceof RegExp ? `, ${JSON.stringify({ directiveMatch: meta.directiveMatch })}` : '' })`, - directive: transformOptions.directive ?? 'use server', + directive: options.directive ?? 'use server', encode: encode ? (v) => `__enc(${v})` : undefined, decode: encode ? (v) => `__dec(${v})` : undefined, }) From 99affdc6020710ba8c31a93fb2aec679764f70a2 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:14:43 +0900 Subject: [PATCH 06/20] nit --- packages/plugin-rsc/src/transforms/hoist.test.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index 9c9bf4a16..cd91f59ed 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -86,21 +86,20 @@ describe('hoistRuntime fixtures', () => { }) describe(transformHoistInlineDirective, () => { - async function testTransform(input: string, options_?: TestTransformOptions) { - const { encode, ...options } = options_ ?? {} + async function testTransform(input: string, options?: TestTransformOptions) { const ast = await parseAstAsync(input) const { output } = transformHoistInlineDirective(input, ast, { - ...options, + ...{ ...options, encode: undefined }, runtime: (value, name, meta) => `$$register(${value}, "", ${JSON.stringify(name)}` + `${ - options.directive instanceof RegExp + options?.directive instanceof RegExp ? `, ${JSON.stringify({ directiveMatch: meta.directiveMatch })}` : '' })`, - directive: options.directive ?? 'use server', - encode: encode ? (v) => `__enc(${v})` : undefined, - decode: encode ? (v) => `__dec(${v})` : undefined, + directive: options?.directive ?? 'use server', + encode: options?.encode ? (v) => `__enc(${v})` : undefined, + decode: options?.encode ? (v) => `__dec(${v})` : undefined, }) if (!output.hasChanged()) { return From 1aa8831e57cd7300edf737c01e82fe604976f2e0 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:31:17 +0900 Subject: [PATCH 07/20] nit --- .../transforms/fixtures/hoist/methods.js.snap.encode.js | 4 ++-- .../src/transforms/fixtures/hoist/methods.js.snap.js | 4 ++-- packages/plugin-rsc/src/transforms/hoist.ts | 9 ++++----- 3 files changed, 8 insertions(+), 9 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 2751af765..bccebe7a9 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 @@ -61,14 +61,14 @@ export function createActions(value) { 'use server' return value + 4 }; -/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "foo-bar" }); +/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_5_anonymous_server_function($$hoist_encoded) { const [value] = __dec($$hoist_encoded); 'use server' return value + 5 }; -/* #__PURE__ */ Object.defineProperty($$hoist_5_anonymous_server_function, "name", { value: "1.5" }); +/* #__PURE__ */ Object.defineProperty($$hoist_5_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_6_action() { 'use server' 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 01fd88d5e..790da1cb6 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 @@ -56,13 +56,13 @@ export function createActions(value) { 'use server' return value + 4 }; -/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "foo-bar" }); +/* #__PURE__ */ Object.defineProperty($$hoist_4_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_5_anonymous_server_function(value) { 'use server' return value + 5 }; -/* #__PURE__ */ Object.defineProperty($$hoist_5_anonymous_server_function, "name", { value: "1.5" }); +/* #__PURE__ */ Object.defineProperty($$hoist_5_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_6_action() { 'use server' diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 26300a0cb..602527d66 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -218,14 +218,13 @@ export function transformHoistInlineDirective( : undefined const originalName = declName || - methodName || + (methodName && /^[$A-Z_a-z][$\w]*$/.test(methodName) + ? methodName + : undefined) || (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 +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}` + (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 From 17db6ed95da7a6d905b6425181362c16dd6508bd Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:32:25 +0900 Subject: [PATCH 08/20] refactor(rsc): isolate method AST analysis Co-authored-by: OpenCode --- packages/plugin-rsc/src/transforms/hoist.ts | 153 ++++++++++++-------- 1 file changed, 95 insertions(+), 58 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 602527d66..458760e36 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -8,6 +8,8 @@ import type { Node, MemberExpression, Identifier, + MethodDefinition, + Property, } from 'estree' import { walk } from 'estree-walker' import MagicString from 'magic-string' @@ -158,39 +160,35 @@ export function transformHoistInlineDirective( 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) { + const method = analyzeMethod(node, parent) + if (method?.kind === 'class' && !method.node.static) { throw Object.assign( new Error( `It is not allowed to define inline ${JSON.stringify(match[0])} class instance methods.`, ), - { pos: parent.start }, + { pos: method.node.start }, ) } - if (isClassMethod && parent.key.type === 'PrivateIdentifier') { + if ( + method?.kind === 'class' && + method.node.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 }, + { pos: method.node.start }, ) } if ( - (isObjectMethod && parent.kind !== 'init') || - (isClassMethod && parent.kind !== 'method') + (method?.kind === 'object' && method.node.kind !== 'init') || + (method?.kind === 'class' && method.node.kind !== 'method') ) { throw Object.assign( new Error( `It is not allowed to define inline ${JSON.stringify(match[0])} getters or setters.`, ), - { pos: parent.start }, + { pos: method.node.start }, ) } if (!node.async && rejectNonAsyncFunction) { @@ -206,21 +204,9 @@ 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 && /^[$A-Z_a-z][$\w]*$/.test(methodName) - ? methodName - : undefined) || + method?.name || (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.id.name) || @@ -295,48 +281,44 @@ 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__"]: ') + if (method?.kind === 'object') { + if (method.forceComputed) { + output.update( + method.node.start, + node.start, + `[${JSON.stringify(method.keyName)}]: `, + ) } else { output.update( - parent.start, - parent.key.start, - parent.computed ? '[' : '', + method.node.start, + method.node.key.start, + method.node.computed ? '[' : '', ) - const suffix = parent.computed ? ']: ' : ': ' - if (parent.key.end === node.start) { + const suffix = method.node.computed ? ']: ' : ': ' + if (method.node.key.end === node.start) { output.appendLeft(node.start, suffix) } else { - output.update(parent.key.end, node.start, suffix) + output.update(method.node.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 if (method?.kind === 'class') { + if (method.forceComputed) { + output.update( + method.node.start, + node.start, + `static [${JSON.stringify(method.keyName)}] = `, + ) } else { output.update( - parent.start, - parent.key.start, - parent.computed ? 'static [' : 'static ', + method.node.start, + method.node.key.start, + method.node.computed ? 'static [' : 'static ', ) - const suffix = parent.computed ? '] = ' : ' = ' - if (parent.key.end === node.start) { + const suffix = method.node.computed ? '] = ' : ' = ' + if (method.node.key.end === node.start) { output.appendLeft(node.start, suffix) } else { - output.update(parent.key.end, node.start, suffix) + output.update(method.node.key.end, node.start, suffix) } } newCode += ';' @@ -368,6 +350,61 @@ export function transformHoistInlineDirective( } } +type Method = + | { + kind: 'object' + node: Property + } + | { + kind: 'class' + node: MethodDefinition + } + +type MethodAnalysis = Method & { + name: string | undefined + keyName: string | undefined + forceComputed: boolean +} + +function analyzeMethod( + node: Node, + parent: Node | null, +): MethodAnalysis | undefined { + if (node.type !== 'FunctionExpression') return + + let method: Method + if ( + parent?.type === 'Property' && + parent.value === node && + (parent.method || parent.kind !== 'init') + ) { + method = { kind: 'object', node: parent } + } else if (parent?.type === 'MethodDefinition') { + method = { kind: 'class', node: parent } + } else { + return + } + + const keyName = + method.node.key.type === 'Literal' || + (!method.node.computed && method.node.key.type === 'Identifier') + ? String( + method.node.key.type === 'Identifier' + ? method.node.key.name + : method.node.key.value, + ) + : undefined + return { + ...method, + name: keyName && /^[$A-Z_a-z][$\w]*$/.test(keyName) ? keyName : undefined, + keyName, + forceComputed: + !method.node.computed && + ((method.kind === 'object' && keyName === '__proto__') || + (method.kind === 'class' && keyName === 'constructor')), + } +} + function getRuntimeHoistPosition(ast: Program): number { // Preserve leading directives so directive-based transforms can // still compose just in case. From 3dad30dc50bcaca8c03e74239b6423ebacaf3d4b Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:37:56 +0900 Subject: [PATCH 09/20] refactor(rsc): normalize transformed method keys Co-authored-by: OpenCode --- .../fixtures/hoist/methods.js.snap.encode.js | 10 +-- .../fixtures/hoist/methods.js.snap.js | 10 +-- .../source-map/hoist/methods.js.map.snap.md | 4 +- .../source-map/hoist/methods.js.snap.md | 4 +- packages/plugin-rsc/src/transforms/hoist.ts | 63 +++++-------------- 5 files changed, 31 insertions(+), 60 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 bccebe7a9..2d05c9c19 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,17 +3,17 @@ const __proto__ = 'computed-proto' export function createObject(value) { return { - action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, __enc([value])), + ["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])), [__proto__]: /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, __enc([value])), - 'foo-bar': /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, __enc([value])), - 1.5: /* #__PURE__ */ $$register($$hoist_5_anonymous_server_function, "", "$$hoist_5_anonymous_server_function").bind(null, __enc([value])), + ['foo-bar']: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, __enc([value])), + [1.5]: /* #__PURE__ */ $$register($$hoist_5_anonymous_server_function, "", "$$hoist_5_anonymous_server_function").bind(null, __enc([value])), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); + static ["action"] = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_computed, "", "$$hoist_7_computed"); @@ -24,7 +24,7 @@ export class Actions { export function createActions(value) { return class Actions { - static action = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, __enc([value])); + static ["action"] = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, __enc([value])); } } 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 790da1cb6..8984d0988 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,17 +3,17 @@ const __proto__ = 'computed-proto' export function createObject(value) { return { - action: /* #__PURE__ */ $$register($$hoist_0_action, "", "$$hoist_0_action").bind(null, value), + ["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), [__proto__]: /* #__PURE__ */ $$register($$hoist_3_anonymous_server_function, "", "$$hoist_3_anonymous_server_function").bind(null, value), - 'foo-bar': /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, value), - 1.5: /* #__PURE__ */ $$register($$hoist_5_anonymous_server_function, "", "$$hoist_5_anonymous_server_function").bind(null, value), + ['foo-bar']: /* #__PURE__ */ $$register($$hoist_4_anonymous_server_function, "", "$$hoist_4_anonymous_server_function").bind(null, value), + [1.5]: /* #__PURE__ */ $$register($$hoist_5_anonymous_server_function, "", "$$hoist_5_anonymous_server_function").bind(null, value), } } export class Actions { - static action = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); + static ["action"] = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_computed, "", "$$hoist_7_computed"); @@ -24,7 +24,7 @@ export class Actions { export function createActions(value) { return class Actions { - static action = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, value); + static ["action"] = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, 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 36e1e565b..b3456c601 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 @@ -10,8 +10,8 @@ (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" +(12:0) " static async " --> (9:0) " static [\"" +(12:15) "action() {\n" --> (9:11) "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) " 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 f0298c826..a335bcdb8 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 @@ -26,7 +26,7 @@ export class Actions { **References:** $$hoist_0_anonymous_server_function, $$hoist_1_action -[Source map visualization](https://evanw.github.io/source-map-visualization/#ODM2AGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgYWN0aW9uID0gLyogI19fUFVSRV9fICovIHJlZ2lzdGVyU2VydmVyUmVmZXJlbmNlKCQkaG9pc3RfMV9hY3Rpb24sICIkJGhvaXN0XzFfYWN0aW9uIik7Cn0KCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8wX2Fub255bW91c19zZXJ2ZXJfZnVuY3Rpb24oJCRob2lzdF9lbmNvZGVkKSB7CiAgICAgIGNvbnN0IFt2YWx1ZV0gPSBhd2FpdCBkZWNyeXB0KCQkaG9pc3RfZW5jb2RlZCk7Cid1c2Ugc2VydmVyJwogICAgICByZXR1cm4gdmFsdWUKICAgIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8wX2Fub255bW91c19zZXJ2ZXJfZnVuY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFub255bW91c19zZXJ2ZXJfZnVuY3Rpb24iIH0pOwoKO2V4cG9ydCBhc3luYyBmdW5jdGlvbiAkJGhvaXN0XzFfYWN0aW9uKCkgewogICAgJ3VzZSBzZXJ2ZXInCiAgICByZXR1cm4gMQogIH07Ci8qICNfX1BVUkVfXyAqLyBPYmplY3QuZGVmaW5lUHJvcGVydHkoJCRob2lzdF8xX2FjdGlvbiwgIm5hbWUiLCB7IHZhbHVlOiAiYWN0aW9uIiB9KTsKODczAHsidmVyc2lvbiI6Mywic291cmNlcyI6WyIiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc3Qga2V5ID0gJ2NvbXB1dGVkJ1xuXG5leHBvcnQgZnVuY3Rpb24gY3JlYXRlT2JqZWN0KHZhbHVlKSB7XG4gIHJldHVybiB7XG4gICAgYXN5bmMgW2dldEtleShrZXksIHZhbHVlKV0oKSB7XG4gICAgICAndXNlIHNlcnZlcidcbiAgICAgIHJldHVybiB2YWx1ZVxuICAgIH0sXG4gIH1cbn1cblxuZXhwb3J0IGNsYXNzIEFjdGlvbnMge1xuICBzdGF0aWMgYXN5bmMgYWN0aW9uKCkge1xuICAgICd1c2Ugc2VydmVyJ1xuICAgIHJldHVybiAxXG4gIH1cbn1cbiJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiQUFBQSxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVE7O0FBRXJCLE1BQU0sQ0FBQyxRQUFRLENBQUMsWUFBWSxDQUFDLEtBQUssQ0FBQyxDQUFDO0FBQ3BDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDVCxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQU8sTUFBTSxDQUFDLEdBQUcsQ0FBQyxDQUFDLEtBQUssQ0FBQyxtSkFHeEI7QUFDTCxDQUFDLENBQUM7QUFDRjs7QUFFQSxNQUFNLENBQUMsS0FBSyxDQUFDLE9BQU8sQ0FBQztBQUNyQixDQUFDLENBQUMsT0FBYTtBQUlmO0FBWjhCO0FBQUEsNEVBQUc7QUFDakMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNqQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDYixDQUFDLENBQUMsQ0FBQyxDQUFDOztBQUtpQjtBQUFBLDBDQUFHO0FBQ3hCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNmLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1gsQ0FBQyxDQUFDOzsifQ==) +[Source map visualization](https://evanw.github.io/source-map-visualization/#ODQwAGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgWyJhY3Rpb24iXSA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzFfYWN0aW9uLCAiJCRob2lzdF8xX2FjdGlvbiIpOwp9Cgo7ZXhwb3J0IGFzeW5jIGZ1bmN0aW9uICQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uKCQkaG9pc3RfZW5jb2RlZCkgewogICAgICBjb25zdCBbdmFsdWVdID0gYXdhaXQgZGVjcnlwdCgkJGhvaXN0X2VuY29kZWQpOwondXNlIHNlcnZlcicKICAgICAgcmV0dXJuIHZhbHVlCiAgICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uLCAibmFtZSIsIHsgdmFsdWU6ICJhbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIiB9KTsKCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8xX2FjdGlvbigpIHsKICAgICd1c2Ugc2VydmVyJwogICAgcmV0dXJuIDEKICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMV9hY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFjdGlvbiIgfSk7Cjg3MwB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImNvbnN0IGtleSA9ICdjb21wdXRlZCdcblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9iamVjdCh2YWx1ZSkge1xuICByZXR1cm4ge1xuICAgIGFzeW5jIFtnZXRLZXkoa2V5LCB2YWx1ZSldKCkge1xuICAgICAgJ3VzZSBzZXJ2ZXInXG4gICAgICByZXR1cm4gdmFsdWVcbiAgICB9LFxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBY3Rpb25zIHtcbiAgc3RhdGljIGFzeW5jIGFjdGlvbigpIHtcbiAgICAndXNlIHNlcnZlcidcbiAgICByZXR1cm4gMVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFROztBQUVyQixNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1QsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFPLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxLQUFLLENBQUMsbUpBR3hCO0FBQ0wsQ0FBQyxDQUFDO0FBQ0Y7O0FBRUEsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUM7QUFDckIsQ0FBQyxDQUFDLFNBQWE7QUFJZjtBQVo4QjtBQUFBLDRFQUFHO0FBQ2pDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUFDLENBQUMsR0FBRyxDQUFDLE1BQU07QUFDakIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ2IsQ0FBQyxDQUFDLENBQUMsQ0FBQzs7QUFLaUI7QUFBQSwwQ0FBRztBQUN4QixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE1BQU07QUFDZixDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUNYLENBQUMsQ0FBQzs7In0=) ```js const key = 'computed' @@ -38,7 +38,7 @@ export function createObject(value) { } export class Actions { - static action = /* #__PURE__ */ registerServerReference($$hoist_1_action, "$$hoist_1_action"); + static ["action"] = /* #__PURE__ */ registerServerReference($$hoist_1_action, "$$hoist_1_action"); } ;export async function $$hoist_0_anonymous_server_function($$hoist_encoded) { diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 458760e36..e265d1286 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -281,47 +281,25 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (method?.kind === 'object') { - if (method.forceComputed) { - output.update( - method.node.start, - node.start, - `[${JSON.stringify(method.keyName)}]: `, - ) - } else { - output.update( - method.node.start, - method.node.key.start, - method.node.computed ? '[' : '', - ) - const suffix = method.node.computed ? ']: ' : ': ' - if (method.node.key.end === node.start) { - output.appendLeft(node.start, suffix) - } else { - output.update(method.node.key.end, node.start, suffix) - } - } - } else if (method?.kind === 'class') { - if (method.forceComputed) { - output.update( - method.node.start, - node.start, - `static [${JSON.stringify(method.keyName)}] = `, - ) + if (method) { + const quoteKey = + !method.node.computed && method.node.key.type === 'Identifier' + output.update( + method.node.start, + method.node.key.start, + `${method.kind === 'class' ? 'static ' : ''}[${ + quoteKey ? '"' : '' + }`, + ) + const suffix = `${quoteKey ? '"' : ''}]${ + method.kind === 'class' ? ' = ' : ': ' + }` + if (method.node.key.end === node.start) { + output.appendLeft(node.start, suffix) } else { - output.update( - method.node.start, - method.node.key.start, - method.node.computed ? 'static [' : 'static ', - ) - const suffix = method.node.computed ? '] = ' : ' = ' - if (method.node.key.end === node.start) { - output.appendLeft(node.start, suffix) - } else { - output.update(method.node.key.end, node.start, suffix) - } + output.update(method.node.key.end, node.start, suffix) } - newCode += ';' + if (method.kind === 'class') 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. @@ -362,8 +340,6 @@ type Method = type MethodAnalysis = Method & { name: string | undefined - keyName: string | undefined - forceComputed: boolean } function analyzeMethod( @@ -397,11 +373,6 @@ function analyzeMethod( return { ...method, name: keyName && /^[$A-Z_a-z][$\w]*$/.test(keyName) ? keyName : undefined, - keyName, - forceComputed: - !method.node.computed && - ((method.kind === 'object' && keyName === '__proto__') || - (method.kind === 'class' && keyName === 'constructor')), } } From 7ec8b3fcec1786c9c13c0972f6172f922c1125b1 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:42:08 +0900 Subject: [PATCH 10/20] refactor(rsc): use method node discriminator Co-authored-by: OpenCode --- packages/plugin-rsc/src/transforms/hoist.ts | 44 ++++++++------------- 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index e265d1286..ee6676620 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -161,7 +161,7 @@ export function transformHoistInlineDirective( if (!match) return const method = analyzeMethod(node, parent) - if (method?.kind === 'class' && !method.node.static) { + if (method?.node.type === 'MethodDefinition' && !method.node.static) { throw Object.assign( new Error( `It is not allowed to define inline ${JSON.stringify(match[0])} class instance methods.`, @@ -170,7 +170,7 @@ export function transformHoistInlineDirective( ) } if ( - method?.kind === 'class' && + method?.node.type === 'MethodDefinition' && method.node.key.type === 'PrivateIdentifier' ) { throw Object.assign( @@ -181,8 +181,9 @@ export function transformHoistInlineDirective( ) } if ( - (method?.kind === 'object' && method.node.kind !== 'init') || - (method?.kind === 'class' && method.node.kind !== 'method') + (method?.node.type === 'Property' && method.node.kind !== 'init') || + (method?.node.type === 'MethodDefinition' && + method.node.kind !== 'method') ) { throw Object.assign( new Error( @@ -287,19 +288,19 @@ export function transformHoistInlineDirective( output.update( method.node.start, method.node.key.start, - `${method.kind === 'class' ? 'static ' : ''}[${ + `${method.node.type === 'MethodDefinition' ? 'static ' : ''}[${ quoteKey ? '"' : '' }`, ) const suffix = `${quoteKey ? '"' : ''}]${ - method.kind === 'class' ? ' = ' : ': ' + method.node.type === 'MethodDefinition' ? ' = ' : ': ' }` if (method.node.key.end === node.start) { output.appendLeft(node.start, suffix) } else { output.update(method.node.key.end, node.start, suffix) } - if (method.kind === 'class') newCode += ';' + if (method.node.type === 'MethodDefinition') 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. @@ -328,17 +329,8 @@ export function transformHoistInlineDirective( } } -type Method = - | { - kind: 'object' - node: Property - } - | { - kind: 'class' - node: MethodDefinition - } - -type MethodAnalysis = Method & { +type MethodAnalysis = { + node: Property | MethodDefinition name: string | undefined } @@ -348,30 +340,28 @@ function analyzeMethod( ): MethodAnalysis | undefined { if (node.type !== 'FunctionExpression') return - let method: Method + let method: Property | MethodDefinition if ( parent?.type === 'Property' && parent.value === node && (parent.method || parent.kind !== 'init') ) { - method = { kind: 'object', node: parent } + method = parent } else if (parent?.type === 'MethodDefinition') { - method = { kind: 'class', node: parent } + method = parent } else { return } const keyName = - method.node.key.type === 'Literal' || - (!method.node.computed && method.node.key.type === 'Identifier') + method.key.type === 'Literal' || + (!method.computed && method.key.type === 'Identifier') ? String( - method.node.key.type === 'Identifier' - ? method.node.key.name - : method.node.key.value, + method.key.type === 'Identifier' ? method.key.name : method.key.value, ) : undefined return { - ...method, + node: method, name: keyName && /^[$A-Z_a-z][$\w]*$/.test(keyName) ? keyName : undefined, } } From 6e1d636b66b411262696dc564cc475f2a97ee2a9 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Thu, 6 Aug 2026 17:43:14 +0900 Subject: [PATCH 11/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index ee6676620..e455b3ada 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -300,7 +300,9 @@ export function transformHoistInlineDirective( } else { output.update(method.node.key.end, node.start, suffix) } - if (method.node.type === 'MethodDefinition') newCode += ';' + if (method.node.type === 'MethodDefinition') { + 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. @@ -331,7 +333,7 @@ export function transformHoistInlineDirective( type MethodAnalysis = { node: Property | MethodDefinition - name: string | undefined + name?: string } function analyzeMethod( From 3429d914ddfc84a6ef457962009275ca1fa72325 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:29:10 +0900 Subject: [PATCH 12/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 35 ++++++++++++++------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index e455b3ada..38a798dcf 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -275,6 +275,11 @@ export function transformHoistInlineDirective( // Replace the original function with either the hoisted runtime result // or the runtime expression for its hoisted declaration. Bind closure // captures to the prepended parameters (or one encoded parameter). + // example: + // const someFn = () => { .... } + // ⬇️ + // const someFn = __WRAP__($$hoist_0_someFn).bind(null, x, y) + // const someFn = $$hoist_0_someFn.bind(null, x, y) // with hoistRuntime let newCode = options.hoistRuntime ? newName : runtimeCode if (bindVars.length > 0) { const bindArgs = options.encode @@ -283,24 +288,32 @@ export function transformHoistInlineDirective( newCode = `${newCode}.bind(null, ${bindArgs})` } if (method) { + // example: + // { async someFn() { ... } } + // ⬇️ + // { ["someFn"]: __WRAP__($$hoist_0_someFn) } + // example: + // class C { static async someFn() { ... } } + // ⬇️ + // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } + + // always quote method name for cases like `constructor` or `__proto__` const quoteKey = !method.node.computed && method.node.key.type === 'Identifier' - output.update( - method.node.start, - method.node.key.start, - `${method.node.type === 'MethodDefinition' ? 'static ' : ''}[${ - quoteKey ? '"' : '' - }`, - ) - const suffix = `${quoteKey ? '"' : ''}]${ - method.node.type === 'MethodDefinition' ? ' = ' : ': ' - }` + + // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } + // ^^^^^^^^^ ^^^^^ + // prefix suffix + const isStatic = method.node.type === 'MethodDefinition' + const prefix = `${isStatic ? 'static ' : ''}[${quoteKey ? '"' : ''}` + const suffix = `${quoteKey ? '"' : ''}]${isStatic ? ' = ' : ': '}` + output.update(method.node.start, method.node.key.start, prefix) if (method.node.key.end === node.start) { output.appendLeft(node.start, suffix) } else { output.update(method.node.key.end, node.start, suffix) } - if (method.node.type === 'MethodDefinition') { + if (isStatic) { newCode += ';' } } else if (declName) { From 46934db28a0027a4c6703674a922415708fa0889 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:36:20 +0900 Subject: [PATCH 13/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 67 +++++++++++---------- 1 file changed, 35 insertions(+), 32 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 38a798dcf..f60b36f43 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -160,38 +160,7 @@ export function transformHoistInlineDirective( const match = matchDirective(node.body.body, directive)?.match if (!match) return - const method = analyzeMethod(node, parent) - if (method?.node.type === 'MethodDefinition' && !method.node.static) { - throw Object.assign( - new Error( - `It is not allowed to define inline ${JSON.stringify(match[0])} class instance methods.`, - ), - { pos: method.node.start }, - ) - } - if ( - method?.node.type === 'MethodDefinition' && - method.node.key.type === 'PrivateIdentifier' - ) { - throw Object.assign( - new Error( - `It is not allowed to define inline ${JSON.stringify(match[0])} private class methods.`, - ), - { pos: method.node.start }, - ) - } - if ( - (method?.node.type === 'Property' && method.node.kind !== 'init') || - (method?.node.type === 'MethodDefinition' && - method.node.kind !== 'method') - ) { - throw Object.assign( - new Error( - `It is not allowed to define inline ${JSON.stringify(match[0])} getters or setters.`, - ), - { pos: method.node.start }, - ) - } + const method = analyzeMethod(node, parent, match[0]) if (!node.async && rejectNonAsyncFunction) { throw Object.assign( new Error(`"${directive}" doesn't allow non async function`), @@ -345,13 +314,16 @@ export function transformHoistInlineDirective( } type MethodAnalysis = { + /** The object property or class method containing the function expression. */ node: Property | MethodDefinition + /** A valid identifier name inferred from a non-computed method key. */ name?: string } function analyzeMethod( node: Node, parent: Node | null, + directive: string, ): MethodAnalysis | undefined { if (node.type !== 'FunctionExpression') return @@ -368,6 +340,37 @@ function analyzeMethod( return } + if (method.type === 'MethodDefinition' && !method.static) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(directive)} class instance methods.`, + ), + { pos: method.start }, + ) + } + if ( + method.type === 'MethodDefinition' && + method.key.type === 'PrivateIdentifier' + ) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(directive)} private class methods.`, + ), + { pos: method.start }, + ) + } + if ( + (method.type === 'Property' && method.kind !== 'init') || + (method.type === 'MethodDefinition' && method.kind !== 'method') + ) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(directive)} getters or setters.`, + ), + { pos: method.start }, + ) + } + const keyName = method.key.type === 'Literal' || (!method.computed && method.key.type === 'Identifier') From e580ad3e4362b24c3f1eef781164fdc9a4295037 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:57:20 +0900 Subject: [PATCH 14/20] refactor(rsc): simplify method metadata Co-authored-by: OpenCode --- .../fixtures/hoist/methods.js.snap.encode.js | 6 +-- .../fixtures/hoist/methods.js.snap.js | 6 +-- packages/plugin-rsc/src/transforms/hoist.ts | 38 ++++++++++--------- 3 files changed, 26 insertions(+), 24 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 2d05c9c19..28bf40311 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 @@ -15,7 +15,7 @@ export function createObject(value) { export class Actions { static ["action"] = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_computed, "", "$$hoist_7_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_anonymous_server_function, "", "$$hoist_7_anonymous_server_function"); static [key] = /* #__PURE__ */ $$register($$hoist_8_anonymous_server_function, "", "$$hoist_8_anonymous_server_function"); @@ -76,11 +76,11 @@ export function createActions(value) { }; /* #__PURE__ */ Object.defineProperty($$hoist_6_action, "name", { value: "action" }); -;export async function $$hoist_7_computed() { +;export async function $$hoist_7_anonymous_server_function() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_7_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_7_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_8_anonymous_server_function() { 'use server' 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 8984d0988..b16a54610 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 @@ -15,7 +15,7 @@ export function createObject(value) { export class Actions { static ["action"] = /* #__PURE__ */ $$register($$hoist_6_action, "", "$$hoist_6_action"); - static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_computed, "", "$$hoist_7_computed"); + static ['computed'] = /* #__PURE__ */ $$register($$hoist_7_anonymous_server_function, "", "$$hoist_7_anonymous_server_function"); static [key] = /* #__PURE__ */ $$register($$hoist_8_anonymous_server_function, "", "$$hoist_8_anonymous_server_function"); @@ -70,11 +70,11 @@ export function createActions(value) { }; /* #__PURE__ */ Object.defineProperty($$hoist_6_action, "name", { value: "action" }); -;export async function $$hoist_7_computed() { +;export async function $$hoist_7_anonymous_server_function() { 'use server' return 2 }; -/* #__PURE__ */ Object.defineProperty($$hoist_7_computed, "name", { value: "computed" }); +/* #__PURE__ */ Object.defineProperty($$hoist_7_anonymous_server_function, "name", { value: "anonymous_server_function" }); ;export async function $$hoist_8_anonymous_server_function() { 'use server' diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index f60b36f43..8c13db401 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -160,7 +160,7 @@ export function transformHoistInlineDirective( const match = matchDirective(node.body.body, directive)?.match if (!match) return - const method = analyzeMethod(node, parent, match[0]) + const methodInfo = getMethodInfo(node, parent, match[0]) if (!node.async && rejectNonAsyncFunction) { throw Object.assign( new Error(`"${directive}" doesn't allow non async function`), @@ -176,7 +176,7 @@ export function transformHoistInlineDirective( const declName = node.type === 'FunctionDeclaration' && node.id.name const originalName = declName || - method?.name || + methodInfo?.name || (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.id.name) || @@ -256,7 +256,7 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (method) { + if (methodInfo) { // example: // { async someFn() { ... } } // ⬇️ @@ -268,19 +268,24 @@ export function transformHoistInlineDirective( // always quote method name for cases like `constructor` or `__proto__` const quoteKey = - !method.node.computed && method.node.key.type === 'Identifier' + !methodInfo.node.computed && + methodInfo.node.key.type === 'Identifier' // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } // ^^^^^^^^^ ^^^^^ // prefix suffix - const isStatic = method.node.type === 'MethodDefinition' + const isStatic = methodInfo.node.type === 'MethodDefinition' const prefix = `${isStatic ? 'static ' : ''}[${quoteKey ? '"' : ''}` const suffix = `${quoteKey ? '"' : ''}]${isStatic ? ' = ' : ': '}` - output.update(method.node.start, method.node.key.start, prefix) - if (method.node.key.end === node.start) { + output.update( + methodInfo.node.start, + methodInfo.node.key.start, + prefix, + ) + if (methodInfo.node.key.end === node.start) { output.appendLeft(node.start, suffix) } else { - output.update(method.node.key.end, node.start, suffix) + output.update(methodInfo.node.key.end, node.start, suffix) } if (isStatic) { newCode += ';' @@ -313,18 +318,18 @@ export function transformHoistInlineDirective( } } -type MethodAnalysis = { +type MethodInfo = { /** The object property or class method containing the function expression. */ node: Property | MethodDefinition - /** A valid identifier name inferred from a non-computed method key. */ + /** The name of a non-computed identifier method. */ name?: string } -function analyzeMethod( +function getMethodInfo( node: Node, parent: Node | null, directive: string, -): MethodAnalysis | undefined { +): MethodInfo | undefined { if (node.type !== 'FunctionExpression') return let method: Property | MethodDefinition @@ -372,15 +377,12 @@ function analyzeMethod( } const keyName = - method.key.type === 'Literal' || - (!method.computed && method.key.type === 'Identifier') - ? String( - method.key.type === 'Identifier' ? method.key.name : method.key.value, - ) + !method.computed && method.key.type === 'Identifier' + ? method.key.name : undefined return { node: method, - name: keyName && /^[$A-Z_a-z][$\w]*$/.test(keyName) ? keyName : undefined, + name: keyName, } } From 8c445cc91d26d99308a9c7d6fa6a5014a679e0f3 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:02:49 +0900 Subject: [PATCH 15/20] test(rsc): cover method arguments Co-authored-by: OpenCode --- packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js | 4 ++-- .../src/transforms/fixtures/hoist/methods.js.snap.encode.js | 4 ++-- .../src/transforms/fixtures/hoist/methods.js.snap.js | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js index ff330a21c..7095ec62d 100644 --- a/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -3,9 +3,9 @@ const __proto__ = 'computed-proto' export function createObject(value) { return { - async action() { + async action(arg) { 'use server' - return value + return value + arg }, async [key]() { 'use server' 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 28bf40311..4438cd5de 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 @@ -28,10 +28,10 @@ export function createActions(value) { } } -;export async function $$hoist_0_action($$hoist_encoded) { +;export async function $$hoist_0_action($$hoist_encoded, arg) { const [value] = __dec($$hoist_encoded); 'use server' - return value + return value + arg }; /* #__PURE__ */ Object.defineProperty($$hoist_0_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 b16a54610..8404ec0ed 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 @@ -28,9 +28,9 @@ export function createActions(value) { } } -;export async function $$hoist_0_action(value) { +;export async function $$hoist_0_action(value, arg) { 'use server' - return value + return value + arg }; /* #__PURE__ */ Object.defineProperty($$hoist_0_action, "name", { value: "action" }); From 1bdc081122c719becd34e92f267e36d857bce9d6 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:12:11 +0900 Subject: [PATCH 16/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 25 +++++++++++---------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 8c13db401..ce34fcf35 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -271,22 +271,17 @@ export function transformHoistInlineDirective( !methodInfo.node.computed && methodInfo.node.key.type === 'Identifier' - // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } - // ^^^^^^^^^ ^^^^^ - // prefix suffix const isStatic = methodInfo.node.type === 'MethodDefinition' - const prefix = `${isStatic ? 'static ' : ''}[${quoteKey ? '"' : ''}` - const suffix = `${quoteKey ? '"' : ''}]${isStatic ? ' = ' : ': '}` + const key = input.slice( + methodInfo.node.key.start, + methodInfo.node.key.end, + ) + const propertyKey = quoteKey ? `["${key}"]` : `[${key}]` output.update( methodInfo.node.start, - methodInfo.node.key.start, - prefix, + node.start, + `${isStatic ? 'static ' : ''}${propertyKey}${isStatic ? ' = ' : ': '}`, ) - if (methodInfo.node.key.end === node.start) { - output.appendLeft(node.start, suffix) - } else { - output.update(methodInfo.node.key.end, node.start, suffix) - } if (isStatic) { newCode += ';' } @@ -345,6 +340,8 @@ function getMethodInfo( return } + // Next.js error fixture 25 rejects directive-bearing instance methods: + // https://github.com/vercel/next.js/tree/153bf8ac5fa00888ef5fbb2b65cac12f0942a44f/crates/next-custom-transforms/tests/errors/server-actions/server-graph/25 if (method.type === 'MethodDefinition' && !method.static) { throw Object.assign( new Error( @@ -353,6 +350,8 @@ function getMethodInfo( { pos: method.start }, ) } + // No Next.js error fixture covers this. Private methods cannot be replaced + // with public callable fields. if ( method.type === 'MethodDefinition' && method.key.type === 'PrivateIdentifier' @@ -364,6 +363,8 @@ function getMethodInfo( { pos: method.start }, ) } + // No Next.js error fixture covers this. Accessors cannot be replaced with + // value properties without changing their contract. if ( (method.type === 'Property' && method.kind !== 'init') || (method.type === 'MethodDefinition' && method.kind !== 'method') From e336e8795e45e7fa5341188f8631073b13defb44 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:12:41 +0900 Subject: [PATCH 17/20] test(rsc): update method source maps Co-authored-by: OpenCode --- .../fixtures/source-map/hoist/methods.js.map.snap.md | 6 ++---- .../transforms/fixtures/source-map/hoist/methods.js.snap.md | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) 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 b3456c601..94813d869 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,14 +4,12 @@ (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]))" +(4:0) " async [getKey(key, value)]() {\n" --> (4:0) " [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:11) "action\"] = /* #__PURE__ */ registerServerReference($$hoist_1_action, \"$$hoist_1_action\");\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:30) "() {\n" --> (11:0) "\n" (4:30) "() " --> (12:0) ";export async function $$hoist_0_anonymous_server_function($$hoist_encoded) " 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 a335bcdb8..9e8173f4b 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 @@ -26,7 +26,7 @@ export class Actions { **References:** $$hoist_0_anonymous_server_function, $$hoist_1_action -[Source map visualization](https://evanw.github.io/source-map-visualization/#ODQwAGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgWyJhY3Rpb24iXSA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzFfYWN0aW9uLCAiJCRob2lzdF8xX2FjdGlvbiIpOwp9Cgo7ZXhwb3J0IGFzeW5jIGZ1bmN0aW9uICQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uKCQkaG9pc3RfZW5jb2RlZCkgewogICAgICBjb25zdCBbdmFsdWVdID0gYXdhaXQgZGVjcnlwdCgkJGhvaXN0X2VuY29kZWQpOwondXNlIHNlcnZlcicKICAgICAgcmV0dXJuIHZhbHVlCiAgICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uLCAibmFtZSIsIHsgdmFsdWU6ICJhbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIiB9KTsKCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8xX2FjdGlvbigpIHsKICAgICd1c2Ugc2VydmVyJwogICAgcmV0dXJuIDEKICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMV9hY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFjdGlvbiIgfSk7Cjg3MwB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImNvbnN0IGtleSA9ICdjb21wdXRlZCdcblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9iamVjdCh2YWx1ZSkge1xuICByZXR1cm4ge1xuICAgIGFzeW5jIFtnZXRLZXkoa2V5LCB2YWx1ZSldKCkge1xuICAgICAgJ3VzZSBzZXJ2ZXInXG4gICAgICByZXR1cm4gdmFsdWVcbiAgICB9LFxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBY3Rpb25zIHtcbiAgc3RhdGljIGFzeW5jIGFjdGlvbigpIHtcbiAgICAndXNlIHNlcnZlcidcbiAgICByZXR1cm4gMVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFROztBQUVyQixNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1QsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFPLE1BQU0sQ0FBQyxHQUFHLENBQUMsQ0FBQyxLQUFLLENBQUMsbUpBR3hCO0FBQ0wsQ0FBQyxDQUFDO0FBQ0Y7O0FBRUEsTUFBTSxDQUFDLEtBQUssQ0FBQyxPQUFPLENBQUM7QUFDckIsQ0FBQyxDQUFDLFNBQWE7QUFJZjtBQVo4QjtBQUFBLDRFQUFHO0FBQ2pDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUFDLENBQUMsR0FBRyxDQUFDLE1BQU07QUFDakIsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ2IsQ0FBQyxDQUFDLENBQUMsQ0FBQzs7QUFLaUI7QUFBQSwwQ0FBRztBQUN4QixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsR0FBRyxDQUFDLE1BQU07QUFDZixDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQztBQUNYLENBQUMsQ0FBQzs7In0=) +[Source map visualization](https://evanw.github.io/source-map-visualization/#ODQwAGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgWyJhY3Rpb24iXSA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzFfYWN0aW9uLCAiJCRob2lzdF8xX2FjdGlvbiIpOwp9Cgo7ZXhwb3J0IGFzeW5jIGZ1bmN0aW9uICQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uKCQkaG9pc3RfZW5jb2RlZCkgewogICAgICBjb25zdCBbdmFsdWVdID0gYXdhaXQgZGVjcnlwdCgkJGhvaXN0X2VuY29kZWQpOwondXNlIHNlcnZlcicKICAgICAgcmV0dXJuIHZhbHVlCiAgICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uLCAibmFtZSIsIHsgdmFsdWU6ICJhbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIiB9KTsKCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8xX2FjdGlvbigpIHsKICAgICd1c2Ugc2VydmVyJwogICAgcmV0dXJuIDEKICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMV9hY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFjdGlvbiIgfSk7CjgyNwB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImNvbnN0IGtleSA9ICdjb21wdXRlZCdcblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9iamVjdCh2YWx1ZSkge1xuICByZXR1cm4ge1xuICAgIGFzeW5jIFtnZXRLZXkoa2V5LCB2YWx1ZSldKCkge1xuICAgICAgJ3VzZSBzZXJ2ZXInXG4gICAgICByZXR1cm4gdmFsdWVcbiAgICB9LFxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBY3Rpb25zIHtcbiAgc3RhdGljIGFzeW5jIGFjdGlvbigpIHtcbiAgICAndXNlIHNlcnZlcidcbiAgICByZXR1cm4gMVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFROztBQUVyQixNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1QsQ0FBQyxDQUFDLENBQUMsQ0FBQyxzS0FHQztBQUNMLENBQUMsQ0FBQztBQUNGOztBQUVBLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDO0FBQ3JCLENBQUMsQ0FBQztBQUlGO0FBWjhCO0FBQUEsNEVBQUc7QUFDakMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNqQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDYixDQUFDLENBQUMsQ0FBQyxDQUFDOztBQUtpQjtBQUFBLDBDQUFHO0FBQ3hCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNmLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1gsQ0FBQyxDQUFDOzsifQ==) ```js const key = 'computed' From 8c9fcd799e1bf617f0c1d71fa8d88ae005459777 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:13:53 +0900 Subject: [PATCH 18/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index ce34fcf35..8604e498a 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -266,7 +266,7 @@ export function transformHoistInlineDirective( // ⬇️ // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } - // always quote method name for cases like `constructor` or `__proto__` + // always quote identifier method name for cases like `constructor` or `__proto__` const quoteKey = !methodInfo.node.computed && methodInfo.node.key.type === 'Identifier' From 91672c26a9dcf993517ed5b4109cf3b6c1e39d79 Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:16:34 +0900 Subject: [PATCH 19/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 8604e498a..8884481a0 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -270,7 +270,6 @@ export function transformHoistInlineDirective( const quoteKey = !methodInfo.node.computed && methodInfo.node.key.type === 'Identifier' - const isStatic = methodInfo.node.type === 'MethodDefinition' const key = input.slice( methodInfo.node.key.start, From 489cc16f20e0f90da680adae7f220b4a93ad8fcb Mon Sep 17 00:00:00 2001 From: Hiroshi Ogawa <4232207+hi-ogawa@users.noreply.github.com> Date: Fri, 7 Aug 2026 13:25:49 +0900 Subject: [PATCH 20/20] nit --- packages/plugin-rsc/src/transforms/hoist.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 8884481a0..956eb96b4 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -266,16 +266,17 @@ export function transformHoistInlineDirective( // ⬇️ // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } - // always quote identifier method name for cases like `constructor` or `__proto__` - const quoteKey = - !methodInfo.node.computed && - methodInfo.node.key.type === 'Identifier' const isStatic = methodInfo.node.type === 'MethodDefinition' const key = input.slice( methodInfo.node.key.start, methodInfo.node.key.end, ) - const propertyKey = quoteKey ? `["${key}"]` : `[${key}]` + // always quote identifier method name for edge cases like `constructor` or `__proto__` + const propertyKey = + !methodInfo.node.computed && + methodInfo.node.key.type === 'Identifier' + ? `["${key}"]` + : `[${key}]` output.update( methodInfo.node.start, node.start,