diff --git a/CHANGELOG.md b/CHANGELOG.md index d819e75070a..41b5ee9a422 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,7 +43,7 @@ - Preserve trailing comments between the type and `=` in locally abstract value constraints (`let f: type a. t /* comment */ = value`). https://github.com/rescript-lang/rescript/pull/8575 - Enforce function arity in interface/module inclusion and type coercion. Previously a curried implementation (e.g. `int => int => int`) could satisfy an uncurried interface (`(int, int) => int`) or be coerced to it, which could miscompile calls made through the interface type. Such mismatches are now compile errors with an explanatory hint. https://github.com/rescript-lang/rescript/pull/8559 - Fix termination-analysis false positives for functions whose progress flows through un-annotated helpers: collecting the callees of a function binding was accidentally disabled in 2024 (the collection guard required a node shape that uncurried code never produces), so helpers calling `@progress` functions were no longer added to the function table. https://github.com/rescript-lang/rescript/pull/8568 -- Fix default values of optional parameters being computed at the wrong time for curried functions: in `(~x=default, y) => (~z=default, w) => ...`, `x`'s default was only computed when the *inner* function was applied. Each default is now computed when its own parameter group is applied. https://github.com/rescript-lang/rescript/pull/8568 +- Fix default values of optional parameters being computed at the wrong time for curried functions: in `(~x=default, y) => (~z=default, w) => ...`, `x`'s default was only computed when the _inner_ function was applied. Each default is now computed when its own parameter group is applied. https://github.com/rescript-lang/rescript/pull/8568 - Fix bare labeled arrow types (`~x: int => string`) getting no arity: they printed identically to their parenthesized form (`(~x: int) => string`) but did not unify with it. https://github.com/rescript-lang/rescript/pull/8563 - Fix losses of fidelity when code passes through an external PPX: the internal `@res.async` marker no longer leaks into the program, attributes on an arrow type or on an `await` expression are no longer dropped or relocated (previously this could crash the formatter), JSX elements keep their closing tag, and PPX-emitted OCaml-style `function` is desugared instead of crashing the compiler. https://github.com/rescript-lang/rescript/pull/8561 - Preserve multibyte characters when wrapping long source lines in compiler code frames. https://github.com/rescript-lang/rescript/pull/8520 @@ -60,6 +60,7 @@ #### :house: Internal +- Add genType and source map controls and output to the developer playground. https://github.com/rescript-lang/rescript/pull/8448 - Rework the object-type representation end to end: object rows are plain field chains carrying a per-field mutability state (no phantom setter members), object literals are typed directly and property access and assignment are first-class AST and Lambda nodes shared between the Lambda and JS pipelines, and dead class-system remnants (the field-presence lattice, the class-abbreviation memo on object types, method-send typing) are removed. https://github.com/rescript-lang/rescript/pull/8597 - Upgrade the development toolchain and primary CI builds to OCaml 5.5 while retaining OCaml 5.0 as the minimum supported version. https://github.com/rescript-lang/rescript/pull/8589 - Upgrade the vendored Flow parser from 0.267.0 to 0.320.0, the final release of the OCaml implementation. https://github.com/rescript-lang/rescript/pull/8588 diff --git a/compiler/core/js_source_map.ml b/compiler/core/js_source_map.ml index 487fe2dadef..a3a96df5d9a 100644 --- a/compiler/core/js_source_map.ml +++ b/compiler/core/js_source_map.ml @@ -41,6 +41,7 @@ type t = { generated_dir: string; source_root: string; sources_content: bool; + provided_source_contents: (string, string) Hashtbl.t; sources: (string, int) Hashtbl.t; mutable source_list: source list; mutable mappings: mapping list; @@ -109,20 +110,27 @@ let relative_path ~from_dir ~to_file = let parts = repeat ".." (List.length from_rest) @ to_rest in if parts = [] then Filename.basename to_file else String.concat "/" parts -let make ~generated_file ~source_root ~sources_content = +let make ~source_contents ~generated_file ~source_root ~sources_content = + let provided_source_contents = Hashtbl.create (List.length source_contents) in + source_contents + |> List.iter (fun (filename, content) -> + Hashtbl.replace provided_source_contents (absolute_path filename) content); { generated_file = Filename.basename generated_file; generated_dir = Filename.dirname generated_file; source_root; sources_content; + provided_source_contents; sources = Hashtbl.create 4; source_list = []; mappings = []; last_generated = None; } -let load_content filename = - try Some (Ext_io.load_file filename) with _ -> None +let load_content builder filename = + match Hashtbl.find_opt builder.provided_source_contents filename with + | Some content -> Some content + | None -> ( try Some (Ext_io.load_file filename) with _ -> None) let add_source builder filename = let filename = @@ -138,7 +146,7 @@ let add_source builder filename = { relative_path = relative_path ~from_dir:builder.generated_dir ~to_file:filename; - content = load_content filename; + content = load_content builder filename; } in let index = List.length builder.source_list in diff --git a/compiler/core/js_source_map.mli b/compiler/core/js_source_map.mli index 1079bff2b64..4baa7fedcef 100644 --- a/compiler/core/js_source_map.mli +++ b/compiler/core/js_source_map.mli @@ -1,7 +1,11 @@ type t val make : - generated_file:string -> source_root:string -> sources_content:bool -> t + source_contents:(string * string) list -> + generated_file:string -> + source_root:string -> + sources_content:bool -> + t val with_builder : t -> (unit -> 'a) -> 'a diff --git a/compiler/core/lam_compile_main.ml b/compiler/core/lam_compile_main.ml index e68358bcccd..8cdc0c02c74 100644 --- a/compiler/core/lam_compile_main.ml +++ b/compiler/core/lam_compile_main.ml @@ -437,7 +437,7 @@ let remove_stale_source_map ?(remove_stale_map = true) target_file = let dump_deps_program_with_source_map ?(remove_stale_map = true) ~target_file ~output_prefix module_system lambda_output chan = let builder = - Js_source_map.make ~generated_file:target_file + Js_source_map.make ~source_contents:[] ~generated_file:target_file ~source_root:!Js_config.source_map_root ~sources_content:!Js_config.source_map_sources_content in diff --git a/compiler/jsoo/dune b/compiler/jsoo/dune index af24cc17d1d..62a7d127d65 100644 --- a/compiler/jsoo/dune +++ b/compiler/jsoo/dune @@ -7,4 +7,4 @@ (= %{profile} browser)) (flags (:standard -w +a-4-9-40-42-44-45)) - (libraries core syntax ml js_of_ocaml)) + (libraries core syntax ml gentype js_of_ocaml)) diff --git a/compiler/jsoo/jsoo_playground_main.ml b/compiler/jsoo/jsoo_playground_main.ml index 2e4536b6958..b627af0c842 100644 --- a/compiler/jsoo/jsoo_playground_main.ml +++ b/compiler/jsoo/jsoo_playground_main.ml @@ -51,7 +51,8 @@ * modules in the playground. * v5: Removed .ml support. * v6: Added `config.experimental_features` and `config.jsx_preserve_mode` to the BundleConfig. - * v7: Added debug dump output APIs for developer playground tooling. + * v7: Added debug dump output APIs for developer playground tooling, + * including gentype and source map output. * *) let api_version = "7" @@ -78,6 +79,10 @@ module Bundle_config = struct mutable open_modules: string list; mutable experimental_features: string list; mutable jsx_preserve_mode: bool; + mutable gentype_enabled: bool; + mutable source_map_mode: Js_config.source_map; + mutable source_map_sources_content: bool; + mutable source_map_root: string; } let make () = @@ -88,6 +93,10 @@ module Bundle_config = struct open_modules = []; experimental_features = []; jsx_preserve_mode = false; + gentype_enabled = false; + source_map_mode = No_source_map; + source_map_sources_content = false; + source_map_root = ""; } let default_filename (lang : Lang.t) = "playground." ^ Lang.to_string lang @@ -96,6 +105,21 @@ module Bundle_config = struct match m with | Ext_module_system.Commonjs -> "commonjs" | Esmodule -> "esmodule" + + let source_map_of_string value = + match String.lowercase_ascii value with + | "linked" -> Some Js_config.Linked + | "inline" -> Some Inline + | "hidden" -> Some Hidden + | "false" | "none" | "disabled" -> Some No_source_map + | _ -> None + + let string_of_source_map mode = + match mode with + | Js_config.Linked -> "linked" + | Inline -> "inline" + | Hidden -> "hidden" + | No_source_map -> "false" end type loc_err_info = { @@ -473,6 +497,116 @@ module Compile = struct List.iter Iter.iter_structure_item structure.str_items; Js.array (!acc |> Array.of_list) + let gentype_output ~module_system ~modulename ~sourcefile structure env = + let cmt_annots = Cmt_format.Implementation structure in + let input_cmt = + { + Cmt_format.cmt_modname = modulename; + cmt_annots; + cmt_value_dependencies = []; + cmt_comments = []; + cmt_args = [||]; + cmt_sourcefile = Some sourcefile; + cmt_builddir = Sys.getcwd (); + cmt_loadpath = !Config.load_path; + cmt_source_digest = None; + cmt_initial_env = env; + cmt_imports = []; + cmt_interface_digest = None; + cmt_use_summaries = false; + cmt_extra_info = {Cmt_utils.deprecated_used = []}; + } + in + let has_gentype_annotations = + Gentype_main.cmt_check_annotations input_cmt + ~check_annotation:(fun ~loc:_ attributes -> + attributes + |> Annotation.get_attribute_payload + Annotation.tag_is_one_of_the_gentype_annotations + <> None) + in + if has_gentype_annotations then + let module_ = + match module_system with + | Ext_module_system.Commonjs -> Gentype_config.CommonJS + | Esmodule -> ESModule + in + let project_root = Sys.getcwd () in + let config = + { + Gentype_config.default with + module_; + platform_lib = "rescript"; + project_root; + bsb_project_root = project_root; + suffix = Literals.suffix_js; + } + in + let source_file = sourcefile in + let output_file_relative = + source_file |> Paths.get_output_file_relative ~config + in + let file_name = modulename |> Module_name.from_string_unsafe in + let resolver = + Module_resolver.create_lazy_resolver ~config + ~extensions:[".res"; ".shim.ts"] ~exclude_file:(fun fname -> + fname = "React.res" || fname = "ReasonReact.res") + in + let code_text = + input_cmt + |> Gentype_main.translate_cmt ~config ~output_file_relative ~resolver + |> Emit_js.emit_translation_as_string ~config ~file_name + ~output_file_relative ~resolver + ~input_cmt_translate_type_declarations: + Gentype_main.input_cmt_translate_type_declarations + in + Emit_type.file_header ~source_file:(Filename.basename source_file) + ^ "\n" ^ code_text ^ "\n" + else "No @gentype annotations found." + + let render_javascript ~module_system ~filename ~source ~source_map_mode + ~source_map_sources_content ~source_map_root lambda_output = + let buffer = Buffer.create 1000 in + let generated_file = + Filename.concat (Sys.getcwd ()) + (Filename.remove_extension (Filename.basename filename) + ^ Literals.suffix_js) + in + let source_map_builder = + match source_map_mode with + | Js_config.No_source_map -> None + | Linked | Inline | Hidden -> + Some + (Js_source_map.make + ~source_contents:[(filename, source)] + ~generated_file ~source_root:source_map_root + ~sources_content:source_map_sources_content) + in + let print_javascript () = + Js_dump_program.pp_deps_program ~output_prefix:"" module_system + lambda_output + (Ext_pp.from_buffer buffer) + in + (match source_map_builder with + | None -> print_javascript () + | Some builder -> Js_source_map.with_builder builder print_javascript); + let source_map = + match source_map_builder with + | None -> None + | Some builder -> + let json = Js_source_map.json builder in + (match source_map_mode with + | Linked -> + Buffer.add_string buffer + (Js_source_map.linked_comment ~map_file:(generated_file ^ ".map")) + | Inline -> + Buffer.add_string buffer (Js_source_map.inline_comment ~json) + | Hidden -> () + | No_source_map -> assert false); + Some json + in + (Buffer.contents buffer, source_map) + let implementation ?(include_debug_outputs = false) ~(config : Bundle_config.t) ~lang str = let { @@ -481,6 +615,10 @@ module Compile = struct open_modules; experimental_features; jsx_preserve_mode; + gentype_enabled; + source_map_mode; + source_map_sources_content; + source_map_root; } = config in @@ -502,6 +640,9 @@ module Compile = struct let types_signature = ref [] in Js_config.jsx_version := Some Js_config.Jsx_v4; Js_config.jsx_preserve := jsx_preserve_mode; + Js_config.source_map := source_map_mode; + Js_config.source_map_sources_content := source_map_sources_content; + Js_config.source_map_root := source_map_root; experimental_features |> List.iter Experimental_features.enable_from_string; (* default *) @@ -519,19 +660,18 @@ module Compile = struct let {Translmod.lambda; exports; hoisted_functions} = Translmod.transl_implementation modulename typed_tree in - let buffer = Buffer.create 1000 in - let () = - Js_dump_program.pp_deps_program ~output_prefix:"" - (* does not matter here *) module_system - (Lam_compile_main.compile "" exports hoisted_functions lambda) - (Ext_pp.from_buffer buffer) + let lambda_output = + Lam_compile_main.compile "" exports hoisted_functions lambda + in + let js_code, source_map = + render_javascript ~module_system ~filename ~source:str ~source_map_mode + ~source_map_sources_content ~source_map_root lambda_output in - let v = Buffer.contents buffer in let type_hints = collect_type_hints typed_tree in let attrs = Js.Unsafe. [| - ("js_code", inject @@ Js.string v); + ("js_code", inject @@ Js.string js_code); ( "warnings", inject @@ (!warning_infos @@ -549,6 +689,25 @@ module Compile = struct let lambda_output = Printer.to_string Printlambda.lambda lambda in let lam, _ = Lam_convert.convert lambda in let lam = Lam_print.lambda_to_string lam in + let gentype_attrs = + if gentype_enabled then + let structure, _ = typed_tree in + Js.Unsafe. + [| + ( "gentype", + inject + @@ Js.string + (gentype_output ~module_system ~modulename + ~sourcefile:filename structure env) ); + |] + else [||] + in + let source_map_attrs = + match source_map with + | None -> [||] + | Some source_map -> + Js.Unsafe.[|("source_map", inject @@ Js.string source_map)|] + in let debug_attrs = Js.Unsafe. [| @@ -558,7 +717,8 @@ module Compile = struct ("lam", inject @@ Js.string lam); |] in - Js.Unsafe.obj (Array.append attrs debug_attrs) + Js.Unsafe.obj + (Array.concat [attrs; debug_attrs; gentype_attrs; source_map_attrs]) else Js.Unsafe.obj attrs with e -> ( match e with @@ -659,6 +819,25 @@ module Export = struct config.jsx_preserve_mode <- value; true in + let set_gentype_enabled value = + config.gentype_enabled <- value; + true + in + let set_source_map_mode value = + match Bundle_config.source_map_of_string value with + | Some source_map_mode -> + config.source_map_mode <- source_map_mode; + true + | None -> false + in + let set_source_map_sources_content value = + config.source_map_sources_content <- value; + true + in + let set_source_map_root value = + config.source_map_root <- value; + true + in let convert_syntax ~(from_lang : string) ~(to_lang : string) (src : string) = let open Lang in @@ -717,6 +896,22 @@ module Export = struct inject @@ Js.wrap_meth_callback (fun _ value -> Js.bool (set_jsx_preserve_mode (Js.to_bool value))) ); + ( "setGentypeEnabled", + inject + @@ Js.wrap_meth_callback (fun _ value -> + Js.bool (set_gentype_enabled (Js.to_bool value))) ); + ( "setSourceMapMode", + inject + @@ Js.wrap_meth_callback (fun _ value -> + Js.bool (set_source_map_mode (Js.to_string value))) ); + ( "setSourceMapSourcesContent", + inject + @@ Js.wrap_meth_callback (fun _ value -> + Js.bool (set_source_map_sources_content (Js.to_bool value))) ); + ( "setSourceMapRoot", + inject + @@ Js.wrap_meth_callback (fun _ value -> + Js.bool (set_source_map_root (Js.to_string value))) ); ( "getConfig", inject @@ Js.wrap_meth_callback (fun _ -> @@ -731,6 +926,17 @@ module Export = struct ("warn_flags", inject @@ Js.string config.warn_flags); ( "jsx_preserve_mode", inject @@ (config.jsx_preserve_mode |> Js.bool) ); + ( "gentype_enabled", + inject @@ (config.gentype_enabled |> Js.bool) ); + ( "source_map_mode", + inject + @@ (config.source_map_mode + |> Bundle_config.string_of_source_map |> Js.string) ); + ( "source_map_sources_content", + inject @@ (config.source_map_sources_content |> Js.bool) + ); + ( "source_map_root", + inject @@ Js.string config.source_map_root ); ( "experimental_features", inject @@ (config.experimental_features |> Array.of_list diff --git a/packages/dev-playground/package.json b/packages/dev-playground/package.json index 2c0cd72a293..7e93f5ffee1 100644 --- a/packages/dev-playground/package.json +++ b/packages/dev-playground/package.json @@ -9,11 +9,13 @@ "prepare-pages-site": "node scripts/prepare-pages-site.mjs", "res:build": "rescript", "res:watch": "rescript -w", + "test": "rescript && node --test scripts/source-map-navigation.test.mjs", "dev": "vite --host 127.0.0.1", "build": "rescript && vite build", "preview": "vite preview --host 127.0.0.1" }, "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", "@rescript/runtime": "12.3.0", "rescript": "12.3.0", "vite": "^8.0.14", diff --git a/packages/dev-playground/scripts/source-map-navigation.test.mjs b/packages/dev-playground/scripts/source-map-navigation.test.mjs new file mode 100644 index 00000000000..1bad699bec4 --- /dev/null +++ b/packages/dev-playground/scripts/source-map-navigation.test.mjs @@ -0,0 +1,50 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { + decode, + generatedForOriginal, +} from "../src/SourceMapNavigation.res.mjs"; + +const sourceMap = JSON.stringify({ + version: 3, + file: "Playground.js", + sources: ["Playground.res"], + names: [], + mappings: "AAAA;AACA", +}); + +test("decodes source map positions", () => { + assert.deepEqual(decode(sourceMap), [ + { + generated: { line: 1, col: 0 }, + original: { + source: "Playground.res", + position: { line: 1, col: 0 }, + }, + }, + { + generated: { line: 2, col: 0 }, + original: { + source: "Playground.res", + position: { line: 2, col: 0 }, + }, + }, + ]); +}); + +test("finds the closest generated position for a source position", () => { + const mapping = generatedForOriginal(decode(sourceMap), { line: 2, col: 5 }); + + assert.deepEqual(mapping?.generated, { line: 2, col: 0 }); +}); + +test("does not carry a generated position onto an unmapped source line", () => { + const mapping = generatedForOriginal(decode(sourceMap), { line: 3, col: 0 }); + + assert.equal(mapping, undefined); +}); + +test("handles invalid source maps", () => { + assert.deepEqual(decode("not a source map"), []); +}); diff --git a/packages/dev-playground/src/Bindings.res b/packages/dev-playground/src/Bindings.res index 1b7f3f497f6..1337a7eb347 100644 --- a/packages/dev-playground/src/Bindings.res +++ b/packages/dev-playground/src/Bindings.res @@ -30,6 +30,12 @@ module Instance = { @send external setWarnFlags: (compilerInstance, string) => unit = "setWarnFlags" @send external setFilename: (compilerInstance, string) => unit = "setFilename" @send external setJsxPreserveMode: (compilerInstance, bool) => unit = "setJsxPreserveMode" + @send external setGentypeEnabled: (compilerInstance, bool) => unit = "setGentypeEnabled" + @send external setSourceMapMode: (compilerInstance, string) => unit = "setSourceMapMode" + @send + external setSourceMapSourcesContent: (compilerInstance, bool) => unit = + "setSourceMapSourcesContent" + @send external setSourceMapRoot: (compilerInstance, string) => unit = "setSourceMapRoot" @send external setExperimentalFeatures: (compilerInstance, array) => unit = "setExperimentalFeatures" @@ -51,6 +57,11 @@ module Config = { @get external jsxPreserveMode: compilerConfig => option = "jsx_preserve_mode" @get external experimentalFeatures: compilerConfig => option> = "experimental_features" + @get external gentypeEnabled: compilerConfig => option = "gentype_enabled" + @get external sourceMapMode: compilerConfig => option = "source_map_mode" + @get + external sourceMapSourcesContent: compilerConfig => option = "source_map_sources_content" + @get external sourceMapRoot: compilerConfig => option = "source_map_root" } module Diagnostic = { @@ -70,6 +81,8 @@ module CompileResult = { @get external typedtree: compileResult => option = "typedtree" @get external lambda: compileResult => option = "lambda" @get external lam: compileResult => option = "lam" + @get external gentype: compileResult => option = "gentype" + @get external sourceMap: compileResult => option = "source_map" @get external errors: compileResult => option> = "errors" @get external warnings: compileResult => option> = "warnings" @get external msg: compileResult => option = "msg" @@ -84,6 +97,13 @@ module Window = { @val external isSecureContext: bool = "window.isSecureContext" } +module WindowSelection = { + type t + + @val @scope("window") @return(nullable) external get: unit => option = "getSelection" + @get external isCollapsed: t => bool = "isCollapsed" +} + module Url = { type t @@ -133,6 +153,11 @@ module CssStyle = { @set external setLeft: (t, string) => unit = "left" } +type scrollIntoViewOptions = { + block: string, + inline: string, +} + module Element = { @send external setAttribute: (Dom.element, string, string) => unit = "setAttribute" @send @@ -142,6 +167,9 @@ module Element = { "removeEventListener" @send external appendChild: (Dom.element, Dom.element) => unit = "appendChild" @send external removeChild: (Dom.element, Dom.element) => unit = "removeChild" + @send external focus: Dom.element => unit = "focus" + @send + external scrollIntoView: (Dom.element, scrollIntoViewOptions) => unit = "scrollIntoView" @get external style: Dom.element => CssStyle.t = "style" @get @return(nullable) external getScrollHandler: Dom.element => option unit> = @@ -161,6 +189,10 @@ module ScriptElement = { module TextAreaElement = { @set external setValue: (Dom.element, string) => unit = "value" @send external select: Dom.element => unit = "select" + @send external setSelectionRange: (Dom.element, int, int) => unit = "setSelectionRange" + @get external clientHeight: Dom.element => int = "clientHeight" + @get external scrollTop: Dom.element => int = "scrollTop" + @set external setScrollTop: (Dom.element, int) => unit = "scrollTop" } module Document = { diff --git a/packages/dev-playground/src/CompilerApi.res b/packages/dev-playground/src/CompilerApi.res index 901eb314738..6b3bec25584 100644 --- a/packages/dev-playground/src/CompilerApi.res +++ b/packages/dev-playground/src/CompilerApi.res @@ -50,6 +50,10 @@ type info = { warnFlags: string, jsxPreserveMode: bool, experimentalFeatures: array, + gentypeEnabled: bool, + sourceMapMode: PlaygroundConfig.sourceMapMode, + sourceMapSourcesContent: bool, + sourceMapRoot: string, libraries: array, } @@ -59,6 +63,8 @@ type success = { typedtree: string, lambda: string, lam: string, + gentype: option, + sourceMap: option, warnings: array, time: float, } @@ -78,6 +84,10 @@ type normalizedConfig = { warnFlags: string, jsxPreserveMode: bool, experimentalFeatures: array, + gentypeEnabled: bool, + sourceMapMode: PlaygroundConfig.sourceMapMode, + sourceMapSourcesContent: bool, + sourceMapRoot: string, } let defaultWarnFlags = "+a-4-9-20-40-41-42-50-61-102-109" @@ -90,6 +100,10 @@ let defaultConfig: PlaygroundConfig.t = { warnFlags: defaultWarnFlags, jsxPreserveMode: false, experimentalFeatures: [], + gentypeEnabled: false, + sourceMapMode: Disabled, + sourceMapSourcesContent: true, + sourceMapRoot: "", } let pathFromBase = relativePath => { @@ -215,6 +229,10 @@ let applyConfig = ( ~moduleSystem: PlaygroundConfig.moduleSystem, ~warnFlags, ~jsxPreserveMode, + ~gentypeEnabled, + ~sourceMapMode: PlaygroundConfig.sourceMapMode, + ~sourceMapSourcesContent, + ~sourceMapRoot, ~experimentalFeatures: array, ) => { if hasFunction(instance, "setModuleSystem") { @@ -229,6 +247,18 @@ let applyConfig = ( if hasFunction(instance, "setJsxPreserveMode") { instance->Instance.setJsxPreserveMode(jsxPreserveMode) } + if hasFunction(instance, "setGentypeEnabled") { + instance->Instance.setGentypeEnabled(gentypeEnabled) + } + if hasFunction(instance, "setSourceMapMode") { + instance->Instance.setSourceMapMode(sourceMapMode->PlaygroundConfig.sourceMapModeCompilerValue) + } + if hasFunction(instance, "setSourceMapSourcesContent") { + instance->Instance.setSourceMapSourcesContent(sourceMapSourcesContent) + } + if hasFunction(instance, "setSourceMapRoot") { + instance->Instance.setSourceMapRoot(sourceMapRoot) + } if hasFunction(instance, "setExperimentalFeatures") { instance->Instance.setExperimentalFeatures( experimentalFeatures->Array.map(feature => (feature :> string)), @@ -253,6 +283,16 @@ let experimentalFeaturesFromConfig = configValue => | None => [] } +let sourceMapModeFromConfig = configValue => + switch configValue->Config.sourceMapMode { + | Some(sourceMapMode) => + switch sourceMapMode->PlaygroundConfig.parseSourceMapMode { + | Some(sourceMapMode) => sourceMapMode + | None => defaultConfig.sourceMapMode + } + | None => defaultConfig.sourceMapMode + } + let normalizeConfig = (configValue: option): normalizedConfig => switch configValue { | None => { @@ -260,6 +300,10 @@ let normalizeConfig = (configValue: option): normalizedConfig => warnFlags: defaultConfig.warnFlags, jsxPreserveMode: false, experimentalFeatures: [], + gentypeEnabled: defaultConfig.gentypeEnabled, + sourceMapMode: defaultConfig.sourceMapMode, + sourceMapSourcesContent: defaultConfig.sourceMapSourcesContent, + sourceMapRoot: defaultConfig.sourceMapRoot, } | Some(configValue) => { moduleSystem: configValue->moduleSystemFromConfig, @@ -272,6 +316,19 @@ let normalizeConfig = (configValue: option): normalizedConfig => | None => false }, experimentalFeatures: configValue->experimentalFeaturesFromConfig, + gentypeEnabled: switch configValue->Config.gentypeEnabled { + | Some(gentypeEnabled) => gentypeEnabled + | None => defaultConfig.gentypeEnabled + }, + sourceMapMode: configValue->sourceMapModeFromConfig, + sourceMapSourcesContent: switch configValue->Config.sourceMapSourcesContent { + | Some(sourceMapSourcesContent) => sourceMapSourcesContent + | None => defaultConfig.sourceMapSourcesContent + }, + sourceMapRoot: switch configValue->Config.sourceMapRoot { + | Some(sourceMapRoot) => sourceMapRoot + | None => defaultConfig.sourceMapRoot + }, } } @@ -359,7 +416,10 @@ let normalize = (compileOutput, elapsedMs): compileResult => { | None => "" } - Ok({jsCode, parsetree, typedtree, lambda, lam, warnings, time: elapsedMs}) + let gentype = compileOutput->CompileResult.gentype + let sourceMap = compileOutput->CompileResult.sourceMap + + Ok({jsCode, parsetree, typedtree, lambda, lam, gentype, sourceMap, warnings, time: elapsedMs}) | _ => Error(failureFromCompileOutput(compileOutput, elapsedMs)) } @@ -440,6 +500,10 @@ let ensureCompiler = async version => { ~moduleSystem=Esmodule, ~warnFlags=defaultConfig.warnFlags, ~jsxPreserveMode=false, + ~gentypeEnabled=defaultConfig.gentypeEnabled, + ~sourceMapMode=defaultConfig.sourceMapMode, + ~sourceMapSourcesContent=defaultConfig.sourceMapSourcesContent, + ~sourceMapRoot=defaultConfig.sourceMapRoot, ~experimentalFeatures=[], ) @@ -492,6 +556,10 @@ let init = async version => { warnFlags: config.warnFlags, jsxPreserveMode: config.jsxPreserveMode, experimentalFeatures: config.experimentalFeatures, + gentypeEnabled: config.gentypeEnabled, + sourceMapMode: config.sourceMapMode, + sourceMapSourcesContent: config.sourceMapSourcesContent, + sourceMapRoot: config.sourceMapRoot, libraries, } } @@ -505,6 +573,10 @@ let compile = async (source, config: PlaygroundConfig.t) => { ~moduleSystem=config.moduleSystem, ~warnFlags=config.warnFlags, ~jsxPreserveMode=config.jsxPreserveMode, + ~gentypeEnabled=config.gentypeEnabled, + ~sourceMapMode=config.sourceMapMode, + ~sourceMapSourcesContent=config.sourceMapSourcesContent, + ~sourceMapRoot=config.sourceMapRoot, ~experimentalFeatures=config.experimentalFeatures, ) @@ -529,6 +601,10 @@ let format = async (source, config: PlaygroundConfig.t) => { ~moduleSystem=config.moduleSystem, ~warnFlags=config.warnFlags, ~jsxPreserveMode=config.jsxPreserveMode, + ~gentypeEnabled=config.gentypeEnabled, + ~sourceMapMode=config.sourceMapMode, + ~sourceMapSourcesContent=config.sourceMapSourcesContent, + ~sourceMapRoot=config.sourceMapRoot, ~experimentalFeatures=config.experimentalFeatures, ) diff --git a/packages/dev-playground/src/CompilerApi.resi b/packages/dev-playground/src/CompilerApi.resi index 45c4c7c540b..6271924b2c5 100644 --- a/packages/dev-playground/src/CompilerApi.resi +++ b/packages/dev-playground/src/CompilerApi.resi @@ -13,6 +13,10 @@ type info = { warnFlags: string, jsxPreserveMode: bool, experimentalFeatures: array, + gentypeEnabled: bool, + sourceMapMode: PlaygroundConfig.sourceMapMode, + sourceMapSourcesContent: bool, + sourceMapRoot: string, libraries: array, } @@ -22,6 +26,8 @@ type success = { typedtree: string, lambda: string, lam: string, + gentype: option, + sourceMap: option, warnings: array, time: float, } diff --git a/packages/dev-playground/src/Main.res b/packages/dev-playground/src/Main.res index 53e3c8597a3..d3395e522be 100644 --- a/packages/dev-playground/src/Main.res +++ b/packages/dev-playground/src/Main.res @@ -6,6 +6,8 @@ type tab = | Lambda | Lam | JavaScript + | GenType + | SourceMap | Settings type compilerStatus = @@ -19,8 +21,23 @@ type sourcePosition = { col: int, } -let tabs: array = [Parsetree, Typedtree, Lambda, Lam, JavaScript, Settings] +let baseTabs: array = [Parsetree, Typedtree, Lambda, Lam, JavaScript] let moduleSystems: array = [Esmodule, Commonjs] +let sourceMapModes: array = [Disabled, Linked, Inline, Hidden] + +let tabIsVisible = (config: PlaygroundConfig.t, tab) => + switch tab { + | GenType => config.gentypeEnabled + | SourceMap => config.sourceMapMode !== Disabled + | Parsetree | Typedtree | Lambda | Lam | JavaScript | Settings => true + } + +let tabsForConfig = (config: PlaygroundConfig.t) => { + let withGentype = config.gentypeEnabled ? Array.concat(baseTabs, [GenType]) : baseTabs + let withSourceMap = + config.sourceMapMode !== Disabled ? Array.concat(withGentype, [SourceMap]) : withGentype + Array.concat(withSourceMap, [Settings]) +} let defaultSource = `type person = { name: string, @@ -43,6 +60,8 @@ let tabLabel = tab => | Lambda => "lambda" | Lam => "lam" | JavaScript => "js" + | GenType => "gentype" + | SourceMap => "source map" | Settings => "settings" } @@ -127,6 +146,19 @@ let cursorPositionForOffset = (source, offset): sourcePosition => { walk(0, 1, 0) } +let keyMovesCursor = key => + switch key { + | "ArrowDown" + | "ArrowLeft" + | "ArrowRight" + | "ArrowUp" + | "End" + | "Home" + | "PageDown" + | "PageUp" => true + | _ => false + } + let editorShellStyle = (activeLine, scrollTop, scrollLeft) => { let activeLineIndex = activeLine <= 1 ? 0 : activeLine - 1 let activeLineTop = 18 + activeLineIndex * 22 - scrollTop @@ -141,6 +173,42 @@ let toggleFeature = (features: array, feature: experimental ? features->Array.filter(item => item !== feature) : Array.concat(features, [feature]) +let optionalOutput = (output, fallback) => + switch output { + | Some(output) => output + | None => fallback + } + +let prettyPrintJson = value => + try value->JSON.parseOrThrow->JSON.stringify(~space=2) catch { + | _ => value + } + +let sourceMapDirective = "//# sourceMappingURL=" + +let offsetForPosition = (source, position: SourceMapNavigation.position) => { + let index = ref(0) + let line = ref(1) + let col = ref(0) + let length = source->String.length + + while ( + index.contents < length && + (line.contents < position.line || + (line.contents === position.line && col.contents < position.col)) + ) { + if source->String.charAt(index.contents) === "\n" { + line := line.contents + 1 + col := 0 + } else { + col := col.contents + 1 + } + index := index.contents + 1 + } + + index.contents +} + let selectedOutput = (result: option, activeTab: tab) => switch result { | None => "The compiler is loading. Results will appear here after the first compile." @@ -154,10 +222,168 @@ let selectedOutput = (result: option, activeTab: tab) | Lambda => result.lambda | Lam => result.lam | JavaScript => result.jsCode + | GenType => + optionalOutput(result.gentype, "This compiler bundle does not expose gentype output yet.") + | SourceMap => + result.sourceMap + ->optionalOutput("This compiler bundle does not expose source map output yet.") + ->prettyPrintJson | Settings => "" } } +let outputNode = (output, activeTab, onSourceMapSelect): View.node => { + let directiveIndex = output->String.indexOf(sourceMapDirective) + if activeTab !== JavaScript || directiveIndex < 0 { + View.text(output) + } else { + let directiveEnd = directiveIndex + sourceMapDirective->String.length + View.fragment([ + View.text(output->String.slice(~start=0, ~end=directiveIndex)), + { + event->Event.preventDefault + onSourceMapSelect() + }} + > + {View.text(sourceMapDirective)} + , + View.text(output->String.slice(~start=directiveEnd)), + ]) + } +} + +let pushOutputText = (nodes: array, text, onSourceMapSelect) => { + let directiveIndex = text->String.indexOf(sourceMapDirective) + if directiveIndex < 0 { + nodes->Array.push(View.text(text)) + } else { + let directiveEnd = directiveIndex + sourceMapDirective->String.length + nodes->Array.push(View.text(text->String.slice(~start=0, ~end=directiveIndex))) + nodes->Array.push( + { + event->Event.preventDefault + onSourceMapSelect() + }} + > + {View.text(sourceMapDirective)} + , + ) + nodes->Array.push(View.text(text->String.slice(~start=directiveEnd))) + } +} + +let mappedJavaScriptNode = ( + output, + mappings: array, + selectedPosition: option, + onMappingSelect, + onSourceMapSelect, +): View.node => { + let nodes: array = [] + let lines = output->String.split("\n") + lines->Array.forEachWithIndex((lineText, lineIndex) => { + let lineNumber = lineIndex + 1 + let lineLength = lineText->String.length + let lineMappings = mappings->Array.filter(mapping => mapping.generated.line === lineNumber) + let cursor = ref(0) + + lineMappings->Array.forEachWithIndex((mapping, mappingIndex) => { + let start = Math.Int.max(0, Math.Int.min(mapping.generated.col, lineLength)) + if start > cursor.contents { + pushOutputText( + nodes, + lineText->String.slice(~start=cursor.contents, ~end=start), + onSourceMapSelect, + ) + } + + let nextColumn = switch lineMappings->Array.get(mappingIndex + 1) { + | Some(nextMapping) => nextMapping.generated.col + | None => lineLength + } + let end_ = Math.Int.max(start, Math.Int.min(nextColumn, lineLength)) + if end_ > start { + let text = lineText->String.slice(~start, ~end=end_) + switch mapping.original { + | Some(original) => { + let isSelected = switch selectedPosition { + | Some(position) => + position.line === mapping.generated.line && position.col === mapping.generated.col + | None => false + } + let className = isSelected + ? "source-map-mapped-segment source-map-mapped-segment-active" + : "source-map-mapped-segment" + let title = `${original.source}:${original.position.line->Int.toString}:${(original.position.col + 1) + ->Int.toString} — click to reveal in source` + nodes->Array.push( + { + let shouldNavigate = switch WindowSelection.get() { + | Some(selection) => selection->WindowSelection.isCollapsed + | None => true + } + if shouldNavigate { + onMappingSelect(mapping) + } + }} + > + {View.text(text)} + , + ) + } + | None => pushOutputText(nodes, text, onSourceMapSelect) + } + } + cursor := Math.Int.max(cursor.contents, end_) + }) + + if cursor.contents < lineLength { + pushOutputText(nodes, lineText->String.slice(~start=cursor.contents), onSourceMapSelect) + } + if lineIndex < lines->Array.length - 1 { + nodes->Array.push(View.text("\n")) + } + }) + View.fragment(nodes) +} + +let interactiveOutputNode = ( + result: option, + activeTab, + selectedPosition, + onMappingSelect, + onSourceMapSelect, +) => { + let output = selectedOutput(result, activeTab) + switch (result, activeTab) { + | (Some(Ok({sourceMap: Some(sourceMap)})), JavaScript) => { + let mappings = SourceMapNavigation.decode(sourceMap) + mappings->Array.length > 0 + ? mappedJavaScriptNode( + output, + mappings, + selectedPosition, + onMappingSelect, + onSourceMapSelect, + ) + : outputNode(output, activeTab, onSourceMapSelect) + } + | _ => outputNode(output, activeTab, onSourceMapSelect) + } +} + let resultSummary = (result: option) => switch result { | None => "No compile result yet" @@ -211,7 +437,20 @@ module SettingsPanel = { ~scheduleCompile: unit => unit, ~scheduleUrlSync: unit => unit, ) => { - let updateConfig = f => Signal.update(config, f) + let updateConfig = f => { + let nextConfig = f(Signal.peek(config)) + Signal.set(config, nextConfig) + if !tabIsVisible(nextConfig, Signal.peek(activeTab)) { + Signal.set(activeTab, JavaScript) + } + } + let compilerVersionOptions: Signal.t> = Obj.magic( + Computed.make(() => + CompilerApi.selectableCompilerVersions( + Signal.get(config).compilerVersion, + )->Array.map(version => ) + ), + )
@@ -230,11 +469,7 @@ module SettingsPanel = { switchCompiler(nextVersion) }} > - {View.fragment( - CompilerApi.selectableCompilerVersions( - Signal.get(config).compilerVersion, - )->Array.map(version => ), - )} + {View.signalFragment(compilerVersionOptions)}
@@ -307,6 +542,85 @@ module SettingsPanel = { />
+
+ Signal.get(config).gentypeEnabled} + onChange={event => { + updateConfig(config => {...config, gentypeEnabled: Event.checked(event)}) + scheduleUrlSync() + compileNow() + }} + /> + +
+
+
{View.text("Source Map")}
+
+
+ + +
+
+ Signal.get(config).sourceMapMode === Disabled + ? "source-map-options source-map-options-disabled" + : "source-map-options"} + > + +
+ + Signal.get(config).sourceMapMode === Disabled} + value={() => Signal.get(config).sourceMapRoot} + spellcheck=false + onInput={event => { + updateConfig(config => {...config, sourceMapRoot: Event.value(event)}) + scheduleUrlSync() + scheduleCompile() + }} + /> +
+
+
+
{ let source = Signal.make(defaultSource) let activeTab = Signal.make(JavaScript) + let mappedSourcePosition: Signal.t> = Signal.make(None) + let mappedGeneratedPosition: Signal.t> = Signal.make(None) let status = Signal.make(Loading) let compilerInfo: Signal.t> = Signal.make(None) let compileResult: Signal.t> = Signal.make(None) let config = Signal.make(CompilerApi.defaultConfig) + let visibleTabNodes: Signal.t> = Obj.magic( + Computed.make(() => + tabsForConfig(Signal.get(config))->Array.map(tab => + Signal.set(activeTab, tab)} /> + ) + ), + ) let activeLine = Signal.make(1) let editorScrollTop = Signal.make(0) let editorScrollLeft = Signal.make(0) @@ -382,6 +705,11 @@ module App = { let compileSequence = ref(0) let shareToast: Signal.t> = Signal.make(None) + let clearMappedPositions = () => { + Signal.set(mappedSourcePosition, None) + Signal.set(mappedGeneratedPosition, None) + } + let syncEditorState = event => { let currentSource = Event.value(event) let cursorPosition = cursorPositionForOffset(currentSource, Event.selectionStart(event)) @@ -396,6 +724,67 @@ module App = { Signal.set(editorScrollLeft, Event.scrollLeft(event)) } + let scrollToGeneratedMapping = () => + Window.requestAnimationFrame(() => + switch Document.current->Document.getElementById("generated-map-selection") { + | Some(element) => element->Element.scrollIntoView({block: "center", inline: "nearest"}) + | None => () + } + ) + + let revealOriginalMapping = (mapping: SourceMapNavigation.mapping) => + switch mapping.original { + | Some(original) => { + Signal.set(mappedSourcePosition, Some(original.position)) + Signal.set(mappedGeneratedPosition, Some(mapping.generated)) + Signal.set(activeLine, original.position.line) + Window.requestAnimationFrame(() => + switch Document.current->Document.getElementById("source-editor") { + | Some(editor) => { + let offset = offsetForPosition(Signal.peek(source), original.position) + editor->TextAreaElement.setSelectionRange(offset, offset) + editor->Element.focus + Signal.set(activeLine, original.position.line) + let scrollTop = Math.Int.max( + 0, + 18 + (original.position.line - 1) * 22 - editor->TextAreaElement.clientHeight / 2, + ) + editor->TextAreaElement.setScrollTop(scrollTop) + Signal.set(editorScrollTop, editor->TextAreaElement.scrollTop) + } + | None => () + } + ) + } + | None => () + } + + let navigateFromSource = event => { + syncEditorState(event) + let position = cursorPositionForOffset(Event.value(event), Event.selectionStart(event)) + switch Signal.peek(compileResult) { + | Some(Ok({sourceMap: Some(sourceMap)})) => { + let mappings = SourceMapNavigation.decode(sourceMap) + switch SourceMapNavigation.generatedForOriginal( + mappings, + { + line: position.line, + col: position.col, + }, + ) { + | Some(mapping) => { + Signal.set(mappedSourcePosition, Some({line: position.line, col: position.col})) + Signal.set(mappedGeneratedPosition, Some(mapping.generated)) + Signal.set(activeTab, JavaScript) + scrollToGeneratedMapping() + } + | None => clearMappedPositions() + } + } + | _ => clearMappedPositions() + } + } + let compileNow = () => { compileSequence := compileSequence.contents + 1 let sequence = compileSequence.contents @@ -409,6 +798,7 @@ module App = { try { let result = await CompilerApi.compile(Signal.peek(source), Signal.peek(config)) if sequence === compileSequence.contents { + clearMappedPositions() Signal.set(compileResult, Some(result)) Signal.set(status, Ready) } @@ -464,6 +854,7 @@ module App = { if sequence === compileSequence.contents { if Signal.peek(source) === sourceBeforeFormat { Signal.set(source, formattedSource) + clearMappedPositions() Signal.set(activeLine, 1) Signal.set(editorScrollTop, 0) Signal.set(editorScrollLeft, 0) @@ -542,10 +933,17 @@ module App = { warnFlags: info.warnFlags, jsxPreserveMode: info.jsxPreserveMode, experimentalFeatures: info.experimentalFeatures, + gentypeEnabled: info.gentypeEnabled, + sourceMapMode: info.sourceMapMode, + sourceMapSourcesContent: info.sourceMapSourcesContent, + sourceMapRoot: info.sourceMapRoot, } } Signal.set(compilerInfo, Some(info)) Signal.set(config, nextConfig) + if !tabIsVisible(nextConfig, Signal.peek(activeTab)) { + Signal.set(activeTab, JavaScript) + } Signal.set(status, Ready) switch firstLoadConfigValue { | Some(_) => () @@ -612,6 +1010,7 @@ module App = { class="secondary-action" onClick={_ => { Signal.set(source, defaultSource) + clearMappedPositions() Signal.set(activeLine, 1) Signal.set(editorScrollTop, 0) Signal.set(editorScrollLeft, 0) @@ -627,7 +1026,11 @@ module App = {
+ switch Signal.get(mappedSourcePosition) { + | Some(_) => "editor-shell source-map-source-active" + | None => "editor-shell" + }} style={() => editorShellStyle( Signal.get(activeLine), @@ -651,13 +1054,20 @@ module App = { spellcheck=false onInput={event => { Signal.set(source, Event.value(event)) + clearMappedPositions() syncEditorState(event) scheduleUrlSync() scheduleCompile() }} - onClick={syncEditorState} + onClick={navigateFromSource} onMouseUp={syncEditorState} - onKeyUp={syncEditorState} + onKeyUp={event => { + if event->Event.key->keyMovesCursor { + navigateFromSource(event) + } else { + syncEditorState(event) + } + }} onFocus={syncEditorState} onKeyDown={event => switch insertTabIndent(event) { @@ -672,13 +1082,7 @@ module App = {
-
- {View.fragment( - tabs->Array.map(tab => - Signal.set(activeTab, tab)} /> - ), - )} -
+
{View.signalFragment(visibleTabNodes)}
Signal.get(activeTab) === Settings ? "output-panel hidden-panel" : "output-panel"} @@ -688,9 +1092,16 @@ module App = {
-                {View.signalText(() =>
-                  selectedOutput(Signal.get(compileResult), Signal.get(activeTab))
-                )}
+                {View.tracked(() => {
+                  let selectedTab = Signal.get(activeTab)
+                  interactiveOutputNode(
+                    Signal.get(compileResult),
+                    selectedTab,
+                    Signal.get(mappedGeneratedPosition),
+                    revealOriginalMapping,
+                    () => Signal.set(activeTab, SourceMap),
+                  )
+                })}
               
diff --git a/packages/dev-playground/src/PlaygroundConfig.res b/packages/dev-playground/src/PlaygroundConfig.res index c12959c3687..750c4f0c8c6 100644 --- a/packages/dev-playground/src/PlaygroundConfig.res +++ b/packages/dev-playground/src/PlaygroundConfig.res @@ -17,10 +17,37 @@ let parseExperimentalFeature = value => | _ => None } +type sourceMapMode = + | @as("disabled") Disabled + | @as("linked") Linked + | @as("inline") Inline + | @as("hidden") Hidden + +let parseSourceMapMode = value => + switch value { + | "disabled" | "false" | "none" => Some(Disabled) + | "linked" => Some(Linked) + | "inline" => Some(Inline) + | "hidden" => Some(Hidden) + | _ => None + } + +let sourceMapModeCompilerValue = mode => + switch mode { + | Disabled => "false" + | Linked => "linked" + | Inline => "inline" + | Hidden => "hidden" + } + type t = { compilerVersion: string, moduleSystem: moduleSystem, warnFlags: string, jsxPreserveMode: bool, experimentalFeatures: array, + gentypeEnabled: bool, + sourceMapMode: sourceMapMode, + sourceMapSourcesContent: bool, + sourceMapRoot: string, } diff --git a/packages/dev-playground/src/SourceMapNavigation.res b/packages/dev-playground/src/SourceMapNavigation.res new file mode 100644 index 00000000000..74d467da974 --- /dev/null +++ b/packages/dev-playground/src/SourceMapNavigation.res @@ -0,0 +1,86 @@ +type traceMap +type rawMapping + +type position = { + line: int, + col: int, +} + +type originalPosition = { + source: string, + position: position, +} + +type mapping = { + generated: position, + original: option, +} + +@new @module("@jridgewell/trace-mapping") +external makeTraceMap: string => traceMap = "TraceMap" + +@module("@jridgewell/trace-mapping") +external eachMapping: (traceMap, rawMapping => unit) => unit = "eachMapping" + +module RawMapping = { + @get external generatedLine: rawMapping => int = "generatedLine" + @get external generatedColumn: rawMapping => int = "generatedColumn" + @get @return(nullable) external source: rawMapping => option = "source" + @get @return(nullable) external originalLine: rawMapping => option = "originalLine" + @get @return(nullable) external originalColumn: rawMapping => option = "originalColumn" +} + +let decode = sourceMap => { + let mappings: array = [] + try { + sourceMap + ->makeTraceMap + ->eachMapping(rawMapping => { + let original = switch ( + rawMapping->RawMapping.source, + rawMapping->RawMapping.originalLine, + rawMapping->RawMapping.originalColumn, + ) { + | (Some(source), Some(line), Some(col)) => Some({source, position: {line, col}}) + | _ => None + } + mappings->Array.push({ + generated: { + line: rawMapping->RawMapping.generatedLine, + col: rawMapping->RawMapping.generatedColumn, + }, + original, + }) + }) + mappings + } catch { + | _ => [] + } +} + +let distance = (left: position, right: position) => { + let lineDistance = left.line - right.line + let colDistance = left.col - right.col + let lineDistance = lineDistance < 0 ? -lineDistance : lineDistance + let colDistance = colDistance < 0 ? -colDistance : colDistance + lineDistance * 1000000 + colDistance +} + +let generatedForOriginal = (mappings, position) => { + let closest: ref> = ref(None) + mappings->Array.forEach(mapping => + switch mapping.original { + | Some(original) if original.position.line === position.line => { + let nextDistance = distance(original.position, position) + switch closest.contents { + | None => closest := Some((nextDistance, mapping)) + | Some((currentDistance, _)) if nextDistance < currentDistance => + closest := Some((nextDistance, mapping)) + | Some(_) => () + } + } + | Some(_) | None => () + } + ) + closest.contents->Option.map(((_, mapping)) => mapping) +} diff --git a/packages/dev-playground/src/UrlState.res b/packages/dev-playground/src/UrlState.res index 39a9fbeb2eb..68e234a16b0 100644 --- a/packages/dev-playground/src/UrlState.res +++ b/packages/dev-playground/src/UrlState.res @@ -30,6 +30,30 @@ let applyUrlState = (~encoded, ~config: PlaygroundConfig.t) => { params->UrlSearchParams.delete("experimental") } + if config.gentypeEnabled { + params->UrlSearchParams.set("gentype", "true") + } else { + params->UrlSearchParams.delete("gentype") + } + + switch config.sourceMapMode { + | Disabled => + params->UrlSearchParams.delete("sourceMap") + params->UrlSearchParams.delete("sourceMapSourcesContent") + params->UrlSearchParams.delete("sourceMapRoot") + | sourceMapMode => + params->UrlSearchParams.set("sourceMap", (sourceMapMode :> string)) + params->UrlSearchParams.set( + "sourceMapSourcesContent", + config.sourceMapSourcesContent ? "true" : "false", + ) + if config.sourceMapRoot === "" { + params->UrlSearchParams.delete("sourceMapRoot") + } else { + params->UrlSearchParams.set("sourceMapRoot", config.sourceMapRoot) + } + } + let query = params->UrlSearchParams.toString let nextUrl = Location.pathname ++ (query === "" ? "" : "?" ++ query) ++ Location.hash History.replaceState(nextUrl) @@ -85,6 +109,29 @@ let queryExperimentalFeatures = defaultExperimentalFeatures => | _ => defaultExperimentalFeatures } +let querySourceMapMode = defaultValue => + switch getParam("sourceMap") { + | Some(value) => + switch value->PlaygroundConfig.parseSourceMapMode { + | Some(sourceMapMode) => sourceMapMode + | None => defaultValue + } + | None => defaultValue + } + +let queryBool = (~name, ~defaultValue) => + switch getParam(name) { + | Some(value) if value === "true" || value === "1" => true + | Some(value) if value === "false" || value === "0" => false + | _ => defaultValue + } + +let querySourceMapRoot = defaultValue => + switch getParam("sourceMapRoot") { + | Some(value) => value + | None => defaultValue + } + let queryConfig = (~defaultConfig: PlaygroundConfig.t) => { let requestedCompilerVersion = queryCompilerVersion(defaultConfig.compilerVersion) let compilerVersion = @@ -98,6 +145,13 @@ let queryConfig = (~defaultConfig: PlaygroundConfig.t) => { warnFlags: queryWarnFlags(defaultConfig.warnFlags), jsxPreserveMode: queryJsxPreserveMode(defaultConfig.jsxPreserveMode), experimentalFeatures: queryExperimentalFeatures(defaultConfig.experimentalFeatures), + gentypeEnabled: queryBool(~name="gentype", ~defaultValue=defaultConfig.gentypeEnabled), + sourceMapMode: querySourceMapMode(defaultConfig.sourceMapMode), + sourceMapSourcesContent: queryBool( + ~name="sourceMapSourcesContent", + ~defaultValue=defaultConfig.sourceMapSourcesContent, + ), + sourceMapRoot: querySourceMapRoot(defaultConfig.sourceMapRoot), } } diff --git a/packages/dev-playground/src/styles.css b/packages/dev-playground/src/styles.css index 3a4069f246b..fd04a9609bf 100644 --- a/packages/dev-playground/src/styles.css +++ b/packages/dev-playground/src/styles.css @@ -226,6 +226,11 @@ button { pointer-events: none; } +.source-map-source-active .active-line { + background: rgba(221, 140, 27, 0.12); + border-color: rgba(221, 140, 27, 0.24); +} + .line-number-gutter { grid-column: 1; grid-row: 1; @@ -426,6 +431,34 @@ button { background: var(--playground-panel); } +.source-map-output-link { + color: var(--ocean-dark); + text-decoration-line: underline; + text-decoration-style: dotted; + text-underline-offset: 3px; +} + +.source-map-output-link:hover, +.source-map-output-link:focus-visible { + color: var(--white); + text-decoration-style: solid; +} + +.source-map-mapped-segment { + border-radius: 2px; + cursor: pointer; + transition: background-color 100ms ease; +} + +.source-map-mapped-segment:hover { + background: rgba(138, 174, 200, 0.16); +} + +.source-map-mapped-segment-active { + background: rgba(221, 140, 27, 0.18); + box-shadow: inset 0 -1px var(--orange); +} + .problems { min-height: 0; display: grid; @@ -487,6 +520,47 @@ button { overflow-wrap: anywhere; } +.source-map-settings { + align-items: start; +} + +.source-map-controls, +.source-map-options { + display: grid; + gap: 12px; +} + +.source-map-control { + display: grid; + grid-template-columns: minmax(100px, 140px) minmax(0, 1fr); + align-items: center; + gap: 12px; +} + +.source-map-control > label { + color: var(--playground-text-secondary); + font-size: 13px; +} + +.source-map-options { + margin-left: 12px; + padding: 4px 0 4px 18px; + border-left: 2px solid var(--playground-border-soft); + transition: opacity 120ms ease; +} + +.source-map-options-disabled { + opacity: 0.45; +} + +.source-map-checkbox { + display: flex; + align-items: center; + gap: 10px; + color: var(--playground-text-primary); + font-size: 13px; +} + select, input { min-height: 34px; @@ -499,6 +573,15 @@ input { outline: 0; } +select { + appearance: none; + padding-right: 40px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='8' viewBox='0 0 14 8' fill='none'%3E%3Cpath d='M1 1l6 6 6-6' stroke='%23edf0f2' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E"); + background-position: right 14px center; + background-repeat: no-repeat; + background-size: 14px 8px; +} + select:focus, input:focus, textarea:focus { @@ -628,6 +711,10 @@ input[type="checkbox"] { grid-template-columns: 1fr; } + .source-map-control { + grid-template-columns: 1fr; + } + .toast { right: 16px; bottom: 16px; diff --git a/packages/playground/playground_test.cjs b/packages/playground/playground_test.cjs index 0aa38bea263..82699a223b0 100644 --- a/packages/playground/playground_test.cjs +++ b/packages/playground/playground_test.cjs @@ -1,5 +1,6 @@ // Playground bundle is UMD module // It uses `module.exports` in current context, or fallback to `globalThis` +const assert = require("node:assert/strict"); const { rescript_compiler } = require("./compiler.js"); require("./packages/compiler-builtins/cmij.js"); @@ -76,3 +77,71 @@ if (result.js_code !== "") { console.log(result.js_code); console.log("-- Playground test complete --"); } + +compiler.setFilename("Playground.res"); +assert.equal(compiler.setGentypeEnabled(true), true); +const gentypeResult = compiler.rescript.compileWithDebug("@genType let answer = 42\n"); +assert.equal(gentypeResult.type, "success"); +assert.match(gentypeResult.gentype, /require\(['"]\.\/Playground\.js['"]\)/); +assert.doesNotMatch(gentypeResult.gentype, /Playground\.bs\.js/); +assert.equal(compiler.setGentypeEnabled(false), true); + +console.log("-- Playground gentype suffix test complete --"); + +const sourceMapSource = `let double = value => value * 2 +let result = double(21) +`; + +assert.equal(compiler.setSourceMapMode("linked"), true); +assert.equal(compiler.setSourceMapSourcesContent(true), true); +assert.equal(compiler.setSourceMapRoot("rescript://playground/"), true); +assert.deepEqual( + { + mode: compiler.getConfig().source_map_mode, + sourcesContent: compiler.getConfig().source_map_sources_content, + sourceRoot: compiler.getConfig().source_map_root, + }, + { + mode: "linked", + sourcesContent: true, + sourceRoot: "rescript://playground/", + }, +); + +const linkedResult = compiler.rescript.compileWithDebug(sourceMapSource); +assert.equal(linkedResult.type, "success"); +assert.match(linkedResult.js_code, /\/\/# sourceMappingURL=Playground\.js\.map\n$/); + +const sourceMap = JSON.parse(linkedResult.source_map); +assert.equal(sourceMap.version, 3); +assert.equal(sourceMap.file, "Playground.js"); +assert.equal(sourceMap.sourceRoot, "rescript://playground/"); +assert.ok(sourceMap.sources.some(source => source.endsWith("Playground.res"))); +assert.ok(sourceMap.sourcesContent.includes(sourceMapSource)); +assert.ok(sourceMap.mappings.length > 0); + +assert.equal(compiler.setSourceMapMode("inline"), true); +const inlineResult = compiler.rescript.compileWithDebug(sourceMapSource); +assert.equal(inlineResult.type, "success"); +assert.match( + inlineResult.js_code, + /\/\/# sourceMappingURL=data:application\/json;base64,[A-Za-z0-9+/=]+\n$/, +); +assert.deepEqual(JSON.parse(inlineResult.source_map), sourceMap); + +assert.equal(compiler.setSourceMapMode("hidden"), true); +const hiddenResult = compiler.rescript.compileWithDebug(sourceMapSource); +assert.equal(hiddenResult.type, "success"); +assert.doesNotMatch(hiddenResult.js_code, /\/\/# sourceMappingURL=/); +assert.deepEqual(JSON.parse(hiddenResult.source_map), sourceMap); + +assert.equal(compiler.setSourceMapMode("unsupported"), false); +assert.equal(compiler.getConfig().source_map_mode, "hidden"); + +assert.equal(compiler.setSourceMapMode("false"), true); +const disabledResult = compiler.rescript.compileWithDebug(sourceMapSource); +assert.equal(disabledResult.type, "success"); +assert.equal(disabledResult.source_map, undefined); +assert.doesNotMatch(disabledResult.js_code, /\/\/# sourceMappingURL=/); + +console.log("-- Playground source map test complete --"); diff --git a/yarn.lock b/yarn.lock index f0777c68a95..aa4b9d8d18c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -376,7 +376,7 @@ __metadata: languageName: node linkType: hard -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": +"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28, @jridgewell/trace-mapping@npm:^0.3.31": version: 0.3.31 resolution: "@jridgewell/trace-mapping@npm:0.3.31" dependencies: @@ -1420,6 +1420,7 @@ __metadata: version: 0.0.0-use.local resolution: "dev-playground@workspace:packages/dev-playground" dependencies: + "@jridgewell/trace-mapping": "npm:^0.3.31" "@rescript/runtime": "npm:12.3.0" rescript: "npm:12.3.0" vite: "npm:^8.0.14"