Skip to content
Open
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
1 change: 1 addition & 0 deletions changes/jsx-async-directive.fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- JSX v4: keep `@directive` on an async component's own function instead of its generated sync wrapper, so `@react.component let make = @directive("'use cache'") async (...) => ...` compiles to an async function starting with `'use cache'`, which Next.js accepts.
25 changes: 20 additions & 5 deletions compiler/syntax/src/jsx_v4.ml
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,15 @@ let filename_from_loc (pstr_loc : Location.t) =
file_name

(* Build a string representation of a module name with segments separated by $ *)
(* An async component's code runs in the inner [make]; the generated wrapper
only returns its promise. A directive ('use cache', 'use server') has to sit
on the async function, so it stays on [make]. A sync wrapper has [make]
inlined into it, so there the directive stays on the wrapper. Returns the
wrapper's attributes and [make]'s. *)
let split_directive_attrs ~async attrs =
if async then List.partition (fun ({txt}, _) -> txt <> "directive") attrs
else (attrs, [])

let make_module_name file_name nested_modules fn_name =
let full_module_name =
match (file_name, nested_modules, fn_name) with
Expand Down Expand Up @@ -602,6 +611,9 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
modified_binding ~binding_loc ~binding_pat_loc ~fn_name binding
in
let is_async = Ast_async.dig_async_payload_from_function binding.pvb_expr in
let wrapper_attrs, make_attrs =
split_directive_attrs ~async:is_async binding.pvb_expr.pexp_attributes
in
let named_arg_list, newtypes, _typeConstraints =
recursively_transform_named_args_for_make
(modified_binding_old binding)
Expand Down Expand Up @@ -652,7 +664,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
]
else [props_param]
in
Exp.fun_ ~attrs:binding.pvb_expr.pexp_attributes params inner_expression
Exp.fun_ ~attrs:wrapper_attrs params inner_expression
in
let full_expression =
match full_module_name with
Expand Down Expand Up @@ -801,7 +813,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
| _ -> [Typ.any ()]))))
in
(* Add the collected newtypes (type a b c) to make's definition *)
Exp.fun_ ~async:is_async ~newtypes:(List.rev newtypes)
Exp.fun_ ~attrs:make_attrs ~async:is_async ~newtypes:(List.rev newtypes)
(props_param :: nolabel_params)
expression
in
Expand All @@ -825,6 +837,9 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
let is_async =
Ast_async.dig_async_payload_from_function modified_binding.pvb_expr
in
let wrapper_attrs, make_attrs =
split_directive_attrs ~async:is_async binding.pvb_expr.pexp_attributes
in

let make_new_binding ~loc ~full_module_name binding =
let props_pattern =
Expand Down Expand Up @@ -884,7 +899,7 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
in
let applied_expression = constrain_jsx_return applied_expression in
let wrapper_expr =
Exp.fun_ ~attrs:binding.pvb_expr.pexp_attributes
Exp.fun_ ~attrs:wrapper_attrs
[Exp.fun_param Nolabel props_pattern]
applied_expression
in
Expand All @@ -908,8 +923,8 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name binding =
let binding_expr =
{
binding.pvb_expr with
(* moved to wrapper_expr *)
pexp_attributes = [];
(* moved to wrapper_expr, but for a directive on an async component *)
pexp_attributes = make_attrs;
}
in
( None,
Expand Down
19 changes: 19 additions & 0 deletions tests/syntax_tests/data/ppx/react/asyncDirective.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module Async = {
@react.component
let make =
@directive("'use cache';") async (~id: string) => <div>{React.string(id)}</div>
}

module AsyncWithProps = {
type props = {id: string}

@react.componentWithProps
let make =
@directive("'use cache';") async (props: props) => <div>{React.string(props.id)}</div>
}

module Sync = {
@react.component
let make =
@directive("'use no memo';") (~id: string) => <div>{React.string(id)}</div>
}
44 changes: 44 additions & 0 deletions tests/syntax_tests/data/ppx/react/expected/asyncDirective.res.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Async = {
@res.jsxComponentProps
type props<'id> = {
id: 'id,
}

let make =
@directive("'use cache';") async ({id, _}: props<string>): React.element =>
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.string(id)})})

let make = React.component({
let \"AsyncDirective$Async" = (props: props<_>) => Jsx.promise(make(props))
\"AsyncDirective$Async"
})
}

module AsyncWithProps = {
type props = {id: string}

let make =
@directive("'use cache';") async (props: props): React.element =>
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.string(props.id)})})

let make = React.component({
let \"AsyncDirective$AsyncWithProps" = (props: props): React.element => Jsx.promise(make(props))
\"AsyncDirective$AsyncWithProps"
})
}

module Sync = {
@res.jsxComponentProps
type props<'id> = {
id: 'id,
}

let make = ({id, _}: props<string>): React.element =>
ReactDOM.jsx("div", {children: ?ReactDOM.someElement({React.string(id)})})

let make = React.component({
let \"AsyncDirective$Sync" = @directive("'use no memo';") (props: props<_>) => make(props)

\"AsyncDirective$Sync"
})
}
56 changes: 56 additions & 0 deletions tests/tests/src/async_jsx_directive.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Generated by ReScript, PLEASE EDIT WITH CARE

import * as JsxRuntime from "react/jsx-runtime";

async function make(param) {
'use cache';
return JsxRuntime.jsx("div", {
children: param.id
});
}

let Async_jsx_directive$Cached = make;

let Cached = {
make: Async_jsx_directive$Cached
};

async function make$1(props) {
'use cache';
return JsxRuntime.jsx("div", {
children: props.id
});
}

let Async_jsx_directive$CachedWithProps = make$1;

let CachedWithProps = {
make: Async_jsx_directive$CachedWithProps
};

function Async_jsx_directive$NoMemo(props) {
'use no memo';
return JsxRuntime.jsx("div", {
children: props.id
});
}

let NoMemo = {
make: Async_jsx_directive$NoMemo
};

let Cached$make = Async_jsx_directive$Cached;

let CachedWithProps$make = Async_jsx_directive$CachedWithProps;

let NoMemo$make = Async_jsx_directive$NoMemo;

export {
Cached,
CachedWithProps,
NoMemo,
Cached$make,
CachedWithProps$make,
NoMemo$make,
}
/* react/jsx-runtime Not a pure module */
28 changes: 28 additions & 0 deletions tests/tests/src/async_jsx_directive.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
@@config({
flags: ["-bs-jsx", "4"],
})

// A directive on an async component belongs on the async function itself,
// not on the generated sync wrapper: 'use cache' on a sync function is
// rejected by Next.js.

module Cached = {
@react.component
let make =
@directive("'use cache'") async (~id: string) => <div>{React.string(id)}</div>
}

module CachedWithProps = {
type props = {id: string}

@react.componentWithProps
let make =
@directive("'use cache'") async (props: props) => <div>{React.string(props.id)}</div>
}

// A sync component's wrapper inlines its body, so it keeps the directive.
module NoMemo = {
@react.component
let make =
@directive("'use no memo'") (~id: string) => <div>{React.string(id)}</div>
}
Loading