Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ESM ScriptType #5963

Merged
merged 11 commits into from
Jan 25, 2024
Merged

ESM ScriptType #5963

merged 11 commits into from
Jan 25, 2024

Conversation

marklundin
Copy link
Member

@marklundin marklundin commented Jan 22, 2024

This PR adds support for es modules using the existing ScriptType system. It conditionally loads scripts as a module depending on the file name (ending in.mjs). This can be used in the editor as-is although there is work necessary in the editor to parse Scripts as modules.

Issues

  1. Scripts cannot define attributes as getter/setters. Scripts 2.1 (Class support) #5910 (comment)
  2. Script class names can be mangled during a build, which would registerScript Scripts 2.1 (Class support) #5910 (comment)

I confirm I have read the contributing guidelines and signed the Contributor License Agreement.

@marklundin marklundin changed the title Esm loading ESM ScriptType loading Jan 22, 2024

head.appendChild(element);
const path = url.split('?')[0];
const isEsmScript = path.endsWith('.esm.js');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wondering why not .mjs?

Copy link
Member Author

@marklundin marklundin Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Open to suggestion, however one thing the V8 team and Mozilla pointed out is that '.js' will get served with the correct Content-Type headers that would already exist on the server. '.mjs', '.jsm' etc may not have those configured and would need to be manually setup. Okay for us, but maybe difficult if users don't have access to their server configuration. (Source MDN)[https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#aside_%E2%80%94_.mjs_versus_.js]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although build systems, today do support mjs pretty well, e.g. Babel.
We do have page that does require setting up server already: https://developer.playcanvas.com/en/user-manual/publishing/web/self-hosting-for-beginners/#content-types
It worth adding there configs too.

Copy link
Member Author

@marklundin marklundin Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true. I'm not particularly wedded to either. Currently the editor doesn't let you change the file suffix, so that would need to be changed, and I'm not sure what it really buys you. The nice thing about keeping it '.js' is that the user can just change directly as-is. Also one other thing, there's gonna be a distinction between ScriptType modules, and just plain vanilla modules (config/utils/etc). Arguably these shouldn't only work if they're '.mjs'. Also potentially, if we enforced the ESM ScriptType at the file name level then we can guard against it being added as a script in the editor, rather than having to load it and check through exports.


const baseUrl = platform.browser ? window.location.origin : '';
const importUrl = new URL(url, baseUrl);
importUrl.searchParams.append('cacheBust', new Date().valueOf().toString());
Copy link
Contributor

@Maksims Maksims Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As script is an asset, we should use asset.file.hash here to ensure that cache can be used reliably.

Copy link
Member Author

@marklundin marklundin Jan 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh the file.hash changes on asset save! Of course it does :D

@mvaligursky
Copy link
Contributor

Personally, I'd like to see an example of this, perhaps with limitations in comments do demonstrate what works and what does not. It does not need to be an engine example (but could be), but at least one in the PR description, so that we know what this enables / what needs to be avoided, for this to work in engine only / editor environment.

@marklundin
Copy link
Member Author

@mvaligursky There isn't any changes in the engine use, it's only where files get loaded in the Script Handler. Here's a Editor example, which will work if you run the launcher or edit code. Although don't parse the script in the editor as this needs an editor update

https://playcanvas.com/editor/scene/1927088

@mvaligursky
Copy link
Contributor

Great example. So how do we let people know they can use it?

@LeXXik
Copy link
Contributor

LeXXik commented Jan 24, 2024

The example doesn't work for me or do I need to run it against this PR?

@mvaligursky
Copy link
Contributor

The example doesn't work for me or do I need to run it against this PR?

Yes you would need that.

@marklundin
Copy link
Member Author

marklundin commented Jan 24, 2024

I have an editor fix that parses attributes which we'd need before recommending to users.

Also we'd need to get documentation in place

const importUrl = new URL(url, baseUrl);

// @ts-ignore
import(importUrl.toString()).then((module) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this uses import, which based on https://caniuse.com/?search=import is supported by Chrome from 2017, Safari 2018, Firefox 2019 - I wonder how we could handle the compatibility issues here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens on browsers using old script style .. are they going to get exception when this code gets parsed? Or is there some babel polyfill for this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There has to be an assumption that if a user chooses to use ES modules, then there is a neccessary baseline level of support required. ES6 is around 97.2% which is only 0.7% less than our legacy build.

If someone uses the old script mechanism, the ESM path won't get triggered so it will just work as is.

_loadModule(url, callback) {

// if we're in the browser, we need to use the full URL
const baseUrl = platform.browser ? window.location.origin : import.meta.url;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can an es5 version of the engine load module scripts? If so then import.meta.url would fail, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Even though that'll only ever be used in node).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The target is marginally higher than ES5 now, but babel should transpile this according to the preset.env. See here

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That sounds good, just double check it does and we're not missing some settings please.

@LeXXik
Copy link
Contributor

LeXXik commented Jan 24, 2024

Will the script that was extended from ScriptType be parsed by Editor as well?

@marklundin
Copy link
Member Author

marklundin commented Jan 24, 2024

Will the script that was extended from ScriptType be parsed by Editor as well?

How do you mean @LeXXik? Do you mean subclassing something that extends ScriptType?

class CustomBase extends ScriptType { static attributeDefinitions: { ... } }
class MyClass extends CustomBase {}

This should work and pick up the correct attributes if you attach MyClass to a Script Component, ie, it will 'inherit' the CustomBase attributes. Although, for the moment you can't import a relative module from the asset registry. Not until we address the pathing issues.

@Maksims
Copy link
Contributor

Maksims commented Jan 24, 2024

Will the script that was extended from ScriptType be parsed by Editor as well?

I think you just mean if current Editor workflow will work pretty much the same with a ESM script? It will.

class Test extends pc.ScriptType { }
pc.registerScript(Test, 'test');

This already works in Editor.

With ESM script no need to call pc.registerScript (you still should be able to though), instead you would export, like so:

export Test;

And asset loader will automatically add it to the script registry.

@marklundin
Copy link
Member Author

Yep, all exports from a module will be tested to see if they are a valid ScriptType, so the following are both valid Scripts

class Test extends ScriptType {};
class OtherTest extends ScriptType {};

export Test;
export default OtherTest;

@LeXXik
Copy link
Contributor

LeXXik commented Jan 24, 2024

Right, I was referring to the child class, as in

class A extends pc.ScriptType {}
class B extends A {} // can't parse B in Editor at the moment

@Maksims
Copy link
Contributor

Maksims commented Jan 25, 2024

Right, I was referring to the child class, as in

class A extends pc.ScriptType {}
class B extends A {} // can't parse B in Editor at the moment

That would be the Editor. Technically it could already be improved to load other scripts. With ESM Modules and import maps - this becomes easier. You would need to do this:

/scripts/scriptA.js

class A extends pc.ScriptType {}
export A;

/scripts/scriptB.js

import A from '/scripts/scriptA.js';
class B extends A {}
export B;

Copy link
Contributor

@Maksims Maksims left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

Copy link
Member

@slimbuck slimbuck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉

@marklundin marklundin merged commit c80ab34 into main Jan 25, 2024
7 checks passed
@marklundin
Copy link
Member Author

Quietly pretty excited about this one!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

6 participants