diff --git a/packages/plugin-rsc/src/transforms/hoist.test.ts b/packages/plugin-rsc/src/transforms/hoist.test.ts index 3dde6bc91..35f2ba661 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -508,4 +508,54 @@ export async function test() { " `) }) + + it('supports object and static class methods', async () => { + const input = ` +const object = { + async cached() { "use server"; return 1 }, + async ["computed"]() { "use server"; return 2 }, +}; +class Actions { + static async cached() { "use server"; return 3 } + static async ["computed"]() { "use server"; return 4 } +} +` + const transformed = await testTransform(input) + expect(transformed).toMatchInlineSnapshot(` + " + const object = { + cached: /* #__PURE__ */ $$register($$hoist_0_cached, "", "$$hoist_0_cached"), + "computed": /* #__PURE__ */ $$register($$hoist_1_computed, "", "$$hoist_1_computed"), + }; + class Actions { + static cached = /* #__PURE__ */ $$register($$hoist_2_cached, "", "$$hoist_2_cached"); + static ["computed"] = /* #__PURE__ */ $$register($$hoist_3_computed, "", "$$hoist_3_computed"); + } + + ;export async function $$hoist_0_cached() { "use server"; return 1 }; + /* #__PURE__ */ Object.defineProperty($$hoist_0_cached, "name", { value: "cached" }); + + ;export async function $$hoist_1_computed() { "use server"; return 2 }; + /* #__PURE__ */ Object.defineProperty($$hoist_1_computed, "name", { value: "computed" }); + + ;export async function $$hoist_2_cached() { "use server"; return 3 }; + /* #__PURE__ */ Object.defineProperty($$hoist_2_cached, "name", { value: "cached" }); + + ;export async function $$hoist_3_computed() { "use server"; return 4 }; + /* #__PURE__ */ Object.defineProperty($$hoist_3_computed, "name", { value: "computed" }); + " + `) + await parseAstAsync(transformed!) + }) + + 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"; return 1 } }`, + `class Actions { static set action(value) { "use server" } }`, + ]) { + await expect(testTransform(input)).rejects.toThrow(/not allowed/) + } + }) }) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index 8d1665a70..f738e2c1d 100644 --- a/packages/plugin-rsc/src/transforms/hoist.ts +++ b/packages/plugin-rsc/src/transforms/hoist.ts @@ -65,12 +65,63 @@ export function transformHoistInlineDirective( ) } + const isClassMethod = + node.type === 'FunctionExpression' && + parent?.type === 'MethodDefinition' + if (isClassMethod) { + if (!parent.static) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(match[0])} annotated class instance methods. Use a function, object method property, or static class method instead.`, + ), + { pos: parent.start }, + ) + } + if (parent.key.type === 'PrivateIdentifier') { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(match[0])} annotated private class methods.`, + ), + { pos: parent.start }, + ) + } + } + + const isObjectMethod = + node.type === 'FunctionExpression' && + parent?.type === 'Property' && + (parent.method || parent.kind !== 'init') + 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])} annotated getters or setters.`, + ), + { pos: parent.start }, + ) + } + const declName = node.type === 'FunctionDeclaration' && node.id.name + const expressionName = + node.type === 'FunctionExpression' ? node.id?.name : undefined + const methodName = + (isObjectMethod || isClassMethod) && + (parent.key.type === 'Identifier' || parent.key.type === 'Literal') + ? String( + parent.key.type === 'Identifier' + ? parent.key.name + : parent.key.value, + ) + : undefined const originalName = declName || + methodName || (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.id.name) || + expressionName || 'anonymous_server_function' const bindVars = getBindVars(node, scopeTree) @@ -120,7 +171,19 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (declName) { + if (isObjectMethod) { + output.update( + parent.start, + node.start, + `${input.slice(parent.key.start, parent.key.end)}: `, + ) + } else if (isClassMethod) { + const key = parent.computed + ? `[${input.slice(parent.key.start, parent.key.end)}]` + : input.slice(parent.key.start, parent.key.end) + output.update(parent.start, node.start, `static ${key} = `) + newCode += ';' + } else if (declName) { newCode = `const ${declName} = ${newCode};` if (parent?.type === 'ExportDefaultDeclaration') { output.remove(parent.start, node.start)