From add21c27780ece2868bbd5387603115a0528ba85 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 12 Mar 2026 18:46:42 +0100 Subject: [PATCH 01/21] Rewrite nested JSX component paths to direct hoisted exports --- compiler/core/js_of_lam_block.ml | 2 +- compiler/core/lam.ml | 6 +- compiler/core/lam_analysis.ml | 7 +- compiler/core/lam_arity_analysis.ml | 4 +- compiler/core/lam_compat.ml | 2 +- compiler/core/lam_compat.mli | 2 +- compiler/core/lam_compile.ml | 122 ++++++++++++++++++ compiler/core/lam_pass_remove_alias.ml | 3 +- compiler/core/lam_print.ml | 2 +- compiler/ml/lambda.ml | 26 +++- compiler/ml/lambda.mli | 5 +- compiler/ml/printlambda.ml | 2 +- compiler/ml/translcore.ml | 9 +- compiler/ml/translmod.ml | 15 ++- compiler/syntax/src/jsx_common.ml | 1 + compiler/syntax/src/jsx_ppx.ml | 9 ++ compiler/syntax/src/jsx_v4.ml | 41 +++++- .../rsc_nested_jsx_members/input.js | 38 ++++++ .../rsc_nested_jsx_members/rescript.json | 16 +++ .../rsc_nested_jsx_members/src/MainLayout.res | 4 + .../rsc_nested_jsx_members/src/React.res | 30 +++++ .../rsc_nested_jsx_members/src/Sidebar.res | 13 ++ tests/tests/src/alias_default_value_test.mjs | 28 ++++ tests/tests/src/async_jsx.mjs | 8 ++ tests/tests/src/jsx_optional_props_test.mjs | 4 + tests/tests/src/jsxv4_newtype.mjs | 16 +++ 26 files changed, 396 insertions(+), 19 deletions(-) create mode 100644 tests/build_tests/rsc_nested_jsx_members/input.js create mode 100644 tests/build_tests/rsc_nested_jsx_members/rescript.json create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/MainLayout.res create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/React.res create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/Sidebar.res diff --git a/compiler/core/js_of_lam_block.ml b/compiler/core/js_of_lam_block.ml index 850e8ad2f94..bfc943d99f5 100644 --- a/compiler/core/js_of_lam_block.ml +++ b/compiler/core/js_of_lam_block.ml @@ -43,7 +43,7 @@ let field (field_info : Lam_compat.field_dbg_info) e (i : int32) = | Fld_cons -> E.cons_access e i | Fld_record_inline {name} -> E.inline_record_access e name i | Fld_record {name} -> E.record_access e name i - | Fld_module {name} -> E.module_access e name i + | Fld_module {name; jsx_component = _} -> E.module_access e name i let field_by_exp e i = E.array_index e i diff --git a/compiler/core/lam.ml b/compiler/core/lam.ml index 51b8bb3e382..3eb1b227845 100644 --- a/compiler/core/lam.ml +++ b/compiler/core/lam.ml @@ -555,7 +555,8 @@ let prim ~primitive:(prim : Lam_primitive.t) ~args loc : t = | ( f :: fields, Lprim { - primitive = Pfield (pos, Fld_module {name = f1}); + primitive = + Pfield (pos, Fld_module {name = f1; jsx_component = false}); args = [(Lglobal_module (v1, _) | Lvar v1)]; } :: args ) -> @@ -566,7 +567,8 @@ let prim ~primitive:(prim : Lam_primitive.t) ~args loc : t = | ( field1 :: rest, Lprim { - primitive = Pfield (pos, Fld_module {name = f1}); + primitive = + Pfield (pos, Fld_module {name = f1; jsx_component = false}); args = [((Lglobal_module (v1, _) | Lvar v1) as lam)]; } :: args1 ) -> diff --git a/compiler/core/lam_analysis.ml b/compiler/core/lam_analysis.ml index a4b78bea0eb..90f4c6c4cf9 100644 --- a/compiler/core/lam_analysis.ml +++ b/compiler/core/lam_analysis.ml @@ -123,7 +123,12 @@ let rec no_side_effects (lam : Lam.t) : bool = (* | Lsend _ -> false *) | Lapply { - ap_func = Lprim {primitive = Pfield (_, Fld_module {name = "from_fun"})}; + ap_func = + Lprim + { + primitive = + Pfield (_, Fld_module {name = "from_fun"; jsx_component = _}); + }; ap_args = [arg]; } -> no_side_effects arg diff --git a/compiler/core/lam_arity_analysis.ml b/compiler/core/lam_arity_analysis.ml index ee3d91f1541..c3608365049 100644 --- a/compiler/core/lam_arity_analysis.ml +++ b/compiler/core/lam_arity_analysis.ml @@ -42,7 +42,7 @@ let rec get_arity (meta : Lam_stats.t) (lam : Lam.t) : Lam_arity.t = | Llet (_, _, _, l) -> get_arity meta l | Lprim { - primitive = Pfield (_, Fld_module {name}); + primitive = Pfield (_, Fld_module {name; jsx_component = _}); args = [Lglobal_module (id, dynamic_import)]; _; } -> ( @@ -58,7 +58,7 @@ let rec get_arity (meta : Lam_stats.t) (lam : Lam.t) : Lam_arity.t = [ Lprim { - primitive = Pfield (_, Fld_module {name}); + primitive = Pfield (_, Fld_module {name; jsx_component = _}); args = [Lglobal_module (id, dynamic_import)]; }; ]; diff --git a/compiler/core/lam_compat.ml b/compiler/core/lam_compat.ml index a652d74ca43..70c3486c5e9 100644 --- a/compiler/core/lam_compat.ml +++ b/compiler/core/lam_compat.ml @@ -64,7 +64,7 @@ type let_kind = Lambda.let_kind = Strict | Alias | StrictOpt | Variable type field_dbg_info = Lambda.field_dbg_info = | Fld_record of {name: string; mutable_flag: Asttypes.mutable_flag} - | Fld_module of {name: string} + | Fld_module of {name: string; jsx_component: bool} | Fld_record_inline of {name: string} | Fld_record_extension of {name: string} | Fld_tuple diff --git a/compiler/core/lam_compat.mli b/compiler/core/lam_compat.mli index 4d67d95242e..cbea5cd5ea8 100644 --- a/compiler/core/lam_compat.mli +++ b/compiler/core/lam_compat.mli @@ -28,7 +28,7 @@ type let_kind = Lambda.let_kind = Strict | Alias | StrictOpt | Variable type field_dbg_info = Lambda.field_dbg_info = | Fld_record of {name: string; mutable_flag: Asttypes.mutable_flag} - | Fld_module of {name: string} + | Fld_module of {name: string; jsx_component: bool} | Fld_record_inline of {name: string} | Fld_record_extension of {name: string} | Fld_tuple diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 42844bc99c0..966fd29e59c 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -232,6 +232,64 @@ type initialization = J.block *) let compile output_prefix = + let root_module_name (id : Ident.t) = + match String.index_opt id.name '$' with + | Some index -> String.sub id.name 0 index + | None -> id.name + in + let rec extract_nested_external_component_segments segments + ((lam : Lam.t), (make_dynamic_import : bool option ref)) : + (Ident.t * bool * string list) option = + match lam with + | Lprim + { + primitive = Pfield (_, Fld_module {name; jsx_component = _}); + args = [arg]; + _; + } -> + extract_nested_external_component_segments (name :: segments) + (arg, make_dynamic_import) + | Lvar id -> + make_dynamic_import := Some false; + Some (id, false, List.rev segments) + | Lglobal_module (id, dynamic_import) -> + make_dynamic_import := Some dynamic_import; + Some (id, dynamic_import, List.rev segments) + | _ -> None + in + let extract_nested_external_component_field (lam : Lam.t) : + (Ident.t * bool * string) option = + match lam with + | Lprim + { + primitive = Pfield (_, Fld_module {name = "make"; jsx_component = _}); + args = [arg]; + _; + } -> ( + let dynamic_import = ref None in + match + extract_nested_external_component_segments [] (arg, dynamic_import) + with + | Some (id, dynamic_import, segments) -> ( + let segments = + match segments with + | head :: rest + when head = id.name + || head = root_module_name id + || Ext_string.starts_with head (root_module_name id ^ "$") -> + rest + | _ -> segments + in + match segments with + | [] -> None + | _ -> + Some + ( id, + dynamic_import, + String.concat "$" (root_module_name id :: segments) )) + | None -> None) + | _ -> None + in let rec compile_external_field (* Like [List.empty]*) ?(dynamic_import = false) (lamba_cxt : Lam_compile_context.t) (id : Ident.t) name : Js_output.t = @@ -300,6 +358,17 @@ let compile output_prefix = (Ext_list.append block args_code, b :: args) | _ -> assert false) in + let args = + if appinfo.ap_transformed_jsx then + match (appinfo.ap_args, args) with + | jsx_tag :: _, _ :: rest_args -> ( + match extract_nested_external_component_field jsx_tag with + | Some (id, dynamic_import, hidden_name) -> + E.ml_var_dot ~dynamic_import id hidden_name :: rest_args + | None -> args) + | _ -> args + else args + in let fn = E.ml_var_dot ~dynamic_import module_id ident_info.name in let expression = @@ -1524,6 +1593,17 @@ let compile output_prefix = (Ext_list.append block args_code, b :: fn_code) | {value = None} -> assert false) in + let args = + if appinfo.ap_transformed_jsx then + match (appinfo.ap_args, args) with + | jsx_tag :: _, _ :: rest_args -> ( + match extract_nested_external_component_field jsx_tag with + | Some (id, dynamic_import, hidden_name) -> + E.ml_var_dot ~dynamic_import id hidden_name :: rest_args + | None -> args) + | _ -> args + else args + in match (ap_func, lambda_cxt.continuation) with | ( Lvar fn_id, ( EffectCall (Maybe_tail_is_return (Tail_with_name {label = Some ret})) @@ -1583,6 +1663,48 @@ let compile output_prefix = and compile_prim (prim_info : Lam.prim_info) (lambda_cxt : Lam_compile_context.t) = match prim_info with + | { + primitive = + Pjs_call + { + prim_name = "jsx" | "jsxs" | "jsxKeyed" | "jsxsKeyed"; + transformed_jsx = true; + _; + }; + args = jsx_tag :: rest_args; + loc; + } -> + let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in + let tag_block, tag_expr = + match extract_nested_external_component_field jsx_tag with + | Some (id, dynamic_import, hidden_name) -> ( + match + Lam_compile_env.query_external_id_info ~dynamic_import id + (hidden_name ^ "$jsx") + with + | exception Not_found -> ( + match compile_lambda new_cxt jsx_tag with + | {block; value = Some b} -> (block, b) + | {value = None} -> assert false) + | _ -> ([], E.ml_var_dot ~dynamic_import id hidden_name)) + | None -> ( + match compile_lambda new_cxt jsx_tag with + | {block; value = Some b} -> (block, b) + | {value = None} -> assert false) + in + let rest_blocks, rest_exprs = + Ext_list.split_map rest_args (fun x -> + match compile_lambda new_cxt x with + | {block; value = Some b} -> (block, b) + | {value = None} -> assert false) + in + let args_code : J.block = List.concat (tag_block :: rest_blocks) in + let exp = + Lam_compile_primitive.translate output_prefix loc lambda_cxt + prim_info.primitive (tag_expr :: rest_exprs) + in + Js_output.output_of_block_and_expression lambda_cxt.continuation args_code + exp | { primitive = Pfield (_, fld_info); args = [Lglobal_module (id, dynamic_import)]; diff --git a/compiler/core/lam_pass_remove_alias.ml b/compiler/core/lam_pass_remove_alias.ml index 1dad7d3865c..cd72556a775 100644 --- a/compiler/core/lam_pass_remove_alias.ml +++ b/compiler/core/lam_pass_remove_alias.ml @@ -133,7 +133,8 @@ let simplify_alias (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = ap_func = Lprim { - primitive = Pfield (_, Fld_module {name = fld_name}); + primitive = + Pfield (_, Fld_module {name = fld_name; jsx_component = _}); args = [Lglobal_module (ident, dynamic_import)]; _; } as l1; diff --git a/compiler/core/lam_print.ml b/compiler/core/lam_print.ml index 172e219abb7..0607724e232 100644 --- a/compiler/core/lam_print.ml +++ b/compiler/core/lam_print.ml @@ -323,7 +323,7 @@ let lambda ppf v = fprintf ppf ")@ %a)@]" lam body | Lprim { - primitive = Pfield (n, Fld_module {name = s}); + primitive = Pfield (n, Fld_module {name = s; jsx_component = _}); args = [Lglobal_module (id, dynamic_import)]; _; } -> diff --git a/compiler/ml/lambda.ml b/compiler/ml/lambda.ml index db810d4f912..540b9204c0a 100644 --- a/compiler/ml/lambda.ml +++ b/compiler/ml/lambda.ml @@ -113,7 +113,7 @@ let ref_tag_info : tag_info = type field_dbg_info = | Fld_record of {name: string; mutable_flag: Asttypes.mutable_flag} - | Fld_module of {name: string} + | Fld_module of {name: string; jsx_component: bool} | Fld_record_inline of {name: string} | Fld_record_extension of {name: string} | Fld_tuple @@ -134,6 +134,8 @@ let fld_record_extension (lbl : label) = Fld_record_extension {name = Ext_list.find_def lbl.lbl_attributes find_name lbl.lbl_name} +let fld_module ~name ~jsx_component = Fld_module {name; jsx_component} + let ref_field_info : field_dbg_info = Fld_record {name = "contents"; mutable_flag = Mutable} @@ -620,11 +622,28 @@ let rec transl_normal_path = function else Lvar id | Pdot (p, s, pos) -> Lprim - ( Pfield (pos, Fld_module {name = s}), + ( Pfield (pos, Fld_module {name = s; jsx_component = false}), [transl_normal_path p], Location.none ) | Papply _ -> assert false +let transl_jsx_path path = + let rec aux ~is_final = function + | Path.Pident id -> + if Ident.global id then Lprim (Pgetglobal id, [], Location.none) + else Lvar id + | Pdot (p, s, pos) -> + Lprim + ( Pfield + ( pos, + Fld_module + {name = s; jsx_component = is_final && String.equal s "make"} ), + [aux ~is_final:false p], + Location.none ) + | Papply _ -> assert false + in + aux ~is_final:true path + (* Translation of identifiers *) let transl_module_path ?(loc = Location.none) env path = @@ -633,6 +652,9 @@ let transl_module_path ?(loc = Location.none) env path = let transl_value_path ?(loc = Location.none) env path = transl_normal_path (Env.normalize_path_prefix (Some loc) env path) +let transl_jsx_value_path ?(loc = Location.none) env path = + transl_jsx_path (Env.normalize_path_prefix (Some loc) env path) + let transl_extension_path = transl_value_path (* Apply a substitution to a lambda-term. diff --git a/compiler/ml/lambda.mli b/compiler/ml/lambda.mli index d8eaf57be6f..3658437d902 100644 --- a/compiler/ml/lambda.mli +++ b/compiler/ml/lambda.mli @@ -84,7 +84,7 @@ val ref_tag_info : tag_info type field_dbg_info = | Fld_record of {name: string; mutable_flag: Asttypes.mutable_flag} - | Fld_module of {name: string} + | Fld_module of {name: string; jsx_component: bool} | Fld_record_inline of {name: string} | Fld_record_extension of {name: string} | Fld_tuple @@ -100,6 +100,8 @@ val fld_record_inline : Types.label_description -> field_dbg_info val fld_record_extension : Types.label_description -> field_dbg_info +val fld_module : name:string -> jsx_component:bool -> field_dbg_info + val ref_field_info : field_dbg_info type set_field_dbg_info = @@ -393,6 +395,7 @@ val transl_normal_path : Path.t -> lambda (* Path.t is already normal *) val transl_module_path : ?loc:Location.t -> Env.t -> Path.t -> lambda val transl_value_path : ?loc:Location.t -> Env.t -> Path.t -> lambda +val transl_jsx_value_path : ?loc:Location.t -> Env.t -> Path.t -> lambda val transl_extension_path : ?loc:Location.t -> Env.t -> Path.t -> lambda val subst_lambda : lambda Ident.tbl -> lambda -> lambda diff --git a/compiler/ml/printlambda.ml b/compiler/ml/printlambda.ml index 0282f6e113c..ecafa38cd6e 100644 --- a/compiler/ml/printlambda.ml +++ b/compiler/ml/printlambda.ml @@ -72,7 +72,7 @@ let string_of_loc_kind = function let str_of_field_info (fld_info : Lambda.field_dbg_info) = match fld_info with - | Fld_module {name} + | Fld_module {name; jsx_component = _} | Fld_record {name} | Fld_record_inline {name} | Fld_record_extension {name} -> diff --git a/compiler/ml/translcore.ml b/compiler/ml/translcore.ml index 078cbf133a0..670a91a8a82 100644 --- a/compiler/ml/translcore.ml +++ b/compiler/ml/translcore.ml @@ -652,6 +652,11 @@ let extract_directive_for_fn exp = if txt = "directive" then Ast_payload.is_single_string payload else None) +let has_jsx_component_path_attr (exp : Typedtree.expression) = + List.exists + (fun ({txt; _}, _) -> String.equal txt "res.jsxComponentPath") + exp.exp_attributes + let rec transl_exp e = List.iter (Translattribute.check_attribute e) e.exp_attributes; transl_exp0 e @@ -661,7 +666,9 @@ and transl_exp0 (e : Typedtree.expression) : Lambda.lambda = | Texp_ident (_, _, {val_kind = Val_prim p}) -> transl_primitive e.exp_loc p e.exp_env e.exp_type | Texp_ident (path, _, {val_kind = Val_reg}) -> - transl_value_path ~loc:e.exp_loc e.exp_env path + if has_jsx_component_path_attr e then + transl_jsx_value_path ~loc:e.exp_loc e.exp_env path + else transl_value_path ~loc:e.exp_loc e.exp_env path | Texp_constant cst -> Lconst (Const_base cst) | Texp_let (rec_flag, pat_expr_list, body) -> transl_let rec_flag pat_expr_list (transl_exp body) diff --git a/compiler/ml/translmod.ml b/compiler/ml/translmod.ml index 87471ac26bf..caf2b47dde6 100644 --- a/compiler/ml/translmod.ml +++ b/compiler/ml/translmod.ml @@ -64,14 +64,20 @@ let rec apply_coercion loc strict (restr : Typedtree.module_coercion) arg = | Tcoerce_structure (pos_cc_list, id_pos_list, runtime_fields) -> Lambda.name_lambda strict arg (fun id -> let get_field_name name pos = - Lambda.Lprim (Pfield (pos, Fld_module {name}), [Lvar id], loc) + Lambda.Lprim + ( Pfield (pos, Fld_module {name; jsx_component = false}), + [Lvar id], + loc ) in let lam = Lambda.Lprim ( Pmakeblock (Blk_module runtime_fields), Ext_list.map2 pos_cc_list runtime_fields (fun (pos, cc) name -> apply_coercion loc Alias cc - (Lprim (Pfield (pos, Fld_module {name}), [Lvar id], loc))), + (Lprim + ( Pfield (pos, Fld_module {name; jsx_component = false}), + [Lvar id], + loc ))), loc ) in wrap_id_pos_list loc id_pos_list get_field_name lam) @@ -432,7 +438,10 @@ and transl_structure loc fields cc rootpath final_env = function Pgenval, id, Lprim - ( Pfield (pos, Fld_module {name = Ident.name id}), + ( Pfield + ( pos, + Fld_module {name = Ident.name id; jsx_component = false} + ), [Lvar mid], incl.incl_loc ), body ), diff --git a/compiler/syntax/src/jsx_common.ml b/compiler/syntax/src/jsx_common.ml index a48cf11bc97..e7b8698c701 100644 --- a/compiler/syntax/src/jsx_common.ml +++ b/compiler/syntax/src/jsx_common.ml @@ -6,6 +6,7 @@ type jsx_config = { mutable module_: string; mutable nested_modules: string list; mutable has_component: bool; + mutable hoisted_structure_items: structure_item list; } (* Helper method to look up the [@react.component] attribute *) diff --git a/compiler/syntax/src/jsx_ppx.ml b/compiler/syntax/src/jsx_ppx.ml index 4b05e1995d9..078d812eca9 100644 --- a/compiler/syntax/src/jsx_ppx.ml +++ b/compiler/syntax/src/jsx_ppx.ml @@ -117,7 +117,9 @@ let get_mapper ~config = in let structure mapper items = let old_config = save_config () in + let is_top_level = config.nested_modules = [] in config.has_component <- false; + if is_top_level then config.hoisted_structure_items <- []; let result = List.map (fun item -> @@ -129,6 +131,11 @@ let get_mapper ~config = items |> List.flatten in + let result = + if config.version = 4 && is_top_level then + result @ List.rev config.hoisted_structure_items + else result + in restore_config old_config; result in @@ -143,6 +150,7 @@ let rewrite_implementation ~jsx_version ~jsx_module (code : Parsetree.structure) module_ = jsx_module; nested_modules = []; has_component = false; + hoisted_structure_items = []; } in let mapper = get_mapper ~config in @@ -156,6 +164,7 @@ let rewrite_signature ~jsx_version ~jsx_module (code : Parsetree.signature) : module_ = jsx_module; nested_modules = []; has_component = false; + hoisted_structure_items = []; } in let mapper = get_mapper ~config in diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 83bbb0dcdc4..8f8f0cd4216 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -80,6 +80,34 @@ let make_new_binding binding expression new_name = Jsx_common.raise_error ~loc:pvb_loc "JSX component calls cannot be destructured." +let longident_of_segments = function + | [] -> assert false + | head :: rest -> + List.fold_left (fun acc name -> Ldot (acc, name)) (Lident head) rest + +let make_hoisted_component_binding ~empty_loc ~full_module_name nested_modules = + let path = + nested_modules |> List.rev |> longident_of_segments |> fun txt -> + {loc = empty_loc; txt = Ldot (txt, "make")} + in + let marker_name = full_module_name ^ "$jsx" in + { + pstr_loc = empty_loc; + pstr_desc = + Pstr_value + ( Nonrecursive, + [ + Vb.mk ~loc:empty_loc + (Pat.var ~loc:empty_loc {loc = empty_loc; txt = full_module_name}) + (Exp.ident ~loc:empty_loc path); + Vb.mk ~loc:empty_loc + (Pat.var ~loc:empty_loc {loc = empty_loc; txt = marker_name}) + (Exp.construct ~loc:empty_loc + {loc = empty_loc; txt = Lident "true"} + None); + ] ); + } + (* Lookup the filename from the location information on the AST node and turn it into a valid module identifier *) let filename_from_loc (pstr_loc : Location.t) = let file_name = @@ -216,6 +244,8 @@ let make_type_decls_with_core_type props_name loc core_type typ_vars = let live_attr = ({txt = "live"; loc = Location.none}, PStr []) let jsx_component_props_attr = ({txt = "res.jsxComponentProps"; loc = Location.none}, PStr []) +let jsx_component_path_attr = + ({txt = "res.jsxComponentPath"; loc = Location.none}, PStr []) (* type props<'x, 'y, ...> = { x: 'x, y?: 'y, ... } *) let make_props_record_type ~core_type_of_attr ~external_ ~typ_vars_of_core_type @@ -803,6 +833,15 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = }, Some (binding_wrapper full_expression) ) in + let () = + match (fn_name, config.nested_modules) with + | "make", _ :: _ -> + config.hoisted_structure_items <- + make_hoisted_component_binding ~empty_loc ~full_module_name + config.nested_modules + :: config.hoisted_structure_items + | _ -> () + in (Some props_record_type, binding, new_binding)) else if Jsx_common.has_attr_on_binding Jsx_common.has_attr_with_props binding then @@ -1286,7 +1325,7 @@ let mk_uppercase_tag_name_expr tag_name = | JsxUpperTag path -> Longident.Ldot (path, "make") in let loc = tag_name.loc in - Exp.ident ~loc {txt = tag_identifier; loc} + Exp.ident ~loc ~attrs:[jsx_component_path_attr] {txt = tag_identifier; loc} let expr ~(config : Jsx_common.jsx_config) mapper expression = match expression with diff --git a/tests/build_tests/rsc_nested_jsx_members/input.js b/tests/build_tests/rsc_nested_jsx_members/input.js new file mode 100644 index 00000000000..ddec0f4b716 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/input.js @@ -0,0 +1,38 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.js"); +const output = await fs.readFile(outputPath, "utf8"); +const sidebarOutputPath = path.join( + import.meta.dirname, + "src", + "Sidebar.res.js", +); +const sidebarOutput = await fs.readFile(sidebarOutputPath, "utf8"); + +assert.match( + output, + /import \* as Sidebar\$RscNestedJsxMembers from "\.\/Sidebar\.res\.js";/, +); +assert.match( + output, + /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxMembers\.Sidebar(?:\$RscNestedJsxMembers)?\$Provider,/, +); +assert.doesNotMatch(output, /\.Provider\.make,/); +assert.match( + sidebarOutput, + /export \{[\s\S]*Provider,[\s\S]*Inset,[\s\S]*Sidebar\$Provider,[\s\S]*Sidebar\$Inset,[\s\S]*\}/s, +); +assert.match(sidebarOutput, /Sidebar\$Provider\$jsx/); +assert.match(sidebarOutput, /Sidebar\$Inset\$jsx/); + +await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_members/rescript.json b/tests/build_tests/rsc_nested_jsx_members/rescript.json new file mode 100644 index 00000000000..b83ca40838d --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-nested-jsx-members", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.js" + }, + "namespace": true +} diff --git a/tests/build_tests/rsc_nested_jsx_members/src/MainLayout.res b/tests/build_tests/rsc_nested_jsx_members/src/MainLayout.res new file mode 100644 index 00000000000..e2c753eeb17 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/MainLayout.res @@ -0,0 +1,4 @@ +@react.component +let make = (~children) => { + {children} +} diff --git a/tests/build_tests/rsc_nested_jsx_members/src/React.res b/tests/build_tests/rsc_nested_jsx_members/src/React.res new file mode 100644 index 00000000000..0a2d41f0096 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/React.res @@ -0,0 +1,30 @@ +type element + +@val external null: element = "null" + +external string: string => element = "%identity" + +external array: array => element = "%identity" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react") +external createElement: (component<'props>, 'props) => element = "createElement" + +@variadic @module("react") +external createElementVariadic: (component<'props>, 'props, array) => element = + "createElement" + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" + +@module("react/jsx-runtime") +external jsxKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsx" + +@module("react/jsx-runtime") +external jsxs: (component<'props>, 'props) => element = "jsxs" + +@module("react/jsx-runtime") +external jsxsKeyed: (component<'props>, 'props, ~key: string=?, @ignore unit) => element = "jsxs" diff --git a/tests/build_tests/rsc_nested_jsx_members/src/Sidebar.res b/tests/build_tests/rsc_nested_jsx_members/src/Sidebar.res new file mode 100644 index 00000000000..a88c10803e2 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/Sidebar.res @@ -0,0 +1,13 @@ +module Provider = { + @react.component + let make = (~children) => { + children + } +} + +module Inset = { + @react.component + let make = (~children) => { + children + } +} diff --git a/tests/tests/src/alias_default_value_test.mjs b/tests/tests/src/alias_default_value_test.mjs index 714b76ab98a..49f6103cef0 100644 --- a/tests/tests/src/alias_default_value_test.mjs +++ b/tests/tests/src/alias_default_value_test.mjs @@ -91,6 +91,20 @@ let C8 = { make: Alias_default_value_test$C8 }; +let Alias_default_value_test$C0$jsx = true; + +let Alias_default_value_test$C1$jsx = true; + +let Alias_default_value_test$C2$jsx = true; + +let Alias_default_value_test$C3$jsx = true; + +let Alias_default_value_test$C4$jsx = true; + +let Alias_default_value_test$C6$jsx = true; + +let Alias_default_value_test$C7$jsx = true; + export { C0, C1, @@ -100,5 +114,19 @@ export { C6, C7, C8, + Alias_default_value_test$C0, + Alias_default_value_test$C0$jsx, + Alias_default_value_test$C1, + Alias_default_value_test$C1$jsx, + Alias_default_value_test$C2, + Alias_default_value_test$C2$jsx, + Alias_default_value_test$C3, + Alias_default_value_test$C3$jsx, + Alias_default_value_test$C4, + Alias_default_value_test$C4$jsx, + Alias_default_value_test$C6, + Alias_default_value_test$C6$jsx, + Alias_default_value_test$C7, + Alias_default_value_test$C7$jsx, } /* No side effect */ diff --git a/tests/tests/src/async_jsx.mjs b/tests/tests/src/async_jsx.mjs index f02ec3cf205..9feec016fcb 100644 --- a/tests/tests/src/async_jsx.mjs +++ b/tests/tests/src/async_jsx.mjs @@ -33,9 +33,17 @@ let Bar = { make: Async_jsx$Bar }; +let Async_jsx$Foo$jsx = true; + +let Async_jsx$Bar$jsx = true; + export { getNow, Foo, Bar, + Async_jsx$Foo, + Async_jsx$Foo$jsx, + Async_jsx$Bar, + Async_jsx$Bar$jsx, } /* react/jsx-runtime Not a pure module */ diff --git a/tests/tests/src/jsx_optional_props_test.mjs b/tests/tests/src/jsx_optional_props_test.mjs index ef3a0d326f1..b6d2b8bc792 100644 --- a/tests/tests/src/jsx_optional_props_test.mjs +++ b/tests/tests/src/jsx_optional_props_test.mjs @@ -16,8 +16,12 @@ let _element = JsxRuntime.jsx(Jsx_optional_props_test$ComponentWithOptionalProps element: JsxRuntime.jsx("div", {}) }); +let Jsx_optional_props_test$ComponentWithOptionalProps$jsx = true; + export { ComponentWithOptionalProps, _element, + Jsx_optional_props_test$ComponentWithOptionalProps, + Jsx_optional_props_test$ComponentWithOptionalProps$jsx, } /* _element Not a pure module */ diff --git a/tests/tests/src/jsxv4_newtype.mjs b/tests/tests/src/jsxv4_newtype.mjs index a6e2b5a2dc7..138850c6d73 100644 --- a/tests/tests/src/jsxv4_newtype.mjs +++ b/tests/tests/src/jsxv4_newtype.mjs @@ -33,10 +33,26 @@ let V4A3 = { make: Jsxv4_newtype$V4A3 }; +let Jsxv4_newtype$V4A$jsx = true; + +let Jsxv4_newtype$V4A1$jsx = true; + +let Jsxv4_newtype$V4A2$jsx = true; + +let Jsxv4_newtype$V4A3$jsx = true; + export { V4A, V4A1, V4A2, V4A3, + Jsxv4_newtype$V4A, + Jsxv4_newtype$V4A$jsx, + Jsxv4_newtype$V4A1, + Jsxv4_newtype$V4A1$jsx, + Jsxv4_newtype$V4A2, + Jsxv4_newtype$V4A2$jsx, + Jsxv4_newtype$V4A3, + Jsxv4_newtype$V4A3$jsx, } /* No side effect */ From 97bafbbab2e59b2d0cb007e6eee78054074e1a58 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 12 Mar 2026 19:03:38 +0100 Subject: [PATCH 02/21] Fix syntax snapshots --- compiler/syntax/src/jsx_v4.ml | 11 +++++++++++ .../data/ppx/react/expected/aliasProps.res.txt | 9 ++++++++- .../data/ppx/react/expected/asyncAwait.res.txt | 2 ++ .../ppx/react/expected/defaultValueProp.res.txt | 4 ++++ .../react/expected/externalWithCustomName.res.txt | 2 +- .../ppx/react/expected/fileLevelConfig.res.txt | 1 + .../data/ppx/react/expected/forwardRef.res.txt | 15 +++++++++++++-- .../data/ppx/react/expected/fragment.res.txt | 8 ++++---- .../data/ppx/react/expected/interface.res.txt | 2 ++ .../data/ppx/react/expected/mangleKeyword.res.txt | 5 +++-- .../data/ppx/react/expected/nested.res.txt | 8 +++++--- .../data/ppx/react/expected/newtype.res.txt | 1 + .../ppx/react/expected/noPropsWithKey.res.txt | 6 ++++-- .../react/expected/optimizeAutomaticMode.res.txt | 1 + .../ppx/react/expected/optionalKeyType.res.txt | 6 +++--- .../ppx/react/expected/returnConstraint.res.txt | 3 +++ .../data/ppx/react/expected/sharedProps.res.txt | 4 ++++ .../data/ppx/react/expected/spreadProps.res.txt | 8 ++++---- .../data/ppx/react/expected/topLevel.res.txt | 1 + .../ppx/react/expected/typeConstraint.res.txt | 1 + .../ppx/react/expected/uncurriedProps.res.txt | 5 ++++- .../data/ppx/react/expected/v4.res.txt | 3 +++ 22 files changed, 83 insertions(+), 23 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 8f8f0cd4216..c254433ee0f 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -1329,6 +1329,17 @@ let mk_uppercase_tag_name_expr tag_name = let expr ~(config : Jsx_common.jsx_config) mapper expression = match expression with + | {pexp_desc = Pexp_letmodule (name, module_expr, body); pexp_loc = loc; pexp_attributes = attrs} + -> + config.nested_modules <- name.txt :: config.nested_modules; + let mapped_module_expr = default_mapper.module_expr mapper module_expr in + let mapped_body = mapper.expr mapper body in + let () = + match config.nested_modules with + | _ :: rest -> config.nested_modules <- rest + | [] -> () + in + Exp.letmodule ~loc ~attrs name mapped_module_expr mapped_body | { pexp_desc = Pexp_jsx_element jsx_element; pexp_loc = loc; diff --git a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt index f2ae63a276f..106e33b8ff5 100644 --- a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt @@ -155,10 +155,17 @@ module C6 = { } let make = ({comp: module(Comp: Comp), x: (a, b), _}: props<_, _>): React.element => - React.jsx(Comp.make, {}) + React.jsx(@res.jsxComponentPath Comp.make, {}) let make = { let \"AliasProps$C6" = (props: props<_>) => make(props) \"AliasProps$C6" } } +let \"AliasProps$C0" = C0.make and \"AliasProps$C0$jsx" = true +let \"AliasProps$C1" = C1.make and \"AliasProps$C1$jsx" = true +let \"AliasProps$C2" = C2.make and \"AliasProps$C2$jsx" = true +let \"AliasProps$C3" = C3.make and \"AliasProps$C3$jsx" = true +let \"AliasProps$C4" = C4.make and \"AliasProps$C4$jsx" = true +let \"AliasProps$C5" = C5.make and \"AliasProps$C5$jsx" = true +let \"AliasProps$C6" = C6.make and \"AliasProps$C6$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt index d77b11decd3..dea1b17712c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt @@ -35,3 +35,5 @@ module C1 = { \"AsyncAwait$C1" } } +let \"AsyncAwait$C0" = C0.make and \"AsyncAwait$C0$jsx" = true +let \"AsyncAwait$C1" = C1.make and \"AsyncAwait$C1$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt index c8a7759839e..fa928be4406 100644 --- a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt @@ -91,3 +91,7 @@ module C3 = { \"DefaultValueProp$C3" } } +let \"DefaultValueProp$C0" = C0.make and \"DefaultValueProp$C0$jsx" = true +let \"DefaultValueProp$C1" = C1.make and \"DefaultValueProp$C1$jsx" = true +let \"DefaultValueProp$C2" = C2.make and \"DefaultValueProp$C2$jsx" = true +let \"DefaultValueProp$C3" = C3.make and \"DefaultValueProp$C3$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt index dd98fc72ff3..a7b96145c72 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithCustomName.res.txt @@ -11,4 +11,4 @@ module Foo = { external component: React.component> = "component" } -let t = React.jsx(Foo.component, {a: 1, b: {"1"}}) +let t = React.jsx(@res.jsxComponentPath Foo.component, {a: 1, b: {"1"}}) diff --git a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt index 77cde0caea1..2a3dae60b75 100644 --- a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt @@ -15,3 +15,4 @@ module V4A = { \"FileLevelConfig$V4A" } } +let \"FileLevelConfig$V4A" = V4A.make and \"FileLevelConfig$V4A$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt index 8cd3bcff8ce..abe2a0b7e9e 100644 --- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt @@ -45,7 +45,10 @@ module V4A = { "div", { children: ?ReactDOM.someElement( - React.jsx(FancyInput.make, {ref: input, children: {React.string("Click to focus")}}), + React.jsx( + @res.jsxComponentPath FancyInput.make, + {ref: input, children: {React.string("Click to focus")}}, + ), ), }, ): React.element @@ -103,7 +106,10 @@ module V4AUncurried = { "div", { children: ?ReactDOM.someElement( - React.jsx(FancyInput.make, {ref: input, children: {React.string("Click to focus")}}), + React.jsx( + @res.jsxComponentPath FancyInput.make, + {ref: input, children: {React.string("Click to focus")}}, + ), ), }, ): React.element @@ -115,3 +121,8 @@ module V4AUncurried = { \"ForwardRef$V4AUncurried" } } +let \"ForwardRef$V4A$FancyInput" = V4A.FancyInput.make and \"ForwardRef$V4A$FancyInput$jsx" = true +let \"ForwardRef$V4A" = V4A.make and \"ForwardRef$V4A$jsx" = true +let \"ForwardRef$V4AUncurried$FancyInput" = V4AUncurried.FancyInput.make +and \"ForwardRef$V4AUncurried$FancyInput$jsx" = true +let \"ForwardRef$V4AUncurried" = V4AUncurried.make and \"ForwardRef$V4AUncurried$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/fragment.res.txt b/tests/syntax_tests/data/ppx/react/expected/fragment.res.txt index 80c3bb1d868..3af2218962c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/fragment.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/fragment.res.txt @@ -7,11 +7,11 @@ let _ = React.jsxs( {children: React.array([ReactDOM.jsx("div", {}), ReactDOM.jsx("div", {})])}, ) let _ = React.jsx(React.jsxFragment, {children: React.jsx(React.jsxFragment, {})}) -let _ = React.jsx(Z.make, {}) -let _ = React.jsx(Z.make, {children: ReactDOM.jsx("div", {})}) -let _ = React.jsx(Z.make, {a: "a", children: ReactDOM.jsx("div", {})}) +let _ = React.jsx(@res.jsxComponentPath Z.make, {}) +let _ = React.jsx(@res.jsxComponentPath Z.make, {children: ReactDOM.jsx("div", {})}) +let _ = React.jsx(@res.jsxComponentPath Z.make, {a: "a", children: ReactDOM.jsx("div", {})}) let _ = React.jsxs( - Z.make, + @res.jsxComponentPath Z.make, {children: React.array([ReactDOM.jsx("div", {}), ReactDOM.jsx("div", {})])}, ) let _ = ReactDOM.jsx("div", {}) diff --git a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt index df0f7137fb5..363f9439a0a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt @@ -21,3 +21,5 @@ module NoProps = { \"Interface$NoProps" } } +let \"Interface$A" = A.make and \"Interface$A$jsx" = true +let \"Interface$NoProps" = NoProps.make and \"Interface$NoProps$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt index f1e40360ec5..85a8e1a0302 100644 --- a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt @@ -25,5 +25,6 @@ module C4A1 = { external make: React.component> = "default" } -let c4a0 = React.jsx(C4A0.make, {_open: "x", _type: "t"}) -let c4a1 = React.jsx(C4A1.make, {_open: "x", _type: "t"}) +let c4a0 = React.jsx(@res.jsxComponentPath C4A0.make, {_open: "x", _type: "t"}) +let c4a1 = React.jsx(@res.jsxComponentPath C4A1.make, {_open: "x", _type: "t"}) +let \"MangleKeyword$C4A0" = C4A0.make and \"MangleKeyword$C4A0$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt index 70f8efde0e3..6cb92900d5e 100644 --- a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt @@ -8,16 +8,18 @@ module Outer = { let make = (_: props): React.element => ReactDOM.jsx("div", {}) let make = { - let \"Nested$Outer" = props => make(props) + let \"Nested$Outer$Inner" = props => make(props) - \"Nested$Outer" + \"Nested$Outer$Inner" } } - React.jsx(Inner.make, {}) + React.jsx(@res.jsxComponentPath Inner.make, {}) } let make = { let \"Nested$Outer" = props => make(props) \"Nested$Outer" } } +let \"Nested$Outer$Inner" = Outer.Inner.make and \"Nested$Outer$Inner$jsx" = true +let \"Nested$Outer" = Outer.make and \"Nested$Outer$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt index 2fdecb86740..838fd0daa57 100644 --- a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt @@ -99,3 +99,4 @@ module Uncurried = { \"Newtype$Uncurried" } } +let \"Newtype$Uncurried" = Uncurried.make and \"Newtype$Uncurried$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt index aa8dc64f353..f6efd90c3b4 100644 --- a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt @@ -29,8 +29,8 @@ module V4C = { React.jsxFragment, { children: React.array([ - React.jsxKeyed(V4CA.make, {}, ~key="k", ()), - React.jsxKeyed(V4CB.make, {}, ~key="k", ()), + React.jsxKeyed(@res.jsxComponentPath V4CA.make, {}, ~key="k", ()), + React.jsxKeyed(@res.jsxComponentPath V4CB.make, {}, ~key="k", ()), ]), }, ) @@ -40,3 +40,5 @@ module V4C = { \"NoPropsWithKey$V4C" } } +let \"NoPropsWithKey$V4CA" = V4CA.make and \"NoPropsWithKey$V4CA$jsx" = true +let \"NoPropsWithKey$V4C" = V4C.make and \"NoPropsWithKey$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt index dfb3506590e..7797329571a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt @@ -18,3 +18,4 @@ module User = { \"OptimizeAutomaticMode$User" } } +let \"OptimizeAutomaticMode$User" = User.make and \"OptimizeAutomaticMode$User$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/optionalKeyType.res.txt b/tests/syntax_tests/data/ppx/react/expected/optionalKeyType.res.txt index 0420bce343e..24d5b15c217 100644 --- a/tests/syntax_tests/data/ppx/react/expected/optionalKeyType.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/optionalKeyType.res.txt @@ -2,9 +2,9 @@ let key = None @@jsxConfig({version: 4}) -let _ = React.jsxKeyed(C.make, {}, ~key="k", ()) -let _ = React.jsxKeyed(C.make, {}, ~key=?Some("k"), ()) -let _ = React.jsxKeyed(C.make, {}, ~key?, ()) +let _ = React.jsxKeyed(@res.jsxComponentPath C.make, {}, ~key="k", ()) +let _ = React.jsxKeyed(@res.jsxComponentPath C.make, {}, ~key=?Some("k"), ()) +let _ = React.jsxKeyed(@res.jsxComponentPath C.make, {}, ~key?, ()) let _ = ReactDOM.jsxKeyed("div", {}, ~key="k", ()) let _ = ReactDOM.jsxKeyed("div", {}, ~key=?Some("k"), ()) let _ = ReactDOM.jsxKeyed("div", {}, ~key?, ()) diff --git a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt index 7d017b6d9b7..84cd1b66fd8 100644 --- a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt @@ -47,3 +47,6 @@ module Async = { \"ReturnConstraint$Async" } } +let \"ReturnConstraint$Standard" = Standard.make and \"ReturnConstraint$Standard$jsx" = true +let \"ReturnConstraint$ForwardRef" = ForwardRef.make and \"ReturnConstraint$ForwardRef$jsx" = true +let \"ReturnConstraint$Async" = Async.make and \"ReturnConstraint$Async$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt index 83c7a5fb041..63c7f492103 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt @@ -67,3 +67,7 @@ module V4A8 = { external make: React.component = "default" } +let \"SharedProps$V4A1" = V4A1.make and \"SharedProps$V4A1$jsx" = true +let \"SharedProps$V4A2" = V4A2.make and \"SharedProps$V4A2$jsx" = true +let \"SharedProps$V4A3" = V4A3.make and \"SharedProps$V4A3$jsx" = true +let \"SharedProps$V4A4" = V4A4.make and \"SharedProps$V4A4$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/spreadProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/spreadProps.res.txt index db116ee8d81..acc79ec0fee 100644 --- a/tests/syntax_tests/data/ppx/react/expected/spreadProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/spreadProps.res.txt @@ -6,10 +6,10 @@ // let c0 = // only spread props -let c1 = React.jsx(A.make, p) +let c1 = React.jsx(@res.jsxComponentPath A.make, p) // reversed order -let c2 = React.jsx(A.make, {...p, x: "x"}) +let c2 = React.jsx(@res.jsxComponentPath A.make, {...p, x: "x"}) let c3 = ReactDOM.jsx("div", p) @@ -23,5 +23,5 @@ let c5 = ReactDOM.jsxsKeyed( ) // both need to be parsed -let c6 = React.jsx(A.make, params->Obj.magic) -let c7 = React.jsx(A.make, params->Obj.magic) +let c6 = React.jsx(@res.jsxComponentPath A.make, params->Obj.magic) +let c7 = React.jsx(@res.jsxComponentPath A.make, params->Obj.magic) diff --git a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt index 9330346a961..cc329602999 100644 --- a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt @@ -17,3 +17,4 @@ module V4A = { \"TopLevel$V4A" } } +let \"TopLevel$V4A" = V4A.make and \"TopLevel$V4A$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt index 68a3b0278b3..92432c4be8c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt @@ -14,3 +14,4 @@ module V4A = { \"TypeConstraint$V4A" } } +let \"TypeConstraint$V4A" = V4A.make and \"TypeConstraint$V4A$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt index 9d81169bd1e..77a78d4f6ff 100644 --- a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt @@ -57,10 +57,13 @@ module Bar = { @res.jsxComponentProps type props = {} - let make = (_: props): React.element => React.jsx(Foo.make, {callback: {(_, _, _) => ()}}) + let make = (_: props): React.element => + React.jsx(@res.jsxComponentPath Foo.make, {callback: {(_, _, _) => ()}}) let make = { let \"UncurriedProps$Bar" = props => make(props) \"UncurriedProps$Bar" } } +let \"UncurriedProps$Foo" = Foo.make and \"UncurriedProps$Foo$jsx" = true +let \"UncurriedProps$Bar" = Bar.make and \"UncurriedProps$Bar$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt index a6f05b0e17f..2a8c1348d8f 100644 --- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt @@ -116,3 +116,6 @@ module Rec2 = { } and mm = x => make(x) } +let \"V4$Rec" = Rec.make and \"V4$Rec$jsx" = true +let \"V4$Rec1" = Rec1.make and \"V4$Rec1$jsx" = true +let \"V4$Rec2" = Rec2.make and \"V4$Rec2$jsx" = true From fb1a22faea4b8936a41f2c0e77c44e0c900b37df Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 12 Mar 2026 19:08:00 +0100 Subject: [PATCH 03/21] Format --- compiler/syntax/src/jsx_v4.ml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index c254433ee0f..6ecb8aa444e 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -1329,8 +1329,11 @@ let mk_uppercase_tag_name_expr tag_name = let expr ~(config : Jsx_common.jsx_config) mapper expression = match expression with - | {pexp_desc = Pexp_letmodule (name, module_expr, body); pexp_loc = loc; pexp_attributes = attrs} - -> + | { + pexp_desc = Pexp_letmodule (name, module_expr, body); + pexp_loc = loc; + pexp_attributes = attrs; + } -> config.nested_modules <- name.txt :: config.nested_modules; let mapped_module_expr = default_mapper.module_expr mapper module_expr in let mapped_body = mapper.expr mapper body in From 27bd27cb6555cbf49f2b07b1d2d104ffafe53d6d Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 12 Mar 2026 19:15:08 +0100 Subject: [PATCH 04/21] Fix gentype tests --- .../typescript-react-example/src/Hooks.res.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res.js b/tests/gentype_tests/typescript-react-example/src/Hooks.res.js index 8669c8eafed..7eafb7e5f95 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.res.js +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res.js @@ -204,6 +204,10 @@ let make$1 = Hooks; let $$default = Hooks; +let Hooks$RenderPropRequiresConversion$jsx = true; + +let Hooks$DD$jsx = true; + export { make$1 as make, $$default as default, @@ -219,5 +223,9 @@ export { RenderPropRequiresConversion, WithChildren, DD, + Hooks$RenderPropRequiresConversion, + Hooks$RenderPropRequiresConversion$jsx, + Hooks$DD, + Hooks$DD$jsx, } /* make Not a pure module */ From e20ac3b136078ecf8f910ef775d0f9d1e03d9d40 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 12 Mar 2026 22:48:01 +0100 Subject: [PATCH 05/21] Fix rewatch test --- rewatch/tests/watch/06-watch-missing-source-folder.sh | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rewatch/tests/watch/06-watch-missing-source-folder.sh b/rewatch/tests/watch/06-watch-missing-source-folder.sh index 08436c4fe19..5240152d6b3 100755 --- a/rewatch/tests/watch/06-watch-missing-source-folder.sh +++ b/rewatch/tests/watch/06-watch-missing-source-folder.sh @@ -54,7 +54,11 @@ fi # where the config change triggers a full rebuild that runs concurrently # with the subsequent `rewatch build`. exit_watcher -sleep 1 +if ! wait_for_file_gone "lib/rescript.lock" 20; then + error "Watcher did not stop in time" + git checkout "$DEP01_CONFIG" + exit 1 +fi # Restore dep01's rescript.json git checkout "$DEP01_CONFIG" From f78664b431ef2380eb6ceb8d6014600553279540 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Thu, 12 Mar 2026 22:59:13 +0100 Subject: [PATCH 06/21] Another fixed snapshot --- .../tests/src/expected/CreateInterface.res.txt | 4 ++++ tests/analysis_tests/tests/src/expected/JsxV4.res.txt | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt b/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt index 998459a8a1c..e71ea422ce1 100644 --- a/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt +++ b/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt @@ -132,4 +132,8 @@ module OrderedSet: { } let make: (~id: module(Belt.Id.Comparable with type identity = 'a and type t = 'b)) => t<'b, 'a> } +let CreateInterface$ComponentWithPolyProp: ComponentWithPolyProp.props< + [< #large | #small], +> => React.element +let CreateInterface$ComponentWithPolyProp$jsx: bool diff --git a/tests/analysis_tests/tests/src/expected/JsxV4.res.txt b/tests/analysis_tests/tests/src/expected/JsxV4.res.txt index ea1e781cb4e..45682c0a53c 100644 --- a/tests/analysis_tests/tests/src/expected/JsxV4.res.txt +++ b/tests/analysis_tests/tests/src/expected/JsxV4.res.txt @@ -32,4 +32,10 @@ module Other: { @react.component let make: (~name: string) => React.element } +let JsxV4$M4: M4.props => React.element +let JsxV4$M4$jsx: bool +let JsxV4$MM: MM.props => React.element +let JsxV4$MM$jsx: bool +let JsxV4$Other: Other.props => React.element +let JsxV4$Other$jsx: bool From 2bdb0e517f28f8c10f56794408c683c1f47556b9 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 13 Mar 2026 10:35:52 +0100 Subject: [PATCH 07/21] Fix namespace handling --- compiler/core/lam_compile.ml | 124 ++++++++++++++---- compiler/syntax/src/jsx_v4.ml | 9 ++ .../rsc_nested_jsx_members/input.js | 6 +- 3 files changed, 109 insertions(+), 30 deletions(-) diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 966fd29e59c..e4b84587962 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -233,9 +233,12 @@ type initialization = J.block let compile output_prefix = let root_module_name (id : Ident.t) = - match String.index_opt id.name '$' with - | Some index -> String.sub id.name 0 index - | None -> id.name + match Ext_namespace.try_split_module_name id.name with + | Some (_namespace, module_name) -> module_name + | None -> ( + match String.index_opt id.name '$' with + | Some index -> String.sub id.name 0 index + | None -> id.name) in let rec extract_nested_external_component_segments segments ((lam : Lam.t), (make_dynamic_import : bool option ref)) : @@ -271,6 +274,16 @@ let compile output_prefix = extract_nested_external_component_segments [] (arg, dynamic_import) with | Some (id, dynamic_import, segments) -> ( + let denamespace_segment segment = + let root_name = root_module_name id in + let namespaced_prefix = root_name ^ "$" in + if Ext_string.starts_with segment namespaced_prefix then + match String.split_on_char '$' segment with + | root :: _namespace :: rest when rest <> [] -> + String.concat "$" (root :: rest) + | _ -> segment + else segment + in let segments = match segments with | head :: rest @@ -280,6 +293,11 @@ let compile output_prefix = rest | _ -> segments in + let segments = + match segments with + | head :: rest -> denamespace_segment head :: rest + | [] -> [] + in match segments with | [] -> None | _ -> @@ -290,6 +308,72 @@ let compile output_prefix = | None -> None) | _ -> None in + let normalize_hidden_component_name (id : Ident.t) (hidden_name : string) = + let root_name = root_module_name id in + let id_parts = String.split_on_char '$' id.name in + let namespace_parts = + match id_parts with + | _root :: rest -> rest + | [] -> [] + in + let hidden_parts = String.split_on_char '$' hidden_name in + let hidden_parts_without_root = + match hidden_parts with + | first :: rest when String.equal first root_name -> rest + | _ -> hidden_parts + in + let rec drop_prefix prefix parts = + match (prefix, parts) with + | [], _ -> parts + | x :: xs, y :: ys when String.equal x y -> drop_prefix xs ys + | _ -> parts + in + let tail = drop_prefix namespace_parts hidden_parts_without_root in + match tail with + | [] -> hidden_name + | _ -> String.concat "$" (root_name :: tail) + in + let hidden_component_name_candidates (id : Ident.t) (hidden_name : string) = + let candidates = ref [] in + let push candidate = + if not (List.mem candidate !candidates) then + candidates := candidate :: !candidates + in + (match String.split_on_char '$' hidden_name with + | root :: _namespace :: rest when rest <> [] -> + push (String.concat "$" (root :: rest)) + | _ -> ()); + push (normalize_hidden_component_name id hidden_name); + push hidden_name; + List.rev !candidates + in + let rewrite_nested_jsx_component_expr (jsx_tag : Lam.t) + (compiled_expr : J.expression) : J.expression = + let rec extract_module_id (expr : J.expression) = + match expr.expression_desc with + | Var (Qualified (module_id, _)) -> Some module_id + | Static_index (inner, _, _) -> extract_module_id inner + | _ -> None + in + match extract_nested_external_component_field jsx_tag with + | Some (id, dynamic_import, hidden_name) -> ( + let hidden_name_candidates = + hidden_component_name_candidates id hidden_name + in + let resolved_hidden_name = + match hidden_name_candidates with + | candidate :: _ -> Some candidate + | [] -> None + in + match (resolved_hidden_name, extract_module_id compiled_expr) with + | Some hidden_name, Some module_id -> + { + compiled_expr with + expression_desc = Var (Qualified (module_id, Some hidden_name)); + } + | _ -> compiled_expr) + | None -> compiled_expr + in let rec compile_external_field (* Like [List.empty]*) ?(dynamic_import = false) (lamba_cxt : Lam_compile_context.t) (id : Ident.t) name : Js_output.t = @@ -361,15 +445,11 @@ let compile output_prefix = let args = if appinfo.ap_transformed_jsx then match (appinfo.ap_args, args) with - | jsx_tag :: _, _ :: rest_args -> ( - match extract_nested_external_component_field jsx_tag with - | Some (id, dynamic_import, hidden_name) -> - E.ml_var_dot ~dynamic_import id hidden_name :: rest_args - | None -> args) + | jsx_tag :: _, jsx_expr :: rest_args -> + rewrite_nested_jsx_component_expr jsx_tag jsx_expr :: rest_args | _ -> args else args in - let fn = E.ml_var_dot ~dynamic_import module_id ident_info.name in let expression = match appinfo.ap_info.ap_status with @@ -1596,11 +1676,8 @@ let compile output_prefix = let args = if appinfo.ap_transformed_jsx then match (appinfo.ap_args, args) with - | jsx_tag :: _, _ :: rest_args -> ( - match extract_nested_external_component_field jsx_tag with - | Some (id, dynamic_import, hidden_name) -> - E.ml_var_dot ~dynamic_import id hidden_name :: rest_args - | None -> args) + | jsx_tag :: _, jsx_expr :: rest_args -> + rewrite_nested_jsx_component_expr jsx_tag jsx_expr :: rest_args | _ -> args else args in @@ -1676,21 +1753,10 @@ let compile output_prefix = } -> let new_cxt = {lambda_cxt with continuation = NeedValue Not_tail} in let tag_block, tag_expr = - match extract_nested_external_component_field jsx_tag with - | Some (id, dynamic_import, hidden_name) -> ( - match - Lam_compile_env.query_external_id_info ~dynamic_import id - (hidden_name ^ "$jsx") - with - | exception Not_found -> ( - match compile_lambda new_cxt jsx_tag with - | {block; value = Some b} -> (block, b) - | {value = None} -> assert false) - | _ -> ([], E.ml_var_dot ~dynamic_import id hidden_name)) - | None -> ( - match compile_lambda new_cxt jsx_tag with - | {block; value = Some b} -> (block, b) - | {value = None} -> assert false) + match compile_lambda new_cxt jsx_tag with + | {block; value = Some b} -> + (block, rewrite_nested_jsx_component_expr jsx_tag b) + | {value = None} -> assert false in let rest_blocks, rest_exprs = Ext_list.split_map rest_args (fun x -> diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 6ecb8aa444e..b52909acb04 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -122,8 +122,17 @@ let filename_from_loc (pstr_loc : Location.t) = let file_name = String.capitalize_ascii file_name in file_name +let unnamespace_module_name file_name = + match String.index_opt file_name '$' with + | Some index -> String.sub file_name 0 index + | None -> ( + match Ext_namespace.try_split_module_name file_name with + | Some (module_name, _namespace) -> module_name + | None -> file_name) + (* Build a string representation of a module name with segments separated by $ *) let make_module_name file_name nested_modules fn_name = + let file_name = unnamespace_module_name file_name in let full_module_name = match (file_name, nested_modules, fn_name) with (* TODO: is this even reachable? It seems like the fileName always exists *) diff --git a/tests/build_tests/rsc_nested_jsx_members/input.js b/tests/build_tests/rsc_nested_jsx_members/input.js index ddec0f4b716..8992279572b 100644 --- a/tests/build_tests/rsc_nested_jsx_members/input.js +++ b/tests/build_tests/rsc_nested_jsx_members/input.js @@ -25,7 +25,11 @@ assert.match( ); assert.match( output, - /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxMembers\.Sidebar(?:\$RscNestedJsxMembers)?\$Provider,/, + /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxMembers\.Sidebar\$Provider,/, +); +assert.doesNotMatch( + output, + /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxMembers\.Sidebar\$RscNestedJsxMembers\$Provider,/, ); assert.doesNotMatch(output, /\.Provider\.make,/); assert.match( From 0b45ebb5e57f51c2b17ded2e4c939554ca1b13ac Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 13 Mar 2026 10:54:02 +0100 Subject: [PATCH 08/21] Fix possible race condition in rewatch test --- .../watch/06-watch-missing-source-folder.sh | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rewatch/tests/watch/06-watch-missing-source-folder.sh b/rewatch/tests/watch/06-watch-missing-source-folder.sh index 5240152d6b3..11d4336c4ca 100755 --- a/rewatch/tests/watch/06-watch-missing-source-folder.sh +++ b/rewatch/tests/watch/06-watch-missing-source-folder.sh @@ -65,7 +65,23 @@ git checkout "$DEP01_CONFIG" # Rebuild to regenerate any artifacts that were removed by `rewatch clean` # but not rebuilt due to the modified config (e.g. Dep01.mjs). -rewatch build > /dev/null 2>&1 +if ! rewatch build > /dev/null 2>&1; then + error "Rebuild after restoring config failed" + rm -f rewatch.log + exit 1 +fi + +# Slow CI runners can still be catching up on file restoration when the build +# command returns. Wait until git no longer sees tracked deletions before doing +# the final cleanliness check. +timeout=20 +while [ "$timeout" -gt 0 ]; do + if [ -z "$(git diff --name-only --diff-filter=D .)" ]; then + break + fi + sleep 1 + timeout=$((timeout - 1)) +done rm -f rewatch.log if git diff --exit-code . > /dev/null 2>&1 && [ -z "$(git ls-files --others --exclude-standard .)" ]; From 22003b4d30f35c2e860dfaa5e702ad25738e1fd8 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 13 Mar 2026 13:00:02 +0100 Subject: [PATCH 09/21] Copilot comment fixes --- compiler/core/lam_compile.ml | 43 +++++++++++++------ compiler/syntax/src/jsx_v4.ml | 33 ++++++++++---- .../rsc_nested_jsx_members/input.js | 27 ++++++++++++ .../src/MainLayoutExternal.res | 4 ++ .../src/SidebarExternal.res | 4 ++ .../src/SidebarExternalImpl.js | 3 ++ tests/tests/src/ExternalArity.mjs | 6 +++ 7 files changed, 98 insertions(+), 22 deletions(-) create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/MainLayoutExternal.res create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/SidebarExternal.res create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/SidebarExternalImpl.js diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index e4b84587962..b3c7da6e1ec 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -347,6 +347,23 @@ let compile output_prefix = push hidden_name; List.rev !candidates in + let exported_hidden_component_name (module_id : J.module_id) + (hidden_name_candidates : string list) = + let rec loop = function + | [] -> None + | candidate :: rest -> ( + match + Lam_compile_env.query_external_id_info + ~dynamic_import:module_id.dynamic_import module_id.id + (candidate ^ "$jsx") + with + | _ -> Some candidate + | exception Not_found -> loop rest) + in + match module_id.kind with + | Ml -> loop hidden_name_candidates + | _ -> None + in let rewrite_nested_jsx_component_expr (jsx_tag : Lam.t) (compiled_expr : J.expression) : J.expression = let rec extract_module_id (expr : J.expression) = @@ -356,22 +373,22 @@ let compile output_prefix = | _ -> None in match extract_nested_external_component_field jsx_tag with - | Some (id, dynamic_import, hidden_name) -> ( + | Some (id, _dynamic_import, hidden_name) -> ( let hidden_name_candidates = hidden_component_name_candidates id hidden_name in - let resolved_hidden_name = - match hidden_name_candidates with - | candidate :: _ -> Some candidate - | [] -> None - in - match (resolved_hidden_name, extract_module_id compiled_expr) with - | Some hidden_name, Some module_id -> - { - compiled_expr with - expression_desc = Var (Qualified (module_id, Some hidden_name)); - } - | _ -> compiled_expr) + match extract_module_id compiled_expr with + | Some module_id -> ( + match + exported_hidden_component_name module_id hidden_name_candidates + with + | Some hidden_name -> + { + compiled_expr with + expression_desc = Var (Qualified (module_id, Some hidden_name)); + } + | None -> compiled_expr) + | None -> compiled_expr) | None -> compiled_expr in let rec compile_external_field (* Like [List.empty]*) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index b52909acb04..9c5a88feaaa 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -127,9 +127,19 @@ let unnamespace_module_name file_name = | Some index -> String.sub file_name 0 index | None -> ( match Ext_namespace.try_split_module_name file_name with - | Some (module_name, _namespace) -> module_name + | Some (_namespace, module_name) -> module_name | None -> file_name) +let maybe_hoist_nested_make_component ~(config : Jsx_common.jsx_config) + ~empty_loc ~full_module_name fn_name = + match (fn_name, config.nested_modules) with + | "make", _ :: _ -> + config.hoisted_structure_items <- + make_hoisted_component_binding ~empty_loc ~full_module_name + config.nested_modules + :: config.hoisted_structure_items + | _ -> () + (* Build a string representation of a module name with segments separated by $ *) let make_module_name file_name nested_modules fn_name = let file_name = unnamespace_module_name file_name in @@ -843,13 +853,8 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = Some (binding_wrapper full_expression) ) in let () = - match (fn_name, config.nested_modules) with - | "make", _ :: _ -> - config.hoisted_structure_items <- - make_hoisted_component_binding ~empty_loc ~full_module_name - config.nested_modules - :: config.hoisted_structure_items - | _ -> () + maybe_hoist_nested_make_component ~config ~empty_loc ~full_module_name + fn_name in (Some props_record_type, binding, new_binding)) else if Jsx_common.has_attr_on_binding Jsx_common.has_attr_with_props binding @@ -981,7 +986,8 @@ let transform_structure_item ~config item = | { pstr_loc; pstr_desc = - Pstr_primitive ({pval_attributes; pval_type} as value_description); + Pstr_primitive + ({pval_attributes; pval_type; pval_name} as value_description); } as pstr -> ( match ( List.filter Jsx_common.has_attr pval_attributes, @@ -1042,6 +1048,15 @@ let transform_structure_item ~config item = }; } in + let file_name = filename_from_loc pstr_loc in + let empty_loc = Location.in_file file_name in + let full_module_name = + make_module_name file_name config.nested_modules pval_name.txt + in + let () = + maybe_hoist_nested_make_component ~config ~empty_loc ~full_module_name + pval_name.txt + in [props_record_type; new_structure] | _ -> Jsx_common.raise_error ~loc:pstr_loc diff --git a/tests/build_tests/rsc_nested_jsx_members/input.js b/tests/build_tests/rsc_nested_jsx_members/input.js index 8992279572b..dda8ebdcd09 100644 --- a/tests/build_tests/rsc_nested_jsx_members/input.js +++ b/tests/build_tests/rsc_nested_jsx_members/input.js @@ -18,6 +18,21 @@ const sidebarOutputPath = path.join( "Sidebar.res.js", ); const sidebarOutput = await fs.readFile(sidebarOutputPath, "utf8"); +const externalOutputPath = path.join( + import.meta.dirname, + "src", + "MainLayoutExternal.res.js", +); +const externalOutput = await fs.readFile(externalOutputPath, "utf8"); +const externalSidebarOutputPath = path.join( + import.meta.dirname, + "src", + "SidebarExternal.res.js", +); +const externalSidebarOutput = await fs.readFile( + externalSidebarOutputPath, + "utf8", +); assert.match( output, @@ -27,10 +42,18 @@ assert.match( output, /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxMembers\.Sidebar\$Provider,/, ); +assert.match( + externalOutput, + /JsxRuntime\.jsx\(SidebarExternal\$RscNestedJsxMembers\.SidebarExternal\$Provider,/, +); assert.doesNotMatch( output, /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxMembers\.Sidebar\$RscNestedJsxMembers\$Provider,/, ); +assert.doesNotMatch( + externalOutput, + /JsxRuntime\.jsx\(SidebarExternal\$RscNestedJsxMembers\.Provider\.make,/, +); assert.doesNotMatch(output, /\.Provider\.make,/); assert.match( sidebarOutput, @@ -38,5 +61,9 @@ assert.match( ); assert.match(sidebarOutput, /Sidebar\$Provider\$jsx/); assert.match(sidebarOutput, /Sidebar\$Inset\$jsx/); +assert.match( + externalSidebarOutput, + /export \{[\s\S]*Provider,[\s\S]*SidebarExternal\$Provider,[\s\S]*SidebarExternal\$Provider\$jsx[\s\S]*\}/s, +); await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_members/src/MainLayoutExternal.res b/tests/build_tests/rsc_nested_jsx_members/src/MainLayoutExternal.res new file mode 100644 index 00000000000..1a25cad0b9a --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/MainLayoutExternal.res @@ -0,0 +1,4 @@ +@react.component +let make = (~children) => { + {children} +} diff --git a/tests/build_tests/rsc_nested_jsx_members/src/SidebarExternal.res b/tests/build_tests/rsc_nested_jsx_members/src/SidebarExternal.res new file mode 100644 index 00000000000..f0015e98896 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/SidebarExternal.res @@ -0,0 +1,4 @@ +module Provider = { + @react.component @module("./SidebarExternalImpl.js") + external make: (~children: React.element=?) => React.element = "default" +} diff --git a/tests/build_tests/rsc_nested_jsx_members/src/SidebarExternalImpl.js b/tests/build_tests/rsc_nested_jsx_members/src/SidebarExternalImpl.js new file mode 100644 index 00000000000..f94e84e1aa9 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/SidebarExternalImpl.js @@ -0,0 +1,3 @@ +export default function SidebarExternalImpl(props) { + return props.children ?? null; +} diff --git a/tests/tests/src/ExternalArity.mjs b/tests/tests/src/ExternalArity.mjs index 1de3ab38f2d..9f669373f66 100644 --- a/tests/tests/src/ExternalArity.mjs +++ b/tests/tests/src/ExternalArity.mjs @@ -37,10 +37,16 @@ let ReactTest = { FormattedMessage: FormattedMessage }; +let ExternalArity$ReactTest$FormattedMessage = ReactIntl.FormattedMessage; + +let ExternalArity$ReactTest$FormattedMessage$jsx = true; + export { f1, f2, FromTypeConstructor, ReactTest, + ExternalArity$ReactTest$FormattedMessage, + ExternalArity$ReactTest$FormattedMessage$jsx, } /* test1 Not a pure module */ From 01e5f438ec7d3a761628af592a9404fadadd26bb Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 13 Mar 2026 13:25:17 +0100 Subject: [PATCH 10/21] Update syntax tests --- .../data/ppx/react/expected/externalWithRef.res.txt | 1 + .../data/ppx/react/expected/externalWithTypeVariables.res.txt | 1 + .../data/ppx/react/expected/mangleKeyword.res.txt | 1 + .../data/ppx/react/expected/noPropsWithKey.res.txt | 1 + .../syntax_tests/data/ppx/react/expected/sharedProps.res.txt | 4 ++++ tests/syntax_tests/data/ppx/react/expected/v4.res.txt | 2 ++ 6 files changed, 10 insertions(+) diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt index 44a3420fd87..f981aad592c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt @@ -10,3 +10,4 @@ module V4C = { @module("componentForwardRef") external make: React.component> = "component" } +let \"ExternalWithRef$V4C" = V4C.make and \"ExternalWithRef$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt index cbdee832996..e271934ecbb 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt @@ -10,3 +10,4 @@ module V4C = { @module("c") external make: React.component, React.element>> = "component" } +let \"ExternalWithTypeVariables$V4C" = V4C.make and \"ExternalWithTypeVariables$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt index 85a8e1a0302..8b9bd814834 100644 --- a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt @@ -28,3 +28,4 @@ module C4A1 = { let c4a0 = React.jsx(@res.jsxComponentPath C4A0.make, {_open: "x", _type: "t"}) let c4a1 = React.jsx(@res.jsxComponentPath C4A1.make, {_open: "x", _type: "t"}) let \"MangleKeyword$C4A0" = C4A0.make and \"MangleKeyword$C4A0$jsx" = true +let \"MangleKeyword$C4A1" = C4A1.make and \"MangleKeyword$C4A1$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt index f6efd90c3b4..bd40f4c83c7 100644 --- a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt @@ -41,4 +41,5 @@ module V4C = { } } let \"NoPropsWithKey$V4CA" = V4CA.make and \"NoPropsWithKey$V4CA$jsx" = true +let \"NoPropsWithKey$V4CB" = V4CB.make and \"NoPropsWithKey$V4CB$jsx" = true let \"NoPropsWithKey$V4C" = V4C.make and \"NoPropsWithKey$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt index 63c7f492103..70cb0dee1dc 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt @@ -71,3 +71,7 @@ let \"SharedProps$V4A1" = V4A1.make and \"SharedProps$V4A1$jsx" = true let \"SharedProps$V4A2" = V4A2.make and \"SharedProps$V4A2$jsx" = true let \"SharedProps$V4A3" = V4A3.make and \"SharedProps$V4A3$jsx" = true let \"SharedProps$V4A4" = V4A4.make and \"SharedProps$V4A4$jsx" = true +let \"SharedProps$V4A5" = V4A5.make and \"SharedProps$V4A5$jsx" = true +let \"SharedProps$V4A6" = V4A6.make and \"SharedProps$V4A6$jsx" = true +let \"SharedProps$V4A7" = V4A7.make and \"SharedProps$V4A7$jsx" = true +let \"SharedProps$V4A8" = V4A8.make and \"SharedProps$V4A8$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt index 2a8c1348d8f..40159d7edbc 100644 --- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt @@ -116,6 +116,8 @@ module Rec2 = { } and mm = x => make(x) } +let \"V4$E" = E.make and \"V4$E$jsx" = true +let \"V4$EUncurried" = EUncurried.make and \"V4$EUncurried$jsx" = true let \"V4$Rec" = Rec.make and \"V4$Rec$jsx" = true let \"V4$Rec1" = Rec1.make and \"V4$Rec1$jsx" = true let \"V4$Rec2" = Rec2.make and \"V4$Rec2$jsx" = true From f0c3ccdd8a938870573b5ae874f3d9104de5a82d Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 13 Mar 2026 14:20:31 +0100 Subject: [PATCH 11/21] Update tests --- .../react_ppx/src/recursive_component_test.res.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/build_tests/react_ppx/src/recursive_component_test.res.js b/tests/build_tests/react_ppx/src/recursive_component_test.res.js index 4e3fd9951f4..f3aa4d5fc91 100644 --- a/tests/build_tests/react_ppx/src/recursive_component_test.res.js +++ b/tests/build_tests/react_ppx/src/recursive_component_test.res.js @@ -18,7 +18,13 @@ let Rec = { mm: mm }; +let Recursive_component_test$Rec = make; + +let Recursive_component_test$Rec$jsx = true; + export { Rec, + Recursive_component_test$Rec, + Recursive_component_test$Rec$jsx, } /* No side effect */ From 15ded2db24eff77e93104b8f535bb59cb9f82719 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Fri, 13 Mar 2026 17:57:25 +0100 Subject: [PATCH 12/21] Add more regression tests --- compiler/core/lam_compile.ml | 45 ++++++++++--------- compiler/syntax/src/jsx_v4.ml | 4 ++ .../rsc_component_with_props_nested/input.js | 33 ++++++++++++++ .../rescript.json | 16 +++++++ .../src/MainLayout.res | 4 ++ .../src/React.res | 12 +++++ .../src/Sidebar.res | 6 +++ .../rsc_dynamic_import_nested_jsx/input.js | 30 +++++++++++++ .../rescript.json | 16 +++++++ .../src/MainLayout.res | 8 ++++ .../src/React.res | 12 +++++ .../src/Sidebar.res | 6 +++ .../rsc_mixed_runtime_import/input.js | 34 ++++++++++++++ .../rsc_mixed_runtime_import/rescript.json | 16 +++++++ .../src/MainLayout.res | 8 ++++ .../rsc_mixed_runtime_import/src/React.res | 10 +++++ .../rsc_mixed_runtime_import/src/Sidebar.res | 4 ++ .../build_tests/rsc_nested_jsx_deep/input.js | 33 ++++++++++++++ .../rsc_nested_jsx_deep/rescript.json | 16 +++++++ .../rsc_nested_jsx_deep/src/MainLayout.res | 4 ++ .../rsc_nested_jsx_deep/src/React.res | 10 +++++ .../rsc_nested_jsx_deep/src/Sidebar.res | 6 +++ .../rsc_nested_jsx_members/input.js | 15 +++++++ .../src/PlainAccess.res | 3 ++ .../input.js | 29 ++++++++++++ .../rescript.json | 16 +++++++ .../src/MainLayout.res | 4 ++ .../src/React.res | 10 +++++ .../src/Sidebar.res | 6 +++ .../rsc_suffix_runtime_import/input.js | 34 ++++++++++++++ .../rsc_suffix_runtime_import/rescript.json | 16 +++++++ .../src/MainLayout.res | 5 +++ .../rsc_suffix_runtime_import/src/React.res | 12 +++++ .../rsc_suffix_runtime_import/src/Sidebar.res | 4 ++ tests/tests/src/alias_default_value_test.mjs | 4 ++ 35 files changed, 471 insertions(+), 20 deletions(-) create mode 100644 tests/build_tests/rsc_component_with_props_nested/input.js create mode 100644 tests/build_tests/rsc_component_with_props_nested/rescript.json create mode 100644 tests/build_tests/rsc_component_with_props_nested/src/MainLayout.res create mode 100644 tests/build_tests/rsc_component_with_props_nested/src/React.res create mode 100644 tests/build_tests/rsc_component_with_props_nested/src/Sidebar.res create mode 100644 tests/build_tests/rsc_dynamic_import_nested_jsx/input.js create mode 100644 tests/build_tests/rsc_dynamic_import_nested_jsx/rescript.json create mode 100644 tests/build_tests/rsc_dynamic_import_nested_jsx/src/MainLayout.res create mode 100644 tests/build_tests/rsc_dynamic_import_nested_jsx/src/React.res create mode 100644 tests/build_tests/rsc_dynamic_import_nested_jsx/src/Sidebar.res create mode 100644 tests/build_tests/rsc_mixed_runtime_import/input.js create mode 100644 tests/build_tests/rsc_mixed_runtime_import/rescript.json create mode 100644 tests/build_tests/rsc_mixed_runtime_import/src/MainLayout.res create mode 100644 tests/build_tests/rsc_mixed_runtime_import/src/React.res create mode 100644 tests/build_tests/rsc_mixed_runtime_import/src/Sidebar.res create mode 100644 tests/build_tests/rsc_nested_jsx_deep/input.js create mode 100644 tests/build_tests/rsc_nested_jsx_deep/rescript.json create mode 100644 tests/build_tests/rsc_nested_jsx_deep/src/MainLayout.res create mode 100644 tests/build_tests/rsc_nested_jsx_deep/src/React.res create mode 100644 tests/build_tests/rsc_nested_jsx_deep/src/Sidebar.res create mode 100644 tests/build_tests/rsc_nested_jsx_members/src/PlainAccess.res create mode 100644 tests/build_tests/rsc_nested_jsx_members_no_namespace/input.js create mode 100644 tests/build_tests/rsc_nested_jsx_members_no_namespace/rescript.json create mode 100644 tests/build_tests/rsc_nested_jsx_members_no_namespace/src/MainLayout.res create mode 100644 tests/build_tests/rsc_nested_jsx_members_no_namespace/src/React.res create mode 100644 tests/build_tests/rsc_nested_jsx_members_no_namespace/src/Sidebar.res create mode 100644 tests/build_tests/rsc_suffix_runtime_import/input.js create mode 100644 tests/build_tests/rsc_suffix_runtime_import/rescript.json create mode 100644 tests/build_tests/rsc_suffix_runtime_import/src/MainLayout.res create mode 100644 tests/build_tests/rsc_suffix_runtime_import/src/React.res create mode 100644 tests/build_tests/rsc_suffix_runtime_import/src/Sidebar.res diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index b3c7da6e1ec..d72440c85d1 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -347,46 +347,51 @@ let compile output_prefix = push hidden_name; List.rev !candidates in - let exported_hidden_component_name (module_id : J.module_id) + let exported_hidden_component_name ~(id : Ident.t) ~(dynamic_import : bool) (hidden_name_candidates : string list) = let rec loop = function | [] -> None | candidate :: rest -> ( match - Lam_compile_env.query_external_id_info - ~dynamic_import:module_id.dynamic_import module_id.id + Lam_compile_env.query_external_id_info ~dynamic_import id (candidate ^ "$jsx") with | _ -> Some candidate | exception Not_found -> loop rest) in - match module_id.kind with - | Ml -> loop hidden_name_candidates - | _ -> None + loop hidden_name_candidates in let rewrite_nested_jsx_component_expr (jsx_tag : Lam.t) (compiled_expr : J.expression) : J.expression = - let rec extract_module_id (expr : J.expression) = + let rec extract_root_expr (expr : J.expression) = match expr.expression_desc with - | Var (Qualified (module_id, _)) -> Some module_id - | Static_index (inner, _, _) -> extract_module_id inner + | Var (Qualified (module_id, Some _)) -> + Some {expr with expression_desc = Var (Qualified (module_id, None))} + | Static_index (inner, _, _) -> extract_root_expr inner + | Var _ -> Some expr | _ -> None in + let hidden_component_access (root_expr : J.expression) hidden_name = + match root_expr.expression_desc with + | Var (Qualified (module_id, None)) -> + { + root_expr with + expression_desc = Var (Qualified (module_id, Some hidden_name)); + } + | _ -> E.dot root_expr hidden_name + in match extract_nested_external_component_field jsx_tag with - | Some (id, _dynamic_import, hidden_name) -> ( + | Some (id, dynamic_import, hidden_name) -> ( let hidden_name_candidates = hidden_component_name_candidates id hidden_name in - match extract_module_id compiled_expr with - | Some module_id -> ( + match extract_root_expr compiled_expr with + | Some root_expr -> ( match - exported_hidden_component_name module_id hidden_name_candidates + exported_hidden_component_name ~id ~dynamic_import + hidden_name_candidates with - | Some hidden_name -> - { - compiled_expr with - expression_desc = Var (Qualified (module_id, Some hidden_name)); - } + | Some hidden_name -> hidden_component_access root_expr hidden_name | None -> compiled_expr) | None -> compiled_expr) | None -> compiled_expr @@ -1671,7 +1676,7 @@ let compile output_prefix = }; } -> ( match fld_info with - | Fld_module {name} -> + | Fld_module {name; jsx_component = _} -> compile_external_field_apply ~dynamic_import appinfo id name lambda_cxt | _ -> assert false) | _ -> ( @@ -1795,7 +1800,7 @@ let compile output_prefix = } -> ( (* should be before Lglobal_global *) match fld_info with - | Fld_module {name = field} -> + | Fld_module {name = field; jsx_component = _} -> compile_external_field ~dynamic_import lambda_cxt id field | _ -> assert false) | {primitive = Praise; args = [e]; _} -> ( diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 9c5a88feaaa..566f790cd5f 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -953,6 +953,10 @@ let map_binding ~config ~empty_loc ~pstr_loc ~file_name ~rec_flag binding = Some (make_new_binding ~loc:empty_loc ~full_module_name modified_binding) in + let () = + maybe_hoist_nested_make_component ~config ~empty_loc ~full_module_name + fn_name + in let binding_expr = { binding.pvb_expr with diff --git a/tests/build_tests/rsc_component_with_props_nested/input.js b/tests/build_tests/rsc_component_with_props_nested/input.js new file mode 100644 index 00000000000..ac0939c3778 --- /dev/null +++ b/tests/build_tests/rsc_component_with_props_nested/input.js @@ -0,0 +1,33 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.js"); +const output = await fs.readFile(outputPath, "utf8"); +const sidebarOutputPath = path.join( + import.meta.dirname, + "src", + "Sidebar.res.js", +); +const sidebarOutput = await fs.readFile(sidebarOutputPath, "utf8"); + +assert.match( + output, + /JsxRuntime\.jsx\(Sidebar\$RscComponentWithPropsNested\.Sidebar\$Provider,/, +); +assert.doesNotMatch(output, /\.Provider\.make,/); +assert.match(sidebarOutput, /Sidebar\$Provider\$jsx/); +assert.match( + sidebarOutput, + /export \{[\s\S]*Provider,[\s\S]*Sidebar\$Provider,[\s\S]*Sidebar\$Provider\$jsx[\s\S]*\}/s, +); + +await execClean(); diff --git a/tests/build_tests/rsc_component_with_props_nested/rescript.json b/tests/build_tests/rsc_component_with_props_nested/rescript.json new file mode 100644 index 00000000000..537397ae493 --- /dev/null +++ b/tests/build_tests/rsc_component_with_props_nested/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-component-with-props-nested", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.js" + }, + "namespace": true +} diff --git a/tests/build_tests/rsc_component_with_props_nested/src/MainLayout.res b/tests/build_tests/rsc_component_with_props_nested/src/MainLayout.res new file mode 100644 index 00000000000..e2c753eeb17 --- /dev/null +++ b/tests/build_tests/rsc_component_with_props_nested/src/MainLayout.res @@ -0,0 +1,4 @@ +@react.component +let make = (~children) => { + {children} +} diff --git a/tests/build_tests/rsc_component_with_props_nested/src/React.res b/tests/build_tests/rsc_component_with_props_nested/src/React.res new file mode 100644 index 00000000000..75e3b2c2d78 --- /dev/null +++ b/tests/build_tests/rsc_component_with_props_nested/src/React.res @@ -0,0 +1,12 @@ +type element + +@val external null: element = "null" + +external string: string => element = "%identity" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" diff --git a/tests/build_tests/rsc_component_with_props_nested/src/Sidebar.res b/tests/build_tests/rsc_component_with_props_nested/src/Sidebar.res new file mode 100644 index 00000000000..b667cc58fe6 --- /dev/null +++ b/tests/build_tests/rsc_component_with_props_nested/src/Sidebar.res @@ -0,0 +1,6 @@ +module Provider = { + type props = {children: React.element} + + @react.componentWithProps + let make = props => props.children +} diff --git a/tests/build_tests/rsc_dynamic_import_nested_jsx/input.js b/tests/build_tests/rsc_dynamic_import_nested_jsx/input.js new file mode 100644 index 00000000000..13ae1728e8f --- /dev/null +++ b/tests/build_tests/rsc_dynamic_import_nested_jsx/input.js @@ -0,0 +1,30 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.mjs"); +const output = await fs.readFile(outputPath, "utf8"); + +assert.match( + output, + /let DynamicSidebar = await import\("\.\/Sidebar\.res\.mjs"\);/, +); +assert.match(output, /let dynamicProvider = DynamicSidebar\.Provider\.make;/); +assert.match( + output, + /JsxRuntime\.jsx\(Sidebar\$RscDynamicImportNestedJsx\.Sidebar\$Provider,/, +); +assert.doesNotMatch( + output, + /let dynamicProvider = DynamicSidebar\.Sidebar\$Provider;/, +); + +await execClean(); diff --git a/tests/build_tests/rsc_dynamic_import_nested_jsx/rescript.json b/tests/build_tests/rsc_dynamic_import_nested_jsx/rescript.json new file mode 100644 index 00000000000..f4772ad950b --- /dev/null +++ b/tests/build_tests/rsc_dynamic_import_nested_jsx/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-dynamic-import-nested-jsx", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.mjs" + }, + "namespace": true +} diff --git a/tests/build_tests/rsc_dynamic_import_nested_jsx/src/MainLayout.res b/tests/build_tests/rsc_dynamic_import_nested_jsx/src/MainLayout.res new file mode 100644 index 00000000000..42ebacc8096 --- /dev/null +++ b/tests/build_tests/rsc_dynamic_import_nested_jsx/src/MainLayout.res @@ -0,0 +1,8 @@ +module DynamicSidebar = await Sidebar + +let dynamicProvider = DynamicSidebar.Provider.make + +@react.component +let make = (~children) => { + {Option.getOr(children, React.null)} +} diff --git a/tests/build_tests/rsc_dynamic_import_nested_jsx/src/React.res b/tests/build_tests/rsc_dynamic_import_nested_jsx/src/React.res new file mode 100644 index 00000000000..75e3b2c2d78 --- /dev/null +++ b/tests/build_tests/rsc_dynamic_import_nested_jsx/src/React.res @@ -0,0 +1,12 @@ +type element + +@val external null: element = "null" + +external string: string => element = "%identity" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" diff --git a/tests/build_tests/rsc_dynamic_import_nested_jsx/src/Sidebar.res b/tests/build_tests/rsc_dynamic_import_nested_jsx/src/Sidebar.res new file mode 100644 index 00000000000..ad06cf9dc20 --- /dev/null +++ b/tests/build_tests/rsc_dynamic_import_nested_jsx/src/Sidebar.res @@ -0,0 +1,6 @@ +module Provider = { + @react.component + let make = (~children) => { + children + } +} diff --git a/tests/build_tests/rsc_mixed_runtime_import/input.js b/tests/build_tests/rsc_mixed_runtime_import/input.js new file mode 100644 index 00000000000..1e4f73b8bd2 --- /dev/null +++ b/tests/build_tests/rsc_mixed_runtime_import/input.js @@ -0,0 +1,34 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.mjs"); +const output = await fs.readFile(outputPath, "utf8"); + +assert.match( + output, + /import \* as Sidebar\$RscMixedRuntimeImport from "\.\/Sidebar\.res\.mjs";/, +); +assert.match( + output, + /import \* as Stdlib_OptionJs from "@rescript\/runtime\/lib\/es6\/Stdlib_Option\.js";/, +); +assert.match( + output, + /JsxRuntime\.jsx\(Sidebar\$RscMixedRuntimeImport\.Sidebar\$Provider,/, +); +assert.doesNotMatch(output, /@rescript\/runtime\/lib\/es6\/Stdlib_Option\.mjs/); +assert.equal( + output.match(/@rescript\/runtime\/lib\/es6\/Stdlib_Option\.js/g)?.length ?? 0, + 1, +); + +await execClean(); diff --git a/tests/build_tests/rsc_mixed_runtime_import/rescript.json b/tests/build_tests/rsc_mixed_runtime_import/rescript.json new file mode 100644 index 00000000000..c9a0312aa05 --- /dev/null +++ b/tests/build_tests/rsc_mixed_runtime_import/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-mixed-runtime-import", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.mjs" + }, + "namespace": true +} diff --git a/tests/build_tests/rsc_mixed_runtime_import/src/MainLayout.res b/tests/build_tests/rsc_mixed_runtime_import/src/MainLayout.res new file mode 100644 index 00000000000..4a2dfa741f1 --- /dev/null +++ b/tests/build_tests/rsc_mixed_runtime_import/src/MainLayout.res @@ -0,0 +1,8 @@ +@module("@rescript/runtime/lib/es6/Stdlib_Option.js") +external getOr: (option<'a>, 'a) => 'a = "getOr" + +@react.component +let make = (~children) => { + let child = getOr(children, React.null) + {child} +} diff --git a/tests/build_tests/rsc_mixed_runtime_import/src/React.res b/tests/build_tests/rsc_mixed_runtime_import/src/React.res new file mode 100644 index 00000000000..733b8ee02f0 --- /dev/null +++ b/tests/build_tests/rsc_mixed_runtime_import/src/React.res @@ -0,0 +1,10 @@ +type element + +@val external null: element = "null" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" diff --git a/tests/build_tests/rsc_mixed_runtime_import/src/Sidebar.res b/tests/build_tests/rsc_mixed_runtime_import/src/Sidebar.res new file mode 100644 index 00000000000..25e1db24bca --- /dev/null +++ b/tests/build_tests/rsc_mixed_runtime_import/src/Sidebar.res @@ -0,0 +1,4 @@ +module Provider = { + @react.component + let make = (~children) => children +} diff --git a/tests/build_tests/rsc_nested_jsx_deep/input.js b/tests/build_tests/rsc_nested_jsx_deep/input.js new file mode 100644 index 00000000000..e9128fe30ed --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/input.js @@ -0,0 +1,33 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.js"); +const output = await fs.readFile(outputPath, "utf8"); +const sidebarOutputPath = path.join( + import.meta.dirname, + "src", + "Sidebar.res.js", +); +const sidebarOutput = await fs.readFile(sidebarOutputPath, "utf8"); + +assert.match( + output, + /JsxRuntime\.jsx\(Sidebar\$RscNestedJsxDeep\.Sidebar\$Group,/, +); +assert.doesNotMatch(output, /\.Group\.make,/); +assert.match(sidebarOutput, /Sidebar\$Group\$jsx/); +assert.match( + sidebarOutput, + /export \{[\s\S]*Group,[\s\S]*Sidebar\$Group,[\s\S]*Sidebar\$Group\$jsx[\s\S]*\}/s, +); + +await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_deep/rescript.json b/tests/build_tests/rsc_nested_jsx_deep/rescript.json new file mode 100644 index 00000000000..7f67937417b --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-nested-jsx-deep", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.js" + }, + "namespace": true +} diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/MainLayout.res b/tests/build_tests/rsc_nested_jsx_deep/src/MainLayout.res new file mode 100644 index 00000000000..3e31dfdfcbc --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/src/MainLayout.res @@ -0,0 +1,4 @@ +@react.component +let make = (~children) => { + {children} +} diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/React.res b/tests/build_tests/rsc_nested_jsx_deep/src/React.res new file mode 100644 index 00000000000..733b8ee02f0 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/src/React.res @@ -0,0 +1,10 @@ +type element + +@val external null: element = "null" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/Sidebar.res b/tests/build_tests/rsc_nested_jsx_deep/src/Sidebar.res new file mode 100644 index 00000000000..a7feecaa77a --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/src/Sidebar.res @@ -0,0 +1,6 @@ +module Group = { + @react.component + let make = (~children) => { + children + } +} diff --git a/tests/build_tests/rsc_nested_jsx_members/input.js b/tests/build_tests/rsc_nested_jsx_members/input.js index dda8ebdcd09..2814bdf5a4e 100644 --- a/tests/build_tests/rsc_nested_jsx_members/input.js +++ b/tests/build_tests/rsc_nested_jsx_members/input.js @@ -33,6 +33,12 @@ const externalSidebarOutput = await fs.readFile( externalSidebarOutputPath, "utf8", ); +const plainAccessOutputPath = path.join( + import.meta.dirname, + "src", + "PlainAccess.res.js", +); +const plainAccessOutput = await fs.readFile(plainAccessOutputPath, "utf8"); assert.match( output, @@ -65,5 +71,14 @@ assert.match( externalSidebarOutput, /export \{[\s\S]*Provider,[\s\S]*SidebarExternal\$Provider,[\s\S]*SidebarExternal\$Provider\$jsx[\s\S]*\}/s, ); +assert.match( + plainAccessOutput, + /let provider = Sidebar\$RscNestedJsxMembers\.Provider\.make;/, +); +assert.match( + plainAccessOutput, + /let callProvider = Sidebar\$RscNestedJsxMembers\.Provider\.make\(\{/, +); +assert.doesNotMatch(plainAccessOutput, /Sidebar\$Provider/); await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_members/src/PlainAccess.res b/tests/build_tests/rsc_nested_jsx_members/src/PlainAccess.res new file mode 100644 index 00000000000..50ad67b4bb5 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members/src/PlainAccess.res @@ -0,0 +1,3 @@ +let provider = Sidebar.Provider.make + +let callProvider = Sidebar.Provider.make({children: React.null}) diff --git a/tests/build_tests/rsc_nested_jsx_members_no_namespace/input.js b/tests/build_tests/rsc_nested_jsx_members_no_namespace/input.js new file mode 100644 index 00000000000..0a672353d49 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members_no_namespace/input.js @@ -0,0 +1,29 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.js"); +const output = await fs.readFile(outputPath, "utf8"); +const sidebarOutputPath = path.join( + import.meta.dirname, + "src", + "Sidebar.res.js", +); +const sidebarOutput = await fs.readFile(sidebarOutputPath, "utf8"); + +assert.match(output, /JsxRuntime\.jsx\(Sidebar\.Sidebar\$Provider,/); +assert.doesNotMatch(output, /Sidebar\.Provider\.make/); +assert.match( + sidebarOutput, + /export \{[\s\S]*Provider,[\s\S]*Sidebar\$Provider,[\s\S]*Sidebar\$Provider\$jsx[\s\S]*\}/s, +); + +await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_members_no_namespace/rescript.json b/tests/build_tests/rsc_nested_jsx_members_no_namespace/rescript.json new file mode 100644 index 00000000000..dba3aa44e5a --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members_no_namespace/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-nested-jsx-members-no-namespace", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.js" + }, + "namespace": false +} diff --git a/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/MainLayout.res b/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/MainLayout.res new file mode 100644 index 00000000000..e2c753eeb17 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/MainLayout.res @@ -0,0 +1,4 @@ +@react.component +let make = (~children) => { + {children} +} diff --git a/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/React.res b/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/React.res new file mode 100644 index 00000000000..733b8ee02f0 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/React.res @@ -0,0 +1,10 @@ +type element + +@val external null: element = "null" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" diff --git a/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/Sidebar.res b/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/Sidebar.res new file mode 100644 index 00000000000..ad06cf9dc20 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_members_no_namespace/src/Sidebar.res @@ -0,0 +1,6 @@ +module Provider = { + @react.component + let make = (~children) => { + children + } +} diff --git a/tests/build_tests/rsc_suffix_runtime_import/input.js b/tests/build_tests/rsc_suffix_runtime_import/input.js new file mode 100644 index 00000000000..ee1dd122963 --- /dev/null +++ b/tests/build_tests/rsc_suffix_runtime_import/input.js @@ -0,0 +1,34 @@ +// @ts-check + +import * as assert from "node:assert"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; +import { setup } from "#dev/process"; + +const { execBuild, execClean } = setup(import.meta.dirname); + +await execClean(); +await execBuild(); + +const outputPath = path.join(import.meta.dirname, "src", "MainLayout.res.mjs"); +const output = await fs.readFile(outputPath, "utf8"); + +assert.match( + output, + /import \* as Stdlib_Option from "@rescript\/runtime\/lib\/es6\/Stdlib_Option\.mjs";/, +); +assert.match( + output, + /import \* as Sidebar\$RscSuffixRuntimeImport from "\.\/Sidebar\.res\.mjs";/, +); +assert.match( + output, + /JsxRuntime\.jsx\(Sidebar\$RscSuffixRuntimeImport\.Sidebar\$Provider,/, +); +assert.equal(output.match(/Stdlib_Option\.(js|mjs)/g)?.length ?? 0, 1); +assert.doesNotMatch( + output, + /@rescript\/runtime\/lib\/es6\/Stdlib_Option\.(js|mjs)";[\s\S]*@rescript\/runtime\/lib\/es6\/Stdlib_Option\.(js|mjs)";/s, +); + +await execClean(); diff --git a/tests/build_tests/rsc_suffix_runtime_import/rescript.json b/tests/build_tests/rsc_suffix_runtime_import/rescript.json new file mode 100644 index 00000000000..177e276e8f0 --- /dev/null +++ b/tests/build_tests/rsc_suffix_runtime_import/rescript.json @@ -0,0 +1,16 @@ +{ + "name": "rsc-suffix-runtime-import", + "jsx": { + "version": 4 + }, + "sources": { + "dir": "src", + "subdirs": true + }, + "package-specs": { + "module": "esmodule", + "in-source": true, + "suffix": ".res.mjs" + }, + "namespace": true +} diff --git a/tests/build_tests/rsc_suffix_runtime_import/src/MainLayout.res b/tests/build_tests/rsc_suffix_runtime_import/src/MainLayout.res new file mode 100644 index 00000000000..a45377b4f3d --- /dev/null +++ b/tests/build_tests/rsc_suffix_runtime_import/src/MainLayout.res @@ -0,0 +1,5 @@ +@react.component +let make = (~children) => { + let child = Option.getOr(children, React.null) + {child} +} diff --git a/tests/build_tests/rsc_suffix_runtime_import/src/React.res b/tests/build_tests/rsc_suffix_runtime_import/src/React.res new file mode 100644 index 00000000000..75e3b2c2d78 --- /dev/null +++ b/tests/build_tests/rsc_suffix_runtime_import/src/React.res @@ -0,0 +1,12 @@ +type element + +@val external null: element = "null" + +external string: string => element = "%identity" + +type componentLike<'props, 'return> = 'props => 'return + +type component<'props> = componentLike<'props, element> + +@module("react/jsx-runtime") +external jsx: (component<'props>, 'props) => element = "jsx" diff --git a/tests/build_tests/rsc_suffix_runtime_import/src/Sidebar.res b/tests/build_tests/rsc_suffix_runtime_import/src/Sidebar.res new file mode 100644 index 00000000000..25e1db24bca --- /dev/null +++ b/tests/build_tests/rsc_suffix_runtime_import/src/Sidebar.res @@ -0,0 +1,4 @@ +module Provider = { + @react.component + let make = (~children) => children +} diff --git a/tests/tests/src/alias_default_value_test.mjs b/tests/tests/src/alias_default_value_test.mjs index 49f6103cef0..22e6ada439e 100644 --- a/tests/tests/src/alias_default_value_test.mjs +++ b/tests/tests/src/alias_default_value_test.mjs @@ -105,6 +105,8 @@ let Alias_default_value_test$C6$jsx = true; let Alias_default_value_test$C7$jsx = true; +let Alias_default_value_test$C8$jsx = true; + export { C0, C1, @@ -128,5 +130,7 @@ export { Alias_default_value_test$C6$jsx, Alias_default_value_test$C7, Alias_default_value_test$C7$jsx, + Alias_default_value_test$C8, + Alias_default_value_test$C8$jsx, } /* No side effect */ From a6a5dd1de1291d6f40ac0c2da6cbd44f11c2fd17 Mon Sep 17 00:00:00 2001 From: tsnobip Date: Sun, 22 Mar 2026 18:43:53 +0100 Subject: [PATCH 13/21] fix syntax tests --- .../data/ppx/react/expected/defaultPatternProp.res.txt | 4 +++- .../data/ppx/react/expected/returnConstraint.res.txt | 1 + .../data/ppx/react/expected/sharedPropsWithProps.res.txt | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt index 97ed9f45567..4f7291dacc9 100644 --- a/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt @@ -25,7 +25,7 @@ module C0 = { | None => module(M: S) } let module(C: S) = __component_value - (React.jsx(C.make, {}): React.element) + (React.jsx(@res.jsxComponentPath C.make, {}): React.element) } let make = { let \"DefaultPatternProp$C0" = (props: props<_>) => make(props) @@ -33,3 +33,5 @@ module C0 = { \"DefaultPatternProp$C0" } } +let \"DefaultPatternProp$C0$M" = C0.M.make and \"DefaultPatternProp$C0$M$jsx" = true +let \"DefaultPatternProp$C0" = C0.make and \"DefaultPatternProp$C0$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt index 84cd1b66fd8..eff4facc7bd 100644 --- a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt @@ -49,4 +49,5 @@ module Async = { } let \"ReturnConstraint$Standard" = Standard.make and \"ReturnConstraint$Standard$jsx" = true let \"ReturnConstraint$ForwardRef" = ForwardRef.make and \"ReturnConstraint$ForwardRef$jsx" = true +let \"ReturnConstraint$WithProps" = WithProps.make and \"ReturnConstraint$WithProps$jsx" = true let \"ReturnConstraint$Async" = Async.make and \"ReturnConstraint$Async$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt index 8abf4452b0f..f03c5e7248f 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt @@ -76,3 +76,10 @@ module V4A7 = { \"SharedPropsWithProps$V4A7" } } +let \"SharedPropsWithProps$V4A1" = V4A1.make and \"SharedPropsWithProps$V4A1$jsx" = true +let \"SharedPropsWithProps$V4A2" = V4A2.make and \"SharedPropsWithProps$V4A2$jsx" = true +let \"SharedPropsWithProps$V4A3" = V4A3.make and \"SharedPropsWithProps$V4A3$jsx" = true +let \"SharedPropsWithProps$V4A4" = V4A4.make and \"SharedPropsWithProps$V4A4$jsx" = true +let \"SharedPropsWithProps$V4A5" = V4A5.make and \"SharedPropsWithProps$V4A5$jsx" = true +let \"SharedPropsWithProps$V4A6" = V4A6.make and \"SharedPropsWithProps$V4A6$jsx" = true +let \"SharedPropsWithProps$V4A7" = V4A7.make and \"SharedPropsWithProps$V4A7$jsx" = true From 18c14c75acd09f55e58d3004eaf4eff4192dad75 Mon Sep 17 00:00:00 2001 From: tsnobip Date: Mon, 23 Mar 2026 15:50:20 +0100 Subject: [PATCH 14/21] add repro where direct component function doesn't get exported --- tests/build_tests/rsc_nested_jsx_deep/input.js | 3 +++ .../rsc_nested_jsx_deep/src/BrandIcons.res | 12 ++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res diff --git a/tests/build_tests/rsc_nested_jsx_deep/input.js b/tests/build_tests/rsc_nested_jsx_deep/input.js index e9128fe30ed..eca3b4b5334 100644 --- a/tests/build_tests/rsc_nested_jsx_deep/input.js +++ b/tests/build_tests/rsc_nested_jsx_deep/input.js @@ -30,4 +30,7 @@ assert.match( /export \{[\s\S]*Group,[\s\S]*Sidebar\$Group,[\s\S]*Sidebar\$Group\$jsx[\s\S]*\}/s, ); +const brandIcons = await import("./src/BrandIcons.res.js"); +assert.match(Object.keys(brandIcons).join(", "), /ReScript, BrandIcons\$ReScript, BrandIcons\$ReScript\$jsx, getIconForLanguageExtension/); + await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res b/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res new file mode 100644 index 00000000000..3f435fab92c --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res @@ -0,0 +1,12 @@ +module ReScript = { + @react.component + let make = () => React.null +} + + +let getIconForLanguageExtension = (language: string) => { + switch language { + | "res" | "rescript" => + | _ => React.null + } +} From e3ad8b00ab568428ed446a63ffe4851a0735a5fc Mon Sep 17 00:00:00 2001 From: tsnobip Date: Mon, 23 Mar 2026 16:30:49 +0100 Subject: [PATCH 15/21] fix export of JSX components inside module with regular functions --- compiler/syntax/src/jsx_common.ml | 6 ++++ compiler/syntax/src/jsx_ppx.ml | 6 +++- .../react_ppx/src/gpr_3695_test.res.js | 8 +++++- .../react_ppx/src/gpr_3987_test.res.js | 8 ++++++ .../react_ppx/src/issue_7917_test.res.js | 4 +++ .../build_tests/rsc_nested_jsx_deep/input.js | 23 ++++++++++++++- .../rsc_nested_jsx_deep/src/BrandIcons.res | 3 +- .../src/MultipleNested.res | 13 +++++++++ .../data/ppx/react/expected/newtype.res.txt | 4 +++ .../data/ppx/react/expected/v4.res.txt | 1 + tests/tests/src/jsx_preserve_test.mjs | 28 +++++++++++++++++++ 11 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res diff --git a/compiler/syntax/src/jsx_common.ml b/compiler/syntax/src/jsx_common.ml index e7b8698c701..259d9496113 100644 --- a/compiler/syntax/src/jsx_common.ml +++ b/compiler/syntax/src/jsx_common.ml @@ -7,6 +7,12 @@ type jsx_config = { mutable nested_modules: string list; mutable has_component: bool; mutable hoisted_structure_items: structure_item list; + (* Nesting depth of [structure] calls while rewriting one implementation. + Used so we only clear/append hoisted items at the true file root — not for + nested [Pmod_structure] reached from expression traversal (e.g. via + [module_expr]), which would otherwise see [nested_modules = []] and wipe + hoisted bindings added for earlier structure items. *) + mutable structure_depth: int; } (* Helper method to look up the [@react.component] attribute *) diff --git a/compiler/syntax/src/jsx_ppx.ml b/compiler/syntax/src/jsx_ppx.ml index 078d812eca9..89d170a4bb7 100644 --- a/compiler/syntax/src/jsx_ppx.ml +++ b/compiler/syntax/src/jsx_ppx.ml @@ -117,7 +117,8 @@ let get_mapper ~config = in let structure mapper items = let old_config = save_config () in - let is_top_level = config.nested_modules = [] in + let is_top_level = config.structure_depth = 0 in + config.structure_depth <- config.structure_depth + 1; config.has_component <- false; if is_top_level then config.hoisted_structure_items <- []; let result = @@ -136,6 +137,7 @@ let get_mapper ~config = result @ List.rev config.hoisted_structure_items else result in + config.structure_depth <- config.structure_depth - 1; restore_config old_config; result in @@ -151,6 +153,7 @@ let rewrite_implementation ~jsx_version ~jsx_module (code : Parsetree.structure) nested_modules = []; has_component = false; hoisted_structure_items = []; + structure_depth = 0; } in let mapper = get_mapper ~config in @@ -165,6 +168,7 @@ let rewrite_signature ~jsx_version ~jsx_module (code : Parsetree.signature) : nested_modules = []; has_component = false; hoisted_structure_items = []; + structure_depth = 0; } in let mapper = get_mapper ~config in diff --git a/tests/build_tests/react_ppx/src/gpr_3695_test.res.js b/tests/build_tests/react_ppx/src/gpr_3695_test.res.js index ebaf897020a..11c0cca5c42 100644 --- a/tests/build_tests/react_ppx/src/gpr_3695_test.res.js +++ b/tests/build_tests/react_ppx/src/gpr_3695_test.res.js @@ -10,9 +10,15 @@ function test(className) { return Foo; } +let Gpr_3695_test$Test = Foo; + +let Gpr_3695_test$Test$jsx = true; + export { React, Test, test, + Gpr_3695_test$Test, + Gpr_3695_test$Test$jsx, } -/* Foo Not a pure module */ +/* Gpr_3695_test$Test Not a pure module */ diff --git a/tests/build_tests/react_ppx/src/gpr_3987_test.res.js b/tests/build_tests/react_ppx/src/gpr_3987_test.res.js index 378f9156caf..b2b8ed47b8a 100644 --- a/tests/build_tests/react_ppx/src/gpr_3987_test.res.js +++ b/tests/build_tests/react_ppx/src/gpr_3987_test.res.js @@ -55,10 +55,18 @@ JsxRuntime.jsx(Gpr_3987_test$Gpr3987ReproError, { onChange: (param, param$1) => {} }); +let Gpr_3987_test$Gpr3987ReproOk2$jsx = true; + +let Gpr_3987_test$Gpr3987ReproError$jsx = true; + export { makeContainer, Gpr3987ReproOk, Gpr3987ReproOk2, Gpr3987ReproError, + Gpr_3987_test$Gpr3987ReproOk2, + Gpr_3987_test$Gpr3987ReproOk2$jsx, + Gpr_3987_test$Gpr3987ReproError, + Gpr_3987_test$Gpr3987ReproError$jsx, } /* Not a pure module */ diff --git a/tests/build_tests/react_ppx/src/issue_7917_test.res.js b/tests/build_tests/react_ppx/src/issue_7917_test.res.js index 9df85100b95..14a6b09fba1 100644 --- a/tests/build_tests/react_ppx/src/issue_7917_test.res.js +++ b/tests/build_tests/react_ppx/src/issue_7917_test.res.js @@ -18,8 +18,12 @@ function Issue_7917_test(props) { let make = Issue_7917_test; +let Issue_7917_test$M$jsx = true; + export { M, make, + Issue_7917_test$M, + Issue_7917_test$M$jsx, } /* react/jsx-runtime Not a pure module */ diff --git a/tests/build_tests/rsc_nested_jsx_deep/input.js b/tests/build_tests/rsc_nested_jsx_deep/input.js index eca3b4b5334..9e9ad8fa159 100644 --- a/tests/build_tests/rsc_nested_jsx_deep/input.js +++ b/tests/build_tests/rsc_nested_jsx_deep/input.js @@ -31,6 +31,27 @@ assert.match( ); const brandIcons = await import("./src/BrandIcons.res.js"); -assert.match(Object.keys(brandIcons).join(", "), /ReScript, BrandIcons\$ReScript, BrandIcons\$ReScript\$jsx, getIconForLanguageExtension/); +assert.deepStrictEqual( + new Set(Object.keys(brandIcons)), + new Set([ + "ReScript", + "BrandIcons$ReScript", + "BrandIcons$ReScript$jsx", + "getIconForLanguageExtension", + ]), +); + +const multipleNested = await import("./src/MultipleNested.res.js"); +assert.deepStrictEqual( + new Set(Object.keys(multipleNested)), + new Set([ + "Group", + "MultipleNested$Group", + "MultipleNested$Group$jsx", + "Other", + "MultipleNested$Other", + "MultipleNested$Other$jsx", + ]), +); await execClean(); diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res b/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res index 3f435fab92c..05ec1396656 100644 --- a/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res +++ b/tests/build_tests/rsc_nested_jsx_deep/src/BrandIcons.res @@ -3,10 +3,9 @@ module ReScript = { let make = () => React.null } - let getIconForLanguageExtension = (language: string) => { switch language { - | "res" | "rescript" => + | "res" | "rescript" => | _ => React.null } } diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res b/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res new file mode 100644 index 00000000000..4e6780c8eee --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res @@ -0,0 +1,13 @@ +module Group = { + @react.component + let make = (~children) => { + children + } +} + +module Other = { + @react.component + let make = (~children) => { + children + } +} diff --git a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt index 838fd0daa57..8296472d4d1 100644 --- a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt @@ -99,4 +99,8 @@ module Uncurried = { \"Newtype$Uncurried" } } +let \"Newtype$V4A" = V4A.make and \"Newtype$V4A$jsx" = true +let \"Newtype$V4A1" = V4A1.make and \"Newtype$V4A1$jsx" = true +let \"Newtype$V4A2" = V4A2.make and \"Newtype$V4A2$jsx" = true +let \"Newtype$V4A3" = V4A3.make and \"Newtype$V4A3$jsx" = true let \"Newtype$Uncurried" = Uncurried.make and \"Newtype$Uncurried$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt index 40159d7edbc..c4de276ae23 100644 --- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt @@ -116,6 +116,7 @@ module Rec2 = { } and mm = x => make(x) } +let \"V4$Uncurried" = Uncurried.make and \"V4$Uncurried$jsx" = true let \"V4$E" = E.make and \"V4$E$jsx" = true let \"V4$EUncurried" = EUncurried.make and \"V4$EUncurried$jsx" = true let \"V4$Rec" = Rec.make and \"V4$Rec$jsx" = true diff --git a/tests/tests/src/jsx_preserve_test.mjs b/tests/tests/src/jsx_preserve_test.mjs index 570ca4607cf..5d074e9aa84 100644 --- a/tests/tests/src/jsx_preserve_test.mjs +++ b/tests/tests/src/jsx_preserve_test.mjs @@ -247,8 +247,24 @@ function Jsx_preserve_test(props) { ; } +let Jsx_preserve_test$A = QueryClientProvider; + let make$3 = Jsx_preserve_test; +let Jsx_preserve_test$Icon$jsx = true; + +let Jsx_preserve_test$A$jsx = true; + +let Jsx_preserve_test$B$jsx = true; + +let Jsx_preserve_test$MyWeirdComponent$jsx = true; + +let Jsx_preserve_test$ComponentWithOptionalProps$jsx = true; + +let Jsx_preserve_test$Y$1 = make$1; + +let Jsx_preserve_test$Y$jsx = true; + export { Icon, _single_element_child, @@ -281,5 +297,17 @@ export { context, ContextProvider, make$3 as make, + Jsx_preserve_test$Icon, + Jsx_preserve_test$Icon$jsx, + Jsx_preserve_test$A, + Jsx_preserve_test$A$jsx, + Jsx_preserve_test$B, + Jsx_preserve_test$B$jsx, + Jsx_preserve_test$MyWeirdComponent, + Jsx_preserve_test$MyWeirdComponent$jsx, + Jsx_preserve_test$ComponentWithOptionalProps, + Jsx_preserve_test$ComponentWithOptionalProps$jsx, + Jsx_preserve_test$Y$1 as Jsx_preserve_test$Y, + Jsx_preserve_test$Y$jsx, } /* _single_element_child Not a pure module */ From 04a584623a1284efff857b1bd32c6e6b3ee3cce7 Mon Sep 17 00:00:00 2001 From: tsnobip Date: Tue, 24 Mar 2026 12:46:10 +0100 Subject: [PATCH 16/21] update analysis/gentype tests --- .../tests/src/expected/CreateInterface.res.txt | 4 ++++ .../typescript-react-example/src/Hooks.res.js | 18 ++++++++++++++++++ .../typescript-react-example/src/JSXV4.res.js | 4 ++++ 3 files changed, 26 insertions(+) diff --git a/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt b/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt index e71ea422ce1..6a135ff1b04 100644 --- a/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt +++ b/tests/analysis_tests/tests/src/expected/CreateInterface.res.txt @@ -132,6 +132,10 @@ module OrderedSet: { } let make: (~id: module(Belt.Id.Comparable with type identity = 'a and type t = 'b)) => t<'b, 'a> } +let CreateInterface$Mod: Mod.props => React.element +let CreateInterface$Mod$jsx: bool +let CreateInterface$Memo: React.component> +let CreateInterface$Memo$jsx: bool let CreateInterface$ComponentWithPolyProp: ComponentWithPolyProp.props< [< #large | #small], > => React.element diff --git a/tests/gentype_tests/typescript-react-example/src/Hooks.res.js b/tests/gentype_tests/typescript-react-example/src/Hooks.res.js index 7eafb7e5f95..874f987d625 100644 --- a/tests/gentype_tests/typescript-react-example/src/Hooks.res.js +++ b/tests/gentype_tests/typescript-react-example/src/Hooks.res.js @@ -204,6 +204,16 @@ let make$1 = Hooks; let $$default = Hooks; +let Hooks$Inner$jsx = true; + +let Hooks$Inner$Inner2$jsx = true; + +let Hooks$NoProps$jsx = true; + +let Hooks$WithRef = make; + +let Hooks$WithRef$jsx = true; + let Hooks$RenderPropRequiresConversion$jsx = true; let Hooks$DD$jsx = true; @@ -223,6 +233,14 @@ export { RenderPropRequiresConversion, WithChildren, DD, + Hooks$Inner, + Hooks$Inner$jsx, + Hooks$Inner$Inner2, + Hooks$Inner$Inner2$jsx, + Hooks$NoProps, + Hooks$NoProps$jsx, + Hooks$WithRef, + Hooks$WithRef$jsx, Hooks$RenderPropRequiresConversion, Hooks$RenderPropRequiresConversion$jsx, Hooks$DD, diff --git a/tests/gentype_tests/typescript-react-example/src/JSXV4.res.js b/tests/gentype_tests/typescript-react-example/src/JSXV4.res.js index 016dda6790c..4a12f2143f3 100644 --- a/tests/gentype_tests/typescript-react-example/src/JSXV4.res.js +++ b/tests/gentype_tests/typescript-react-example/src/JSXV4.res.js @@ -12,8 +12,12 @@ let CompV4 = { let make = JSXV4Gen.make; +let JSXV4$CompV4$jsx = true; + export { CompV4, make, + JSXV4$CompV4, + JSXV4$CompV4$jsx, } /* make Not a pure module */ From 573dd37e275cdf8c4d381780a3ecf065ca4cfc5a Mon Sep 17 00:00:00 2001 From: tsnobip Date: Tue, 24 Mar 2026 12:46:20 +0100 Subject: [PATCH 17/21] fix functors --- compiler/syntax/src/jsx_common.ml | 3 +++ compiler/syntax/src/jsx_ppx.ml | 17 ++++++++++++++++- compiler/syntax/src/jsx_v4.ml | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/compiler/syntax/src/jsx_common.ml b/compiler/syntax/src/jsx_common.ml index 259d9496113..d07e3338dcc 100644 --- a/compiler/syntax/src/jsx_common.ml +++ b/compiler/syntax/src/jsx_common.ml @@ -13,6 +13,9 @@ type jsx_config = { [module_expr]), which would otherwise see [nested_modules = []] and wipe hoisted bindings added for earlier structure items. *) mutable structure_depth: int; + (* Inside [Pmod_functor] bodies, hoisting [File$M = M.make] at the file top + would reference [M] as a functor (illegal). Skip hoists when > 0. *) + mutable functor_depth: int; } (* Helper method to look up the [@react.component] attribute *) diff --git a/compiler/syntax/src/jsx_ppx.ml b/compiler/syntax/src/jsx_ppx.ml index 89d170a4bb7..0b4ab0f2def 100644 --- a/compiler/syntax/src/jsx_ppx.ml +++ b/compiler/syntax/src/jsx_ppx.ml @@ -85,6 +85,19 @@ let get_mapper ~config = | 4 -> module_binding4 mapper mb | _ -> default_mapper.module_binding mapper mb in + let module_expr mapper me = + match (config.version, me.pmod_desc) with + | 4, Pmod_functor _ -> ( + config.functor_depth <- config.functor_depth + 1; + try + let m = default_mapper.module_expr mapper me in + config.functor_depth <- config.functor_depth - 1; + m + with e -> + config.functor_depth <- config.functor_depth - 1; + raise e) + | _ -> default_mapper.module_expr mapper me + in let save_config () = { config with @@ -142,7 +155,7 @@ let get_mapper ~config = result in - {default_mapper with expr; module_binding; signature; structure} + {default_mapper with expr; module_binding; module_expr; signature; structure} let rewrite_implementation ~jsx_version ~jsx_module (code : Parsetree.structure) : Parsetree.structure = @@ -154,6 +167,7 @@ let rewrite_implementation ~jsx_version ~jsx_module (code : Parsetree.structure) has_component = false; hoisted_structure_items = []; structure_depth = 0; + functor_depth = 0; } in let mapper = get_mapper ~config in @@ -169,6 +183,7 @@ let rewrite_signature ~jsx_version ~jsx_module (code : Parsetree.signature) : has_component = false; hoisted_structure_items = []; structure_depth = 0; + functor_depth = 0; } in let mapper = get_mapper ~config in diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 566f790cd5f..5eb52ece30b 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -132,8 +132,8 @@ let unnamespace_module_name file_name = let maybe_hoist_nested_make_component ~(config : Jsx_common.jsx_config) ~empty_loc ~full_module_name fn_name = - match (fn_name, config.nested_modules) with - | "make", _ :: _ -> + match (fn_name, config.nested_modules, config.functor_depth) with + | "make", _ :: _, 0 -> config.hoisted_structure_items <- make_hoisted_component_binding ~empty_loc ~full_module_name config.nested_modules From f0ebf0d1491758b945b7c0e32ac2483bca4c507b Mon Sep 17 00:00:00 2001 From: tsnobip Date: Tue, 24 Mar 2026 16:34:14 +0100 Subject: [PATCH 18/21] add failing test with interface file --- .../rsc_nested_jsx_deep/src/MultipleNested.resi | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.resi diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.resi b/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.resi new file mode 100644 index 00000000000..961a38e9aa2 --- /dev/null +++ b/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.resi @@ -0,0 +1,9 @@ +module Group: { + @react.component + let make: (~children: React.element) => React.element +} + +module Other: { + @react.component + let make: (~children: React.element) => React.element +} From 26cf5e255ea8914c0c40f5860ce3d389bb0ee0fa Mon Sep 17 00:00:00 2001 From: tsnobip Date: Tue, 24 Mar 2026 16:46:42 +0100 Subject: [PATCH 19/21] fix signature of @jsx.component --- compiler/syntax/src/jsx_common.ml | 13 +-- compiler/syntax/src/jsx_ppx.ml | 53 ++++++++++- compiler/syntax/src/jsx_v4.ml | 89 +++++++++++++++++-- .../react/expected/firstClassModules.resi.txt | 9 ++ .../ppx/react/expected/forwardRef.resi.txt | 32 +++++++ .../ppx/react/expected/interface.resi.txt | 4 + .../ppx/react/expected/sharedProps.resi.txt | 16 ++++ 7 files changed, 201 insertions(+), 15 deletions(-) diff --git a/compiler/syntax/src/jsx_common.ml b/compiler/syntax/src/jsx_common.ml index d07e3338dcc..84667be8124 100644 --- a/compiler/syntax/src/jsx_common.ml +++ b/compiler/syntax/src/jsx_common.ml @@ -7,14 +7,15 @@ type jsx_config = { mutable nested_modules: string list; mutable has_component: bool; mutable hoisted_structure_items: structure_item list; - (* Nesting depth of [structure] calls while rewriting one implementation. + mutable hoisted_signature_items: signature_item list; + (* Nesting depth of [structure] / [signature] while rewriting one file. Used so we only clear/append hoisted items at the true file root — not for - nested [Pmod_structure] reached from expression traversal (e.g. via - [module_expr]), which would otherwise see [nested_modules = []] and wipe - hoisted bindings added for earlier structure items. *) + nested [Pmod_structure] / [Pmty_signature] reached from inner traversal, + which would otherwise wipe hoists from earlier items. *) mutable structure_depth: int; - (* Inside [Pmod_functor] bodies, hoisting [File$M = M.make] at the file top - would reference [M] as a functor (illegal). Skip hoists when > 0. *) + (* Inside [Pmod_functor] / [Pmty_functor] bodies, hoisting [File$M = M.make] + at the file top would reference [M] as a functor (illegal). Skip hoists + when > 0. *) mutable functor_depth: int; } diff --git a/compiler/syntax/src/jsx_ppx.ml b/compiler/syntax/src/jsx_ppx.ml index 0b4ab0f2def..e3b22f0fca4 100644 --- a/compiler/syntax/src/jsx_ppx.ml +++ b/compiler/syntax/src/jsx_ppx.ml @@ -98,6 +98,37 @@ let get_mapper ~config = raise e) | _ -> default_mapper.module_expr mapper me in + let module_type mapper mt = + match (config.version, mt.pmty_desc) with + | 4, Pmty_functor _ -> ( + config.functor_depth <- config.functor_depth + 1; + try + let m = default_mapper.module_type mapper mt in + config.functor_depth <- config.functor_depth - 1; + m + with e -> + config.functor_depth <- config.functor_depth - 1; + raise e) + | _ -> default_mapper.module_type mapper mt + in + let module_declaration mapper md = + match config.version with + | 4 -> + config.nested_modules <- md.pmd_name.txt :: config.nested_modules; + let mapped = + try default_mapper.module_declaration mapper md + with e -> + (match config.nested_modules with + | _ :: rest -> config.nested_modules <- rest + | [] -> ()); + raise e + in + (match config.nested_modules with + | _ :: rest -> config.nested_modules <- rest + | [] -> ()); + mapped + | _ -> default_mapper.module_declaration mapper md + in let save_config () = { config with @@ -113,7 +144,10 @@ let get_mapper ~config = in let signature mapper items = let old_config = save_config () in + let is_top_level = config.structure_depth = 0 in + config.structure_depth <- config.structure_depth + 1; config.has_component <- false; + if is_top_level then config.hoisted_signature_items <- []; let result = List.map (fun item -> @@ -125,6 +159,12 @@ let get_mapper ~config = items |> List.flatten in + let result = + if config.version = 4 && is_top_level then + result @ List.rev config.hoisted_signature_items + else result + in + config.structure_depth <- config.structure_depth - 1; restore_config old_config; result in @@ -155,7 +195,16 @@ let get_mapper ~config = result in - {default_mapper with expr; module_binding; module_expr; signature; structure} + { + default_mapper with + expr; + module_binding; + module_expr; + module_type; + module_declaration; + signature; + structure; + } let rewrite_implementation ~jsx_version ~jsx_module (code : Parsetree.structure) : Parsetree.structure = @@ -166,6 +215,7 @@ let rewrite_implementation ~jsx_version ~jsx_module (code : Parsetree.structure) nested_modules = []; has_component = false; hoisted_structure_items = []; + hoisted_signature_items = []; structure_depth = 0; functor_depth = 0; } @@ -182,6 +232,7 @@ let rewrite_signature ~jsx_version ~jsx_module (code : Parsetree.signature) : nested_modules = []; has_component = false; hoisted_structure_items = []; + hoisted_signature_items = []; structure_depth = 0; functor_depth = 0; } diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 5eb52ece30b..17803f6be80 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -85,6 +85,17 @@ let longident_of_segments = function | head :: rest -> List.fold_left (fun acc name -> Ldot (acc, name)) (Lident head) rest +(* [nested_modules] is the same stack as [config.nested_modules] while inside a + nested [module M: { ... }]: outermost submodule name is at the tail. *) +let props_longident_for_nested_module nested_modules = + match List.rev nested_modules with + | [] -> Lident "props" + | m :: rest -> + let mod_path = + List.fold_left (fun acc name -> Ldot (acc, name)) (Lident m) rest + in + Ldot (mod_path, "props") + let make_hoisted_component_binding ~empty_loc ~full_module_name nested_modules = let path = nested_modules |> List.rev |> longident_of_segments |> fun txt -> @@ -140,6 +151,44 @@ let maybe_hoist_nested_make_component ~(config : Jsx_common.jsx_config) :: config.hoisted_structure_items | _ -> () +let make_hoisted_component_signature ~empty_loc ~full_module_name + (component_type : Parsetree.core_type) = + let marker_name = full_module_name ^ "$jsx" in + let bool_ty = + Typ.constr ~loc:empty_loc {loc = empty_loc; txt = Lident "bool"} [] + in + let full_sig = + { + psig_loc = empty_loc; + psig_desc = + Psig_value + (Val.mk ~loc:empty_loc + {loc = empty_loc; txt = full_module_name} + component_type); + } + in + let jsx_sig = + { + psig_loc = empty_loc; + psig_desc = + Psig_value + (Val.mk ~loc:empty_loc {loc = empty_loc; txt = marker_name} bool_ty); + } + in + (full_sig, jsx_sig) + +let maybe_hoist_nested_make_signature ~(config : Jsx_common.jsx_config) + ~empty_loc ~full_module_name ~component_type fn_name = + match (fn_name, config.nested_modules, config.functor_depth) with + | "make", _ :: _, 0 -> + let full_sig, jsx_sig = + make_hoisted_component_signature ~empty_loc ~full_module_name + component_type + in + config.hoisted_signature_items <- + jsx_sig :: full_sig :: config.hoisted_signature_items + | _ -> () + (* Build a string representation of a module name with segments separated by $ *) let make_module_name file_name nested_modules fn_name = let file_name = unnamespace_module_name file_name in @@ -1101,7 +1150,8 @@ let transform_signature_item ~config item = match item with | { psig_loc; - psig_desc = Psig_value ({pval_attributes; pval_type} as psig_desc); + psig_desc = + Psig_value ({pval_attributes; pval_type; pval_name} as psig_desc); } as psig -> ( match List.filter Jsx_common.has_attr pval_attributes with | [] -> [item] @@ -1117,15 +1167,23 @@ let transform_signature_item ~config item = in let prop_types = collect_prop_types [] pval_type in let named_type_list = List.fold_left arg_to_concrete_type [] prop_types in + let props_type_args = + match core_type_of_attr with + | None -> make_props_type_params named_type_list + | Some _ -> ( + match typ_vars_of_core_type with + | [] -> [] + | _ -> [Typ.any ()]) + in let ret_props_type = + Typ.constr (Location.mkloc (Lident "props") psig_loc) props_type_args + in + let ret_props_type_for_hoist = Typ.constr - (Location.mkloc (Lident "props") psig_loc) - (match core_type_of_attr with - | None -> make_props_type_params named_type_list - | Some _ -> ( - match typ_vars_of_core_type with - | [] -> [] - | _ -> [Typ.any ()])) + (Location.mkloc + (props_longident_for_nested_module config.nested_modules) + psig_loc) + props_type_args in let external_ = psig_desc.pval_prim <> [] in let props_record_type = @@ -1138,6 +1196,11 @@ let transform_signature_item ~config item = ( {loc = psig_loc; txt = module_access_name config "component"}, [ret_props_type] ) in + let new_external_type_for_hoist = + Ptyp_constr + ( {loc = psig_loc; txt = module_access_name config "component"}, + [ret_props_type_for_hoist] ) + in let new_structure = { psig with @@ -1150,6 +1213,16 @@ let transform_signature_item ~config item = }; } in + let file_name = filename_from_loc psig_loc in + let empty_loc = Location.in_file file_name in + let full_module_name = + make_module_name file_name config.nested_modules pval_name.txt + in + let component_type = + {pval_type with ptyp_desc = new_external_type_for_hoist} + in + maybe_hoist_nested_make_signature ~config ~empty_loc ~full_module_name + ~component_type pval_name.txt; [props_record_type; new_structure] | _ -> Jsx_common.raise_error ~loc:psig_loc diff --git a/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt b/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt index feabbdaf5f4..9a3cb5536f3 100644 --- a/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/firstClassModules.resi.txt @@ -22,3 +22,12 @@ module Select: { >, > } +let \"FirstClassModules$Select": React.component< + Select.props< + module(T with type t = 'a and type key = 'key), + option<'key>, + option<'key> => unit, + array<'a>, + >, +> +let \"FirstClassModules$Select$jsx": bool diff --git a/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt b/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt index 47cd6f10105..89055b25426 100644 --- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.resi.txt @@ -89,3 +89,35 @@ module V4AUncurried: { let make: React.component>>> } } +let \"ForwardRef$V4C$FancyInput": React.component< + V4C.FancyInput.props, +> +let \"ForwardRef$V4C$FancyInput$jsx": bool +let \"ForwardRef$V4C$ForwardRef": React.component< + V4C.ForwardRef.props>>, +> +let \"ForwardRef$V4C$ForwardRef$jsx": bool +let \"ForwardRef$V4CUncurried$FancyInput": React.component< + V4CUncurried.FancyInput.props, +> +let \"ForwardRef$V4CUncurried$FancyInput$jsx": bool +let \"ForwardRef$V4CUncurried$ForwardRef": React.component< + V4CUncurried.ForwardRef.props>>, +> +let \"ForwardRef$V4CUncurried$ForwardRef$jsx": bool +let \"ForwardRef$V4A$FancyInput": React.component< + V4A.FancyInput.props, +> +let \"ForwardRef$V4A$FancyInput$jsx": bool +let \"ForwardRef$V4A$ForwardRef": React.component< + V4A.ForwardRef.props>>, +> +let \"ForwardRef$V4A$ForwardRef$jsx": bool +let \"ForwardRef$V4AUncurried$FancyInput": React.component< + V4AUncurried.FancyInput.props, +> +let \"ForwardRef$V4AUncurried$FancyInput$jsx": bool +let \"ForwardRef$V4AUncurried$ForwardRef": React.component< + V4AUncurried.ForwardRef.props>>, +> +let \"ForwardRef$V4AUncurried$ForwardRef$jsx": bool diff --git a/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt b/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt index d0cc914abf7..6a21c4d722c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interface.resi.txt @@ -12,3 +12,7 @@ module NoProps: { let make: React.component } +let \"Interface$A": React.component> +let \"Interface$A$jsx": bool +let \"Interface$NoProps": React.component +let \"Interface$NoProps$jsx": bool diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt b/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt index 3deabcfbe3e..7674ec9fc3a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.resi.txt @@ -49,3 +49,19 @@ module V4A4: { let make: React.component } +let \"SharedProps$V4C1": React.component +let \"SharedProps$V4C1$jsx": bool +let \"SharedProps$V4C2": React.component> +let \"SharedProps$V4C2$jsx": bool +let \"SharedProps$V4C3": React.component> +let \"SharedProps$V4C3$jsx": bool +let \"SharedProps$V4C4": React.component +let \"SharedProps$V4C4$jsx": bool +let \"SharedProps$V4A1": React.component +let \"SharedProps$V4A1$jsx": bool +let \"SharedProps$V4A2": React.component> +let \"SharedProps$V4A2$jsx": bool +let \"SharedProps$V4A3": React.component> +let \"SharedProps$V4A3$jsx": bool +let \"SharedProps$V4A4": React.component +let \"SharedProps$V4A4$jsx": bool From 69603a7c86c3b50e8885a8209a5ad3e4cdc16479 Mon Sep 17 00:00:00 2001 From: tsnobip Date: Tue, 24 Mar 2026 19:07:39 +0100 Subject: [PATCH 20/21] repro: private JSX components raise warning 32 --- tests/build_tests/rsc_nested_jsx_deep/rescript.json | 7 +++++-- .../rsc_nested_jsx_deep/src/MultipleNested.res | 9 ++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/build_tests/rsc_nested_jsx_deep/rescript.json b/tests/build_tests/rsc_nested_jsx_deep/rescript.json index 7f67937417b..07fe7f0788c 100644 --- a/tests/build_tests/rsc_nested_jsx_deep/rescript.json +++ b/tests/build_tests/rsc_nested_jsx_deep/rescript.json @@ -12,5 +12,8 @@ "in-source": true, "suffix": ".res.js" }, - "namespace": true -} + "namespace": true, + "warnings": { + "error": "+A" + } +} \ No newline at end of file diff --git a/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res b/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res index 4e6780c8eee..d3a41698541 100644 --- a/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res +++ b/tests/build_tests/rsc_nested_jsx_deep/src/MultipleNested.res @@ -1,10 +1,17 @@ -module Group = { +module Internal = { @react.component let make = (~children) => { children } } +module Group = { + @react.component + let make = (~children) => { + {children} + } +} + module Other = { @react.component let make = (~children) => { From 88e45fa69297ff6f60f2bcf39aa6e104ca75e1d0 Mon Sep 17 00:00:00 2001 From: tsnobip Date: Tue, 24 Mar 2026 19:37:28 +0100 Subject: [PATCH 21/21] remove warning -32 when emitting the namespaced function and jsx status --- compiler/syntax/src/jsx_v4.ml | 13 ++++++++-- .../rsc_nested_jsx_deep/rescript.json | 2 +- .../ppx/react/expected/aliasProps.res.txt | 14 +++++------ .../ppx/react/expected/asyncAwait.res.txt | 4 ++-- .../react/expected/defaultPatternProp.res.txt | 6 +++-- .../react/expected/defaultValueProp.res.txt | 12 ++++++---- .../react/expected/externalWithRef.res.txt | 3 ++- .../externalWithTypeVariables.res.txt | 3 ++- .../react/expected/fileLevelConfig.res.txt | 3 ++- .../ppx/react/expected/forwardRef.res.txt | 12 ++++++---- .../data/ppx/react/expected/interface.res.txt | 5 ++-- .../ppx/react/expected/mangleKeyword.res.txt | 6 +++-- .../data/ppx/react/expected/nested.res.txt | 5 ++-- .../data/ppx/react/expected/newtype.res.txt | 11 +++++---- .../ppx/react/expected/noPropsWithKey.res.txt | 9 ++++--- .../expected/optimizeAutomaticMode.res.txt | 3 ++- .../react/expected/returnConstraint.res.txt | 12 ++++++---- .../ppx/react/expected/sharedProps.res.txt | 24 ++++++++++++------- .../expected/sharedPropsWithProps.res.txt | 21 ++++++++++------ .../data/ppx/react/expected/topLevel.res.txt | 2 +- .../ppx/react/expected/typeConstraint.res.txt | 3 ++- .../ppx/react/expected/uncurriedProps.res.txt | 6 +++-- .../data/ppx/react/expected/v4.res.txt | 13 +++++----- 23 files changed, 122 insertions(+), 70 deletions(-) diff --git a/compiler/syntax/src/jsx_v4.ml b/compiler/syntax/src/jsx_v4.ml index 17803f6be80..9412449a604 100644 --- a/compiler/syntax/src/jsx_v4.ml +++ b/compiler/syntax/src/jsx_v4.ml @@ -96,6 +96,15 @@ let props_longident_for_nested_module nested_modules = in Ldot (mod_path, "props") +(* Hoisted File$Nested / File$Nested$jsx exist for JS/RSC exports; they are not + always referenced from ReScript when a nested module is absent from the + [.resi], so suppress unused-value (32) on these bindings only. *) +let jsx_hoisted_binding_warning_attrs = + [ + ( Location.mknoloc "warning", + PStr [Str.eval (Exp.constant (Pconst_string ("-32", None)))] ); + ] + let make_hoisted_component_binding ~empty_loc ~full_module_name nested_modules = let path = nested_modules |> List.rev |> longident_of_segments |> fun txt -> @@ -108,10 +117,10 @@ let make_hoisted_component_binding ~empty_loc ~full_module_name nested_modules = Pstr_value ( Nonrecursive, [ - Vb.mk ~loc:empty_loc + Vb.mk ~loc:empty_loc ~attrs:jsx_hoisted_binding_warning_attrs (Pat.var ~loc:empty_loc {loc = empty_loc; txt = full_module_name}) (Exp.ident ~loc:empty_loc path); - Vb.mk ~loc:empty_loc + Vb.mk ~loc:empty_loc ~attrs:jsx_hoisted_binding_warning_attrs (Pat.var ~loc:empty_loc {loc = empty_loc; txt = marker_name}) (Exp.construct ~loc:empty_loc {loc = empty_loc; txt = Lident "true"} diff --git a/tests/build_tests/rsc_nested_jsx_deep/rescript.json b/tests/build_tests/rsc_nested_jsx_deep/rescript.json index 07fe7f0788c..4b94b2840cb 100644 --- a/tests/build_tests/rsc_nested_jsx_deep/rescript.json +++ b/tests/build_tests/rsc_nested_jsx_deep/rescript.json @@ -16,4 +16,4 @@ "warnings": { "error": "+A" } -} \ No newline at end of file +} diff --git a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt index 106e33b8ff5..ec718c8b55d 100644 --- a/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/aliasProps.res.txt @@ -162,10 +162,10 @@ module C6 = { \"AliasProps$C6" } } -let \"AliasProps$C0" = C0.make and \"AliasProps$C0$jsx" = true -let \"AliasProps$C1" = C1.make and \"AliasProps$C1$jsx" = true -let \"AliasProps$C2" = C2.make and \"AliasProps$C2$jsx" = true -let \"AliasProps$C3" = C3.make and \"AliasProps$C3$jsx" = true -let \"AliasProps$C4" = C4.make and \"AliasProps$C4$jsx" = true -let \"AliasProps$C5" = C5.make and \"AliasProps$C5$jsx" = true -let \"AliasProps$C6" = C6.make and \"AliasProps$C6$jsx" = true +@warning("-32") let \"AliasProps$C0" = C0.make @warning("-32") and \"AliasProps$C0$jsx" = true +@warning("-32") let \"AliasProps$C1" = C1.make @warning("-32") and \"AliasProps$C1$jsx" = true +@warning("-32") let \"AliasProps$C2" = C2.make @warning("-32") and \"AliasProps$C2$jsx" = true +@warning("-32") let \"AliasProps$C3" = C3.make @warning("-32") and \"AliasProps$C3$jsx" = true +@warning("-32") let \"AliasProps$C4" = C4.make @warning("-32") and \"AliasProps$C4$jsx" = true +@warning("-32") let \"AliasProps$C5" = C5.make @warning("-32") and \"AliasProps$C5$jsx" = true +@warning("-32") let \"AliasProps$C6" = C6.make @warning("-32") and \"AliasProps$C6$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt index dea1b17712c..4a2ccb3500c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/asyncAwait.res.txt @@ -35,5 +35,5 @@ module C1 = { \"AsyncAwait$C1" } } -let \"AsyncAwait$C0" = C0.make and \"AsyncAwait$C0$jsx" = true -let \"AsyncAwait$C1" = C1.make and \"AsyncAwait$C1$jsx" = true +@warning("-32") let \"AsyncAwait$C0" = C0.make @warning("-32") and \"AsyncAwait$C0$jsx" = true +@warning("-32") let \"AsyncAwait$C1" = C1.make @warning("-32") and \"AsyncAwait$C1$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt index 4f7291dacc9..e4a96d16e0c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/defaultPatternProp.res.txt @@ -33,5 +33,7 @@ module C0 = { \"DefaultPatternProp$C0" } } -let \"DefaultPatternProp$C0$M" = C0.M.make and \"DefaultPatternProp$C0$M$jsx" = true -let \"DefaultPatternProp$C0" = C0.make and \"DefaultPatternProp$C0$jsx" = true +@warning("-32") let \"DefaultPatternProp$C0$M" = C0.M.make +@warning("-32") and \"DefaultPatternProp$C0$M$jsx" = true +@warning("-32") let \"DefaultPatternProp$C0" = C0.make +@warning("-32") and \"DefaultPatternProp$C0$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt index fa928be4406..7b24b91d0b4 100644 --- a/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/defaultValueProp.res.txt @@ -91,7 +91,11 @@ module C3 = { \"DefaultValueProp$C3" } } -let \"DefaultValueProp$C0" = C0.make and \"DefaultValueProp$C0$jsx" = true -let \"DefaultValueProp$C1" = C1.make and \"DefaultValueProp$C1$jsx" = true -let \"DefaultValueProp$C2" = C2.make and \"DefaultValueProp$C2$jsx" = true -let \"DefaultValueProp$C3" = C3.make and \"DefaultValueProp$C3$jsx" = true +@warning("-32") let \"DefaultValueProp$C0" = C0.make +@warning("-32") and \"DefaultValueProp$C0$jsx" = true +@warning("-32") let \"DefaultValueProp$C1" = C1.make +@warning("-32") and \"DefaultValueProp$C1$jsx" = true +@warning("-32") let \"DefaultValueProp$C2" = C2.make +@warning("-32") and \"DefaultValueProp$C2$jsx" = true +@warning("-32") let \"DefaultValueProp$C3" = C3.make +@warning("-32") and \"DefaultValueProp$C3$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt index f981aad592c..13e42c9dae9 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithRef.res.txt @@ -10,4 +10,5 @@ module V4C = { @module("componentForwardRef") external make: React.component> = "component" } -let \"ExternalWithRef$V4C" = V4C.make and \"ExternalWithRef$V4C$jsx" = true +@warning("-32") let \"ExternalWithRef$V4C" = V4C.make +@warning("-32") and \"ExternalWithRef$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt b/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt index e271934ecbb..72c71617ae4 100644 --- a/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/externalWithTypeVariables.res.txt @@ -10,4 +10,5 @@ module V4C = { @module("c") external make: React.component, React.element>> = "component" } -let \"ExternalWithTypeVariables$V4C" = V4C.make and \"ExternalWithTypeVariables$V4C$jsx" = true +@warning("-32") let \"ExternalWithTypeVariables$V4C" = V4C.make +@warning("-32") and \"ExternalWithTypeVariables$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt index 2a3dae60b75..98b0b1dd63a 100644 --- a/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/fileLevelConfig.res.txt @@ -15,4 +15,5 @@ module V4A = { \"FileLevelConfig$V4A" } } -let \"FileLevelConfig$V4A" = V4A.make and \"FileLevelConfig$V4A$jsx" = true +@warning("-32") let \"FileLevelConfig$V4A" = V4A.make +@warning("-32") and \"FileLevelConfig$V4A$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt index abe2a0b7e9e..ee69fc32062 100644 --- a/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/forwardRef.res.txt @@ -121,8 +121,10 @@ module V4AUncurried = { \"ForwardRef$V4AUncurried" } } -let \"ForwardRef$V4A$FancyInput" = V4A.FancyInput.make and \"ForwardRef$V4A$FancyInput$jsx" = true -let \"ForwardRef$V4A" = V4A.make and \"ForwardRef$V4A$jsx" = true -let \"ForwardRef$V4AUncurried$FancyInput" = V4AUncurried.FancyInput.make -and \"ForwardRef$V4AUncurried$FancyInput$jsx" = true -let \"ForwardRef$V4AUncurried" = V4AUncurried.make and \"ForwardRef$V4AUncurried$jsx" = true +@warning("-32") let \"ForwardRef$V4A$FancyInput" = V4A.FancyInput.make +@warning("-32") and \"ForwardRef$V4A$FancyInput$jsx" = true +@warning("-32") let \"ForwardRef$V4A" = V4A.make @warning("-32") and \"ForwardRef$V4A$jsx" = true +@warning("-32") let \"ForwardRef$V4AUncurried$FancyInput" = V4AUncurried.FancyInput.make +@warning("-32") and \"ForwardRef$V4AUncurried$FancyInput$jsx" = true +@warning("-32") let \"ForwardRef$V4AUncurried" = V4AUncurried.make +@warning("-32") and \"ForwardRef$V4AUncurried$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt index 363f9439a0a..674cfc85dcf 100644 --- a/tests/syntax_tests/data/ppx/react/expected/interface.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/interface.res.txt @@ -21,5 +21,6 @@ module NoProps = { \"Interface$NoProps" } } -let \"Interface$A" = A.make and \"Interface$A$jsx" = true -let \"Interface$NoProps" = NoProps.make and \"Interface$NoProps$jsx" = true +@warning("-32") let \"Interface$A" = A.make @warning("-32") and \"Interface$A$jsx" = true +@warning("-32") let \"Interface$NoProps" = NoProps.make +@warning("-32") and \"Interface$NoProps$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt index 8b9bd814834..1d79a25571c 100644 --- a/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/mangleKeyword.res.txt @@ -27,5 +27,7 @@ module C4A1 = { let c4a0 = React.jsx(@res.jsxComponentPath C4A0.make, {_open: "x", _type: "t"}) let c4a1 = React.jsx(@res.jsxComponentPath C4A1.make, {_open: "x", _type: "t"}) -let \"MangleKeyword$C4A0" = C4A0.make and \"MangleKeyword$C4A0$jsx" = true -let \"MangleKeyword$C4A1" = C4A1.make and \"MangleKeyword$C4A1$jsx" = true +@warning("-32") let \"MangleKeyword$C4A0" = C4A0.make +@warning("-32") and \"MangleKeyword$C4A0$jsx" = true +@warning("-32") let \"MangleKeyword$C4A1" = C4A1.make +@warning("-32") and \"MangleKeyword$C4A1$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt index 6cb92900d5e..76e62072ce8 100644 --- a/tests/syntax_tests/data/ppx/react/expected/nested.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/nested.res.txt @@ -21,5 +21,6 @@ module Outer = { \"Nested$Outer" } } -let \"Nested$Outer$Inner" = Outer.Inner.make and \"Nested$Outer$Inner$jsx" = true -let \"Nested$Outer" = Outer.make and \"Nested$Outer$jsx" = true +@warning("-32") let \"Nested$Outer$Inner" = Outer.Inner.make +@warning("-32") and \"Nested$Outer$Inner$jsx" = true +@warning("-32") let \"Nested$Outer" = Outer.make @warning("-32") and \"Nested$Outer$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt index 8296472d4d1..fdfc5714bfa 100644 --- a/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/newtype.res.txt @@ -99,8 +99,9 @@ module Uncurried = { \"Newtype$Uncurried" } } -let \"Newtype$V4A" = V4A.make and \"Newtype$V4A$jsx" = true -let \"Newtype$V4A1" = V4A1.make and \"Newtype$V4A1$jsx" = true -let \"Newtype$V4A2" = V4A2.make and \"Newtype$V4A2$jsx" = true -let \"Newtype$V4A3" = V4A3.make and \"Newtype$V4A3$jsx" = true -let \"Newtype$Uncurried" = Uncurried.make and \"Newtype$Uncurried$jsx" = true +@warning("-32") let \"Newtype$V4A" = V4A.make @warning("-32") and \"Newtype$V4A$jsx" = true +@warning("-32") let \"Newtype$V4A1" = V4A1.make @warning("-32") and \"Newtype$V4A1$jsx" = true +@warning("-32") let \"Newtype$V4A2" = V4A2.make @warning("-32") and \"Newtype$V4A2$jsx" = true +@warning("-32") let \"Newtype$V4A3" = V4A3.make @warning("-32") and \"Newtype$V4A3$jsx" = true +@warning("-32") let \"Newtype$Uncurried" = Uncurried.make +@warning("-32") and \"Newtype$Uncurried$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt index bd40f4c83c7..7b4e9bd7ea3 100644 --- a/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/noPropsWithKey.res.txt @@ -40,6 +40,9 @@ module V4C = { \"NoPropsWithKey$V4C" } } -let \"NoPropsWithKey$V4CA" = V4CA.make and \"NoPropsWithKey$V4CA$jsx" = true -let \"NoPropsWithKey$V4CB" = V4CB.make and \"NoPropsWithKey$V4CB$jsx" = true -let \"NoPropsWithKey$V4C" = V4C.make and \"NoPropsWithKey$V4C$jsx" = true +@warning("-32") let \"NoPropsWithKey$V4CA" = V4CA.make +@warning("-32") and \"NoPropsWithKey$V4CA$jsx" = true +@warning("-32") let \"NoPropsWithKey$V4CB" = V4CB.make +@warning("-32") and \"NoPropsWithKey$V4CB$jsx" = true +@warning("-32") let \"NoPropsWithKey$V4C" = V4C.make +@warning("-32") and \"NoPropsWithKey$V4C$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt index 7797329571a..b0b191ffde8 100644 --- a/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/optimizeAutomaticMode.res.txt @@ -18,4 +18,5 @@ module User = { \"OptimizeAutomaticMode$User" } } -let \"OptimizeAutomaticMode$User" = User.make and \"OptimizeAutomaticMode$User$jsx" = true +@warning("-32") let \"OptimizeAutomaticMode$User" = User.make +@warning("-32") and \"OptimizeAutomaticMode$User$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt index eff4facc7bd..3320905230f 100644 --- a/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/returnConstraint.res.txt @@ -47,7 +47,11 @@ module Async = { \"ReturnConstraint$Async" } } -let \"ReturnConstraint$Standard" = Standard.make and \"ReturnConstraint$Standard$jsx" = true -let \"ReturnConstraint$ForwardRef" = ForwardRef.make and \"ReturnConstraint$ForwardRef$jsx" = true -let \"ReturnConstraint$WithProps" = WithProps.make and \"ReturnConstraint$WithProps$jsx" = true -let \"ReturnConstraint$Async" = Async.make and \"ReturnConstraint$Async$jsx" = true +@warning("-32") let \"ReturnConstraint$Standard" = Standard.make +@warning("-32") and \"ReturnConstraint$Standard$jsx" = true +@warning("-32") let \"ReturnConstraint$ForwardRef" = ForwardRef.make +@warning("-32") and \"ReturnConstraint$ForwardRef$jsx" = true +@warning("-32") let \"ReturnConstraint$WithProps" = WithProps.make +@warning("-32") and \"ReturnConstraint$WithProps$jsx" = true +@warning("-32") let \"ReturnConstraint$Async" = Async.make +@warning("-32") and \"ReturnConstraint$Async$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt index 70cb0dee1dc..777b9cff874 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedProps.res.txt @@ -67,11 +67,19 @@ module V4A8 = { external make: React.component = "default" } -let \"SharedProps$V4A1" = V4A1.make and \"SharedProps$V4A1$jsx" = true -let \"SharedProps$V4A2" = V4A2.make and \"SharedProps$V4A2$jsx" = true -let \"SharedProps$V4A3" = V4A3.make and \"SharedProps$V4A3$jsx" = true -let \"SharedProps$V4A4" = V4A4.make and \"SharedProps$V4A4$jsx" = true -let \"SharedProps$V4A5" = V4A5.make and \"SharedProps$V4A5$jsx" = true -let \"SharedProps$V4A6" = V4A6.make and \"SharedProps$V4A6$jsx" = true -let \"SharedProps$V4A7" = V4A7.make and \"SharedProps$V4A7$jsx" = true -let \"SharedProps$V4A8" = V4A8.make and \"SharedProps$V4A8$jsx" = true +@warning("-32") let \"SharedProps$V4A1" = V4A1.make +@warning("-32") and \"SharedProps$V4A1$jsx" = true +@warning("-32") let \"SharedProps$V4A2" = V4A2.make +@warning("-32") and \"SharedProps$V4A2$jsx" = true +@warning("-32") let \"SharedProps$V4A3" = V4A3.make +@warning("-32") and \"SharedProps$V4A3$jsx" = true +@warning("-32") let \"SharedProps$V4A4" = V4A4.make +@warning("-32") and \"SharedProps$V4A4$jsx" = true +@warning("-32") let \"SharedProps$V4A5" = V4A5.make +@warning("-32") and \"SharedProps$V4A5$jsx" = true +@warning("-32") let \"SharedProps$V4A6" = V4A6.make +@warning("-32") and \"SharedProps$V4A6$jsx" = true +@warning("-32") let \"SharedProps$V4A7" = V4A7.make +@warning("-32") and \"SharedProps$V4A7$jsx" = true +@warning("-32") let \"SharedProps$V4A8" = V4A8.make +@warning("-32") and \"SharedProps$V4A8$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt index f03c5e7248f..98ea0b39a8f 100644 --- a/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/sharedPropsWithProps.res.txt @@ -76,10 +76,17 @@ module V4A7 = { \"SharedPropsWithProps$V4A7" } } -let \"SharedPropsWithProps$V4A1" = V4A1.make and \"SharedPropsWithProps$V4A1$jsx" = true -let \"SharedPropsWithProps$V4A2" = V4A2.make and \"SharedPropsWithProps$V4A2$jsx" = true -let \"SharedPropsWithProps$V4A3" = V4A3.make and \"SharedPropsWithProps$V4A3$jsx" = true -let \"SharedPropsWithProps$V4A4" = V4A4.make and \"SharedPropsWithProps$V4A4$jsx" = true -let \"SharedPropsWithProps$V4A5" = V4A5.make and \"SharedPropsWithProps$V4A5$jsx" = true -let \"SharedPropsWithProps$V4A6" = V4A6.make and \"SharedPropsWithProps$V4A6$jsx" = true -let \"SharedPropsWithProps$V4A7" = V4A7.make and \"SharedPropsWithProps$V4A7$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A1" = V4A1.make +@warning("-32") and \"SharedPropsWithProps$V4A1$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A2" = V4A2.make +@warning("-32") and \"SharedPropsWithProps$V4A2$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A3" = V4A3.make +@warning("-32") and \"SharedPropsWithProps$V4A3$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A4" = V4A4.make +@warning("-32") and \"SharedPropsWithProps$V4A4$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A5" = V4A5.make +@warning("-32") and \"SharedPropsWithProps$V4A5$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A6" = V4A6.make +@warning("-32") and \"SharedPropsWithProps$V4A6$jsx" = true +@warning("-32") let \"SharedPropsWithProps$V4A7" = V4A7.make +@warning("-32") and \"SharedPropsWithProps$V4A7$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt index cc329602999..36a14fdaf62 100644 --- a/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/topLevel.res.txt @@ -17,4 +17,4 @@ module V4A = { \"TopLevel$V4A" } } -let \"TopLevel$V4A" = V4A.make and \"TopLevel$V4A$jsx" = true +@warning("-32") let \"TopLevel$V4A" = V4A.make @warning("-32") and \"TopLevel$V4A$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt index 92432c4be8c..30f6be7931d 100644 --- a/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/typeConstraint.res.txt @@ -14,4 +14,5 @@ module V4A = { \"TypeConstraint$V4A" } } -let \"TypeConstraint$V4A" = V4A.make and \"TypeConstraint$V4A$jsx" = true +@warning("-32") let \"TypeConstraint$V4A" = V4A.make +@warning("-32") and \"TypeConstraint$V4A$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt index 77a78d4f6ff..8cb02b596d9 100644 --- a/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/uncurriedProps.res.txt @@ -65,5 +65,7 @@ module Bar = { \"UncurriedProps$Bar" } } -let \"UncurriedProps$Foo" = Foo.make and \"UncurriedProps$Foo$jsx" = true -let \"UncurriedProps$Bar" = Bar.make and \"UncurriedProps$Bar$jsx" = true +@warning("-32") let \"UncurriedProps$Foo" = Foo.make +@warning("-32") and \"UncurriedProps$Foo$jsx" = true +@warning("-32") let \"UncurriedProps$Bar" = Bar.make +@warning("-32") and \"UncurriedProps$Bar$jsx" = true diff --git a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt index c4de276ae23..1bd7275a719 100644 --- a/tests/syntax_tests/data/ppx/react/expected/v4.res.txt +++ b/tests/syntax_tests/data/ppx/react/expected/v4.res.txt @@ -116,9 +116,10 @@ module Rec2 = { } and mm = x => make(x) } -let \"V4$Uncurried" = Uncurried.make and \"V4$Uncurried$jsx" = true -let \"V4$E" = E.make and \"V4$E$jsx" = true -let \"V4$EUncurried" = EUncurried.make and \"V4$EUncurried$jsx" = true -let \"V4$Rec" = Rec.make and \"V4$Rec$jsx" = true -let \"V4$Rec1" = Rec1.make and \"V4$Rec1$jsx" = true -let \"V4$Rec2" = Rec2.make and \"V4$Rec2$jsx" = true +@warning("-32") let \"V4$Uncurried" = Uncurried.make @warning("-32") and \"V4$Uncurried$jsx" = true +@warning("-32") let \"V4$E" = E.make @warning("-32") and \"V4$E$jsx" = true +@warning("-32") let \"V4$EUncurried" = EUncurried.make +@warning("-32") and \"V4$EUncurried$jsx" = true +@warning("-32") let \"V4$Rec" = Rec.make @warning("-32") and \"V4$Rec$jsx" = true +@warning("-32") let \"V4$Rec1" = Rec1.make @warning("-32") and \"V4$Rec1$jsx" = true +@warning("-32") let \"V4$Rec2" = Rec2.make @warning("-32") and \"V4$Rec2$jsx" = true