[SOLVED] Bundlers + TypeScript + ESM Scripts

I am currently exploring what a complete external build pipeline looks like for fairly large-scale commercial PlayCanvas game projects. I recently made a post about combining TypeScript with ESM Scripts synced with playcanvas-sync. Cool!

Now I want to understand how I can create re-usable libraries for all my company’s games. I create each library as a separate Node.js package, keep them in a monorepo and publish them privately via GitHub Packages. I can then either add a dependency to the published library, or use npm link to symlink a local version if I also want to develop the library in parallel. All of this works now and I’m starting to understand how Node.js in particular works. Nice!

To make the libraries usable in a PlayCanvas-ready build, I use a bundler that resolves npm imports and emits ESM output with shared chunks for browser execution. This all looks good and seems to work.

However.

The bundler rewrites the module structure and emits the script class using an export list form (export { TestScript }) instead of a direct class export (export class TestScript extends Script).

PlayCanvas’ ESM script detection does not appear to register scripts unless the class is exported using the direct export class syntax.

If anyone tried anything similar it would be interesting to know if I’m doing it wrong, if you’ve run into similar problems. Maybe it’s a matter of an additional argument to my bundler to retain direct exports or just using a different bundler. I don’t think syntax-wise the output is wrong, but PlayCanvas doesn’t like it.

Original TS-file (the external library is not of interest in this example):

import {Script} from "playcanvas";
import {TestLibraryClass} from "@mikim-entertainment/test-library";

export class TestScript extends Script { // Note export syntax
  static scriptName = "testScript";
 
  initialize() { 
    const test = new TestLibraryClass();
  } 
} 

And the bundler output, which is semantically equivalent ESM, but isn’t picked up by PlayCanvas ESM script detection:

import { TestLibraryClass } from "./chunk-QQOFT3CH.mjs";

// src/pc/test-script.ts
import { Script } from "playcanvas";
var TestScript = class extends Script { // Note export syntax
  static scriptName = "testScript";
  initialize() {
    const test = new TestLibraryClass();
  }
};
export { // Note export syntax
  TestScript
};
//# sourceMappingURL=test-script.mjs.map

If I manually change the export syntax to this (below) it works, but I’d like to be able to use the bundler output without having to modify the files (obviously):

export class TestScript extends Script {

I’m using esbuild to bundle and pcwatch to sync to PlayCanvas. Is this a limitation of PlayCanvas’ current ESM script detection (possibly a bug or unimplemented case), or is there a bundler configuration or alternative bundler that preserves the export class form for entry modules?

I did just find another post with a very similar problem. Didn’t find it before when searching.

The answer might be “PlayCanvas simply only supports ‘vanilla’ ES Modules. Use a different bundler.”.

I did notice that this works in ES Script parsing:

class TestScript extends Script {
    ...
};
export {TestScript};

It’s just this that doesn’t work (and it is the default syntax output by esbuild):

var TestScript = class extends Script {
    ...
};
export {TestScript};

I’ve tried using Rollup as bundler too now. It did structure the exports in a compatible way, but had some other problems generating PlayCanvas friendly code. To be investigated.

Made it work with Rollup instead of esbuild.

The problem with Rollup was much more easily fixed than the export syntax used with esbuild.

So, for anyone stumbling upon this in the future. I’d recommend using Rollup instead of esbuild, otherwise the ES Scripts might not export properly so that PlayCanvas can pick them up.

Cheers!