"not defined" JS error when trying to use extern

I have the following Haxe class, ModList.hx

class ModList
{

	public function new ()
	{
		trace("Mod List Constructor");
	}

	public function getDataPacks(): Array<String>
	{
		trace ("getDataPacks was called");
		return new DataPacks().getDataPacks();
	}

	static public function main ()
	{
		trace("Hello from Mod List.");
	}

}

I’m targeting this to JavaScript because I want to load some content in another Haxe project without using Haxelib and thus requiring a recompile. Instead, I will use a JS file that I load as extern. The JS file is generated from the Haxe project that I can recompile (all this is for a moddable game and modders shouldn’t have to rely on anybody recompiling the engine).

So, this is my Extern class for the class above, also called ModList.hx but in the package externs.modlist.

package externs.modlist;
extern class ModList
{
	public function new();
	public function getDataPacks(): Array<String>;
}

Both these files reside in the first Haxe project, the “mod”, and the extern is loaded in a second Haxe project, a game engine.

Now, when I load the page in the game engine, the JavaScript-targeted Haxe project where I’m using this extern, the main method executes because I get “Hello from Mod List”, so everything seems to be in working order. (yes, I included the JS in the HTML page).

However, when I try to call getDataPacks, I get a JS error directly on the call, no stack trace, saying “externs is not defined”.

Note that “externs” is the first word in the package name. I tried adding another name in the package, for example “john.externs.modlist”. It then says john is not defined.

Any ideas?

When generating JavaScript output, Haxe wraps everything in an immediately-invoked function, so outer namespace is not polluted with whatever Haxe generates. And then there’s @:expose metadata that you can add to classes and static functions to make them available in the global namespace (window on the browser, exports in node). So you need to add @:expose to your ModList class when compiling the library.

See more in the manual! :slight_smile:

1 Like

Ah, thank you @nadako :). I read that in the manual yesterday but I forgot soon due to the torrent of information now when I’m adopting (and adapting to) Haxe. I added the metadata but unfortunately I get the same error. I even changed package to “externz” so that it doesn’t for some weird reason conflict with any Haxe convention (such as using externs for Node even though I’m not targeting Node).

@:expose
class ModList
{

	public function new ()
	{
		trace("Mod List Constructor");
	}

	public function getDataPacks(): Array<String>
	{
		trace ("getDataPacks was called");
		return new DataPacks().getDataPacks();
	}

	static public function main ()
	{
		trace("Hello from Mod List2.");
	}

}

I even tried adding @:expose to the DataPacks class too. Still no luck :(.

Ok, I think I’m getting somewhere. Or maybe not. I tried exposing the extern class itself. Now I’m getting a JS console error saying externz.datapacklist.DataPackList is not a constructor

How are you compiling the ModList class? It may not be included/kept, so the expose does nothing.

1 Like

Yes, that might be the reason: if you compile with full DCE (dead code elimination), Haxe will remove anything that is not directly used from your entry points and their dependencies, so you might want to also add @:keep to your classes to prevent that.

See more in the manual :slight_smile:

BTW this was recently changed, so in Haxe 4, @:expose will also keep things from being eliminated by DCE.

1 Like

@kLabz @nadako I actually inspected the outputed JS file and it does contain the traces (ahem, console.log) statements I added. Also, I’m on a development build of Haxe 4 (about a week old) so that’s not the issue.

In any case, I think adding @:expose to the extern class should not be necessary. I haven’t seen this used in other extern I looked at while trying to fix this. Probably it’s actually causing confusion. I’m getting the “not a constructor” error probably because there’s some confusion between the two classes which have the same name.

Just to emphasize: I have TWO DataPackList classes. One is the Haxe class and another is the extern class. I even tried renaming the extern class to DataPackList2. No luck. Here are the two classes again:

To create the JS from:

@:expose
class DataPackList
{

	public function new ()
	{
		trace("Data Pack List Constructor");
	}

	public function getDataPacks(): Array<String>
	{
		trace ("getDataPacks was called");
		return new DataPacks().getDataPacks();
	}

	static public function main ()
	{
		trace("Hello from Data Pack List 1!");
	}

}

Extern Haxe to use the generated JS:

package externz.datapacklist;

extern class DataPackList
{
	function new(): Void;
	function getDataPacks(): Array<String>;
}

Here’s the outputed JS file too :slight_smile:

// Generated by Haxe 4.0.0-preview.5+623b768bc
(function ($hx_exports) { "use strict";
var $hxEnums = $hxEnums || {};
var DataPackList = $hx_exports["DataPackList"] = function() {
	console.log("DataPackList.hx:9:","Data Pack List Constructor");
};
DataPackList.main = function() {
	console.log("DataPackList.hx:20:","Hello from Data Pack List 1!");
};
DataPackList.prototype = {
	getDataPacks: function() {
		console.log("DataPackList.hx:14:","getDataPacks was called");
		return new datapacks_DataPacks().getDataPacks();
	}
};
var datapacks_DataPacks = function() {
	console.log("datapacks/DataPacks.hx:8:","Welcome to Mods!");
};
datapacks_DataPacks.prototype = {
	getDataPacks: function() {
		var ret = [];
		ret.push("abc");
		console.log("datapacks/DataPacks.hx:20:","pushed abc");
		return ret;
	}
};
DataPackList.main();
})(typeof exports != "undefined" ? exports : typeof window != "undefined" ? window : typeof self != "undefined" ? self : this);

//# sourceMappingURL=DataPackList.js.map

Try with this?

@:native('DataPackList')
extern class DataPackList { ... }
1 Like

Deymn! It worked! :). Thank you @kLabz

Well, thanks both of you for helping out!

But now… why do we do this? From what I see in the manual, this use of native “Rewrites the path of a class or enum during generation”. But doesn’t this mean that I “lose” the package structure?

Upon closer examination I notice that some externs indeed use @:native, such as beanhx. That escaped me before. But the jQuery extern does not use this notation. Could you also explain to me why this works so?

Your extern class definition is in the externz.datapacklist package, which means that haxe will access it as externz.datapacklist.DataPackList, but your actual implementation is exposed as DataPackList directly at the toplevel, without any packages, so there’s an issue :slight_smile:

1 Like

Oh, so then if I would like to use package, I have to host both classes in the same package, right? But then there would be two classes with the same name… any idea how to do this in a good way, keeping packages?

This should not be a problem, because different classes are used for different compilations (main one vs plugin one), so just place them in distinct source folders

Well, by saying “distinct source folders”, you mean separate packages right? This would be good architecture anyway, since it doesn’t make sense to have something that is not extern in an “extern package”.

Therefore, I created the game.datapacklist page where I host the JS Haxe class called DataPackList. Then, in game.externs.datapacklist I host the extern DataPackList class and I annotate it with @:native('game.datapacklist.DataPackList')

This works and it’a also having a tidy package structure. Thank you both again! :slight_smile:

Well, actually I meant a different thing, but your solution is totally viable too :slight_smile:

What I meant a directory structure like this:

<root>
 |- sdk-src/somepackage/Api.hx
 |- sdk-extern/somepackage/Api.hx
 |- game-src/Main.hx
 |- mod-src/YourMod.hx

And then when you compile your game, you do -cp game-src and -cp sdk-src, so the actual SDK sources are compiled into the game, but when compile the mod, you add sdk-extern and mod-src, so the same somepackage.Api class will be extern for the mod.

1 Like

And, in your example, I assume that the version of the Api used would be sdk-src/Api.hx, however it would compile against the sdk-extern, not the sdk-src (only the game uses that because the game is the Haxe project which uses the mod JS file). Am I getting this right?