From 68c516ba9db9abf212b42c4b6da5089789db8347 Mon Sep 17 00:00:00 2001 From: mununki Date: Mon, 27 Apr 2026 17:06:35 +0900 Subject: [PATCH 1/7] introduce linked external source map --- compiler/bsc/rescript_compiler_main.ml | 23 ++ compiler/common/js_config.ml | 4 + compiler/common/js_config.mli | 7 + compiler/core/dune | 2 +- compiler/core/js_dump.ml | 4 +- compiler/core/js_source_map.ml | 275 +++++++++++++++++++++ compiler/core/js_source_map.mli | 14 ++ compiler/core/lam.ml | 10 +- compiler/core/lam.mli | 2 + compiler/core/lam_bounded_vars.ml | 4 +- compiler/core/lam_compile.ml | 143 +++++++++-- compiler/core/lam_compile_main.ml | 33 ++- compiler/core/lam_convert.ml | 34 ++- compiler/core/lam_eta_conversion.ml | 28 +-- compiler/core/lam_pass_alpha_conversion.ml | 8 +- compiler/core/lam_pass_deep_flatten.ml | 4 +- compiler/core/lam_pass_exits.ml | 4 +- compiler/core/lam_pass_lets_dce.ml | 4 +- compiler/core/lam_pass_remove_alias.ml | 4 +- compiler/core/lam_subst.ml | 4 +- compiler/ext/ext_pp.ml | 62 ++++- compiler/ext/ext_pp.mli | 2 + rescript.opam | 2 +- rescript.opam.template | 2 +- rewatch/src/build/clean.rs | 9 +- rewatch/src/build/compile.rs | 14 ++ rewatch/src/config.rs | 78 ++++++ tests/build_tests/source_map/input.js | 31 +++ tests/build_tests/source_map/rescript.json | 6 + tests/build_tests/source_map/src/Demo.res | 5 + 30 files changed, 734 insertions(+), 88 deletions(-) create mode 100644 compiler/core/js_source_map.ml create mode 100644 compiler/core/js_source_map.mli create mode 100644 tests/build_tests/source_map/input.js create mode 100644 tests/build_tests/source_map/rescript.json create mode 100644 tests/build_tests/source_map/src/Demo.res diff --git a/compiler/bsc/rescript_compiler_main.ml b/compiler/bsc/rescript_compiler_main.ml index 67fbc0043de..300cc391abc 100644 --- a/compiler/bsc/rescript_compiler_main.ml +++ b/compiler/bsc/rescript_compiler_main.ml @@ -209,6 +209,20 @@ let[@inline] string_optional_set s : Bsc_args.spec = let[@inline] unit_call s : Bsc_args.spec = Unit (Unit_call s) let[@inline] string_list_add s : Bsc_args.spec = String (String_list_add s) +let parse_source_map value = + Js_config.source_map := + match String.lowercase_ascii value with + | "true" | "linked" -> Linked + | "false" | "none" -> No_source_map + | value -> Bsc_args.bad_arg ("Unsupported sourceMap value: " ^ value) + +let parse_bool_ref target value = + target := + match String.lowercase_ascii value with + | "true" -> true + | "false" -> false + | value -> Bsc_args.bad_arg ("Expected true or false, got: " ^ value) + (* mostly common used to list in the beginning to make search fast *) let command_line_flags : (string * Bsc_args.spec * string) array = @@ -259,6 +273,15 @@ let command_line_flags : (string * Bsc_args.spec * string) array = string_call ignore, "*internal* Set jsx mode, this is no longer used and is a no-op." ); ("-bs-jsx-preserve", set Js_config.jsx_preserve, "*internal* Preserve jsx"); + ( "-bs-source-map", + string_call parse_source_map, + "*internal* Configure source map output" ); + ( "-bs-source-map-sources-content", + string_call (parse_bool_ref Js_config.source_map_sources_content), + "*internal* Include original source text in source maps" ); + ( "-bs-source-map-root", + string_call (fun value -> Js_config.source_map_root := value), + "*internal* Set sourceRoot in source maps" ); ( "-bs-package-output", string_call Js_packages_state.update_npm_package_path, "*internal* Set npm-output-path: [opt_module]:path, for example: \ diff --git a/compiler/common/js_config.ml b/compiler/common/js_config.ml index 9e2b6f598ca..ffbc36dc717 100644 --- a/compiler/common/js_config.ml +++ b/compiler/common/js_config.ml @@ -26,6 +26,7 @@ type jsx_version = Jsx_v4 type jsx_module = React | Generic of {module_name: string} +type source_map = No_source_map | Linked let no_version_header = ref false @@ -53,6 +54,9 @@ let jsx_version = ref None let jsx_module = ref React let jsx_preserve = ref false let js_stdout = ref true +let source_map = ref No_source_map +let source_map_sources_content = ref false +let source_map_root = ref "" let all_module_aliases = ref false let no_stdlib = ref false let no_export = ref false diff --git a/compiler/common/js_config.mli b/compiler/common/js_config.mli index f19b3c6126c..fa48d9cc4dc 100644 --- a/compiler/common/js_config.mli +++ b/compiler/common/js_config.mli @@ -24,6 +24,7 @@ type jsx_version = Jsx_v4 type jsx_module = React | Generic of {module_name: string} +type source_map = No_source_map | Linked (* val get_packages_info : unit -> Js_packages_info.t *) @@ -86,6 +87,12 @@ val jsx_preserve : bool ref val js_stdout : bool ref +val source_map : source_map ref + +val source_map_sources_content : bool ref + +val source_map_root : string ref + val all_module_aliases : bool ref val no_stdlib : bool ref diff --git a/compiler/core/dune b/compiler/core/dune index d7d75f34437..6f1d77a652d 100644 --- a/compiler/core/dune +++ b/compiler/core/dune @@ -6,4 +6,4 @@ (run %{bin:cppo} %{env:CPPO_FLAGS=} %{input-file}))) (flags (:standard -w +a-4-9-27-30-40-41-42-48-70)) - (libraries depends ext flow_parser frontend gentype)) + (libraries depends ext flow_parser frontend gentype yojson)) diff --git a/compiler/core/js_dump.ml b/compiler/core/js_dump.ml index 4310b1f80d1..324ef4e5421 100644 --- a/compiler/core/js_dump.ml +++ b/compiler/core/js_dump.ml @@ -1291,6 +1291,7 @@ and variable_declaration top cxt f (variable : J.variable_declaration) : cxt = | _ -> ( match e.expression_desc with | Fun {is_method; params; body; env; return_unit; async; directive} -> + pp_comment_option f e.comment; pp_function ?directive ~is_method ~return_unit ~async ~fn_state:(if top then Name_top name else Name_non_top name) cxt f params body env @@ -1311,7 +1312,8 @@ and ipp_comment : 'a. P.t -> 'a -> unit = fun _f _comment -> () *) and pp_comment f comment = - if String.length comment > 0 then ( + if Js_source_map.mark_comment f comment then () + else if String.length comment > 0 then ( P.string f "/* "; P.string f comment; P.string f " */") diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml new file mode 100644 index 00000000000..f473d862288 --- /dev/null +++ b/compiler/core/js_source_map.ml @@ -0,0 +1,275 @@ +type source = {relative_path: string; content: string option} + +type mapping = { + generated_line: int; + generated_column: int; + source_index: int; + original_line: int; + original_column: int; +} + +type t = { + generated_file: string; + generated_dir: string; + source_root: string; + sources_content: bool; + sources: (string, int) Hashtbl.t; + mutable source_list: source list; + mutable mappings: mapping list; + mutable last_generated: (int * int) option; +} + +let current : t option ref = ref None + +let marker_prefix = "\000RESCRIPT_SOURCE_MAP:" +let next_marker = ref 0 +let marker_locs : (int, Location.t) Hashtbl.t = Hashtbl.create 128 + +let is_prefix ~prefix s = + let prefix_len = String.length prefix in + String.length s >= prefix_len + && + let rec loop i = + i = prefix_len + || (String.unsafe_get s i = String.unsafe_get prefix i && loop (i + 1)) + in + loop 0 + +let comment_of_loc (loc : Location.t) = + match !Js_config.source_map with + | No_source_map -> None + | Linked -> + if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then None + else + let id = !next_marker in + incr next_marker; + Hashtbl.replace marker_locs id loc; + Some (marker_prefix ^ string_of_int id) + +let with_builder builder f = + let old = !current in + current := builder; + Ext_pervasives.finally () ~clean:(fun () -> current := old) f + +let normalize_slashes s = + String.map + (function + | '\\' -> '/' + | c -> c) + s + +let absolute_path path = + if path = "" then path + else if Filename.is_relative path then Filename.concat (Sys.getcwd ()) path + else path + +let split_path path = + path |> normalize_slashes |> String.split_on_char '/' + |> List.filter (fun part -> part <> "") + +let rec drop_common xs ys = + match (xs, ys) with + | x :: xs, y :: ys when x = y -> drop_common xs ys + | _ -> (xs, ys) + +let repeat x n = + let rec loop acc n = if n <= 0 then acc else loop (x :: acc) (n - 1) in + loop [] n + +let relative_path ~from_dir ~to_file = + let from_dir = absolute_path from_dir in + let to_file = absolute_path to_file in + let from_parts = split_path from_dir in + let to_parts = split_path to_file in + match (from_parts, to_parts) with + | from_root :: _, to_root :: _ when from_root = to_root -> + let from_rest, to_rest = drop_common from_parts to_parts in + let parts = repeat ".." (List.length from_rest) @ to_rest in + if parts = [] then Filename.basename to_file else String.concat "/" parts + | _ -> Filename.basename to_file + +let make ~generated_file ~source_root ~sources_content = + { + generated_file = Filename.basename generated_file; + generated_dir = Filename.dirname generated_file; + source_root; + sources_content; + sources = Hashtbl.create 4; + source_list = []; + mappings = []; + last_generated = None; + } + +let load_content filename = + try Some (Ext_io.load_file filename) with _ -> None + +let add_source builder filename = + let filename = + match filename with + | "" | "_none_" -> !Location.input_name + | filename -> filename + in + let filename = absolute_path filename in + match Hashtbl.find_opt builder.sources filename with + | Some index -> (index, List.nth builder.source_list index) + | None -> + let source = + { + relative_path = + relative_path ~from_dir:builder.generated_dir ~to_file:filename; + content = load_content filename; + } + in + let index = List.length builder.source_list in + builder.source_list <- builder.source_list @ [source]; + Hashtbl.add builder.sources filename index; + (index, source) + +let utf16_units_in_utf8_slice s start stop = + let len = String.length s in + let stop = min stop len in + let rec loop i count = + if i >= stop then count + else + match String.unsafe_get s i with + | '\n' -> loop (i + 1) 0 + | c -> + let byte = Char.code c in + if byte < 0x80 then loop (i + 1) (count + 1) + else if byte land 0xE0 = 0xC0 && i + 1 < stop then + loop (i + 2) (count + 1) + else if byte land 0xF0 = 0xE0 && i + 2 < stop then + loop (i + 3) (count + 1) + else if byte land 0xF8 = 0xF0 && i + 3 < stop then + loop (i + 4) (count + 2) + else loop (i + 1) (count + 1) + in + loop (max 0 start) 0 + +let original_column source (pos : Lexing.position) = + match source.content with + | None -> max 0 (pos.pos_cnum - pos.pos_bol) + | Some content -> utf16_units_in_utf8_slice content pos.pos_bol pos.pos_cnum + +let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = + if loc.loc_ghost || loc.loc_start.pos_cnum < 0 then () + else + match builder.last_generated with + | Some (line, column) + when line = generated_line && column = generated_column -> + () + | _ -> + let source_index, source = add_source builder loc.loc_start.pos_fname in + let original_line = max 0 (loc.loc_start.pos_lnum - 1) in + let original_column = original_column source loc.loc_start in + builder.mappings <- + { + generated_line; + generated_column; + source_index; + original_line; + original_column; + } + :: builder.mappings; + builder.last_generated <- Some (generated_line, generated_column) + +let mark_comment fmt comment = + if is_prefix ~prefix:marker_prefix comment then ( + let prefix_len = String.length marker_prefix in + let id = + int_of_string + (String.sub comment prefix_len (String.length comment - prefix_len)) + in + (match (!current, Hashtbl.find_opt marker_locs id) with + | Some builder, Some loc -> + let generated_line, generated_column = Ext_pp.position fmt in + add_mapping builder ~generated_line ~generated_column loc + | _ -> ()); + true) + else false + +let base64_vlq_chars = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" + +let add_vlq buf value = + let value = if value < 0 then (-value lsl 1) + 1 else value lsl 1 in + let rec loop value = + let digit = value land 31 in + let value = value lsr 5 in + let digit = if value > 0 then digit lor 32 else digit in + Buffer.add_char buf base64_vlq_chars.[digit]; + if value > 0 then loop value + in + loop value + +let compare_mapping a b = + match compare a.generated_line b.generated_line with + | 0 -> compare a.generated_column b.generated_column + | n -> n + +let encode_mappings mappings = + let buf = Buffer.create 256 in + let current_line = ref 0 in + let previous_generated_column = ref 0 in + let previous_source = ref 0 in + let previous_original_line = ref 0 in + let previous_original_column = ref 0 in + let first_segment = ref true in + mappings |> List.sort compare_mapping + |> List.iter (fun mapping -> + while !current_line < mapping.generated_line do + Buffer.add_char buf ';'; + incr current_line; + previous_generated_column := 0; + first_segment := true + done; + if not !first_segment then Buffer.add_char buf ','; + first_segment := false; + add_vlq buf (mapping.generated_column - !previous_generated_column); + add_vlq buf (mapping.source_index - !previous_source); + add_vlq buf (mapping.original_line - !previous_original_line); + add_vlq buf (mapping.original_column - !previous_original_column); + previous_generated_column := mapping.generated_column; + previous_source := mapping.source_index; + previous_original_line := mapping.original_line; + previous_original_column := mapping.original_column); + Buffer.contents buf + +let json builder = + let mappings = encode_mappings builder.mappings in + let fields = + [ + ("version", `Int 3); + ("file", `String builder.generated_file); + ( "sources", + `List + (List.map + (fun source -> `String source.relative_path) + builder.source_list) ); + ("names", `List []); + ("mappings", `String mappings); + ] + in + let fields = + if builder.source_root = "" then fields + else fields @ [("sourceRoot", `String builder.source_root)] + in + let fields = + if builder.sources_content then + fields + @ [ + ( "sourcesContent", + `List + (List.map + (fun source -> + match source.content with + | None -> `Null + | Some content -> `String content) + builder.source_list) ); + ] + else fields + in + Yojson.Safe.to_string (`Assoc fields) + +let linked_comment ~map_file = + "//# sourceMappingURL=" ^ Filename.basename map_file ^ "\n" diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli new file mode 100644 index 00000000000..adad8e3303a --- /dev/null +++ b/compiler/core/js_source_map.mli @@ -0,0 +1,14 @@ +type t + +val make : + generated_file:string -> source_root:string -> sources_content:bool -> t + +val with_builder : t option -> (unit -> 'a) -> 'a + +val comment_of_loc : Location.t -> string option + +val mark_comment : Ext_pp.t -> string -> bool + +val json : t -> string + +val linked_comment : map_file:string -> string diff --git a/compiler/core/lam.ml b/compiler/core/lam.ml index 15875608b97..d987065b664 100644 --- a/compiler/core/lam.ml +++ b/compiler/core/lam.ml @@ -47,6 +47,7 @@ module Types = struct params: ident list; body: t; attr: Lambda.function_attribute; + loc: Location.t; } (* @@ -142,6 +143,7 @@ module X = struct params: ident list; body: t; attr: Lambda.function_attribute; + loc: Location.t; } and t = Types.t = @@ -181,9 +183,9 @@ let inner_map (l : t) (f : t -> X.t) : X.t = let ap_func = f ap_func in let ap_args = Ext_list.map ap_args f in Lapply {ap_func; ap_args; ap_info; ap_transformed_jsx} - | Lfunction {body; arity; params; attr} -> + | Lfunction {body; arity; params; attr; loc} -> let body = f body in - Lfunction {body; arity; params; attr} + Lfunction {body; arity; params; attr; loc} | Llet (str, id, arg, body) -> let arg = f arg in let body = f body in @@ -475,8 +477,8 @@ let global_module ?(dynamic_import = false) id = Lglobal_module (id, dynamic_import) let const ct : t = Lconst ct -let function_ ~attr ~arity ~params ~body : t = - Lfunction {arity; params; body; attr} +let function_ ~loc ~attr ~arity ~params ~body : t = + Lfunction {arity; params; body; attr; loc} let let_ kind id e body : t = Llet (kind, id, e, body) let letrec bindings body : t = Lletrec (bindings, body) diff --git a/compiler/core/lam.mli b/compiler/core/lam.mli index f6a398d677b..0bd2897fec1 100644 --- a/compiler/core/lam.mli +++ b/compiler/core/lam.mli @@ -53,6 +53,7 @@ and lfunction = { params: ident list; body: t; attr: Lambda.function_attribute; + loc: Location.t; } and prim_info = private { @@ -116,6 +117,7 @@ val const : Lam_constant.t -> t val apply : ?ap_transformed_jsx:bool -> t -> t list -> ap_info -> t val function_ : + loc:Location.t -> attr:Lambda.function_attribute -> arity:int -> params:ident list -> diff --git a/compiler/core/lam_bounded_vars.ml b/compiler/core/lam_bounded_vars.ml index 5499bb77ab4..597a638b770 100644 --- a/compiler/core/lam_bounded_vars.ml +++ b/compiler/core/lam_bounded_vars.ml @@ -88,10 +88,10 @@ let rewrite (map : _ Hash_ident.t) (lam : Lam.t) : Lam.t = in let body = aux body in Lam.letrec bindings body - | Lfunction {arity; params; body; attr} -> + | Lfunction {arity; params; body; attr; loc} -> let params = Ext_list.map params rebind in let body = aux body in - Lam.function_ ~arity ~params ~body ~attr + Lam.function_ ~loc ~arity ~params ~body ~attr | Lstaticcatch (l1, (i, xs), l2) -> let l1 = aux l1 in let xs = Ext_list.map xs rebind in diff --git a/compiler/core/lam_compile.ml b/compiler/core/lam_compile.ml index 9897f0c01e4..0f251a20116 100644 --- a/compiler/core/lam_compile.ml +++ b/compiler/core/lam_compile.ml @@ -25,6 +25,64 @@ module E = Js_exp_make module S = Js_stmt_make +let source_map_comment = Js_source_map.comment_of_loc + +let with_source_loc loc (exp : J.expression) = + match (source_map_comment loc, exp.comment) with + | Some comment, None -> {exp with comment = Some comment} + | _ -> exp + +let rec source_loc_of_lam (lam : Lam.t) = + match lam with + | Lapply {ap_info = {ap_loc}} -> Some ap_loc + | Lprim {loc} | Lfunction {loc} -> Some loc + | Llet (_, _, arg, body) -> ( + match source_loc_of_lam arg with + | Some _ as loc -> loc + | None -> source_loc_of_lam body) + | Lletrec (_, body) | Lsequence (_, body) -> source_loc_of_lam body + | Lifthenelse (_, then_, _) -> source_loc_of_lam then_ + | Lstaticcatch (body, _, _) | Ltrywith (body, _, _) -> source_loc_of_lam body + | Lstringswitch (_, cases, default) -> ( + match cases with + | (_, body) :: _ -> source_loc_of_lam body + | [] -> ( + match default with + | Some body -> source_loc_of_lam body + | None -> None)) + | Lswitch (_, sw) -> ( + match (sw.sw_consts, sw.sw_blocks, sw.sw_failaction) with + | (_, body) :: _, _, _ | _, (_, body) :: _, _ -> source_loc_of_lam body + | [], [], Some body -> source_loc_of_lam body + | [], [], None -> None) + | Lstaticraise (_, args) -> ( + match args with + | arg :: _ -> source_loc_of_lam arg + | [] -> None) + | Lwhile (_, body) + | Lfor (_, _, _, _, body) + | Lfor_of (_, _, body) + | Lfor_await_of (_, _, body) -> + source_loc_of_lam body + | Lassign (_, body) -> source_loc_of_lam body + | Lvar _ | Lglobal_module _ | Lconst _ | Lbreak | Lcontinue -> None + +let source_map_comment_of_lam lam = + match source_loc_of_lam lam with + | Some loc -> source_map_comment loc + | None -> None + +let with_statement_comment comment (stmt : J.statement) = + match (comment, stmt.comment) with + | Some comment, None -> {stmt with comment = Some comment} + | _ -> stmt + +let with_block_source_loc lam block = + match block with + | [] -> [] + | stmt :: rest -> + with_statement_comment (source_map_comment_of_lam lam) stmt :: rest + let args_either_function_or_const (args : Lam.t list) = Ext_list.for_all args (fun x -> match x with @@ -315,6 +373,7 @@ let compile output_prefix = | Single x -> apply_with_arity fn ~arity:(Lam_arity.extract_arity x) args) in + let expression = with_source_loc appinfo.ap_info.ap_loc expression in Js_output.output_of_block_and_expression lambda_cxt.continuation args_code expression (* @@ -329,7 +388,12 @@ let compile output_prefix = (id : Ident.t) (arg : Lam.t) : Js_output.t * initialization = match arg with | Lfunction - {params; body; attr = {return_unit; async; one_unit_arg; directive}} -> + { + params; + body; + attr = {return_unit; async; one_unit_arg; directive}; + loc; + } -> (* TODO: Think about recursive value {[ let rec v = ref (fun _ ... @@ -361,11 +425,13 @@ let compile output_prefix = } body in + let comment = source_map_comment loc in let result = if ret.triggered then let body_block = Js_output.output_as_block output in E.ocaml_fun - (* TODO: save computation of length several times + ?comment + (* TODO: save computation of length several times Here we always create [ocaml_fun], it will be renamed into [method] when it is detected by a primitive @@ -382,7 +448,7 @@ let compile output_prefix = ] else (* TODO: save computation of length several times *) - E.ocaml_fun params + E.ocaml_fun ?comment params (Js_output.output_as_block output) ~return_unit ~async ~one_unit_arg ?directive in @@ -539,8 +605,12 @@ let compile output_prefix = (a * J.case_clause) list -> J.statement) ~(switch_exp : J.expression) ~(default : default_case) ?(merge_cases = fun _ _ -> true) (cases : (a * Lam.t) list) -> + let output_block_with_source_loc cxt lam = + compile_lambda cxt lam |> Js_output.output_as_block + |> with_block_source_loc lam + in match (cases, default) with - | [], Default lam -> Js_output.output_as_block (compile_lambda cxt lam) + | [], Default lam -> output_block_with_source_loc cxt lam | [], (Complete | NonComplete) -> [] | [(_, lam)], Complete -> (* To take advantage of such optimizations, @@ -549,18 +619,18 @@ let compile output_prefix = otherwise the compiler engine would think that it's also complete *) - Js_output.output_as_block (compile_lambda cxt lam) + output_block_with_source_loc cxt lam | [(id, lam)], NonComplete -> morph_declare_to_assign cxt (fun cxt define -> [ S.if_ ?declaration:define (eq_exp None switch_exp (Some id) (make_exp id)) - (Js_output.output_as_block (compile_lambda cxt lam)); + (output_block_with_source_loc cxt lam); ]) | [(id, lam)], Default x | [(id, lam); (_, x)], Complete -> morph_declare_to_assign cxt (fun cxt define -> - let else_block = Js_output.output_as_block (compile_lambda cxt x) in - let then_block = Js_output.output_as_block (compile_lambda cxt lam) in + let else_block = output_block_with_source_loc cxt x in + let then_block = output_block_with_source_loc cxt lam in [ S.if_ ?declaration:define (eq_exp None switch_exp (Some id) (make_exp id)) @@ -599,9 +669,7 @@ let compile output_prefix = | Complete -> None | NonComplete -> None | Default lam -> ( - let statements = - Js_output.output_as_block (compile_lambda switch_cxt lam) - in + let statements = output_block_with_source_loc switch_cxt lam in match statements with | [] -> None | _ -> Some statements) @@ -613,6 +681,7 @@ let compile output_prefix = let switch_body, should_break = Js_output.to_break_block (compile_lambda switch_cxt lam) in + let switch_body = with_block_source_loc lam switch_body in let should_break = if not @@ -621,10 +690,20 @@ let compile output_prefix = then should_break else should_break && Lam_exit_code.has_exit lam in - (switch_case, J.{switch_body; should_break; comment = None}) + ( switch_case, + J. + { + switch_body; + should_break; + comment = source_map_comment_of_lam lam; + } ) else ( switch_case, - {switch_body = []; should_break = false; comment = None} )) + { + switch_body = []; + should_break = false; + comment = source_map_comment_of_lam lam; + } )) (* TODO: we should also group default *) (* The last clause does not need [break] common break through, *) @@ -1630,11 +1709,12 @@ let compile output_prefix = | _ -> Js_output.output_of_block_and_expression lambda_cxt.continuation args_code - (E.call - ~info: - (call_info_of_ap_status appinfo.ap_transformed_jsx - appinfo.ap_info.ap_status) - fn_code args)) + (with_source_loc appinfo.ap_info.ap_loc + (E.call + ~info: + (call_info_of_ap_status appinfo.ap_transformed_jsx + appinfo.ap_info.ap_status) + fn_code args))) and compile_prim (prim_info : Lam.prim_info) (lambda_cxt : Lam_compile_context.t) = match prim_info with @@ -1648,13 +1728,14 @@ let compile output_prefix = | Fld_module {name = field} -> compile_external_field ~dynamic_import lambda_cxt id field | _ -> assert false) - | {primitive = Praise; args = [e]; _} -> ( + | {primitive = Praise; args = [e]; loc} -> ( match compile_lambda {lambda_cxt with continuation = NeedValue Not_tail} e with | {block; value = Some v} -> + let comment = source_map_comment loc in Js_output.make - (Ext_list.append_one block (S.throw_stmt v)) + (Ext_list.append_one block (S.throw_stmt ?comment v)) ~value:E.undefined ~output_finished:True (* FIXME -- breaks invariant when NeedValue, reason is that js [throw] is statement while ocaml it's an expression, we should remove such things in lambda optimizations @@ -1721,9 +1802,10 @@ let compile output_prefix = | {primitive = Pjs_unsafe_downgrade _; args} -> assert false | {primitive = Pjs_fn_method; args = args_lambda} -> ( match args_lambda with - | [Lfunction {params; body; attr = {return_unit; async}}] -> + | [Lfunction {params; body; attr = {return_unit; async}; loc}] -> + let comment = source_map_comment loc in Js_output.output_of_block_and_expression lambda_cxt.continuation [] - (E.method_ ~async ~return_unit params + (E.method_ ?comment ~async ~return_unit params (* Invariant: jmp_table can not across function boundary, here we share env *) @@ -1782,7 +1864,7 @@ let compile output_prefix = [args_expr] in Js_output.output_of_block_and_expression lambda_cxt.continuation - args_code exp + args_code (with_source_loc loc exp) | Lfunction { body = @@ -1811,7 +1893,7 @@ let compile output_prefix = [args_expr] in Js_output.output_of_block_and_expression lambda_cxt.continuation - args_code exp + args_code (with_source_loc loc exp) | _ -> Location.raise_errorf ~loc "Invalid argument: unsupported argument to dynamic import. If you \ @@ -1833,15 +1915,22 @@ let compile output_prefix = args_expr in Js_output.output_of_block_and_expression lambda_cxt.continuation args_code - exp + (with_source_loc loc exp) and compile_lambda (lambda_cxt : Lam_compile_context.t) (cur_lam : Lam.t) : Js_output.t = match cur_lam with | Lfunction - {params; body; attr = {return_unit; async; one_unit_arg; directive}} -> + { + params; + body; + attr = {return_unit; async; one_unit_arg; directive}; + loc; + } -> + let comment = source_map_comment loc in Js_output.output_of_expression lambda_cxt.continuation ~no_effects:no_effects_const - (E.ocaml_fun params ~return_unit ~async ~one_unit_arg ?directive + (E.ocaml_fun ?comment params ~return_unit ~async ~one_unit_arg + ?directive (* Invariant: jmp_table can not across function boundary, here we share env *) diff --git a/compiler/core/lam_compile_main.ml b/compiler/core/lam_compile_main.ml index cdecf32ef8e..54e8a119825 100644 --- a/compiler/core/lam_compile_main.ml +++ b/compiler/core/lam_compile_main.ml @@ -297,6 +297,31 @@ js let (//) = Filename.concat +let source_map_enabled () = + match !Js_config.source_map with + | No_source_map -> false + | Linked -> true + +let dump_deps_program_with_source_map ~target_file ~output_prefix module_system + lambda_output chan = + let builder = + if source_map_enabled () then + Some + (Js_source_map.make ~generated_file:target_file + ~source_root:!Js_config.source_map_root + ~sources_content:!Js_config.source_map_sources_content) + else None + in + Js_source_map.with_builder builder (fun () -> + Js_dump_program.pp_deps_program ~output_prefix module_system lambda_output + (Ext_pp.from_channel chan)); + match (builder, !Js_config.source_map) with + | Some builder, Linked -> + let map_file = target_file ^ ".map" in + output_string chan (Js_source_map.linked_comment ~map_file); + Ext_io.write_file map_file (Js_source_map.json builder) + | _ -> () + let lambda_as_module (lambda_output : J.deps_program) (output_prefix : string) @@ -306,11 +331,6 @@ let lambda_as_module Js_dump_program.dump_deps_program ~output_prefix Commonjs (lambda_output) stdout end else Js_packages_info.iter package_info (fun {module_system; path; suffix} -> - let output_chan chan = - Js_dump_program.dump_deps_program ~output_prefix - module_system - (lambda_output) - chan in let basename = Ext_namespace.change_ext_ns_suffix (Filename.basename output_prefix) suffix in @@ -320,6 +340,9 @@ let lambda_as_module basename (* #913 only generate little-case js file *) ) in + let output_chan chan = + dump_deps_program_with_source_map ~target_file ~output_prefix + module_system (lambda_output) chan in (if not !Clflags.dont_write_files then Ext_pervasives.with_file_as_chan target_file output_chan ); diff --git a/compiler/core/lam_convert.ml b/compiler/core/lam_convert.ml index 3c0c7c058d0..789770d45e5 100644 --- a/compiler/core/lam_convert.ml +++ b/compiler/core/lam_convert.ml @@ -407,18 +407,18 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) : (Ext_list.map args convert_aux) {ap_loc = loc; ap_inlined; ap_status = App_uncurry} ~ap_transformed_jsx - | Lfunction {params; body; attr} -> + | Lfunction {params; body; attr; loc} -> let new_map, body = rename_optional_parameters Map_ident.empty params body in if Map_ident.is_empty new_map then - Lam.function_ ~attr ~arity:(List.length params) ~params + Lam.function_ ~loc ~attr ~arity:(List.length params) ~params ~body:(convert_aux body) else let params = Ext_list.map params (fun x -> Map_ident.find_default new_map x x) in - Lam.function_ ~attr ~arity:(List.length params) ~params + Lam.function_ ~loc ~attr ~arity:(List.length params) ~params ~body:(convert_aux body) | Llet (_, _, _, Lprim (Pgetglobal id, args, _), _body) when dynamic_import -> @@ -565,13 +565,29 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) : } | _ -> Lam.let_ kind id new_e new_body) and convert_pipe (f : Lambda.lambda) (x : Lambda.lambda) outer_loc = + let pipe_loc = + let candidate = + match f with + | Lapply {ap_loc} -> Some ap_loc + | Lfunction {loc} -> Some loc + | Lprim (_, _, loc) + | Lswitch (_, _, loc) + | Lstringswitch (_, _, _, loc) + | Lsend (_, _, loc) -> + Some loc + | _ -> None + in + match candidate with + | Some loc when (not loc.loc_ghost) && loc.loc_start.pos_cnum >= 0 -> loc + | _ -> outer_loc + in let x = convert_aux x in let f = convert_aux f in match f with | Lfunction {params = [param]; body = Lprim {primitive; args = [Lvar inner_arg]}} when Ident.same param inner_arg -> - Lam.prim ~primitive ~args:[x] outer_loc + Lam.prim ~primitive ~args:[x] pipe_loc | Lapply { ap_func = @@ -580,18 +596,14 @@ let convert (exports : Set_ident.t) (lam : Lambda.lambda) : } when Ext_list.for_all2_no_exn inner_args params lam_is_var && Ext_list.length_larger_than_n inner_args args 1 -> - Lam.prim ~primitive ~args:(Ext_list.append_one args x) outer_loc + Lam.prim ~primitive ~args:(Ext_list.append_one args x) pipe_loc | Lapply {ap_func; ap_args; ap_info; ap_transformed_jsx} -> Lam.apply ~ap_transformed_jsx ap_func (Ext_list.append_one ap_args x) - { - ap_loc = outer_loc; - ap_inlined = ap_info.ap_inlined; - ap_status = App_na; - } + {ap_loc = pipe_loc; ap_inlined = ap_info.ap_inlined; ap_status = App_na} | _ -> Lam.apply f [x] - {ap_loc = outer_loc; ap_inlined = Default_inline; ap_status = App_na} + {ap_loc = pipe_loc; ap_inlined = Default_inline; ap_status = App_na} and convert_switch (e : Lambda.lambda) (s : Lambda.lambda_switch) = let e = convert_aux e in match s with diff --git a/compiler/core/lam_eta_conversion.ml b/compiler/core/lam_eta_conversion.ml index 220fa760221..320ecd41647 100644 --- a/compiler/core/lam_eta_conversion.ml +++ b/compiler/core/lam_eta_conversion.ml @@ -61,12 +61,12 @@ let transform_under_supply n ap_info fn args = But it is dangerous to change the arity of an existing function which may cause inconsistency *) - Lam.function_ ~arity:n ~params:extra_args + Lam.function_ ~loc:Location.none ~arity:n ~params:extra_args ~attr:Lambda.default_function_attribute ~body:(Lam.apply fn (Ext_list.append args extra_lambdas) ap_info) | fn :: args, bindings -> let rest : Lam.t = - Lam.function_ ~arity:n ~params:extra_args + Lam.function_ ~loc:Location.none ~arity:n ~params:extra_args ~attr:Lambda.default_function_attribute ~body:(Lam.apply fn (Ext_list.append args extra_lambdas) ap_info) in @@ -123,8 +123,8 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : if from = to_ || is_async_fn then fn else if to_ = 0 then match fn with - | Lfunction {params = [param]; body} -> - Lam.function_ ~arity:0 ~attr:Lambda.default_function_attribute + | Lfunction {params = [param]; body; loc} -> + Lam.function_ ~loc ~arity:0 ~attr:Lambda.default_function_attribute ~params:[] ~body:(Lam.let_ Alias param Lam.unit body) (* could be only introduced by @@ -148,7 +148,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : in let cont = - Lam.function_ ~attr:Lambda.default_function_attribute ~arity:0 + Lam.function_ ~loc ~attr:Lambda.default_function_attribute ~arity:0 ~params:[] ~body:(Lam.apply new_fn [Lam.unit] ap_info) in @@ -158,7 +158,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : | Some partial_arg -> Lam.let_ Strict partial_arg fn cont) else if to_ > from then match fn with - | Lfunction {params; body} -> + | Lfunction {params; body; loc} -> (* {[fun x -> f]} -> {[ fun x y -> f y ]} *) @@ -170,7 +170,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : | [] -> body | var :: vars -> mk_apply (Lam.apply body [var] ap_info) vars in - Lam.function_ ~attr:Lambda.default_function_attribute ~arity:to_ + Lam.function_ ~loc ~attr:Lambda.default_function_attribute ~arity:to_ ~params:(Ext_list.append params extra_args) ~body:(mk_apply body (Ext_list.map extra_args Lam.var)) | _ -> ( @@ -193,7 +193,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : (Some partial_arg, Lam.var partial_arg) in let cont = - Lam.function_ ~arity ~attr:Lambda.default_function_attribute + Lam.function_ ~loc ~arity ~attr:Lambda.default_function_attribute ~params:extra_args ~body: (let first_args, rest_args = Ext_list.split_at extra_args from in @@ -216,16 +216,16 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : This is okay if the function is not held by other.. *) match fn with - | Lfunction {params; body} + | Lfunction {params; body; loc} (* TODO check arity = List.length params in debug mode *) -> let arity = to_ in let extra_outer_args, extra_inner_args = Ext_list.split_at params arity in - Lam.function_ ~arity ~attr:Lambda.default_function_attribute + Lam.function_ ~loc ~arity ~attr:Lambda.default_function_attribute ~params:extra_outer_args ~body: - (Lam.function_ ~arity:(from - to_) + (Lam.function_ ~loc ~arity:(from - to_) ~attr:Lambda.default_function_attribute ~params:extra_inner_args ~body) | _ -> ( @@ -247,14 +247,14 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : (Some partial_arg, Lam.var partial_arg) in let cont = - Lam.function_ ~arity:to_ ~params:extra_outer_args + Lam.function_ ~loc ~arity:to_ ~params:extra_outer_args ~attr:Lambda.default_function_attribute ~body: (let arity = from - to_ in let extra_inner_args = Ext_list.init arity (fun _ -> Ident.create Literals.param) in - Lam.function_ ~arity ~params:extra_inner_args + Lam.function_ ~loc ~arity ~params:extra_inner_args ~attr:Lambda.default_function_attribute ~body: (Lam.apply new_fn @@ -285,7 +285,7 @@ let unsafe_adjust_to_arity loc ~(to_ : int) ?(from : int option) (fn : Lam.t) : in let cont = - Lam.function_ ~attr:Lambda.default_function_attribute ~arity:0 + Lam.function_ ~loc ~attr:Lambda.default_function_attribute ~arity:0 ~params:[] ~body:(Lam.apply new_fn [Lam.unit] ap_info) in diff --git a/compiler/core/lam_pass_alpha_conversion.ml b/compiler/core/lam_pass_alpha_conversion.ml index 7965cfc6011..8ab29e4ca75 100644 --- a/compiler/core/lam_pass_alpha_conversion.ml +++ b/compiler/core/lam_pass_alpha_conversion.ml @@ -75,9 +75,9 @@ let alpha_conversion (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = | Lprim {primitive = Pjs_fn_make_unit; args = [arg]; loc} -> let arg = match arg with - | Lfunction {arity = 1; params = [x]; attr; body} + | Lfunction {arity = 1; params = [x]; attr; body; loc} when Ident.name x = "param" (* "()" *) -> - Lam.function_ ~params:[x] + Lam.function_ ~loc ~params:[x] ~attr:{attr with one_unit_arg = true} ~body ~arity:1 | _ -> arg @@ -85,9 +85,9 @@ let alpha_conversion (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = simpl arg | Lprim {primitive; args; loc} -> Lam.prim ~primitive ~args:(Ext_list.map args simpl) loc - | Lfunction {arity; params; body; attr} -> + | Lfunction {arity; params; body; attr; loc} -> (* Lam_mk.lfunction kind params (simpl l) *) - Lam.function_ ~arity ~params ~body:(simpl body) ~attr + Lam.function_ ~loc ~arity ~params ~body:(simpl body) ~attr | Lswitch ( l, { diff --git a/compiler/core/lam_pass_deep_flatten.ml b/compiler/core/lam_pass_deep_flatten.ml index 6e94a78a587..33ceff7cd07 100644 --- a/compiler/core/lam_pass_deep_flatten.ml +++ b/compiler/core/lam_pass_deep_flatten.ml @@ -229,8 +229,8 @@ let deep_flatten (lam : Lam.t) : Lam.t = | Lprim {primitive; args; loc} -> let args = Ext_list.map args aux in Lam.prim ~primitive ~args loc - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(aux body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(aux body) ~attr | Lswitch ( l, { diff --git a/compiler/core/lam_pass_exits.ml b/compiler/core/lam_pass_exits.ml index e47be329551..355ac011500 100644 --- a/compiler/core/lam_pass_exits.ml +++ b/compiler/core/lam_pass_exits.ml @@ -205,8 +205,8 @@ let subst_helper (subst : subst_tbl) (query : int -> int) (lam : Lam.t) : Lam.t Lam.apply (simplif ap_func) (Ext_list.map ap_args simplif) ap_info ~ap_transformed_jsx - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(simplif body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(simplif body) ~attr | Llet (kind, v, l1, l2) -> Lam.let_ kind v (simplif l1) (simplif l2) | Lletrec (bindings, body) -> Lam.letrec (Ext_list.map_snd bindings simplif) (simplif body) diff --git a/compiler/core/lam_pass_lets_dce.ml b/compiler/core/lam_pass_lets_dce.ml index 503e90c1f81..9f14f0bede3 100644 --- a/compiler/core/lam_pass_lets_dce.ml +++ b/compiler/core/lam_pass_lets_dce.ml @@ -147,8 +147,8 @@ let lets_helper (count_var : Ident.t -> Lam_pass_count.used_info) lam : Lam.t = | Lapply {ap_func = l1; ap_args = ll; ap_info; ap_transformed_jsx} -> Lam.apply (simplif l1) (Ext_list.map ll simplif) ap_info ~ap_transformed_jsx - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(simplif body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(simplif body) ~attr | Lconst _ -> lam | Lletrec (bindings, body) -> Lam.letrec (Ext_list.map_snd bindings simplif) (simplif body) diff --git a/compiler/core/lam_pass_remove_alias.ml b/compiler/core/lam_pass_remove_alias.ml index 52a88ad02eb..c6b10824ad3 100644 --- a/compiler/core/lam_pass_remove_alias.ml +++ b/compiler/core/lam_pass_remove_alias.ml @@ -244,8 +244,8 @@ let simplify_alias (meta : Lam_stats.t) (lam : Lam.t) : Lam.t = (* simpl (Lam_beta_reduce.propogate_beta_reduce meta params body args) *) | Lapply {ap_func = l1; ap_args = ll; ap_info; ap_transformed_jsx} -> Lam.apply (simpl l1) (Ext_list.map ll simpl) ap_info ~ap_transformed_jsx - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(simpl body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(simpl body) ~attr | Lswitch ( l, { diff --git a/compiler/core/lam_subst.ml b/compiler/core/lam_subst.ml index e449102dc5e..3be69db85fb 100644 --- a/compiler/core/lam_subst.ml +++ b/compiler/core/lam_subst.ml @@ -35,8 +35,8 @@ let subst (s : Lam.t Map_ident.t) lam = | Lconst _ -> x | Lapply {ap_func; ap_args; ap_info} -> Lam.apply (subst_aux ap_func) (Ext_list.map ap_args subst_aux) ap_info - | Lfunction {arity; params; body; attr} -> - Lam.function_ ~arity ~params ~body:(subst_aux body) ~attr + | Lfunction {arity; params; body; attr; loc} -> + Lam.function_ ~loc ~arity ~params ~body:(subst_aux body) ~attr | Llet (str, id, arg, body) -> Lam.let_ str id (subst_aux arg) (subst_aux body) | Lletrec (decl, body) -> diff --git a/compiler/ext/ext_pp.ml b/compiler/ext/ext_pp.ml index f9237c271a7..384782f526e 100644 --- a/compiler/ext/ext_pp.ml +++ b/compiler/ext/ext_pp.ml @@ -36,9 +36,46 @@ type t = { flush: unit -> unit; mutable indent_level: int; mutable last_new_line: bool; - (* only when we print newline, we print the indent *) + mutable line: int; + mutable column: int; (* only when we print newline, we print the indent *) } +let update_position t s = + let len = String.length s in + let rec loop i = + if i < len then + match String.unsafe_get s i with + | '\n' -> + t.line <- t.line + 1; + t.column <- 0; + loop (i + 1) + | c -> + let byte = Char.code c in + if byte < 0x80 then ( + t.column <- t.column + 1; + loop (i + 1)) + else if byte land 0xE0 = 0xC0 && i + 1 < len then ( + t.column <- t.column + 1; + loop (i + 2)) + else if byte land 0xF0 = 0xE0 && i + 2 < len then ( + t.column <- t.column + 1; + loop (i + 3)) + else if byte land 0xF8 = 0xF0 && i + 3 < len then ( + t.column <- t.column + 2; + loop (i + 4)) + else ( + t.column <- t.column + 1; + loop (i + 1)) + in + loop 0 + +let update_position_char t c = + match c with + | '\n' -> + t.line <- t.line + 1; + t.column <- 0 + | _ -> t.column <- t.column + 1 + let from_channel chan = { output_string = (fun s -> output_string chan s); @@ -46,6 +83,8 @@ let from_channel chan = flush = (fun _ -> flush chan); indent_level = 0; last_new_line = false; + line = 0; + column = 0; } let from_buffer buf = @@ -55,6 +94,8 @@ let from_buffer buf = flush = (fun _ -> ()); indent_level = 0; last_new_line = false; + line = 0; + column = 0; } (* If we have [newline] in [s], @@ -63,28 +104,37 @@ let from_buffer buf = *) let string t s = t.output_string s; + update_position t s; t.last_new_line <- false let newline t = if not t.last_new_line then ( t.output_char '\n'; + update_position_char t '\n'; for _ = 0 to t.indent_level - 1 do - t.output_string L.indent_str + t.output_string L.indent_str; + update_position t L.indent_str done; t.last_new_line <- true) let at_least_two_lines t = - if not t.last_new_line then t.output_char '\n'; + if not t.last_new_line then ( + t.output_char '\n'; + update_position_char t '\n'); t.output_char '\n'; + update_position_char t '\n'; for _ = 0 to t.indent_level - 1 do - t.output_string L.indent_str + t.output_string L.indent_str; + update_position t L.indent_str done; t.last_new_line <- true let force_newline t = t.output_char '\n'; + update_position_char t '\n'; for _ = 0 to t.indent_level - 1 do - t.output_string L.indent_str + t.output_string L.indent_str; + update_position t L.indent_str done; t.last_new_line <- true @@ -169,3 +219,5 @@ let brace_group st n action = group st n (fun _ -> brace st action) t.indent_level <- t.indent_level + n *) let flush t () = t.flush () + +let position t = (t.line, t.column) diff --git a/compiler/ext/ext_pp.mli b/compiler/ext/ext_pp.mli index aaf2176214a..4990abe7515 100644 --- a/compiler/ext/ext_pp.mli +++ b/compiler/ext/ext_pp.mli @@ -77,3 +77,5 @@ val from_channel : out_channel -> t val from_buffer : Buffer.t -> t val flush : t -> unit -> unit + +val position : t -> int * int diff --git a/rescript.opam b/rescript.opam index 40a9251350f..c4b447cae17 100644 --- a/rescript.opam +++ b/rescript.opam @@ -26,7 +26,7 @@ depends: [ "dune" {>= "3.17"} "flow_parser" {= "0.267.0"} "ocamlformat" {with-test & = "0.27.0"} - "yojson" {with-test & = "2.2.2"} + "yojson" {= "2.2.2"} "ounit2" {with-test & = "2.2.7"} "odoc" {with-doc} "ocaml-lsp-server" {with-dev-setup & = "1.22.0"} diff --git a/rescript.opam.template b/rescript.opam.template index e5629e01d6a..71c93ea688d 100644 --- a/rescript.opam.template +++ b/rescript.opam.template @@ -4,7 +4,7 @@ depends: [ "dune" {>= "3.17"} "flow_parser" {= "0.267.0"} "ocamlformat" {with-test & = "0.27.0"} - "yojson" {with-test & = "2.2.2"} + "yojson" {= "2.2.2"} "ounit2" {with-test & = "2.2.7"} "odoc" {with-doc} "ocaml-lsp-server" {with-dev-setup & = "1.22.0"} diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 2d5b3d1342f..8d88f1f4bc3 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -33,10 +33,15 @@ fn remove_iast(package: &packages::Package, source_file: &Path) { } fn remove_mjs_file(source_file: &Path, suffix: &str) { - let _ = std::fs::remove_file(source_file.with_extension( + let js_file = source_file.with_extension( // suffix.to_string includes the ., so we need to remove it &suffix[1..], - )); + ); + let _ = std::fs::remove_file(&js_file); + + let mut map_file = js_file.into_os_string(); + map_file.push(".map"); + let _ = std::fs::remove_file(PathBuf::from(map_file)); } fn remove_compile_asset(package: &packages::Package, source_file: &Path, extension: &str) { diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index 8042c8cde82..bce155ac4e6 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -686,6 +686,7 @@ pub fn compiler_args( let jsx_module_args = root_config.get_jsx_module_args(); let jsx_mode_args = root_config.get_jsx_mode_args(); let jsx_preserve_args = root_config.get_jsx_preserve_args(); + let source_map_args = root_config.get_source_map_args(); let bsb_project_root = project_context.get_root_path(); let dep_paths: Vec<(String, PathBuf)> = if config.gentype_config.is_some() { let resolved = packages.as_ref().map(|pkgs| { @@ -770,6 +771,7 @@ pub fn compiler_args( jsx_module_args, jsx_mode_args, jsx_preserve_args, + source_map_args, bsc_flags.to_owned(), warning_args, gentype_arg, @@ -1052,6 +1054,18 @@ fn compile_file( if source.exists() { let _ = std::fs::copy(&source, &destination).expect("copying source file failed"); } + + let mut source_map = source.clone().into_os_string(); + source_map.push(".map"); + let source_map = PathBuf::from(source_map); + let mut destination_map = destination.clone().into_os_string(); + destination_map.push(".map"); + let destination_map = PathBuf::from(destination_map); + + if source_map.exists() { + let _ = std::fs::copy(&source_map, &destination_map) + .expect("copying source map file failed"); + } } }); diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 96cce330ae3..4674c1a1c53 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -282,6 +282,13 @@ pub struct JsxSpecs { pub preserve: Option, } +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(untagged)] +pub enum SourceMapConfig { + Bool(bool), + String(String), +} + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub enum GenTypeModule { #[serde(rename = "commonjs")] @@ -488,6 +495,12 @@ pub struct Config { pub namespace: Option, pub jsx: Option, + #[serde(rename = "sourceMap")] + pub source_map: Option, + #[serde(rename = "sourceMapSourcesContent")] + pub source_map_sources_content: Option, + #[serde(rename = "sourceMapRoot")] + pub source_map_root: Option, #[serde(rename = "experimental-features")] pub experimental_features: Option>, #[serde(rename = "gentypeconfig")] @@ -790,6 +803,35 @@ impl Config { } } + pub fn get_source_map_args(&self) -> Vec { + let mut args = Vec::new(); + + if let Some(source_map) = &self.source_map { + let value = match source_map { + SourceMapConfig::Bool(true) => "true".to_string(), + SourceMapConfig::Bool(false) => "false".to_string(), + SourceMapConfig::String(value) => match value.as_str() { + "true" | "linked" | "false" | "none" => value.to_string(), + _ => panic!("sourceMap value {value} is unsupported"), + }, + }; + args.extend(["-bs-source-map".to_string(), value]); + } + + if let Some(sources_content) = self.source_map_sources_content { + args.extend([ + "-bs-source-map-sources-content".to_string(), + sources_content.to_string(), + ]); + } + + if let Some(source_root) = &self.source_map_root { + args.extend(["-bs-source-map-root".to_string(), source_root.to_string()]); + } + + args + } + pub fn get_experimental_features_args(&self) -> Vec { match &self.experimental_features { None => vec![], @@ -1284,6 +1326,9 @@ pub mod tests { compiler_flags: None, namespace: None, jsx: None, + source_map: None, + source_map_sources_content: None, + source_map_root: None, gentype_config: None, js_post_build: None, editor: None, @@ -1586,6 +1631,39 @@ pub mod tests { ); } + #[test] + fn test_source_map_args() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": true, + "sourceMapSourcesContent": true + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!( + config.get_source_map_args(), + vec!["-bs-source-map", "true", "-bs-source-map-sources-content", "true",] + ); + } + + #[test] + #[should_panic(expected = "sourceMap value inline is unsupported")] + fn test_source_map_rejects_inline_for_mvp() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": "inline" + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + let _ = config.get_source_map_args(); + } + #[test] fn test_get_suffix() { let json = r#" diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js new file mode 100644 index 00000000000..e0f53fa8e07 --- /dev/null +++ b/tests/build_tests/source_map/input.js @@ -0,0 +1,31 @@ +// @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 { execBuildOrThrow, execClean } = setup(import.meta.dirname); + +await execBuildOrThrow(); + +const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", "Demo.js"); +const mapPath = `${jsPath}.map`; + +const js = await fs.readFile(jsPath, "utf8"); +assert.match(js, /\/\/# sourceMappingURL=Demo\.js\.map/); + +const map = JSON.parse(await fs.readFile(mapPath, "utf8")); +assert.equal(map.version, 3); +assert.equal(map.file, "Demo.js"); +assert.ok(map.mappings.length > 0, "source map should include mappings"); +assert.ok( + map.sources.some(source => source.endsWith("Demo.res")), + `source map should include Demo.res, got ${map.sources.join(", ")}`, +); +assert.ok( + map.sourcesContent.some(content => content.includes("let add = (a, b)")), + "source map should include source contents", +); + +await execClean(); diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json new file mode 100644 index 00000000000..84483523ad0 --- /dev/null +++ b/tests/build_tests/source_map/rescript.json @@ -0,0 +1,6 @@ +{ + "name": "source_map", + "sources": ["src"], + "sourceMap": true, + "sourceMapSourcesContent": true +} diff --git a/tests/build_tests/source_map/src/Demo.res b/tests/build_tests/source_map/src/Demo.res new file mode 100644 index 00000000000..c10790a27b0 --- /dev/null +++ b/tests/build_tests/source_map/src/Demo.res @@ -0,0 +1,5 @@ +let add = (a, b) => a + b + +let crash = () => Js.Exn.raiseError("source map test") + +let value = add(20, 22) From d0ed388aadde080a76a922a9c05910542ece4fd8 Mon Sep 17 00:00:00 2001 From: mununki Date: Mon, 27 Apr 2026 17:57:32 +0900 Subject: [PATCH 2/7] Remove marker entries after lookup --- compiler/core/js_source_map.ml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index f473d862288..95763b02af4 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -173,6 +173,13 @@ let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = :: builder.mappings; builder.last_generated <- Some (generated_line, generated_column) +let take_marker_loc id = + match Hashtbl.find_opt marker_locs id with + | None -> None + | Some loc -> + Hashtbl.remove marker_locs id; + Some loc + let mark_comment fmt comment = if is_prefix ~prefix:marker_prefix comment then ( let prefix_len = String.length marker_prefix in @@ -180,7 +187,7 @@ let mark_comment fmt comment = int_of_string (String.sub comment prefix_len (String.length comment - prefix_len)) in - (match (!current, Hashtbl.find_opt marker_locs id) with + (match (!current, take_marker_loc id) with | Some builder, Some loc -> let generated_line, generated_column = Ext_pp.position fmt in add_mapping builder ~generated_line ~generated_column loc From b021c2f0df7f9219cc9e973d940e04ee49d2ced5 Mon Sep 17 00:00:00 2001 From: mununki Date: Mon, 27 Apr 2026 18:03:33 +0900 Subject: [PATCH 3/7] Preserve relative source paths in maps --- compiler/core/js_source_map.ml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 95763b02af4..4df5c623304 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -76,17 +76,26 @@ let repeat x n = let rec loop acc n = if n <= 0 then acc else loop (x :: acc) (n - 1) in loop [] n +let drive_root parts = + match parts with + | drive :: _ when String.length drive = 2 && drive.[1] = ':' -> + Some (String.uppercase_ascii drive) + | _ -> None + let relative_path ~from_dir ~to_file = let from_dir = absolute_path from_dir in let to_file = absolute_path to_file in let from_parts = split_path from_dir in let to_parts = split_path to_file in - match (from_parts, to_parts) with - | from_root :: _, to_root :: _ when from_root = to_root -> + match (drive_root from_parts, drive_root to_parts) with + (* Cross-drive Windows paths cannot be represented as a filesystem-relative path. *) + | Some from_drive, Some to_drive when from_drive <> to_drive -> + normalize_slashes to_file + | Some _, None | None, Some _ -> normalize_slashes to_file + | _ -> let from_rest, to_rest = drop_common from_parts to_parts in let parts = repeat ".." (List.length from_rest) @ to_rest in if parts = [] then Filename.basename to_file else String.concat "/" parts - | _ -> Filename.basename to_file let make ~generated_file ~source_root ~sources_content = { From cdea1ce8019db297490741928db21c24fa14bf86 Mon Sep 17 00:00:00 2001 From: mununki Date: Tue, 28 Apr 2026 11:08:20 +0900 Subject: [PATCH 4/7] Change sourceMap field schema to false or object --- rewatch/src/config.rs | 108 +++++++++++++++------ tests/build_tests/source_map/rescript.json | 6 +- 2 files changed, 82 insertions(+), 32 deletions(-) diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index 4674c1a1c53..b96ccefcca7 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -286,7 +286,15 @@ pub struct JsxSpecs { #[serde(untagged)] pub enum SourceMapConfig { Bool(bool), - String(String), + Options(SourceMapOptions), +} + +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct SourceMapOptions { + pub mode: String, + pub sources_content: Option, + pub source_root: Option, } #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] @@ -497,10 +505,6 @@ pub struct Config { pub jsx: Option, #[serde(rename = "sourceMap")] pub source_map: Option, - #[serde(rename = "sourceMapSourcesContent")] - pub source_map_sources_content: Option, - #[serde(rename = "sourceMapRoot")] - pub source_map_root: Option, #[serde(rename = "experimental-features")] pub experimental_features: Option>, #[serde(rename = "gentypeconfig")] @@ -807,26 +811,31 @@ impl Config { let mut args = Vec::new(); if let Some(source_map) = &self.source_map { - let value = match source_map { - SourceMapConfig::Bool(true) => "true".to_string(), - SourceMapConfig::Bool(false) => "false".to_string(), - SourceMapConfig::String(value) => match value.as_str() { - "true" | "linked" | "false" | "none" => value.to_string(), - _ => panic!("sourceMap value {value} is unsupported"), - }, - }; - args.extend(["-bs-source-map".to_string(), value]); - } + match source_map { + SourceMapConfig::Bool(false) => { + args.extend(["-bs-source-map".to_string(), "false".to_string()]); + } + SourceMapConfig::Bool(true) => { + panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") + } + SourceMapConfig::Options(options) => { + match options.mode.as_str() { + "linked" => args.extend(["-bs-source-map".to_string(), "linked".to_string()]), + value => panic!("sourceMap.mode value {value} is unsupported"), + } - if let Some(sources_content) = self.source_map_sources_content { - args.extend([ - "-bs-source-map-sources-content".to_string(), - sources_content.to_string(), - ]); - } + if let Some(sources_content) = options.sources_content { + args.extend([ + "-bs-source-map-sources-content".to_string(), + sources_content.to_string(), + ]); + } - if let Some(source_root) = &self.source_map_root { - args.extend(["-bs-source-map-root".to_string(), source_root.to_string()]); + if let Some(source_root) = &options.source_root { + args.extend(["-bs-source-map-root".to_string(), source_root.to_string()]); + } + } + } } args @@ -1327,8 +1336,6 @@ pub mod tests { namespace: None, jsx: None, source_map: None, - source_map_sources_content: None, - source_map_root: None, gentype_config: None, js_post_build: None, editor: None, @@ -1637,26 +1644,67 @@ pub mod tests { { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], - "sourceMap": true, - "sourceMapSourcesContent": true + "sourceMap": { + "mode": "linked", + "sourcesContent": true, + "sourceRoot": "webpack://testrepo/" + } } "#; let config = serde_json::from_str::(json).unwrap(); assert_eq!( config.get_source_map_args(), - vec!["-bs-source-map", "true", "-bs-source-map-sources-content", "true",] + vec![ + "-bs-source-map", + "linked", + "-bs-source-map-sources-content", + "true", + "-bs-source-map-root", + "webpack://testrepo/", + ] ); } #[test] - #[should_panic(expected = "sourceMap value inline is unsupported")] + fn test_source_map_false_args() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": false + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!(config.get_source_map_args(), vec!["-bs-source-map", "false",]); + } + + #[test] + #[should_panic(expected = "sourceMap true is unsupported")] + fn test_source_map_rejects_true_for_nested_config() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": true + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + let _ = config.get_source_map_args(); + } + + #[test] + #[should_panic(expected = "sourceMap.mode value inline is unsupported")] fn test_source_map_rejects_inline_for_mvp() { let json = r#" { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], - "sourceMap": "inline" + "sourceMap": { + "mode": "inline" + } } "#; diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json index 84483523ad0..2485256bb0f 100644 --- a/tests/build_tests/source_map/rescript.json +++ b/tests/build_tests/source_map/rescript.json @@ -1,6 +1,8 @@ { "name": "source_map", "sources": ["src"], - "sourceMap": true, - "sourceMapSourcesContent": true + "sourceMap": { + "mode": "linked", + "sourcesContent": true + } } From 879cd697020f2cba50f61534a98448b346c3172d Mon Sep 17 00:00:00 2001 From: mununki Date: Tue, 28 Apr 2026 11:08:41 +0900 Subject: [PATCH 5/7] sourcemap schema in rescript.json --- docs/docson/build-schema.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index af78908ea8c..9c0462822e0 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -47,6 +47,33 @@ } ] }, + "source-map-spec": { + "oneOf": [ + { + "enum": [false], + "description": "Disable source map generation." + }, + { + "type": "object", + "properties": { + "mode": { + "enum": ["linked"], + "description": "Generate a separate .js.map file next to each generated JavaScript file and append a sourceMappingURL comment. Only linked source maps are supported for now." + }, + "sourcesContent": { + "type": "boolean", + "description": "Include original .res source text in the source map. Default: false." + }, + "sourceRoot": { + "type": "string", + "description": "Optional sourceRoot value written to the generated source map." + } + }, + "required": ["mode"], + "additionalProperties": false + } + ] + }, "package-specs": { "oneOf": [ { @@ -448,6 +475,10 @@ "$ref": "#/definitions/package-specs", "description": "ReScript can currently output to [Commonjs](https://en.wikipedia.org/wiki/CommonJS), and [ES6 modules](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import)" }, + "sourceMap": { + "$ref": "#/definitions/source-map-spec", + "description": "Configure Source Map v3 output for generated JavaScript." + }, "bs-external-includes": { "type": "array", "items": { From f7ca25e7a655b23e0d087d86783adf2eea442850 Mon Sep 17 00:00:00 2001 From: mununki Date: Wed, 29 Apr 2026 02:56:34 +0900 Subject: [PATCH 6/7] Add source map enabled policy --- docs/docson/build-schema.json | 6 +- rewatch/src/build.rs | 7 +- rewatch/src/build/build_types.rs | 8 +- rewatch/src/build/clean.rs | 4 +- rewatch/src/build/compile.rs | 4 +- rewatch/src/config.rs | 93 ++++++++++++++++++++-- rewatch/src/watcher.rs | 5 +- tests/build_tests/source_map/rescript.json | 1 + 8 files changed, 113 insertions(+), 15 deletions(-) diff --git a/docs/docson/build-schema.json b/docs/docson/build-schema.json index 9c0462822e0..266a8f07186 100644 --- a/docs/docson/build-schema.json +++ b/docs/docson/build-schema.json @@ -56,6 +56,10 @@ { "type": "object", "properties": { + "enabled": { + "enum": ["dev", "always"], + "description": "`dev` generates source maps only during watch mode. `always` generates source maps during both build and watch." + }, "mode": { "enum": ["linked"], "description": "Generate a separate .js.map file next to each generated JavaScript file and append a sourceMappingURL comment. Only linked source maps are supported for now." @@ -69,7 +73,7 @@ "description": "Optional sourceRoot value written to the generated source map." } }, - "required": ["mode"], + "required": ["enabled", "mode"], "additionalProperties": false } ] diff --git a/rewatch/src/build.rs b/rewatch/src/build.rs index 53d61de9808..4852670d2fc 100644 --- a/rewatch/src/build.rs +++ b/rewatch/src/build.rs @@ -12,6 +12,7 @@ pub mod read_compile_state; use self::parse::parser_args; use crate::build::compile::{mark_modules_with_deleted_deps_dirty, mark_modules_with_expired_deps_dirty}; use crate::build::compiler_info::{CompilerCheckResult, verify_compiler_info, write_compiler_info}; +use crate::config::SourceMapCommand; use crate::helpers::emojis::*; use crate::helpers::{self}; use crate::lock::{LockKind, drop_lock, get_lock_or_exit}; @@ -140,7 +141,8 @@ pub fn get_compiler_args(rescript_file_path: &Path) -> Result { is_type_dev, true, None, // No warn_error_override for compiler-args command - &[], // Source dirs not available outside full build; gentype falls back to defaults. + SourceMapCommand::Build, + &[], // Source dirs not available outside full build; gentype falls back to defaults. )?; let result = serde_json::to_string_pretty(&CompilerArgs { @@ -175,6 +177,7 @@ pub fn initialize_build( warn_error: Option, prod: bool, features: Option>, + source_map_command: SourceMapCommand, ) -> Result { let project_context = ProjectContext::new(path)?; let compiler = get_compiler_info(&project_context)?; @@ -195,6 +198,7 @@ pub fn initialize_build( compiler, warn_error, features, + source_map_command, ); packages::parse_packages(&mut build_state)?; @@ -609,6 +613,7 @@ pub fn build( warn_error, prod, features, + SourceMapCommand::Build, ) .with_context(|| "Could not initialize build")?; diff --git a/rewatch/src/build/build_types.rs b/rewatch/src/build/build_types.rs index 4d1f3f8bf0f..3916c6d8ef7 100644 --- a/rewatch/src/build/build_types.rs +++ b/rewatch/src/build/build_types.rs @@ -1,5 +1,5 @@ use crate::build::packages::{Namespace, Package}; -use crate::config::Config; +use crate::config::{Config, SourceMapCommand}; use crate::project_context::ProjectContext; use ahash::{AHashMap, AHashSet}; use blake3::Hash; @@ -110,6 +110,7 @@ pub struct BuildState { pub deleted_modules: AHashSet, pub compiler_info: CompilerInfo, pub deps_initialized: bool, + pub source_map_command: SourceMapCommand, } /// Extended build state that includes command-line specific overrides. @@ -151,6 +152,7 @@ impl BuildState { project_context: ProjectContext, packages: AHashMap, compiler: CompilerInfo, + source_map_command: SourceMapCommand, ) -> Self { Self { project_context, @@ -160,6 +162,7 @@ impl BuildState { deleted_modules: AHashSet::new(), compiler_info: compiler, deps_initialized: false, + source_map_command, } } @@ -181,10 +184,11 @@ impl BuildCommandState { compiler: CompilerInfo, warn_error_override: Option, features: Option>, + source_map_command: SourceMapCommand, ) -> Self { Self { root_folder, - build_state: BuildState::new(project_context, packages, compiler), + build_state: BuildState::new(project_context, packages, compiler, source_map_command), warn_error_override, features, } diff --git a/rewatch/src/build/clean.rs b/rewatch/src/build/clean.rs index 8d88f1f4bc3..a5084a63468 100644 --- a/rewatch/src/build/clean.rs +++ b/rewatch/src/build/clean.rs @@ -2,7 +2,7 @@ use super::build_types::*; use super::packages; use crate::build; use crate::build::packages::Package; -use crate::config::Config; +use crate::config::{Config, SourceMapCommand}; use crate::helpers; use crate::helpers::emojis::*; use crate::project_context::ProjectContext; @@ -373,7 +373,7 @@ pub fn clean(path: &Path, show_progress: bool, plain_output: bool, prod: bool) - } let timing_clean_mjs = Instant::now(); - let mut build_state = BuildState::new(project_context, packages, compiler_info); + let mut build_state = BuildState::new(project_context, packages, compiler_info, SourceMapCommand::Build); packages::parse_packages(&mut build_state)?; let root_config = build_state.get_root_config(); let suffix_for_print = match root_config.package_specs { diff --git a/rewatch/src/build/compile.rs b/rewatch/src/build/compile.rs index bce155ac4e6..9158820b851 100644 --- a/rewatch/src/build/compile.rs +++ b/rewatch/src/build/compile.rs @@ -652,6 +652,7 @@ pub fn compiler_args( is_local_dep: bool, // Command-line --warn-error flag override (takes precedence over rescript.json config) warn_error_override: Option, + source_map_command: config::SourceMapCommand, // Pre-expanded source directories for the current package (used by gentype). // Pass an empty slice when unavailable (e.g. the compiler-args CLI command). current_package_dirs: &[PathBuf], @@ -686,7 +687,7 @@ pub fn compiler_args( let jsx_module_args = root_config.get_jsx_module_args(); let jsx_mode_args = root_config.get_jsx_mode_args(); let jsx_preserve_args = root_config.get_jsx_preserve_args(); - let source_map_args = root_config.get_source_map_args(); + let source_map_args = root_config.get_source_map_args(source_map_command); let bsb_project_root = project_context.get_root_path(); let dep_paths: Vec<(String, PathBuf)> = if config.gentype_config.is_some() { let resolved = packages.as_ref().map(|pkgs| { @@ -919,6 +920,7 @@ fn compile_file( is_type_dev, package.is_local_dep, warn_error_override, + build_state.source_map_command, current_package_dirs, )?; diff --git a/rewatch/src/config.rs b/rewatch/src/config.rs index b96ccefcca7..c9b50541b3c 100644 --- a/rewatch/src/config.rs +++ b/rewatch/src/config.rs @@ -289,14 +289,28 @@ pub enum SourceMapConfig { Options(SourceMapOptions), } +#[derive(Deserialize, Debug, Clone, Eq, PartialEq)] +#[serde(rename_all = "camelCase")] +pub enum SourceMapEnabled { + Dev, + Always, +} + #[derive(Deserialize, Debug, Clone, Eq, PartialEq)] #[serde(rename_all = "camelCase")] pub struct SourceMapOptions { + pub enabled: SourceMapEnabled, pub mode: String, pub sources_content: Option, pub source_root: Option, } +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum SourceMapCommand { + Build, + Watch, +} + #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub enum GenTypeModule { #[serde(rename = "commonjs")] @@ -807,7 +821,7 @@ impl Config { } } - pub fn get_source_map_args(&self) -> Vec { + pub fn get_source_map_args(&self, command: SourceMapCommand) -> Vec { let mut args = Vec::new(); if let Some(source_map) = &self.source_map { @@ -819,11 +833,22 @@ impl Config { panic!("sourceMap true is unsupported; use {{ \"mode\": \"linked\" }}") } SourceMapConfig::Options(options) => { - match options.mode.as_str() { - "linked" => args.extend(["-bs-source-map".to_string(), "linked".to_string()]), + let source_map_mode = match options.mode.as_str() { + "linked" => "linked", value => panic!("sourceMap.mode value {value} is unsupported"), + }; + + let source_map_enabled = match options.enabled { + SourceMapEnabled::Dev => command == SourceMapCommand::Watch, + SourceMapEnabled::Always => true, + }; + + if !source_map_enabled { + return vec!["-bs-source-map".to_string(), "false".to_string()]; } + args.extend(["-bs-source-map".to_string(), source_map_mode.to_string()]); + if let Some(sources_content) = options.sources_content { args.extend([ "-bs-source-map-sources-content".to_string(), @@ -1645,6 +1670,7 @@ pub mod tests { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], "sourceMap": { + "enabled": "always", "mode": "linked", "sourcesContent": true, "sourceRoot": "webpack://testrepo/" @@ -1654,7 +1680,7 @@ pub mod tests { let config = serde_json::from_str::(json).unwrap(); assert_eq!( - config.get_source_map_args(), + config.get_source_map_args(SourceMapCommand::Build), vec![ "-bs-source-map", "linked", @@ -1666,6 +1692,36 @@ pub mod tests { ); } + #[test] + fn test_source_map_dev_args_only_in_watch() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": { + "enabled": "dev", + "mode": "linked", + "sourcesContent": true + } + } + "#; + + let config = serde_json::from_str::(json).unwrap(); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Build), + vec!["-bs-source-map", "false",] + ); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Watch), + vec![ + "-bs-source-map", + "linked", + "-bs-source-map-sources-content", + "true", + ] + ); + } + #[test] fn test_source_map_false_args() { let json = r#" @@ -1677,7 +1733,10 @@ pub mod tests { "#; let config = serde_json::from_str::(json).unwrap(); - assert_eq!(config.get_source_map_args(), vec!["-bs-source-map", "false",]); + assert_eq!( + config.get_source_map_args(SourceMapCommand::Build), + vec!["-bs-source-map", "false",] + ); } #[test] @@ -1692,7 +1751,26 @@ pub mod tests { "#; let config = serde_json::from_str::(json).unwrap(); - let _ = config.get_source_map_args(); + let _ = config.get_source_map_args(SourceMapCommand::Build); + } + + #[test] + fn test_source_map_requires_enabled() { + let json = r#" + { + "name": "testrepo", + "sources": [ { "dir": "src/", "subdirs": true } ], + "sourceMap": { + "mode": "linked" + } + } + "#; + + let error = serde_json::from_str::(json).unwrap_err(); + assert!( + error.to_string().contains("SourceMapConfig"), + "unexpected error: {error}" + ); } #[test] @@ -1703,13 +1781,14 @@ pub mod tests { "name": "testrepo", "sources": [ { "dir": "src/", "subdirs": true } ], "sourceMap": { + "enabled": "always", "mode": "inline" } } "#; let config = serde_json::from_str::(json).unwrap(); - let _ = config.get_source_map_args(); + let _ = config.get_source_map_args(SourceMapCommand::Build); } #[test] diff --git a/rewatch/src/watcher.rs b/rewatch/src/watcher.rs index 89c405cd0ad..fe0ca13931f 100644 --- a/rewatch/src/watcher.rs +++ b/rewatch/src/watcher.rs @@ -2,7 +2,7 @@ use crate::build; use crate::build::build_types::{BuildCommandState, SourceType}; use crate::build::clean; use crate::cmd; -use crate::config; +use crate::config::{self, SourceMapCommand}; use crate::helpers; use crate::helpers::StrippedVerbatimPath; use crate::lock::LockKind; @@ -471,6 +471,7 @@ async fn async_watch( build_state.get_warn_error_override(), prod, features.clone(), + SourceMapCommand::Watch, ) .expect("Could not initialize build"); @@ -569,6 +570,7 @@ pub fn start( warn_error.clone(), prod, features.clone(), + SourceMapCommand::Watch, ) .with_context(|| "Could not initialize build")?; @@ -669,6 +671,7 @@ mod tests { compiler, None, None, + SourceMapCommand::Watch, ); build_state.insert_module(module_name, module); build_state diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json index 2485256bb0f..ac944f2b4fa 100644 --- a/tests/build_tests/source_map/rescript.json +++ b/tests/build_tests/source_map/rescript.json @@ -2,6 +2,7 @@ "name": "source_map", "sources": ["src"], "sourceMap": { + "enabled": "always", "mode": "linked", "sourcesContent": true } From e258af09601d58e1a2e7f069891500e6a5b1a1a4 Mon Sep 17 00:00:00 2001 From: mununki Date: Wed, 29 Apr 2026 10:26:08 +0900 Subject: [PATCH 7/7] Preserve source map markers across package targets --- compiler/core/js_implementation.ml | 23 +++++++------- compiler/core/js_source_map.ml | 18 ++++++----- compiler/core/js_source_map.mli | 2 ++ tests/build_tests/source_map/input.js | 37 ++++++++++++---------- tests/build_tests/source_map/rescript.json | 12 +++++++ 5 files changed, 57 insertions(+), 35 deletions(-) diff --git a/compiler/core/js_implementation.ml b/compiler/core/js_implementation.ml index 5f4e4e6c765..a6ea25a5b1c 100644 --- a/compiler/core/js_implementation.ml +++ b/compiler/core/js_implementation.ml @@ -141,17 +141,18 @@ let after_parsing_impl ppf outputprefix (ast : Parsetree.structure) = let typedtree_coercion = (typedtree, coercion) in print_if ppf Clflags.dump_typedtree Printtyped.implementation_with_coercion typedtree_coercion; - (if !Js_config.cmi_only then Warnings.check_fatal () - else - let lambda, exports = - Translmod.transl_implementation modulename typedtree_coercion - in - let js_program = - print_if_pipe ppf Clflags.dump_rawlambda Printlambda.lambda lambda - |> Lam_compile_main.compile outputprefix exports - in - if not !Js_config.cmj_only then - Lam_compile_main.lambda_as_module js_program outputprefix); + Js_source_map.with_marker_scope (fun () -> + if !Js_config.cmi_only then Warnings.check_fatal () + else + let lambda, exports = + Translmod.transl_implementation modulename typedtree_coercion + in + let js_program = + print_if_pipe ppf Clflags.dump_rawlambda Printlambda.lambda lambda + |> Lam_compile_main.compile outputprefix exports + in + if not !Js_config.cmj_only then + Lam_compile_main.lambda_as_module js_program outputprefix); process_with_gentype (outputprefix ^ ".cmt")) let implementation ~parser ppf ?outputprefix fname = diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 4df5c623304..7b5d90d31a6 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -46,6 +46,15 @@ let comment_of_loc (loc : Location.t) = Hashtbl.replace marker_locs id loc; Some (marker_prefix ^ string_of_int id) +let with_marker_scope f = + let first_marker = !next_marker in + Ext_pervasives.finally () + ~clean:(fun () -> + for id = first_marker to !next_marker - 1 do + Hashtbl.remove marker_locs id + done) + f + let with_builder builder f = let old = !current in current := builder; @@ -182,13 +191,6 @@ let add_mapping builder ~generated_line ~generated_column (loc : Location.t) = :: builder.mappings; builder.last_generated <- Some (generated_line, generated_column) -let take_marker_loc id = - match Hashtbl.find_opt marker_locs id with - | None -> None - | Some loc -> - Hashtbl.remove marker_locs id; - Some loc - let mark_comment fmt comment = if is_prefix ~prefix:marker_prefix comment then ( let prefix_len = String.length marker_prefix in @@ -196,7 +198,7 @@ let mark_comment fmt comment = int_of_string (String.sub comment prefix_len (String.length comment - prefix_len)) in - (match (!current, take_marker_loc id) with + (match (!current, Hashtbl.find_opt marker_locs id) with | Some builder, Some loc -> let generated_line, generated_column = Ext_pp.position fmt in add_mapping builder ~generated_line ~generated_column loc diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli index adad8e3303a..ad267a5b881 100644 --- a/compiler/core/js_source_map.mli +++ b/compiler/core/js_source_map.mli @@ -3,6 +3,8 @@ type t val make : generated_file:string -> source_root:string -> sources_content:bool -> t +val with_marker_scope : (unit -> 'a) -> 'a + val with_builder : t option -> (unit -> 'a) -> 'a val comment_of_loc : Location.t -> string option diff --git a/tests/build_tests/source_map/input.js b/tests/build_tests/source_map/input.js index e0f53fa8e07..1a63ffb80f5 100644 --- a/tests/build_tests/source_map/input.js +++ b/tests/build_tests/source_map/input.js @@ -9,23 +9,28 @@ const { execBuildOrThrow, execClean } = setup(import.meta.dirname); await execBuildOrThrow(); -const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", "Demo.js"); -const mapPath = `${jsPath}.map`; +for (const filename of ["Demo.cjs", "Demo.mjs"]) { + const jsPath = path.join(import.meta.dirname, "lib", "bs", "src", filename); + const mapPath = `${jsPath}.map`; -const js = await fs.readFile(jsPath, "utf8"); -assert.match(js, /\/\/# sourceMappingURL=Demo\.js\.map/); + const js = await fs.readFile(jsPath, "utf8"); + assert.match( + js, + new RegExp(`//# sourceMappingURL=${filename.replace(".", "\\.")}\\.map`), + ); -const map = JSON.parse(await fs.readFile(mapPath, "utf8")); -assert.equal(map.version, 3); -assert.equal(map.file, "Demo.js"); -assert.ok(map.mappings.length > 0, "source map should include mappings"); -assert.ok( - map.sources.some(source => source.endsWith("Demo.res")), - `source map should include Demo.res, got ${map.sources.join(", ")}`, -); -assert.ok( - map.sourcesContent.some(content => content.includes("let add = (a, b)")), - "source map should include source contents", -); + const map = JSON.parse(await fs.readFile(mapPath, "utf8")); + assert.equal(map.version, 3); + assert.equal(map.file, filename); + assert.ok(map.mappings.length > 0, `${filename}.map should include mappings`); + assert.ok( + map.sources.some(source => source.endsWith("Demo.res")), + `${filename}.map should include Demo.res, got ${map.sources.join(", ")}`, + ); + assert.ok( + map.sourcesContent.some(content => content.includes("let add = (a, b)")), + `${filename}.map should include source contents`, + ); +} await execClean(); diff --git a/tests/build_tests/source_map/rescript.json b/tests/build_tests/source_map/rescript.json index ac944f2b4fa..f4556b02c12 100644 --- a/tests/build_tests/source_map/rescript.json +++ b/tests/build_tests/source_map/rescript.json @@ -1,6 +1,18 @@ { "name": "source_map", "sources": ["src"], + "package-specs": [ + { + "module": "commonjs", + "in-source": true, + "suffix": ".cjs" + }, + { + "module": "esmodule", + "in-source": true, + "suffix": ".mjs" + } + ], "sourceMap": { "enabled": "always", "mode": "linked",