jakobhellermann / bevy_mod_js_scripting

Other
83 stars 6 forks source link

Module Loading #8

Open zicklag opened 2 years ago

zicklag commented 2 years ago

It could be pretty handy to be able to load modules, but it's not necessarily simple to implement. Opening this issue as sort of a discussion placeholder and tracking issue ( if you don't mind ). Here are my thoughts so far.

Deno has awesome module loading capabilities that we could make do anything we want. Module loading in Deno is async, and not easily integrated with Bevy's asset loader because of it's non-Send requirements on the JsRuntime, but I managed a simple approach with an async event loop separate from the asset server that listened on a channel for module load events and that worked pretty well.

Then I ran into the issue that the browser could load javascript modules, but not typescript modules, because we can't hook into the import process that the browser uses like we can when we're running on Deno.

It seems like the best user experience would be:

  1. When an import statement is found in a script, during the script loading/transpiling process, we download any dependencies recursively.
  2. We load any loaded dependencies, indexed by their resolved URL, into a global in globalThis so that the scripts can access all the dependencies when running.
  3. During transpiling, we re-write the import statement to something like:
    const mylib = globalThis.dependencies['https://deno.land/x/some_modue/mod.ts']`;
  4. We would also have to re-write export statements, maybe wrapping the whole module in an IIFE and then return-ing the export's so that we can extract the exports in the browser by using something like:
    const module_exports = new Function(transpiled_module_source_code)();

Alternative: Bundling

Another compelling option would be to provide a CLI that could be used to bundle all of the scripts into a single JS file that would be more efficiently loaded in the browser.

But I would really like to avoid requiring a build step for scripts, because that's part of the point of scripting, is to avoid a build/bundle step, even if it is fast.

Still, if you only needed the build step for web, it could be fine.

ghost commented 1 year ago

I managed a simple approach with an async event loop separate from the asset server that listened on a channel for module load events and that worked pretty well.

@zicklag Would you be willing to share this code if you still have it?

zicklag commented 1 year ago

Yeah, here it is: https://github.com/zicklag/punchy/blob/5412954072448dc5991efec6a660ee634c8c1ebb/src/scripting/engines/native_javascript.rs.

zicklag commented 1 year ago

I think the solution I want to try for this is to rewrite import statements to something like const myModule = globalThis.bevyModJsScriptingModules.myModule and when we rewrite the import we'll also add the module to a list of dependencies for the script that is being loaded.

Then we only run that script once it's dependencies have been loaded. I think we can make this work somewhat cleanly with bevy's asset loader for async loading of the scripts.

And once a module has been loaded async by the asset server, we can just synchronously eval() the module into the globalThis.bevyModJsScriptingModules object, similar to how we run scripts already.

We'll also have to rewrite export statements, which we are already doing a little bit.

ghost commented 1 year ago

Thank you!