From 5cae26e2208eba42e44bacbbe5843c12ad78e6d1 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Tue, 21 Apr 2026 23:44:39 +0200 Subject: [PATCH 01/16] Add `js.module` define --- src-json/define.json | 8 ++++++++ src/generators/genjs.ml | 15 +++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src-json/define.json b/src-json/define.json index fbbbecd4b58..ab4d7c8200b 100644 --- a/src-json/define.json +++ b/src-json/define.json @@ -524,6 +524,14 @@ "doc": "Customizes the global object name.", "platforms": ["js"] }, + { + "name": "JsModule", + "define": "js.module", + "doc": "Customizes the JS module output type. (default: iife)", + "platforms": ["js"], + "params": ["type: es | iife"], + "default": "iife" + }, { "name": "JsUnflatten", "define": "js-unflatten", diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index c7c1784a0ad..a65d46ac1da 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -24,6 +24,10 @@ open Error open Gctx open JsSourcemap +type js_module_type = + | Es + | Iife + type ctx = { com : Gctx.t; buf : Rbuffer.t; @@ -32,6 +36,7 @@ type ctx = { smap : sourcemap option; js_modern : bool; js_flatten : bool; + js_module_type : js_module_type; has_resolveClass : bool; has_interface_check : bool; es_version : int; @@ -1616,6 +1621,11 @@ let alloc_ctx com es_version = smap = smap; js_modern = not (Gctx.defined com Define.JsClassic); js_flatten = not (Gctx.defined com Define.JsUnflatten); + js_module_type = (match Gctx.defined_value_safe ~default:"iife" com Define.JsModule with + | "es" -> (if es_version >= 6 then Es else failwith "ES modules require targetting ES6 or higher") + | "iife" -> Iife + | _ -> failwith "Invalid `js.module` define. Use `es` or `iife`" + ); has_resolveClass = Gctx.has_feature com "Type.resolveClass"; has_interface_check = Gctx.has_feature com "js.Boot.__interfLoop"; es_version = es_version; @@ -1833,7 +1843,7 @@ let generate js_gen com = newline ctx ); - if ctx.js_modern then begin + if (ctx.js_modern && ctx.js_module_type = Iife) then begin (* Wrap output in a closure *) print ctx "(function (%s) { \"use strict\"" (String.concat ", " (List.map fst closureArgs)); newline ctx; @@ -1955,7 +1965,8 @@ let generate js_gen com = (match com.main.main_expr with | None -> () | Some e -> gen_expr ctx e; newline ctx); - if ctx.js_modern then begin + + if (ctx.js_modern && ctx.js_module_type = Iife) then begin let closureArgs = if has_feature ctx "js.Lib.global" || defined_global then closureArgs From 7e3d06188a60cdf24effc158dfc4bc30ac5e2a18 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 02:54:54 +0200 Subject: [PATCH 02/16] Use `export` statements with classes and static methods --- src/generators/genjs.ml | 49 +++++++++++++++++++++++++++++++++++++---- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index a65d46ac1da..04b8330e1d7 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1246,6 +1246,37 @@ let generate_class_es5 ctx c = end; flush ctx +let tmp_var_counter = ref 0 +let export_tmp_name() = + let name = Printf.sprintf "$hx_export_tmp_%d" !tmp_var_counter in + tmp_var_counter := !tmp_var_counter + 1; + name + +let generate_export_statement_es6 ctx expr ident = + if (ctx.js_module_type == Es) then + if not (ExtString.String.contains ident '.') then + (* + The lack of dots in ident typically means an explicit identifier (or no package). + Either way, assume the user knows what they're doing: + do not validate identifiers, do not check for name collisions + *) + print ctx "export const %s = %s;" ident expr + else begin + let tmp_name = export_tmp_name() in + let exported_name = if ctx.es_version >= 2022 then + (* Keyword: "arbitrary module namespace identifier names" *) + Printf.sprintf "\"%s\"" ident + else + (* Older versions need mangling *) + let parts = ExtString.String.nsplit ident "." in + String.concat "_" parts + in + print ctx "const %s = %s; export {%s as %s};" tmp_name expr tmp_name exported_name + end + else + print ctx "$hx_exports%s = %s;" (path_to_brackets ident) expr; + newline ctx + let generate_class_es6 ctx c = let cl_path = get_generated_class_path c in let p = s_path ctx cl_path in @@ -1316,16 +1347,15 @@ let generate_class_es6 ctx c = spr ctx "}"; newline ctx; - List.iter (fun (path,name) -> - print ctx "$hx_exports%s = %s.%s;" (path_to_brackets path) p name; - newline ctx + List.iter (fun (path, name) -> + generate_export_statement_es6 ctx (Printf.sprintf "%s.%s" p name) path ) !exposed_static_methods; List.iter (gen_class_static_field ctx c cl_path) nonmethod_statics; let is_abstract_impl = is_abstract_impl c in - begin + if ctx.js_module_type = Iife then begin let added = ref false in if ctx.has_resolveClass && not is_abstract_impl then begin added := true; @@ -1336,6 +1366,12 @@ let generate_class_es6 ctx c = spr ctx p; newline ctx; end; + end else begin + if ctx.has_resolveClass && not is_abstract_impl then begin + print ctx "$hxClasses[\"%s\"] = %s;" dotp p; + newline ctx; + end; + process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement_es6 ctx p s); end; if not is_abstract_impl then begin @@ -1849,6 +1885,11 @@ let generate js_gen com = newline ctx; end; + (* + If necessary, generate objects for subpackages in the main exports object. + For example: `package foo; @:expose class Bar {}` + will generate `exports["foo"]["Bar"] = exports["foo"]["Bar"] || {};`. + *) let rec print_obj f root = ( let path = root ^ (path_to_brackets f.os_name) in print ctx "%s = %s || {}" path path; From 9c9de726a50830cdf5f446e67b151721e080ec24 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 12:53:20 +0200 Subject: [PATCH 03/16] Generate `export class` form if possible --- src/generators/genjs.ml | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 04b8330e1d7..6d34e2aafbf 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1253,17 +1253,13 @@ let export_tmp_name() = name let generate_export_statement_es6 ctx expr ident = - if (ctx.js_module_type == Es) then - if not (ExtString.String.contains ident '.') then - (* - The lack of dots in ident typically means an explicit identifier (or no package). - Either way, assume the user knows what they're doing: - do not validate identifiers, do not check for name collisions - *) + if ctx.js_module_type == Es then + let ident_contains_dots = ExtString.String.contains ident '.' in + if (not ident_contains_dots) && expr <> ident then print ctx "export const %s = %s;" ident expr else begin let tmp_name = export_tmp_name() in - let exported_name = if ctx.es_version >= 2022 then + let exported_name = if ident_contains_dots && ctx.es_version >= 2022 then (* Keyword: "arbitrary module namespace identifier names" *) Printf.sprintf "\"%s\"" ident else @@ -1281,14 +1277,36 @@ let generate_class_es6 ctx c = let cl_path = get_generated_class_path c in let p = s_path ctx cl_path in let dotp = dot_path cl_path in + let class_already_exported = ref false in let cls_name = if not ctx.js_flatten && (fst cl_path) <> [] then begin generate_package_create ctx cl_path; print ctx "%s = " p; Path.flat_path cl_path - end else + end else begin + + if ctx.js_module_type = Es then begin + try + let (_, args, pos) = Meta.get Meta.Expose c.cl_meta in + match args with + | [EConst (String(s, _)), _] -> begin + if p = s then begin + print ctx "export "; + class_already_exported := true; + end + end + | [] -> begin + print ctx "export "; + class_already_exported := true; + end + | _ -> abort "Invalid @:expose parameters" pos + with Not_found -> + (); + end; + p + end in print ctx "class %s" cls_name; @@ -1371,7 +1389,8 @@ let generate_class_es6 ctx c = print ctx "$hxClasses[\"%s\"] = %s;" dotp p; newline ctx; end; - process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement_es6 ctx p s); + if not !class_already_exported then + process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement_es6 ctx p s); end; if not is_abstract_impl then begin From 862bcad89cc80b1b6575d0c4c4f4e3c81dd1ddc4 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 14:29:54 +0200 Subject: [PATCH 04/16] Export static fields, module functions --- src/generators/genjs.ml | 115 ++++++++++++++++++++++++++-------------- 1 file changed, 74 insertions(+), 41 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 6d34e2aafbf..be3ea3b483f 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1040,6 +1040,34 @@ let path_to_brackets path = let parts = ExtString.String.nsplit path "." in "[\"" ^ (String.concat "\"][\"" parts) ^ "\"]" +let mangle_export_name_es6 ctx ident = + if ctx.es_version >= 2022 then + (* Keyword: "arbitrary module namespace identifier names" *) + Printf.sprintf "\"%s\"" ident + else + let parts = ExtString.String.nsplit ident "." in + String.concat "_" parts + +let tmp_var_counter = ref 0 +let export_tmp_name() = + let name = Printf.sprintf "$hx_export_tmp_%d" !tmp_var_counter in + tmp_var_counter := !tmp_var_counter + 1; + name + +let generate_export_statement ctx expr ident = + if ctx.js_module_type == Es then + let ident_contains_dots = ExtString.String.contains ident '.' in + if (not ident_contains_dots) && expr <> ident then + print ctx "export const %s = %s;" ident expr + else begin + let tmp_name = export_tmp_name() in + let exported_name = if ident_contains_dots then mangle_export_name_es6 ctx ident else ident in + print ctx "const %s = %s; export {%s as %s};" tmp_name expr tmp_name exported_name + end + else + print ctx "$hx_exports%s = %s;" (path_to_brackets ident) expr; + newline ctx + let gen_module_fields ctx m c fl = List.iter (fun f -> let name = module_field m f in @@ -1053,14 +1081,25 @@ let gen_module_fields ctx m c fl = match e.eexpr with | TFunction fn -> ctx.id_counter <- 0; + + let already_exported = ref false in + let expose_fallback = module_field_expose_path m.m_path f in + if ctx.js_module_type = Es then + process_expose f.cf_meta (fun () -> expose_fallback) (fun s -> + if name = (mangle_export_name_es6 ctx s) then begin + print ctx "export "; + already_exported := true; + end + ); + print ctx "function %s" name; gen_function ~keyword:"" ctx fn e.epos; ctx.separator <- false; newline ctx; - process_expose f.cf_meta (fun () -> module_field_expose_path m.m_path f) (fun s -> - print ctx "$hx_exports%s = %s" (path_to_brackets s) name; - newline ctx - ) + if not !already_exported then + process_expose f.cf_meta (fun () -> expose_fallback) (fun s -> + generate_export_statement ctx name s + ) | _ -> ctx.statics <- (c,f,e) :: ctx.statics ) fl @@ -1246,33 +1285,6 @@ let generate_class_es5 ctx c = end; flush ctx -let tmp_var_counter = ref 0 -let export_tmp_name() = - let name = Printf.sprintf "$hx_export_tmp_%d" !tmp_var_counter in - tmp_var_counter := !tmp_var_counter + 1; - name - -let generate_export_statement_es6 ctx expr ident = - if ctx.js_module_type == Es then - let ident_contains_dots = ExtString.String.contains ident '.' in - if (not ident_contains_dots) && expr <> ident then - print ctx "export const %s = %s;" ident expr - else begin - let tmp_name = export_tmp_name() in - let exported_name = if ident_contains_dots && ctx.es_version >= 2022 then - (* Keyword: "arbitrary module namespace identifier names" *) - Printf.sprintf "\"%s\"" ident - else - (* Older versions need mangling *) - let parts = ExtString.String.nsplit ident "." in - String.concat "_" parts - in - print ctx "const %s = %s; export {%s as %s};" tmp_name expr tmp_name exported_name - end - else - print ctx "$hx_exports%s = %s;" (path_to_brackets ident) expr; - newline ctx - let generate_class_es6 ctx c = let cl_path = get_generated_class_path c in let p = s_path ctx cl_path in @@ -1366,7 +1378,7 @@ let generate_class_es6 ctx c = newline ctx; List.iter (fun (path, name) -> - generate_export_statement_es6 ctx (Printf.sprintf "%s.%s" p name) path + generate_export_statement ctx (Printf.sprintf "%s.%s" p name) path ) !exposed_static_methods; List.iter (gen_class_static_field ctx c cl_path) nonmethod_statics; @@ -1390,7 +1402,7 @@ let generate_class_es6 ctx c = newline ctx; end; if not !class_already_exported then - process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement_es6 ctx p s); + process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement ctx p s); end; if not is_abstract_impl then begin @@ -1585,18 +1597,39 @@ let generate_enum ctx e = flush ctx let generate_static ctx (c,f,e) = - begin + let already_exported = ref false in match c.cl_kind with | KModuleFields m -> - print ctx "var %s = " (module_field m f); - process_expose f.cf_meta (fun () -> module_field_expose_path m.m_path f) (fun s -> print ctx "$hx_exports%s = " (path_to_brackets s)); + + let kwd = if ctx.js_module_type = Es then "let" else "var" in + let var_name = module_field m f in + let expose_fallback = module_field_expose_path m.m_path f in + + if ctx.js_module_type = Es then + process_expose f.cf_meta (fun () -> expose_fallback) (fun s -> + if var_name = (mangle_export_name_es6 ctx s) then begin + print ctx "export "; + already_exported := true; + end + ); + + print ctx "%s %s = " kwd var_name; + gen_value ctx e; + newline ctx; + + if not !already_exported then + process_expose f.cf_meta (fun () -> expose_fallback) (fun s -> + generate_export_statement ctx var_name s + ) | _ -> let cl_path = get_generated_class_path c in - process_expose f.cf_meta (fun () -> (dot_path cl_path) ^ "." ^ f.cf_name) (fun s -> print ctx "$hx_exports%s = " (path_to_brackets s)); - print ctx "%s%s = " (s_path ctx cl_path) (static_field ctx c f); - end; - gen_value ctx e; - newline ctx + let expr = Printf.sprintf "%s%s" (s_path ctx cl_path) (static_field ctx c f) in + print ctx "%s = " expr; + gen_value ctx e; + newline ctx; + process_expose f.cf_meta (fun () -> (dot_path cl_path) ^ "." ^ f.cf_name) (fun s -> + generate_export_statement ctx expr s + ) let generate_require ctx path meta = let _, args, mp = Meta.get Meta.JsRequire meta in From d4123ecf5e808a576c1e21137e58bcc469f7cc5e Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 14:37:51 +0200 Subject: [PATCH 05/16] Export static class fields that are functions --- src/generators/genjs.ml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index be3ea3b483f..f340008a6f4 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1116,12 +1116,14 @@ let gen_class_static_field ctx c cl_path f = | Some e -> match e.eexpr with | TFunction _ -> - let path = (s_path ctx cl_path) ^ (static_field ctx c f) in ctx.id_counter <- 0; + let path = (s_path ctx cl_path) ^ (static_field ctx c f) in print ctx "%s = " path; - process_expose f.cf_meta (fun () -> (dot_path cl_path) ^ "." ^ f.cf_name) (fun s -> print ctx "$hx_exports%s = " (path_to_brackets s)); gen_value ctx e; newline ctx; + process_expose f.cf_meta (fun () -> (dot_path cl_path) ^ "." ^ f.cf_name) (fun s -> + generate_export_statement ctx path s + ); | _ -> ctx.statics <- (c,f,e) :: ctx.statics From 028e8e62c1028f9da424cd25333f304e8f732ad8 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 14:41:04 +0200 Subject: [PATCH 06/16] Simplify class generation with `resolveClass` feature --- src/generators/genjs.ml | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index f340008a6f4..b245d22fd30 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1387,25 +1387,13 @@ let generate_class_es6 ctx c = let is_abstract_impl = is_abstract_impl c in - if ctx.js_module_type = Iife then begin - let added = ref false in - if ctx.has_resolveClass && not is_abstract_impl then begin - added := true; - print ctx "$hxClasses[\"%s\"] = " dotp - end; - process_expose c.cl_meta (fun () -> dotp) (fun s -> added := true; print ctx "$hx_exports%s = " (path_to_brackets s)); - if !added then begin - spr ctx p; - newline ctx; - end; - end else begin - if ctx.has_resolveClass && not is_abstract_impl then begin - print ctx "$hxClasses[\"%s\"] = %s;" dotp p; - newline ctx; - end; - if not !class_already_exported then - process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement ctx p s); + if ctx.has_resolveClass && not is_abstract_impl then begin + print ctx "$hxClasses[\"%s\"] = %s;" dotp p; + newline ctx; end; + + if not !class_already_exported then + process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement ctx p s); if not is_abstract_impl then begin generate_class___name__ ctx cl_path; From 4a29f30f3f95642baf1db299813d5b51f764ced0 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 14:46:56 +0200 Subject: [PATCH 07/16] Don't prepopulate `$hx_exports` when using ES modules --- src/generators/genjs.ml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index b245d22fd30..d3269c0b51d 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1940,7 +1940,9 @@ let generate js_gen com = concat ctx ";" (fun g -> print_obj g path) f.os_fields ) in - List.iter (fun f -> print_obj f "$hx_exports") exposedObject.os_fields; + + if ctx.js_module_type <> Es then + List.iter (fun f -> print_obj f "$hx_exports") exposedObject.os_fields; List.iter (fun file -> match file with From 506d4f902fdaf1fb87566b643fc417b52ffbe0c8 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Wed, 22 Apr 2026 15:28:36 +0200 Subject: [PATCH 08/16] Generate `$global` in ES modules if `globalThis` is unavailable --- src/generators/genjs.ml | 9 ++++++--- std/js/Lib.hx | 6 +++++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index d3269c0b51d..d4e284f6263 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1391,7 +1391,7 @@ let generate_class_es6 ctx c = print ctx "$hxClasses[\"%s\"] = %s;" dotp p; newline ctx; end; - + if not !class_already_exported then process_expose c.cl_meta (fun () -> dotp) (fun s -> generate_export_statement ctx p s); @@ -1902,7 +1902,7 @@ let generate js_gen com = let var_global = ( "$global", - typeof_join (if defined_global then [defined_global_value] else ["window"; "global"; "self"; "this"]) + typeof_join (if defined_global then [defined_global_value] else ["globalThis"; "window"; "global"; "self"; "this"]) ) in let closureArgs = [var_global] in @@ -1954,7 +1954,10 @@ let generate js_gen com = ) include_files; if (not ctx.js_modern) then - print ctx "var %s = %s;\n" (fst var_global) (snd var_global); + print ctx "var %s = %s;\n" (fst var_global) (snd var_global) + else if ctx.js_module_type = Es then + if has_feature ctx "js.Lib.global" || has_feature ctx "use.$bind" || has_feature ctx "$global.$haxeUID" then + print ctx "const %s = %s;\n" (fst var_global) (snd var_global); let enums_as_objects = not (Gctx.defined com Define.JsEnumsAsArrays) in diff --git a/std/js/Lib.hx b/std/js/Lib.hx index 43722b4e5a5..4b966d99bbc 100644 --- a/std/js/Lib.hx +++ b/std/js/Lib.hx @@ -123,12 +123,16 @@ class Lib { An alias of the JS "global" object. Concretely, it is set as the first defined value in the list of - `window`, `global`, `self`, and `this` in the top-level of the compiled output. + `globalThis`, `window`, `global`, `self`, and `this` in the top-level of the compiled output. **/ public static var global(get, never):Dynamic; extern static inline function get_global():Dynamic { + #if ((js_es >= 2020) && (js.module == "es") && !js_global) + return js.Syntax.code("globalThis"); + #else return untyped __define_feature__("js.Lib.global", js.Syntax.code("$global")); // $global is generated by the compiler + #end } /** From 3a981f39551d2615a7365aed6b270c7f535f9de6 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Thu, 23 Apr 2026 01:12:17 +0200 Subject: [PATCH 09/16] Deprecate `-D js-classic` in favor of `-D js-module=classic` --- src-json/define.json | 6 ++--- src/generators/genjs.ml | 58 +++++++++++++++++++++++------------------ 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src-json/define.json b/src-json/define.json index ab4d7c8200b..b63a47a1344 100644 --- a/src-json/define.json +++ b/src-json/define.json @@ -502,7 +502,8 @@ "name": "JsClassic", "define": "js-classic", "doc": "Don't use a function wrapper and strict mode in JS output.", - "platforms": ["js"] + "platforms": ["js"], + "deprecated": "Use `-D js.module=classic` instead." }, { "name": "JsEs", @@ -529,8 +530,7 @@ "define": "js.module", "doc": "Customizes the JS module output type. (default: iife)", "platforms": ["js"], - "params": ["type: es | iife"], - "default": "iife" + "params": ["type: es | iife | classic"] }, { "name": "JsUnflatten", diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index d4e284f6263..b8570f0484c 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -24,9 +24,10 @@ open Error open Gctx open JsSourcemap -type js_module_type = +type js_module_type = | Es | Iife + | Classic type ctx = { com : Gctx.t; @@ -34,7 +35,6 @@ type ctx = { mutable chan : out_channel option; packages : (string list,unit) Hashtbl.t; smap : sourcemap option; - js_modern : bool; js_flatten : bool; js_module_type : js_module_type; has_resolveClass : bool; @@ -1010,16 +1010,14 @@ let generate_package_create ctx (p,_) = Hashtbl.add ctx.packages (p :: acc) (); (match acc with | [] -> - if ctx.js_modern then - print ctx "var %s = {}" p - else - print ctx "var %s = %s || {}" p p + (match ctx.js_module_type with + | Classic -> print ctx "var %s = %s || {}" p p + | _ -> print ctx "var %s = {}" p) | _ -> let p = String.concat "." (List.rev acc) ^ (field p) in - if ctx.js_modern then - print ctx "%s = {}" p - else - print ctx "if(!%s) %s = {}" p p + (match ctx.js_module_type with + | Classic -> print ctx "if(!%s) %s = {}" p p + | _ -> print ctx "%s = {}" p) ); ctx.separator <- true; newline ctx; @@ -1188,7 +1186,7 @@ let generate_class_es5 ctx c = (* Do not add to $hxClasses on same line as declaration to make sure not to trip js debugger *) (* when it tries to get a string representation of a class. Will be added below *) - if ctx.com.debug || ctx.js_modern || not added_to_hxClasses then + if ctx.com.debug || ctx.js_module_type <> Classic || not added_to_hxClasses then print ctx "%s = " p else print ctx "%s = $hxClasses[\"%s\"] = " p dotp; @@ -1208,7 +1206,7 @@ let generate_class_es5 ctx c = newline ctx; - if (ctx.js_modern || ctx.com.debug) && added_to_hxClasses then begin + if (ctx.js_module_type <> Classic || ctx.com.debug) && added_to_hxClasses then begin print ctx "$hxClasses[\"%s\"] = %s" dotp p; newline ctx; end; @@ -1697,13 +1695,16 @@ let alloc_ctx com es_version = chan = None; packages = Hashtbl.create 0; smap = smap; - js_modern = not (Gctx.defined com Define.JsClassic); js_flatten = not (Gctx.defined com Define.JsUnflatten); - js_module_type = (match Gctx.defined_value_safe ~default:"iife" com Define.JsModule with - | "es" -> (if es_version >= 6 then Es else failwith "ES modules require targetting ES6 or higher") - | "iife" -> Iife - | _ -> failwith "Invalid `js.module` define. Use `es` or `iife`" - ); + js_module_type = begin + let fallback = if (Gctx.defined com Define.JsClassic) then "classic" else "iife" in + (match Gctx.defined_value_safe ~default:fallback com Define.JsModule with + | "es" -> (if es_version >= 6 then Es else failwith "ES modules require targetting ES6 or higher") + | "iife" -> Iife + | "classic" -> Classic + | _ -> failwith "Invalid `js.module` define. Use `es`, `iife`, or `classic`" + ) + end; has_resolveClass = Gctx.has_feature com "Type.resolveClass"; has_interface_check = Gctx.has_feature com "js.Boot.__interfLoop"; es_version = es_version; @@ -1915,13 +1916,13 @@ let generate js_gen com = (* Add node globals to pseudo-keywords, so they are not shadowed by local vars *) List.iter (fun s -> Hashtbl.replace kwds2 s ()) [ "global"; "process"; "__filename"; "__dirname"; "module" ]; - if (anyExposed && ((Gctx.defined com Define.ShallowExpose) || not ctx.js_modern)) then ( + if (anyExposed && ((Gctx.defined com Define.ShallowExpose) || ctx.js_module_type = Classic)) then ( print ctx "var %s = %s" (fst var_exports) (snd var_exports); ctx.separator <- true; newline ctx ); - if (ctx.js_modern && ctx.js_module_type = Iife) then begin + if (ctx.js_module_type = Iife) then begin (* Wrap output in a closure *) print ctx "(function (%s) { \"use strict\"" (String.concat ", " (List.map fst closureArgs)); newline ctx; @@ -1953,17 +1954,22 @@ let generate js_gen com = | _ -> () ) include_files; - if (not ctx.js_modern) then - print ctx "var %s = %s;\n" (fst var_global) (snd var_global) - else if ctx.js_module_type = Es then + (* Define global object *) + (match ctx.js_module_type with + | Es -> if has_feature ctx "js.Lib.global" || has_feature ctx "use.$bind" || has_feature ctx "$global.$haxeUID" then - print ctx "const %s = %s;\n" (fst var_global) (snd var_global); + print ctx "const %s = %s;\n" (fst var_global) (snd var_global) + | Iife -> + () (* provided by the closure *) + | Classic -> + print ctx "var %s = %s;\n" (fst var_global) (snd var_global) + ); let enums_as_objects = not (Gctx.defined com Define.JsEnumsAsArrays) in (* TODO: fix $estr *) let vars = [] in - let vars = (if ctx.has_resolveClass || (not enums_as_objects && has_feature ctx "Type.resolveEnum") then ("$hxClasses = " ^ (if ctx.js_modern then "{}" else "$hxClasses || {}")) :: vars else vars) in + let vars = (if ctx.has_resolveClass || (not enums_as_objects && has_feature ctx "Type.resolveEnum") then ("$hxClasses = " ^ (if ctx.js_module_type <> Classic then "{}" else "$hxClasses || {}")) :: vars else vars) in let vars = if has_feature ctx "has_enum" then ("$estr = function() { return " ^ (ctx.type_accessor (TClassDecl { null_class with cl_path = ["js"],"Boot" })) ^ ".__string_rec(this,''); }") :: vars else vars in @@ -2054,7 +2060,7 @@ let generate js_gen com = | None -> () | Some e -> gen_expr ctx e; newline ctx); - if (ctx.js_modern && ctx.js_module_type = Iife) then begin + if (ctx.js_module_type = Iife) then begin let closureArgs = if has_feature ctx "js.Lib.global" || defined_global then closureArgs From 93af98adb1e8f82f2071a7b856934458b9aca530 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Thu, 23 Apr 2026 02:18:21 +0200 Subject: [PATCH 10/16] Clarify `-D shallow-expose` does nothing when targetting ES modules; fix #7366 --- src-json/define.json | 2 +- src/generators/genjs.ml | 18 ++++++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src-json/define.json b/src-json/define.json index b63a47a1344..88fccd4631e 100644 --- a/src-json/define.json +++ b/src-json/define.json @@ -752,7 +752,7 @@ { "name": "ShallowExpose", "define": "shallow-expose", - "doc": "Expose types to surrounding scope of Haxe generated closure without writing to window object.", + "doc": "Expose types to surrounding scope of Haxe generated closure without writing to window object. Does nothing in ESM mode.", "platforms": ["js"] }, { diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index b8570f0484c..7f1453bf290 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1916,11 +1916,17 @@ let generate js_gen com = (* Add node globals to pseudo-keywords, so they are not shadowed by local vars *) List.iter (fun s -> Hashtbl.replace kwds2 s ()) [ "global"; "process"; "__filename"; "__dirname"; "module" ]; - if (anyExposed && ((Gctx.defined com Define.ShallowExpose) || ctx.js_module_type = Classic)) then ( - print ctx "var %s = %s" (fst var_exports) (snd var_exports); - ctx.separator <- true; - newline ctx - ); + if anyExposed then begin + if (Gctx.defined com Define.ShallowExpose) && ctx.js_module_type <> Es then begin + print ctx "var %s = %s || {}" (fst var_exports) (fst var_exports); + ctx.separator <- true; + newline ctx + end else if ctx.js_module_type = Classic then begin + print ctx "var %s = %s" (fst var_exports) (snd var_exports); + ctx.separator <- true; + newline ctx + end; + end; if (ctx.js_module_type = Iife) then begin (* Wrap output in a closure *) @@ -2076,7 +2082,7 @@ let generate js_gen com = newline ctx; end; - if (anyExposed && (Gctx.defined com Define.ShallowExpose)) then ( + if (anyExposed && (Gctx.defined com Define.ShallowExpose) && ctx.js_module_type <> Es) then ( List.iter (fun f -> print ctx "var %s = $hx_exports%s" f.os_name (path_to_brackets f.os_name); ctx.separator <- true; From cdec984639b06d14b44ec739bafd0a579e0a2c2e Mon Sep 17 00:00:00 2001 From: Frixuu Date: Thu, 23 Apr 2026 12:16:01 +0200 Subject: [PATCH 11/16] Fix package object being created in the wrong scope when targetting ESM --- src/generators/genjs.ml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 7f1453bf290..0e5f741d224 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1012,7 +1012,8 @@ let generate_package_create ctx (p,_) = | [] -> (match ctx.js_module_type with | Classic -> print ctx "var %s = %s || {}" p p - | _ -> print ctx "var %s = {}" p) + | Iife -> print ctx "var %s = {}" p + | Es -> print ctx "const %s = {}" p) | _ -> let p = String.concat "." (List.rev acc) ^ (field p) in (match ctx.js_module_type with From f94ee7e477907a76c8d7b48cf25d0d357ed9ba2e Mon Sep 17 00:00:00 2001 From: Frixuu Date: Thu, 23 Apr 2026 13:34:38 +0200 Subject: [PATCH 12/16] Disallow `:jsRequire` and `js.Lib.require` in non-Node ES modules --- src/generators/genjs.ml | 21 ++++++++++++++++++++- std/js/Lib.hx | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 0e5f741d224..4f723bd2914 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -37,6 +37,7 @@ type ctx = { smap : sourcemap option; js_flatten : bool; js_module_type : js_module_type; + mutable es_synthetic_require_generated : bool; has_resolveClass : bool; has_interface_check : bool; es_version : int; @@ -1620,12 +1621,26 @@ let generate_static ctx (c,f,e) = generate_export_statement ctx expr s ) +let generate_create_require_once ctx = + if ctx.js_module_type = Es then + if (not (Gctx.raw_defined ctx.com "nodejs")) then + failwith "Cannot require() in an ES module.\nIf you are using Node.js, use `--define nodejs` to enable CommonJS module loading,\nor use a different `--define js.module` type, like `iife`." + else if not ctx.es_synthetic_require_generated then begin + ctx.es_synthetic_require_generated <- true; + spr ctx "import { createRequire } from \"node:module\""; + newline ctx; + spr ctx "const require = createRequire(import.meta.url)"; + newline ctx; + end + let generate_require ctx path meta = let _, args, mp = Meta.get Meta.JsRequire meta in let p = (s_path ctx path) in + generate_create_require_once ctx; + if ctx.js_flatten then - spr ctx "var " + spr ctx (if ctx.js_module_type = Es then "const " else "var ") else generate_package_create ctx path; @@ -1706,6 +1721,7 @@ let alloc_ctx com es_version = | _ -> failwith "Invalid `js.module` define. Use `es`, `iife`, or `classic`" ) end; + es_synthetic_require_generated = false; has_resolveClass = Gctx.has_feature com "Type.resolveClass"; has_interface_check = Gctx.has_feature com "js.Boot.__interfLoop"; es_version = es_version; @@ -1821,6 +1837,9 @@ let generate js_gen com = ) (List.rev lines) ); + if has_feature ctx "js.Lib.require" then + generate_create_require_once ctx; + if has_feature ctx "Class" || has_feature ctx "Type.getClassName" then add_feature ctx "js.Boot.isClass"; if has_feature ctx "Enum" || has_feature ctx "Type.getEnumName" then add_feature ctx "js.Boot.isEnum"; diff --git a/std/js/Lib.hx b/std/js/Lib.hx index 4b966d99bbc..486a7ff798d 100644 --- a/std/js/Lib.hx +++ b/std/js/Lib.hx @@ -63,7 +63,7 @@ class Lib { is available, such as Node.js or RequireJS. **/ extern public static inline function require(module:String):Dynamic { - return js.Syntax.code("require")(module); + return untyped __define_feature__("js.Lib.require", js.Syntax.code("require")(module)); } /** From cff47062a7ad57b816bb2ba527c7ba7384e6c82a Mon Sep 17 00:00:00 2001 From: Frixuu Date: Thu, 23 Apr 2026 16:51:33 +0200 Subject: [PATCH 13/16] Always generate `$global`, even in ESM context --- src/generators/genjs.ml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 4f723bd2914..0321fb2ea24 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1923,7 +1923,12 @@ let generate js_gen com = let var_global = ( "$global", - typeof_join (if defined_global then [defined_global_value] else ["globalThis"; "window"; "global"; "self"; "this"]) + typeof_join (if defined_global then + [defined_global_value] + else if ctx.es_version >= 2020 then + ["globalThis"] + else + ["globalThis"; "window"; "global"; "self"; "this"]) ) in let closureArgs = [var_global] in @@ -1983,8 +1988,7 @@ let generate js_gen com = (* Define global object *) (match ctx.js_module_type with | Es -> - if has_feature ctx "js.Lib.global" || has_feature ctx "use.$bind" || has_feature ctx "$global.$haxeUID" then - print ctx "const %s = %s;\n" (fst var_global) (snd var_global) + print ctx "const %s = %s;\n" (fst var_global) (snd var_global) | Iife -> () (* provided by the closure *) | Classic -> From c7aa3039297ebd34a3cb9e83c7cab29f73ca195a Mon Sep 17 00:00:00 2001 From: Frixuu Date: Thu, 23 Apr 2026 16:06:26 +0200 Subject: [PATCH 14/16] Run more JS test permutations --- tests/runci/targets/Js.hx | 20 ++++++++++++++------ tests/unit/src/unit/issues/Issue8710.hx | 4 +++- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/tests/runci/targets/Js.hx b/tests/runci/targets/Js.hx index 67cab7ef869..46dcc88e689 100644 --- a/tests/runci/targets/Js.hx +++ b/tests/runci/targets/Js.hx @@ -45,19 +45,27 @@ class Js { getJSDependencies(); final jsOutputs = [ - for (es_ver in [[], ["-D", "js-es=6"]]) - for (unflatten in [[], ["-D", "js-unflatten"]]) - for (classic in [[], ["-D", "js-classic"]]) + for (es_ver in [[], ["-D", "js-es=6"], ["-D", "js-es=2022" /* the newest Node v18 supports */]]) + for (unflatten in [[], ["-D", "js-unflatten"]]) + for (moduleType in [[], ["-D", "js.module=es"], ["-D", "js.module=classic"]]) for (enums_as_objects in [[], ["-D", "js-enums-as-arrays"]]) { - final extras = args.concat(es_ver).concat(unflatten).concat(classic).concat(enums_as_objects); + final esm = moduleType.length == 2 && moduleType[1].endsWith("es"); + + // Skip ESM tests for ES5 + if (es_ver.length == 0 && esm) { + continue; + } + + final extras = args.concat(es_ver).concat(unflatten).concat(moduleType).concat(enums_as_objects); runCommand("haxe", ["compile-js.hxml"].concat(extras)); + final extension = esm ? "mjs" : "js"; // Node v18 and below cannot automatically detect module type final output = if (extras.length > 0) { - "bin/js/" + extras.join("") + "/unit.js"; + "bin/js/" + extras.join("") + '/unit.${extension}'; } else { - "bin/js/default/unit.js"; + 'bin/js/default/unit.${extension}'; } final outputDir = Path.directory(output); if (!FileSystem.exists(outputDir)) diff --git a/tests/unit/src/unit/issues/Issue8710.hx b/tests/unit/src/unit/issues/Issue8710.hx index 942b5a0147a..a57a5cca6dd 100644 --- a/tests/unit/src/unit/issues/Issue8710.hx +++ b/tests/unit/src/unit/issues/Issue8710.hx @@ -7,7 +7,9 @@ class Issue8710 extends unit.Test { function test() { var actual = - #if js + #if (js && js.module == "es") + js.Syntax.code("exposed"); + #elseif js js.Syntax.code("$hx_exports[\"exposed\"]"); #elseif lua untyped __lua__("_hx_exports[\"exposed\"]"); From f480af026d3d14dcd0010898f069f09d5de480a6 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Fri, 24 Apr 2026 18:55:15 +0200 Subject: [PATCH 15/16] [js] Revise when exports in ES modules do declare a helper variable --- src/generators/genjs.ml | 26 +++++++++++----------- tests/unit/src/unit/issues/Issue8710.hx | 29 ++++++++++++++++++------- 2 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 0321fb2ea24..432f9455e7b 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1041,28 +1041,28 @@ let path_to_brackets path = "[\"" ^ (String.concat "\"][\"" parts) ^ "\"]" let mangle_export_name_es6 ctx ident = - if ctx.es_version >= 2022 then + if not (ExtString.String.contains ident '.') then + ident + else if ctx.es_version >= 2022 then (* Keyword: "arbitrary module namespace identifier names" *) Printf.sprintf "\"%s\"" ident else let parts = ExtString.String.nsplit ident "." in String.concat "_" parts -let tmp_var_counter = ref 0 -let export_tmp_name() = - let name = Printf.sprintf "$hx_export_tmp_%d" !tmp_var_counter in - tmp_var_counter := !tmp_var_counter + 1; - name - +let tmp_export_vars = Atomic.make 0 let generate_export_statement ctx expr ident = if ctx.js_module_type == Es then - let ident_contains_dots = ExtString.String.contains ident '.' in - if (not ident_contains_dots) && expr <> ident then - print ctx "export const %s = %s;" ident expr + let ident = mangle_export_name_es6 ctx ident in + let expr_contains_dots = ExtString.String.contains expr '.' in + if not expr_contains_dots then + if expr = ident then + print ctx "export { %s };" ident + else + print ctx "export { %s as %s };" expr ident else begin - let tmp_name = export_tmp_name() in - let exported_name = if ident_contains_dots then mangle_export_name_es6 ctx ident else ident in - print ctx "const %s = %s; export {%s as %s};" tmp_name expr tmp_name exported_name + let tmp_name = Printf.sprintf "$hx_export_tmp_%d" (Atomic.fetch_and_add tmp_export_vars 1) in + print ctx "const %s = %s; export { %s as %s };" tmp_name expr tmp_name ident end else print ctx "$hx_exports%s = %s;" (path_to_brackets ident) expr; diff --git a/tests/unit/src/unit/issues/Issue8710.hx b/tests/unit/src/unit/issues/Issue8710.hx index a57a5cca6dd..3851284d6c0 100644 --- a/tests/unit/src/unit/issues/Issue8710.hx +++ b/tests/unit/src/unit/issues/Issue8710.hx @@ -1,20 +1,33 @@ package unit.issues; class Issue8710 extends unit.Test { -#if (js || lua) + #if (js || lua) @:expose('exposed') static var field = 10 + Std.random(1); + #if (js && js.module == "es") + // ESM exports are not easily accessible as variables. + // To circumvent this, dynamically import current module: + function test(async:utest.Async) { + js.Lib.dynamicImport(js.Syntax.code("import.meta.url")).then(module -> { + var actual = module.exposed; + eq(10, actual); + async.done(); + }).catchError(e -> { + assert(Std.string(e)); + async.done(); + }); + } + #else function test() { var actual = - #if (js && js.module == "es") - js.Syntax.code("exposed"); - #elseif js - js.Syntax.code("$hx_exports[\"exposed\"]"); + #if js + js.Syntax.code("$hx_exports[\"exposed\"]"); #elseif lua - untyped __lua__("_hx_exports[\"exposed\"]"); + untyped __lua__("_hx_exports[\"exposed\"]"); #end eq(10, actual); } -#end -} \ No newline at end of file + #end + #end +} From 5d3925090f4f08ac07df3717578d1ba43d84cd96 Mon Sep 17 00:00:00 2001 From: Frixuu Date: Mon, 27 Apr 2026 01:30:58 +0200 Subject: [PATCH 16/16] Do not define enums in ES modules as `var` --- src/generators/genjs.ml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 432f9455e7b..8d1659378c0 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1495,7 +1495,7 @@ let generate_enum ctx e = let dotp = dot_path e.e_path in let has_enum_feature = has_feature ctx "has_enum" in if ctx.js_flatten then - print ctx "var " + print ctx (if ctx.js_module_type = Es then "const " else "var ") else generate_package_create ctx e.e_path; print ctx "%s = " p; @@ -2003,7 +2003,7 @@ let generate js_gen com = let vars = if has_feature ctx "has_enum" then ("$estr = function() { return " ^ (ctx.type_accessor (TClassDecl { null_class with cl_path = ["js"],"Boot" })) ^ ".__string_rec(this,''); }") :: vars else vars in - let vars = if (enums_as_objects && (has_feature ctx "has_enum" || has_feature ctx "Type.resolveEnum")) then "$hxEnums = $hxEnums || {}" :: vars else vars in + let vars = if (enums_as_objects && (has_feature ctx "has_enum" || has_feature ctx "Type.resolveEnum")) then ("$hxEnums = " ^ (if ctx.js_module_type = Es then "{}" else "$hxEnums || {}")) :: vars else vars in let vars,has_dollar_underscore = if List.exists (function TEnumDecl e when not (has_enum_flag e EnExtern) -> true | _ -> false) com.types then "$_" :: vars,ref true @@ -2013,7 +2013,8 @@ let generate js_gen com = (match List.rev vars with | [] -> () | vl -> - print ctx "var %s" (String.concat "," vl); + let kwd = if ctx.js_module_type = Es then "let" else "var" in + print ctx "%s %s" kwd (String.concat ", " vl); ctx.separator <- true; newline ctx );