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..7095ec62d --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js @@ -0,0 +1,62 @@ +const key = 'computed' +const __proto__ = 'computed-proto' + +export function createObject(value) { + return { + async action(arg) { + 'use server' + return value + arg + }, + async [key]() { + 'use server' + return value + 1 + }, + async __proto__() { + 'use server' + return value + 2 + }, + async [__proto__]() { + 'use server' + return value + 3 + }, + async 'foo-bar'() { + 'use server' + return value + 4 + }, + async 1.5() { + 'use server' + return value + 5 + }, + } +} + +export class Actions { + static async action() { + 'use server' + return 1 + } + + static async ['computed']() { + 'use server' + return 2 + } + + static async [key]() { + 'use server' + return 3 + } + + static async constructor() { + 'use server' + return 4 + } +} + +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..4438cd5de --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.encode.js @@ -0,0 +1,102 @@ +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])), + [__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_6_action, "", "$$hoist_6_action"); + + 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"); + + static ["constructor"] = /* #__PURE__ */ $$register($$hoist_9_constructor, "", "$$hoist_9_constructor"); +} + +export function createActions(value) { + return class Actions { + static ["action"] = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, __enc([value])); + } +} + +;export async function $$hoist_0_action($$hoist_encoded, arg) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + arg + }; +/* #__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: "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: "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: "anonymous_server_function" }); + +;export async function $$hoist_6_action() { + 'use server' + return 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_6_action, "name", { value: "action" }); + +;export async function $$hoist_7_anonymous_server_function() { + 'use server' + return 2 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_7_anonymous_server_function, "name", { value: "anonymous_server_function" }); + +;export async function $$hoist_8_anonymous_server_function() { + 'use server' + return 3 + }; +/* #__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_10_action($$hoist_encoded) { + const [value] = __dec($$hoist_encoded); +'use server' + return value + }; +/* #__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 new file mode 100644 index 000000000..8404ec0ed --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/hoist/methods.js.snap.js @@ -0,0 +1,95 @@ +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), + [__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_6_action, "", "$$hoist_6_action"); + + 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"); + + static ["constructor"] = /* #__PURE__ */ $$register($$hoist_9_constructor, "", "$$hoist_9_constructor"); +} + +export function createActions(value) { + return class Actions { + static ["action"] = /* #__PURE__ */ $$register($$hoist_10_action, "", "$$hoist_10_action").bind(null, value); + } +} + +;export async function $$hoist_0_action(value, arg) { + 'use server' + return value + arg + }; +/* #__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: "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: "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: "anonymous_server_function" }); + +;export async function $$hoist_6_action() { + 'use server' + return 1 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_6_action, "name", { value: "action" }); + +;export async function $$hoist_7_anonymous_server_function() { + 'use server' + return 2 + }; +/* #__PURE__ */ Object.defineProperty($$hoist_7_anonymous_server_function, "name", { value: "anonymous_server_function" }); + +;export async function $$hoist_8_anonymous_server_function() { + 'use server' + return 3 + }; +/* #__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_10_action(value) { + 'use server' + return value + }; +/* #__PURE__ */ Object.defineProperty($$hoist_10_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..94813d869 --- /dev/null +++ b/packages/plugin-rsc/src/transforms/fixtures/source-map/hoist/methods.js.map.snap.md @@ -0,0 +1,29 @@ +## hoist + +```txt +(0:0) "const key = 'computed'\n" --> (0:0) "const key = 'computed'\n" +(2:0) "export function createObject(value) {\n" --> (2:0) "export function createObject(value) {\n" +(3:0) " return {\n" --> (3:0) " return {\n" +(4:0) " async [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 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) " +(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..9e8173f4b --- /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/#ODQwAGNvbnN0IGtleSA9ICdjb21wdXRlZCcKCmV4cG9ydCBmdW5jdGlvbiBjcmVhdGVPYmplY3QodmFsdWUpIHsKICByZXR1cm4gewogICAgW2dldEtleShrZXksIHZhbHVlKV06IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzBfYW5vbnltb3VzX3NlcnZlcl9mdW5jdGlvbiwgIiQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIikuYmluZChudWxsLCBlbmNyeXB0KFt2YWx1ZV0pKSwKICB9Cn0KCmV4cG9ydCBjbGFzcyBBY3Rpb25zIHsKICBzdGF0aWMgWyJhY3Rpb24iXSA9IC8qICNfX1BVUkVfXyAqLyByZWdpc3RlclNlcnZlclJlZmVyZW5jZSgkJGhvaXN0XzFfYWN0aW9uLCAiJCRob2lzdF8xX2FjdGlvbiIpOwp9Cgo7ZXhwb3J0IGFzeW5jIGZ1bmN0aW9uICQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uKCQkaG9pc3RfZW5jb2RlZCkgewogICAgICBjb25zdCBbdmFsdWVdID0gYXdhaXQgZGVjcnlwdCgkJGhvaXN0X2VuY29kZWQpOwondXNlIHNlcnZlcicKICAgICAgcmV0dXJuIHZhbHVlCiAgICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMF9hbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uLCAibmFtZSIsIHsgdmFsdWU6ICJhbm9ueW1vdXNfc2VydmVyX2Z1bmN0aW9uIiB9KTsKCjtleHBvcnQgYXN5bmMgZnVuY3Rpb24gJCRob2lzdF8xX2FjdGlvbigpIHsKICAgICd1c2Ugc2VydmVyJwogICAgcmV0dXJuIDEKICB9OwovKiAjX19QVVJFX18gKi8gT2JqZWN0LmRlZmluZVByb3BlcnR5KCQkaG9pc3RfMV9hY3Rpb24sICJuYW1lIiwgeyB2YWx1ZTogImFjdGlvbiIgfSk7CjgyNwB7InZlcnNpb24iOjMsInNvdXJjZXMiOlsiIl0sInNvdXJjZXNDb250ZW50IjpbImNvbnN0IGtleSA9ICdjb21wdXRlZCdcblxuZXhwb3J0IGZ1bmN0aW9uIGNyZWF0ZU9iamVjdCh2YWx1ZSkge1xuICByZXR1cm4ge1xuICAgIGFzeW5jIFtnZXRLZXkoa2V5LCB2YWx1ZSldKCkge1xuICAgICAgJ3VzZSBzZXJ2ZXInXG4gICAgICByZXR1cm4gdmFsdWVcbiAgICB9LFxuICB9XG59XG5cbmV4cG9ydCBjbGFzcyBBY3Rpb25zIHtcbiAgc3RhdGljIGFzeW5jIGFjdGlvbigpIHtcbiAgICAndXNlIHNlcnZlcidcbiAgICByZXR1cm4gMVxuICB9XG59XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsQ0FBQyxRQUFROztBQUVyQixNQUFNLENBQUMsUUFBUSxDQUFDLFlBQVksQ0FBQyxLQUFLLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1QsQ0FBQyxDQUFDLENBQUMsQ0FBQyxzS0FHQztBQUNMLENBQUMsQ0FBQztBQUNGOztBQUVBLE1BQU0sQ0FBQyxLQUFLLENBQUMsT0FBTyxDQUFDO0FBQ3JCLENBQUMsQ0FBQztBQUlGO0FBWjhCO0FBQUEsNEVBQUc7QUFDakMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNqQixDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxNQUFNLENBQUM7QUFDYixDQUFDLENBQUMsQ0FBQyxDQUFDOztBQUtpQjtBQUFBLDBDQUFHO0FBQ3hCLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxHQUFHLENBQUMsTUFBTTtBQUNmLENBQUMsQ0FBQyxDQUFDLENBQUMsTUFBTSxDQUFDO0FBQ1gsQ0FBQyxDQUFDOzsifQ==) + +```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..cd91f59ed 100644 --- a/packages/plugin-rsc/src/transforms/hoist.test.ts +++ b/packages/plugin-rsc/src/transforms/hoist.test.ts @@ -1,7 +1,16 @@ 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' + +type TestTransformOptions = Omit< + Partial, + 'encode' +> & { encode?: boolean } describe('fixtures', () => { const fixtures = import.meta.glob( @@ -13,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, { @@ -77,16 +86,10 @@ describe('hoistRuntime fixtures', () => { }) describe(transformHoistInlineDirective, () => { - async function testTransform( - input: string, - options?: { - encode?: boolean - noExport?: boolean - directive?: string | RegExp - }, - ) { + async function testTransform(input: string, options?: TestTransformOptions) { const ast = await parseAstAsync(input) const { output } = transformHoistInlineDirective(input, ast, { + ...{ ...options, encode: undefined }, runtime: (value, name, meta) => `$$register(${value}, "", ${JSON.stringify(name)}` + `${ @@ -97,7 +100,6 @@ describe(transformHoistInlineDirective, () => { directive: options?.directive ?? 'use server', encode: options?.encode ? (v) => `__enc(${v})` : undefined, decode: options?.encode ? (v) => `__dec(${v})` : undefined, - noExport: options?.noExport, }) if (!output.hasChanged()) { return @@ -300,4 +302,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('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.`, + ) + }) }) diff --git a/packages/plugin-rsc/src/transforms/hoist.ts b/packages/plugin-rsc/src/transforms/hoist.ts index cca3d6b7b..956eb96b4 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' @@ -157,6 +159,8 @@ export function transformHoistInlineDirective( // directive. Other function shapes cannot contain directive prologues. const match = matchDirective(node.body.body, directive)?.match if (!match) return + + const methodInfo = getMethodInfo(node, parent, match[0]) if (!node.async && rejectNonAsyncFunction) { throw Object.assign( new Error(`"${directive}" doesn't allow non async function`), @@ -172,6 +176,7 @@ export function transformHoistInlineDirective( const declName = node.type === 'FunctionDeclaration' && node.id.name const originalName = declName || + methodInfo?.name || (parent?.type === 'VariableDeclarator' && parent.id.type === 'Identifier' && parent.id.name) || @@ -239,6 +244,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 @@ -246,7 +256,36 @@ export function transformHoistInlineDirective( : bindVars.map((b) => b.expr).join(', ') newCode = `${newCode}.bind(null, ${bindArgs})` } - if (declName) { + if (methodInfo) { + // example: + // { async someFn() { ... } } + // ⬇️ + // { ["someFn"]: __WRAP__($$hoist_0_someFn) } + // example: + // class C { static async someFn() { ... } } + // ⬇️ + // class C { static ["someFn"] = __WRAP__($$hoist_0_someFn); } + + const isStatic = methodInfo.node.type === 'MethodDefinition' + const key = input.slice( + methodInfo.node.key.start, + methodInfo.node.key.end, + ) + // 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, + `${isStatic ? 'static ' : ''}${propertyKey}${isStatic ? ' = ' : ': '}`, + ) + if (isStatic) { + 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};` @@ -274,6 +313,80 @@ export function transformHoistInlineDirective( } } +type MethodInfo = { + /** The object property or class method containing the function expression. */ + node: Property | MethodDefinition + /** The name of a non-computed identifier method. */ + name?: string +} + +function getMethodInfo( + node: Node, + parent: Node | null, + directive: string, +): MethodInfo | undefined { + if (node.type !== 'FunctionExpression') return + + let method: Property | MethodDefinition + if ( + parent?.type === 'Property' && + parent.value === node && + (parent.method || parent.kind !== 'init') + ) { + method = parent + } else if (parent?.type === 'MethodDefinition') { + method = parent + } else { + 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( + `It is not allowed to define inline ${JSON.stringify(directive)} class instance methods.`, + ), + { 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' + ) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(directive)} private class methods.`, + ), + { 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') + ) { + throw Object.assign( + new Error( + `It is not allowed to define inline ${JSON.stringify(directive)} getters or setters.`, + ), + { pos: method.start }, + ) + } + + const keyName = + !method.computed && method.key.type === 'Identifier' + ? method.key.name + : undefined + return { + node: method, + name: keyName, + } +} + function getRuntimeHoistPosition(ast: Program): number { // Preserve leading directives so directive-based transforms can // still compose just in case.