From 26b1cf9d799b12b44919bc18feec2ed9299f403b Mon Sep 17 00:00:00 2001 From: RblSb Date: Thu, 9 Oct 2025 14:33:50 +0300 Subject: [PATCH] [js] Module import support --- src-json/meta.json | 7 +++ src/generators/genjs.ml | 37 ++++++++++++ tests/misc/projects/Issue10615/Main.hx | 56 +++++++++++++++++++ tests/misc/projects/Issue10615/compile.hxml | 5 ++ tests/misc/projects/Issue10615/compile2.hxml | 6 ++ tests/misc/projects/Issue10615/lib.js | 8 +++ .../projects/Issue10615/lib_default_class.js | 6 ++ .../projects/Issue10615/lib_default_fun.js | 3 + tests/misc/projects/Issue10615/multilib.js | 17 ++++++ 9 files changed, 145 insertions(+) create mode 100644 tests/misc/projects/Issue10615/Main.hx create mode 100644 tests/misc/projects/Issue10615/compile.hxml create mode 100644 tests/misc/projects/Issue10615/compile2.hxml create mode 100644 tests/misc/projects/Issue10615/lib.js create mode 100644 tests/misc/projects/Issue10615/lib_default_class.js create mode 100644 tests/misc/projects/Issue10615/lib_default_fun.js create mode 100644 tests/misc/projects/Issue10615/multilib.js diff --git a/src-json/meta.json b/src-json/meta.json index f542a34a15a..bfbc9f21986 100644 --- a/src-json/meta.json +++ b/src-json/meta.json @@ -550,6 +550,13 @@ "targets": ["TClass"], "links": ["https://haxe.org/manual/target-javascript-require.html"] }, + { + "name": "JsImport", + "metadata": ":js.import", + "doc": "Generate ES module import statement for given extern. Produces import lines at the top of the generated JS output.", + "platforms": ["js"], + "targets": ["TClass"] + }, { "name": "LuaRequire", "metadata": ":luaRequire", diff --git a/src/generators/genjs.ml b/src/generators/genjs.ml index 3ba99abeae3..9cce7042c1b 100644 --- a/src/generators/genjs.ml +++ b/src/generators/genjs.ml @@ -1677,6 +1677,43 @@ let generate js_gen com = let ctx = alloc_ctx com es_version in Gctx.map_source_header com.defines (fun s -> print ctx "// %s\n" s); + + let import_statements = ref [] in + let () = + List.iter (fun mt -> match mt with + | TClassDecl c when (has_class_flag c CExtern) && Meta.has Meta.JsImport c.cl_meta && is_directly_used ctx.com c.cl_meta -> + let _, args, mp = Meta.get Meta.JsImport c.cl_meta in + let id = s_path ctx (get_generated_class_path c) in + (match args with + (* @:js.import(@star "module") - namespace import *) + | [EMeta ((Meta.Custom "star",[],_),(EConst(String(module_name,_)),_)),_] -> + import_statements := (Printf.sprintf "import * as %s from \"%s\";" id module_name) :: !import_statements + (* @:js.import(@default "module") - default import *) + | [EMeta ((Meta.Custom "default",[],_),(EConst(String(module_name,_)),_)),_] -> + import_statements := (Printf.sprintf "import %s from \"%s\";" id module_name) :: !import_statements + (* @:js.import("module") - named import using class name *) + | [(EConst(String(module_name,_)),_)] -> + import_statements := (Printf.sprintf "import { %s } from \"%s\";" id module_name) :: !import_statements + (* @:js.import("module", "exportName") - named import with alias *) + | [(EConst(String(module_name,_)),_); (EConst(String(export_name,_)),_)] -> + if export_name = id then + import_statements := (Printf.sprintf "import { %s } from \"%s\";" id module_name) :: !import_statements + else + import_statements := (Printf.sprintf "import { %s as %s } from \"%s\";" export_name id module_name) :: !import_statements + | exprs -> + abort "Unsupported @:js.import format. Use: @:js.import('module'), @:js.import('module', 'name'), @:js.import(@default 'module'), or @:js.import(@star 'module')" mp + ) + | _ -> () + ) com.types; + in + (match !import_statements with + | [] -> () + | lines -> + List.iter (fun line -> + print ctx "%s\n" line + ) (List.rev lines) + ); + 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/tests/misc/projects/Issue10615/Main.hx b/tests/misc/projects/Issue10615/Main.hx new file mode 100644 index 00000000000..57257d560c4 --- /dev/null +++ b/tests/misc/projects/Issue10615/Main.hx @@ -0,0 +1,56 @@ +@:js.import(@star '../lib.js') +extern class Lib { + @:native("default") static function default_():Bool; + static var str:String; + static function increment(i:Int):Int; +} + +@:js.import(@star '../lib_default_fun.js') +extern class DefaultFun { + @:native("default") static function default_():Bool; +} + +@:js.import(@default '../lib_default_class.js') +extern class DefaultClass { + static function def():Bool; +} + +@:js.import('../multilib.js') +extern class Foo { + static function name():String; +} +@:js.import('../multilib.js', 'Foo') +extern class Foo2 { + static function name():String; +} +@:js.import('../multilib.js', 'Bar') +extern class Bar2 { + static function name():String; +} + +@:js.import(@default '../multilib.js') +extern class DefaultClass2 { + function new(); + function name():Bool; +} + +class Main { + static function main() { + eq("str", Lib.str); + eq(2, Lib.increment(1)); + eq("default function", Lib.default_()); + + eq("foo", Foo.name()); + eq("foo", Foo2.name()); + eq("bar", Bar2.name()); + final def = new DefaultClass2(); + eq("default class", def.name()); + + eq("default function", DefaultFun.default_()); + eq("default static", DefaultClass.def()); + } + + static function eq(a:Any, b:Any):Void { + if (a != b) throw '$a is not $b'; + } +} diff --git a/tests/misc/projects/Issue10615/compile.hxml b/tests/misc/projects/Issue10615/compile.hxml new file mode 100644 index 00000000000..ee96034a71d --- /dev/null +++ b/tests/misc/projects/Issue10615/compile.hxml @@ -0,0 +1,5 @@ +--main Main +-dce full +-D analyzer-optimize +--js bin/main.js +--cmd node bin/main diff --git a/tests/misc/projects/Issue10615/compile2.hxml b/tests/misc/projects/Issue10615/compile2.hxml new file mode 100644 index 00000000000..de8d59e8c89 --- /dev/null +++ b/tests/misc/projects/Issue10615/compile2.hxml @@ -0,0 +1,6 @@ +--main Main +-dce full +-D analyzer-optimize +--js bin/main.js +--cmd node bin/main +-D js-es=6 diff --git a/tests/misc/projects/Issue10615/lib.js b/tests/misc/projects/Issue10615/lib.js new file mode 100644 index 00000000000..6e2f7b71799 --- /dev/null +++ b/tests/misc/projects/Issue10615/lib.js @@ -0,0 +1,8 @@ +export const str = "str"; +export function increment(x) { + return x + 1; +} + +export default function def() { + return "default function"; +} diff --git a/tests/misc/projects/Issue10615/lib_default_class.js b/tests/misc/projects/Issue10615/lib_default_class.js new file mode 100644 index 00000000000..10eef011f47 --- /dev/null +++ b/tests/misc/projects/Issue10615/lib_default_class.js @@ -0,0 +1,6 @@ +export default class Lib { + static def() { + return "default static"; + } +} + diff --git a/tests/misc/projects/Issue10615/lib_default_fun.js b/tests/misc/projects/Issue10615/lib_default_fun.js new file mode 100644 index 00000000000..6ab093a9eaa --- /dev/null +++ b/tests/misc/projects/Issue10615/lib_default_fun.js @@ -0,0 +1,3 @@ +export default function def() { + return "default function"; +} diff --git a/tests/misc/projects/Issue10615/multilib.js b/tests/misc/projects/Issue10615/multilib.js new file mode 100644 index 00000000000..ee9970c8cba --- /dev/null +++ b/tests/misc/projects/Issue10615/multilib.js @@ -0,0 +1,17 @@ +export class Foo { + static name() { + return "foo"; + } +} + +export class Bar { + static name() { + return "bar"; + } +} + +export default class DefClass { + name() { + return "default class"; + } +}