Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle server function directives in class methods #73060

Merged
merged 1 commit into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 90 additions & 9 deletions crates/next-custom-transforms/src/transforms/server_actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,9 +67,15 @@ enum ServerActionsErrorKind {
span: Span,
directive: Directive,
},
InlineUseCacheInClassInstanceMethod {
span: Span,
},
InlineUseCacheInClientComponent {
span: Span,
},
InlineUseServerInClassInstanceMethod {
span: Span,
},
InlineUseServerInClientComponent {
span: Span,
},
Expand Down Expand Up @@ -903,10 +909,9 @@ impl<C: Comments> ServerActions<C> {

impl<C: Comments> VisitMut for ServerActions<C> {
fn visit_mut_export_decl(&mut self, decl: &mut ExportDecl) {
let old = self.in_exported_expr;
self.in_exported_expr = true;
let old_in_exported_expr = replace(&mut self.in_exported_expr, true);
decl.decl.visit_mut_with(self);
self.in_exported_expr = old;
self.in_exported_expr = old_in_exported_expr;
}

fn visit_mut_export_default_decl(&mut self, decl: &mut ExportDefaultDecl) {
Expand Down Expand Up @@ -1231,12 +1236,11 @@ impl<C: Comments> VisitMut for ServerActions<C> {
self.in_exported_expr = old_in_exported_expr;
self.this_status = old_this_status;

if let Some(expr) = &self.rewrite_expr_to_proxy_expr {
if let Some(expr) = self.rewrite_expr_to_proxy_expr.take() {
*n = PropOrSpread::Prop(Box::new(Prop::KeyValue(KeyValueProp {
key,
value: expr.clone(),
value: expr,
})));
self.rewrite_expr_to_proxy_expr = None;
}

return;
Expand All @@ -1259,6 +1263,66 @@ impl<C: Comments> VisitMut for ServerActions<C> {
self.in_exported_expr = old_in_exported_expr;
}

fn visit_mut_class_member(&mut self, n: &mut ClassMember) {
if let ClassMember::Method(ClassMethod {
is_abstract: false,
is_static: true,
kind: MethodKind::Method,
key,
span,
accessibility: None | Some(Accessibility::Public),
..
}) = n
{
let key = key.clone();
let span = *span;
let old_arrow_or_fn_expr_ident = self.arrow_or_fn_expr_ident.clone();

if let PropName::Ident(ident_name) = &key {
self.arrow_or_fn_expr_ident = Some(ident_name.clone().into());
}

let old_this_status = replace(&mut self.this_status, ThisStatus::Allowed);
self.rewrite_expr_to_proxy_expr = None;
self.in_exported_expr = false;
n.visit_mut_children_with(self);
self.this_status = old_this_status;
self.arrow_or_fn_expr_ident = old_arrow_or_fn_expr_ident;

if let Some(expr) = self.rewrite_expr_to_proxy_expr.take() {
*n = ClassMember::ClassProp(ClassProp {
span,
key,
value: Some(expr),
is_static: true,
..Default::default()
});
}
} else {
n.visit_mut_children_with(self);
}
}

fn visit_mut_class_method(&mut self, n: &mut ClassMethod) {
if n.is_static {
n.visit_mut_children_with(self);
} else {
let (is_action_fn, is_cache_fn) = has_body_directive(&n.function.body);

if is_action_fn {
emit_error(
ServerActionsErrorKind::InlineUseServerInClassInstanceMethod { span: n.span },
);
} else if is_cache_fn {
emit_error(
ServerActionsErrorKind::InlineUseCacheInClassInstanceMethod { span: n.span },
);
} else {
n.visit_mut_children_with(self);
}
}
}

fn visit_mut_call_expr(&mut self, n: &mut CallExpr) {
if let Callee::Expr(box Expr::Ident(Ident { sym, .. })) = &mut n.callee {
if sym == "jsxDEV" || sym == "_jsxDEV" {
Expand Down Expand Up @@ -1304,9 +1368,8 @@ impl<C: Comments> VisitMut for ServerActions<C> {

self.rewrite_expr_to_proxy_expr = None;
n.visit_mut_children_with(self);
if let Some(expr) = &self.rewrite_expr_to_proxy_expr {
*n = (**expr).clone();
self.rewrite_expr_to_proxy_expr = None;
if let Some(expr) = self.rewrite_expr_to_proxy_expr.take() {
*n = *expr;
}
}

Expand Down Expand Up @@ -2850,6 +2913,15 @@ fn emit_error(error_kind: ServerActionsErrorKind) {
}
},
),
ServerActionsErrorKind::InlineUseCacheInClassInstanceMethod { span } => (
span,
formatdoc! {
r#"
It is not allowed to define inline "use cache" annotated class instance methods.
To define cached functions, use functions, object method properties, or static class methods instead.
"#
},
),
ServerActionsErrorKind::InlineUseCacheInClientComponent { span } => (
span,
formatdoc! {
Expand All @@ -2859,6 +2931,15 @@ fn emit_error(error_kind: ServerActionsErrorKind) {
"#
},
),
ServerActionsErrorKind::InlineUseServerInClassInstanceMethod { span } => (
span,
formatdoc! {
r#"
It is not allowed to define inline "use server" annotated class instance methods.
To define Server Actions, use functions, object method properties, or static class methods instead.
"#
},
),
ServerActionsErrorKind::InlineUseServerInClientComponent { span } => (
span,
formatdoc! {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class MyClass {
async foo() {
'use cache'

return fetch('https://example.com').then((res) => res.json())
}
async bar() {
'use server'

console.log(42)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export class MyClass {
async foo() {
'use cache';
return fetch('https://example.com').then((res)=>res.json());
}
async bar() {
'use server';
console.log(42);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
x It is not allowed to define inline "use cache" annotated class instance methods.
| To define cached functions, use functions, object method properties, or static class methods instead.
|
,-[input.js:2:1]
1 | export class MyClass {
2 | ,-> async foo() {
3 | | 'use cache'
4 | |
5 | | return fetch('https://example.com').then((res) => res.json())
6 | `-> }
7 | async bar() {
`----
x It is not allowed to define inline "use server" annotated class instance methods.
| To define Server Actions, use functions, object method properties, or static class methods instead.
|
,-[input.js:7:1]
6 | }
7 | ,-> async bar() {
8 | | 'use server'
9 | |
10 | | console.log(42)
11 | `-> }
12 | }
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export class MyClass {
static async foo() {
return fetch('https://example.com').then((res) => res.json())
}
static async bar() {
'use cache'

// arguments is not allowed here
console.log(arguments)
// this is not allowed here
return this.foo()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/* __next_internal_action_entry_do_not_use__ {"803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference";
import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption";
import { cache as $$cache__ } from "private-next-rsc-cache-wrapper";
export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function bar() {
// arguments is not allowed here
console.log(arguments);
// this is not allowed here
return this.foo();
});
Object.defineProperty($$RSC_SERVER_CACHE_0, "name", {
"value": "bar",
"writable": false
});
export class MyClass {
static async foo() {
return fetch('https://example.com').then((res)=>res.json());
}
static bar = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
x "use cache" functions cannot use `arguments`.
|
,-[input.js:9:1]
8 | // arguments is not allowed here
9 | console.log(arguments)
: ^^^^^^^^^
10 | // this is not allowed here
`----
x "use cache" functions cannot use `this`.
|
,-[input.js:11:1]
10 | // this is not allowed here
11 | return this.foo()
: ^^^^
12 | }
`----
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export class MyClass {
static async foo() {
'use cache'

return fetch('https://example.com').then((res) => res.json())
}
static async bar() {
'use server'

console.log(42)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* __next_internal_action_entry_do_not_use__ {"0090b5db271335765a4b0eab01f044b381b5ebd5cd":"$$RSC_SERVER_ACTION_1","803128060c414d59f8552e4788b846c0d2b7f74743":"$$RSC_SERVER_CACHE_0"} */ import { registerServerReference } from "private-next-rsc-server-reference";
import { encryptActionBoundArgs, decryptActionBoundArgs } from "private-next-rsc-action-encryption";
import { cache as $$cache__ } from "private-next-rsc-cache-wrapper";
export var $$RSC_SERVER_CACHE_0 = $$cache__("default", "803128060c414d59f8552e4788b846c0d2b7f74743", 0, /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ async function foo() {
return fetch('https://example.com').then((res)=>res.json());
});
Object.defineProperty($$RSC_SERVER_CACHE_0, "name", {
"value": "foo",
"writable": false
});
export const /*#__TURBOPACK_DISABLE_EXPORT_MERGING__*/ $$RSC_SERVER_ACTION_1 = async function bar() {
console.log(42);
};
export class MyClass {
static foo = registerServerReference($$RSC_SERVER_CACHE_0, "803128060c414d59f8552e4788b846c0d2b7f74743", null);
static bar = registerServerReference($$RSC_SERVER_ACTION_1, "0090b5db271335765a4b0eab01f044b381b5ebd5cd", null);
}
8 changes: 8 additions & 0 deletions test/e2e/app-dir/use-cache/app/static-class-method/cached.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export class Cached {
static async getRandomValue() {
'use cache'
const v = Math.random()
console.log(v)
return v
}
}
18 changes: 18 additions & 0 deletions test/e2e/app-dir/use-cache/app/static-class-method/form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
'use client'

import { useActionState } from 'react'

export function Form({
getRandomValue,
}: {
getRandomValue: () => Promise<number>
}) {
const [result, formAction, isPending] = useActionState(getRandomValue, -1)

return (
<form action={formAction}>
<button>Submit</button>
<p>{isPending ? 'loading...' : result}</p>
</form>
)
}
6 changes: 6 additions & 0 deletions test/e2e/app-dir/use-cache/app/static-class-method/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { Cached } from './cached'
import { Form } from './form'

export default function Page() {
return <Form getRandomValue={Cached.getRandomValue} />
}
21 changes: 21 additions & 0 deletions test/e2e/app-dir/use-cache/use-cache.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,4 +431,25 @@ describe('use-cache', () => {
expect(await browser.elementByCss('#form-2 p').text()).toBe(value2)
})
})

it('works with "use cache" in static class methods', async () => {
const browser = await next.browser('/static-class-method')

let value = await browser.elementByCss('p').text()

expect(value).toBe('-1')

await browser.elementByCss('button').click()

await retry(async () => {
value = await browser.elementByCss('p').text()
expect(value).toMatch(/\d\.\d+/)
})

await browser.elementByCss('button').click()

await retry(async () => {
expect(await browser.elementByCss('p').text()).toBe(value)
})
})
})
Loading