playcanvas / editor

Issue tracker for the PlayCanvas Editor
https://playcanvas.com/
161 stars 28 forks source link

ESM Hot Module Reloading #1115

Open marklundin opened 9 months ago

marklundin commented 9 months ago

Support Hot reloading for ESM Scripts

Existing scripts support "Hot Reloading" by manually calling a 'swap()` on a ScriptType as it updates. ESM scripts however are modular and can contain complex dependancies...

./config.mjs
export const speed = 10.0;
./utils.mjs
import { speed } from './config.mjs';
export const addSpeed = n => n * 2;
./MyRotator.mjs
import { addSpeed } from './utils.mjs'

class MyRotator extends ScriptType {
  update(){
    this.prop = addSpeed(this.prop)
  }
}

This works when MyRotator.mjs changes, but when config.mjs changes, the user needs to manually refresh the page to see the update. In an ideal world when config.mjs changes only it's dependants update.

This is non-trivial, as it requires maintaining a dependancy graph so that for each module update, we can walk the tree and find the relevant ScriptTypes that need to be reloaded.

ESM HMR requirements

Considerations

Maksims commented 9 months ago

I believe that ScriptType.swap - is very straight forward and simple feature, that is not related to actually hot-reloading every possible script.

Current logic is pretty specific: if the script has swap method, then when it is updated, attempt to reload it.

To clarify, this is "ScriptType Hot-Reload". The actual "ESM Hot-Reload" - is a different story.

For ScriptType hot-reload logic very straight forward:

  1. The script asset file has been updated (load event).
  2. Check if that script asset has script definitions.
  3. For every already loaded script that have a swap method, update script registry with a new instance of a ScriptType class if it is defined in exports of a new file.

If I'm not wrong, it should already work based on the current code, as it calls registerScript for each ScriptType from a module, and it will trigger swap on its own based on the logic described above.

marklundin commented 9 months ago

Yep swap works as is. This is just for HMR with sub dependancies etc. For end users, I worry it might be confusing if they have different behaviours.

Given a ScriptType with no swap method

  1. If the ScriptType is modified directly - No update ❌
  2. If one of its' dependancy is modified - Update occurs ✅

That seems like it might be confusing. I was initially thinking that the swap method could control both, so at least it's consistent, but that doesn't really work as ones a class and the other a module

Maksims commented 9 months ago

The full and real hot-swap would be amazing of course. But might be challenging.

Current hot-swap is meant for development and really is a viable solution for small/medium complexity scripts. I think state management for big complex scripts would be challenging.

marklundin commented 9 months ago

Yes exactly. There's lots to work out. However the developer experience when it does work is so good, I believe it's worth the effort.

Maksims commented 3 weeks ago

Hi @marklundin, any status on swap support in Editor for ESM? We've tried to use ESM scripts with engine v1 in Editor on small new (clean) projects, and we rely on swap a lot, especially as the project grows and refreshes become longer and more complex due to the complex states of an app. So we do need a swap a lot to ensure we can efficiently work.

marklundin commented 2 weeks ago

Hi @marklundin, any status on swap support in Editor for ESM? We've tried to use ESM scripts with engine v1 in Editor on small new (clean) projects, and we rely on swap a lot, especially as the project grows and refreshes become longer and more complex due to the complex states of an app. So we do need a swap a lot to ensure we can efficiently work.

The existing hot-swap for ESM Scripts without dependencies should work, however I've just tested and found a caching issue. Will submit a fix