Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
add21c2
Rewrite nested JSX component paths to direct hoisted exports
fhammerschmidt Mar 12, 2026
97bafbb
Fix syntax snapshots
fhammerschmidt Mar 12, 2026
fb1a22f
Format
fhammerschmidt Mar 12, 2026
27bd27c
Fix gentype tests
fhammerschmidt Mar 12, 2026
e20ac3b
Fix rewatch test
fhammerschmidt Mar 12, 2026
f78664b
Another fixed snapshot
fhammerschmidt Mar 12, 2026
2bdb0e5
Fix namespace handling
fhammerschmidt Mar 13, 2026
0b45ebb
Fix possible race condition in rewatch test
fhammerschmidt Mar 13, 2026
22003b4
Copilot comment fixes
fhammerschmidt Mar 13, 2026
01e5f43
Update syntax tests
fhammerschmidt Mar 13, 2026
f0c3ccd
Update tests
fhammerschmidt Mar 13, 2026
15ded2d
Add more regression tests
fhammerschmidt Mar 13, 2026
a6a5dd1
fix syntax tests
tsnobip Mar 22, 2026
18c14c7
add repro where direct component function doesn't get exported
tsnobip Mar 23, 2026
e3ad8b0
fix export of JSX components inside module with regular functions
tsnobip Mar 23, 2026
04a5846
update analysis/gentype tests
tsnobip Mar 24, 2026
573dd37
fix functors
tsnobip Mar 24, 2026
f0ebf0d
add failing test with interface file
tsnobip Mar 24, 2026
26cf5e2
fix signature of @jsx.component
tsnobip Mar 24, 2026
69603a7
repro: private JSX components raise warning 32
tsnobip Mar 24, 2026
88e45fa
remove warning -32 when emitting the namespaced function and jsx status
tsnobip Mar 24, 2026
5d4b296
Even more regression tests
fhammerschmidt Mar 31, 2026
b8ba01b
@react.componentWithProps regression tests
fhammerschmidt Apr 1, 2026
1287542
Make this a breaking change and only export the modules alone
fhammerschmidt Apr 9, 2026
97d37f1
Only export namespaced component
fhammerschmidt Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion compiler/core/js_of_lam_block.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
212 changes: 212 additions & 0 deletions compiler/core/js_pass_nested_component_exports.ml
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
(* Copyright (C) 2026 - Authors of ReScript
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition to the permissions granted to you by the LGPL, you may combine
* or link a "work that uses the Library" with a publicly distributed version
* of this file to produce a combined library or application, then distribute
* that combined work under the terms of your choosing, with no requirement
* to comply with the obligations normally placed on you by section 4 of the
* LGPL version 3 (or the corresponding section of a later version of the LGPL
* should you choose to use a later version).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)

module E = Js_exp_make

module StringSet = Set.Make (String)

type candidate = {
module_ident: Ident.t;
hidden_export_name: string;
}

let dynamic_import_module_root (expr : J.expression) =
match expr.expression_desc with
| Await
{
expression_desc =
Call
({expression_desc = Var (Id import_ident); _}, [arg], _);
_;
}
when String.equal import_ident.name "import" -> (
match arg.expression_desc with
| Str {txt; _} ->
let basename = Filename.basename txt in
let suffixes = [".res.mjs"; ".res.js"; ".mjs"; ".js"] in
let rec strip_suffix = function
| [] -> basename
| suffix :: rest ->
if Filename.check_suffix basename suffix then
Filename.chop_suffix basename suffix
else strip_suffix rest
in
Some (strip_suffix suffixes)
| _ -> None)
| _ -> None

let marker_name hidden_export_name = hidden_export_name ^ "$jsx"

let hidden_component_suffix (module_ident : Ident.t) =
"$" ^ Ident.name module_ident

let is_hidden_component_name_for module_ident ident =
Ext_string.ends_with (Ident.name ident) (hidden_component_suffix module_ident)

let find_hidden_alias_by_value block module_ident value_ident =
List.find_map (fun (st : J.statement) ->
match st.statement_desc with
| Variable
{
ident;
value = Some {expression_desc = Var (Id target); _};
property = _;
ident_info = _;
}
when Ident.same target value_ident
&& is_hidden_component_name_for module_ident ident ->
Some ident
| _ -> None)
block

let has_export_name exports name =
List.exists (fun (ident : Ident.t) -> String.equal (Ident.name ident) name) exports

let candidate_of_statement block exports (st : J.statement) =
match st.statement_desc with
| Variable
{
ident = module_ident;
value =
Some
{
expression_desc =
Caml_block
([{expression_desc = Var (Id value_ident); _}], Immutable, _, Blk_module ["make"]);
_;
};
property = _;
ident_info = _;
} -> (
let hidden_ident =
if is_hidden_component_name_for module_ident value_ident then
Some value_ident
else find_hidden_alias_by_value block module_ident value_ident
in
match hidden_ident with
| Some hidden_ident
when has_export_name exports hidden_ident.name ->
Some
{
module_ident;
hidden_export_name = hidden_ident.name;
}
| Some _ | None -> None)
| _ -> None

let collect_candidates block exports =
Ext_list.filter_map block (candidate_of_statement block exports)

let hidden_export_names_to_remove candidates =
Ext_list.fold_left candidates StringSet.empty (fun acc candidate ->
acc |> StringSet.add (Ident.name candidate.module_ident)
|> StringSet.add (marker_name candidate.hidden_export_name))

let marker_names_to_remove_from_block candidates =
Ext_list.fold_left candidates StringSet.empty (fun acc candidate ->
StringSet.add (marker_name candidate.hidden_export_name) acc)

let candidate_by_module_ident candidates module_ident =
List.find_map (fun candidate ->
if Ident.same candidate.module_ident module_ident then Some candidate
else None)
candidates

let rewrite_block block candidates removed_marker_names =
List.concat_map (fun (st : J.statement) ->
match st.statement_desc with
| Variable {ident; value; property; ident_info} -> (
match candidate_by_module_ident candidates ident with
| Some _ -> [st]
| None ->
if StringSet.mem (Ident.name ident) removed_marker_names then []
else [st])
| _ -> [st])
block

let dynamic_import_aliases block =
List.fold_left
(fun aliases (st : J.statement) ->
match st.statement_desc with
| Variable {ident; value = Some expr; _} -> (
match dynamic_import_module_root expr with
| Some module_root -> Map_ident.add aliases ident module_root
| None -> aliases)
| _ -> aliases)
Map_ident.empty block

let rewrite_dynamic_import_component_access aliases (expr : J.expression) =
let rec collect_segments segments (expr : J.expression) =
match expr.expression_desc with
| Static_index (inner, field, _) -> collect_segments (field :: segments) inner
| Var (Id id) -> (
match Map_ident.find_opt aliases id with
| Some module_root when segments <> [] -> Some (id, module_root, segments)
| Some _ | None -> None)
| _ -> None
in
match expr.expression_desc with
| Static_index (inner, "make", _) -> (
match collect_segments [] inner with
| Some (id, module_root, segments) ->
let segments = List.rev segments in
let hidden_name =
match segments with
| first :: _ when Ext_string.starts_with first (module_root ^ "$") ->
String.concat "$" segments
| _ -> String.concat "$" (module_root :: segments)
in
{expr with expression_desc = Static_index (E.var id, hidden_name, None)}
| None -> expr)
| _ -> expr

let rewrite_dynamic_import_block block =
let aliases = dynamic_import_aliases block in
if Map_ident.is_empty aliases then block
else
let mapper =
{
Js_record_map.super with
expression =
(fun self expr ->
let expr = Js_record_map.super.expression self expr in
rewrite_dynamic_import_component_access aliases expr);
}
in
mapper.block mapper block

let program (js : J.program) : J.program =
let candidates = collect_candidates js.block js.exports in
let removed_export_names = hidden_export_names_to_remove candidates in
let removed_marker_names = marker_names_to_remove_from_block candidates in
let exports =
Ext_list.filter js.exports (fun (ident : Ident.t) ->
not (StringSet.mem (Ident.name ident) removed_export_names))
in
let export_set = Set_ident.of_list exports in
let block =
rewrite_dynamic_import_block
(rewrite_block js.block candidates removed_marker_names)
in
{J.block; exports; export_set}
28 changes: 28 additions & 0 deletions compiler/core/js_pass_nested_component_exports.mli
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
(* Copyright (C) 2026 - Authors of ReScript
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* In addition to the permissions granted to you by the LGPL, you may combine
* or link a "work that uses the Library" with a publicly distributed version
* of this file to produce a combined library or application, then distribute
* that combined work under the terms of your choosing, with no requirement
* to comply with the obligations normally placed on you by section 4 of the
* LGPL version 3 (or the corresponding section of a later version of the LGPL
* should you choose to use a later version).
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. *)

(* Rewrite nested React component module exports from `{make: fn}` wrappers to
direct component exports, while keeping `.make` as a self-reference for
compatibility with existing generated call sites. *)
val program : J.program -> J.program
6 changes: 4 additions & 2 deletions compiler/core/lam.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) ->
Expand All @@ -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 ) ->
Expand Down
7 changes: 6 additions & 1 deletion compiler/core/lam_analysis.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions compiler/core/lam_arity_analysis.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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)];
_;
} -> (
Expand All @@ -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)];
};
];
Expand Down
2 changes: 1 addition & 1 deletion compiler/core/lam_compat.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion compiler/core/lam_compat.mli
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading
Loading