Skip to content

[js] Generate ES modules#12882

Open
Frixuu wants to merge 16 commits into
HaxeFoundation:developmentfrom
Frixuu:js-module
Open

[js] Generate ES modules#12882
Frixuu wants to merge 16 commits into
HaxeFoundation:developmentfrom
Frixuu:js-module

Conversation

@Frixuu
Copy link
Copy Markdown
Contributor

@Frixuu Frixuu commented Apr 22, 2026

Summary

This PR adds a new define called js.module. It accepts one of the following values:

  • es: New, focus of this PR. Generates an ECMAScript (ES) module,
  • iife: Default behavior in Haxe 3 and 4. Generates an IIFE,
  • classic: Default behavior in Haxe 2. Generates code that writes to global scope directly. This option replaces current switch -D js-classic.

If js.module is not defined, the compiler assumes option iife for compatibility with existing Haxe 4 code.

Example

package foo;

@:expose
class Bar {
	@:expose("greeting")
	public static final greeting:String = "SGVsbG8sIFdvcmxkIQ==";

	private static function main() {
		trace(js.Lib.global.atob(greeting));
	}
}

With the following arguments, this Haxe code transpiles to:

-D js.module=es -D js-es=6
const $global = typeof globalThis != "undefined" ? globalThis : typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this;
export class foo_Bar {
	static main() {
		console.log("foo/Bar.hx:9:",$global.atob(foo_Bar.greeting));
	}
}

// ... omitted for brevity

foo_Bar.greeting = "SGVsbG8sIFdvcmxkIQ==";
const $hx_export_tmp_0 = foo_Bar.greeting; export { $hx_export_tmp_0 as greeting };
foo_Bar.main();
-D js.module=es -D js-es=2020
const $global = globalThis; // for compatibility
export class foo_Bar {
	static main() {
		console.log("foo/Bar.hx:9:",globalThis.atob(foo_Bar.greeting));
	}
}

// ... omitted for brevity

foo_Bar.greeting = "SGVsbG8sIFdvcmxkIQ==";
const $hx_export_tmp_0 = foo_Bar.greeting; export { $hx_export_tmp_0 as greeting };
foo_Bar.main();
-D js.module=iife -D js-es=6
(function ($hx_exports, $global) { "use strict";
$hx_exports["foo"] = $hx_exports["foo"] || {};
class foo_Bar {
	static main() {
		console.log("foo/Bar.hx:9:",$global.atob(foo_Bar.greeting));
	}
}
$hx_exports["foo"]["Bar"] = foo_Bar;

// ... omitted for brevity

foo_Bar.greeting = "SGVsbG8sIFdvcmxkIQ==";
$hx_exports["greeting"] = foo_Bar.greeting;
foo_Bar.main();
})(typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this, typeof globalThis != "undefined" ? globalThis : typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this);
-D js.module=classic -D js-es=6
var $hx_exports = typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this;
$hx_exports["foo"] = $hx_exports["foo"] || {};
var $global = typeof globalThis != "undefined" ? globalThis : typeof window != "undefined" ? window : typeof global != "undefined" ? global : typeof self != "undefined" ? self : this;
class foo_Bar {
	static main() {
		console.log("foo/Bar.hx:9:",$global.atob(foo_Bar.greeting));
	}
}
$hx_exports["foo"]["Bar"] = foo_Bar;

// ... omitted for brevity

foo_Bar.greeting = "SGVsbG8sIFdvcmxkIQ==";
$hx_exports["greeting"] = foo_Bar.greeting;
foo_Bar.main();

Assumptions & questions

  • ECMAScript modules were introduced in ES6 (ES2015).
    • Therefore, -D js.module=es will refuse to work with -D js-es=5.
    • ES5 is currently the default target version.
  • Currently all exports are named. There is no export default generation.
    • Should there be an option to export default? What use case would necessitate it?
  • Users who wanted this feature care about bundling and tree-shaking (DCE) their code. Unlike old $hx_exports, which ["created"] ["an"] ["object"] ["for"] ["every"] ["subpackage"], this PR's module implementation exports all symbols as flat identifiers to help with this goal.
    • For instance, picture a class:
    package foo;
    @:expose class Bar { /* ... */ }
    When flattening is enabled (default), this class will be generated as export class foo_Bar. There are no subpackages. Other JS modules may import this class with import { foo_Bar } from "./generated.js".
    • ES2022 allows export names to be string literals. Currently, this PR chooses to use a string literal name instead of mangling it, if -D js-es is set to 2022 or above. This potentially makes the class above importable as { "foo.Bar" as somethingElse }, for example when running -D js-unflatten. Is this desired behavior? Should this choice be declared explicitly, or not exist at all?

Supersedes #10003. Closes #8033.

@CCobaltDev
Copy link
Copy Markdown
Contributor

@:expose? why not @:export?
unless it already exists...

@Frixuu
Copy link
Copy Markdown
Contributor Author

Frixuu commented Apr 23, 2026

@:expose? why not @:export? unless it already exists...

@:expose has been introduced in 2012 and currently works in both JS and Lua targets.

➜ haxe --help-metas | grep -A 1 expose
 @:expose               : (<name>) Includes the class or field in Haxe exports
                          (default name is the classpath). (for lua,js)

@CCobaltDev
Copy link
Copy Markdown
Contributor

@:expose? why not @:export? unless it already exists...

@:expose has been introduced in 2012 and currently works in both JS and Lua targets.

➜ haxe --help-metas | grep -A 1 expose
 @:expose               : (<name>) Includes the class or field in Haxe exports
                          (default name is the classpath). (for lua,js)

the more you know

@Frixuu Frixuu marked this pull request as ready for review April 23, 2026 17:49
@Frixuu Frixuu marked this pull request as draft April 26, 2026 22:59
@Frixuu Frixuu marked this pull request as ready for review April 27, 2026 16:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[js][es6] add ES6 module exports

2 participants