VCVRack / VCV-Prototype

Other
130 stars 22 forks source link

QuickJS: JavaScript modules #15

Open JerrySievert opened 4 years ago

JerrySievert commented 4 years ago

wondering what you think about adding a javascript module loader?

modules would be able to be loaded from either a specific defined path, or from a configured path, either absolute or relative.

a decision would likely need to be made whether to use CommonJS or ES6 modules.

alternately, enabling the std and os modules in QuickJS would allow access to the filesystem, local machine, etc.

Current proposal as of 2020-06-11 (edit by @AndrewBelt )

AndrewBelt commented 4 years ago

This makes it difficult to bundle the script into a sharable patch file.

JerrySievert commented 4 years ago

I realize that, and it was copying/pasting modules into my script to share that made me make the proposal.

while it does make that use-case a bit more difficult, it makes clean code and extendability much easier.

JerrySievert commented 4 years ago

I'll create a PR with the change to add modules so we can have a more "informed" discussion. there would still be a few questions to work out, but might as well get the discussion moving forward.

JerrySievert commented 4 years ago

amusingly, QuickJS has 2 modes of interpretation:

I've sent an email to the devel list, to hopefully get a sane response back about, well, you know, being able to both import modules and run code.

AndrewBelt commented 4 years ago

I'm not really interested in the script engine implementation but the Prototype Module implementation. Scripts are included in patches so people can share their patch so others can try their script. Including multiple scripts, each with a path, in the module data would be a mess.

JerrySievert commented 4 years ago

the overall issue is that without having a mechanism for modules to be loaded, there ends up being a ton of bespoke "modules" and code duplication that becomes very unmaintainable very quickly.

it's possible to build complex and powerful audio applications in javascript, and vcv-prototype is definitely fast enough to be able to run them, but hampering the implementation makes this much more difficult. it becomes the same as removing "#include" from c/c++: possible to do, but untenable in the end.

being able to access the huge module ecosystem, and share audio or vcvrack specific modules (like the CV module I copied/pasted in my community example) ends up making the whole community stronger.

AndrewBelt commented 4 years ago

I'm also not really interested in the motivation because I understand it well. I repeat: I'm interested in the "Prototype Module implementation".

JerrySievert commented 4 years ago

I'm interested in the "Prototype Module implementation".

and I'm interested in beer. without expanding on that I have to guess that you mean that you're interested solely in people making small things and sharing them.

AndrewBelt commented 4 years ago

Perhaps I'm being too vague in what I mean by that. For me to approve this proposal, I'd like to see what changes will need to be done to src/Prototype.cpp and Prototype's JSON "data" tag to make user-included JavaScript modules sharable. This is necessary to keep the promise "You can share your script by sharing your patch."

Separate idea: Instead of allowing users to download and include their own JavaScript modules, why not just bundle popular ones in the Prototype package? I bet we could find 10+ useful open-source BSD/MIT/GPL/etc libraries like https://mathjs.org/, https://github.com/corbanbrook/dsp.js/, etc. to include in a dep/js-modules folder or something. Then, it's ever easier for users to use common JS libraries, because they can skip the process of downloading and including them. We can happily accept recommendations of libraries from users to eventually collect everything everyone needs.

Users could add to their script

import math from 'mathjs'

or

const math = require("mathjs")

or however we do it. Since mathjs would be included on everyone's computers, the patch would not need to include the module source code itself.

As a side-effect of this proposal, I'm happy to then allow users to add their own JS modules and import/require them, because the action of doing this should suggest to them that their script becomes "unsharable". In other words, I accept your proposal to add JS modules, under the condition that we go out and find dozens of JS libraries to bundle so scripts remain mostly sharable.

JerrySievert commented 4 years ago

Perhaps I'm being too vague in what I mean by that.

hence my comment :)

I'd like to see what changes will need to be done to src/Prototype.cpp and Prototype's JSON "data" tag to make user-included JavaScript modules sharable.

to use modules, nothing - the questions that arise from this are a little more subtle imo:

alternately, a change could be made to Prototype.cpp to add a module path. this would still require users to install modules, but give them access to a setting to specify the path.

I'm also ok with a Prototype module distribution as part of the distribution, but it would mean added cognitive load. while I'm guessing you wouldn't be able to take that load, it wouldn't be much for me.

this issue was opened to gauge interest, not to provide a concrete proposal, just so we're on the same page. opening a PR would have solidified more of that.

AndrewBelt commented 4 years ago

which mechanisms should be allowable for including modules:

Don't include any of those. Just mathjs, etc. Anything that can run in a browser. Nothing Node-like such as os or std from QuickJS.

should we reinvent a packaging system?

No. Just install mathjs, etc to some directory so that import can access them, and commit the source. You can add them by downloading them manually, use git submodules, npm, Makefile dep targets, or any other method to add them.

which module system should be supported?

Probably ECMA if possible.

a change could be made to Prototype.cpp to add a module path.

I disagree. It should be hard-coded to asset::plugin(..., "js-modules") or something, which would include only bundled modules unless a user messes with the directory.

JerrySievert commented 4 years ago

I disagree. It should be hard-coded to asset::plugin(..., "js-modules") or something, which would include only bundled modules unless a user messes with the directory.

my issue with this is if there is work in progress by the user, and it is overwritten due to a module update. if module updates were completely non-destructive, this wouldn't be an issue (I have not investigated whether module updates remove the old module and install the new one, or if it is an overwrite preserving content).

Probably ECMA if possible.

actual lowers the number of usable modules, since ECMA is about a decade behind the module curve.

Anything that can run in a browser. Nothing Node-like such as os or std from QuickJS.

you might want to qualify that a little more, browserjs exists, and having been neck deep in plv8 I'm way too aware of how easy it is to wedge a package.

No. Just install mathjs, etc to some directory so that import can access them, and commit the source. You can add them by downloading them manually, use git submodules, npm, Makefile dep targets, or any other method to add them.

I'd love a canonical answer on my first point without having to trace through code.

AndrewBelt commented 4 years ago

my issue with this is if there is work in progress by the user

The plugin updater merely overwrites modules, so it's fine. Question: Can the module loader escape from the module path, e.g. require("../x.js") or require("/x.js")?

actual lowers the number of usable modules

Okay, then CommonJS. I figured there was maybe something built-in that transformed module.exports to ECMA exports.

you might want to qualify that a little more

My clarification is "Modules should be pure JS, not native". os obviously needs to be native because you can't break out of the sandbox with pure JS. Mathjs, etc are written in pure JS.

I'd love a canonical answer on my first point without having to trace through code.

I don't know what you mean by this, but can't you just find the library script and copy is to js-modules manually? For mathjs, that'd be https://unpkg.com/mathjs@6.2.3/dist/math.min.js.

JerrySievert commented 4 years ago

The plugin updater merely overwrites modules, so it's fine.

great, that's what I was worried about.

I don't know what you mean by this

the first point is ^^^ so answered.

Okay, then CommonJS. I figured there was maybe something built-in that transformed module.exports to ECMA exports.

ha. no. that would be too easy. browserjs manages to convert between node and browser modules, and babel does a lot of transformation as part of a build system, but if you're looking for any real commonality then prepare to sigh and roll your eyes.

Can the module loader escape from the module path, e.g. require("../x.js") or require("/x.js")?

yes, no. depends on how the module loader is written: it looks like you're wanting a sandbox - no problem, it's just a matter of checking paths.

fine with pure js modules, and not including os or std: that's been my default for a long time.

AndrewBelt commented 4 years ago

Okay, do we have all the answers we need then? This should require 0 changes to src/Prototype.cpp or src/ScriptEngine.cpp. Let's go crazy with bundling JS libraries then. Could you add one as an example?

JerrySievert commented 4 years ago

Okay, do we have all the answers we need then?

probably not, but enough to proceed.

Let's go crazy with bundling JS libraries then. Could you add one as an example?

only one? geez. yeah, I'll get a couple added and some documentation. it'll take a day or so for me to get something I'm happy enough with for a require but I'll get a PR open when I'm done.

gridsystem commented 4 years ago

I've just found this project and it looks like a lot of fun to experiment with!

Edit: I think I missed some of the nuances of this convo so I've deleted my message below. I see now that you're opting for a system where if you include a third party module you aren't able to share the script which seems like a good compromise!

Is this still in the works?

AndrewBelt commented 4 years ago

@gridsystem

I wonder also if you'd consider something similar to webpack

Can you give me a 60 second pitch of this. Explain what it is, how users would use it, etc.

gridsystem commented 4 years ago

Sure. (For anyone reading later, that quote is from my original comment where I wrongly assumed only modules included in the core would be supported)

Dev writes script. Dev includes module import Foo from 'foo'

Task runner (e.g. webpack, gulp, grunt or other) is running in terminal during dev. It watches for file changes and runs a compilation task when code changes. The compilation task bundles code and all dependencies into an executable script.

A separate final production script could be run after dev, e.g. npm run build or gulp build which would minify the bundled code, strip out console.logs or run any other desired optimisation to prepare for distribution.

AndrewBelt commented 4 years ago

@gridsystem I think you're confusing VCV Prototype for Node or something. Users don't use a terminal when running VCV Prototype.

I've edited the original post to reflect the current proposal that @JerrySievert and I converged on (if I'm interpreting our old discussion above correctly).

gridsystem commented 4 years ago

Those task runners do run using node.js. You could leverage node.js as a development tool, it's a very common way to work.

Applying a normal JS workflow to this, it would work this way.

AndrewBelt commented 4 years ago

That's way complicated. I think the current proposal for modules is fine.

gridsystem commented 4 years ago

Do you know if any progress has been made or planned with the current proposal?

That idea is achievable without changing the module. I'm not sure if I have time coming up to throw it together, but it could be done as an independent project.

AndrewBelt commented 4 years ago

Do you know if any progress has been made or planned with the current proposal?

No. I'd guess it'd be a 10-20 line feature so it would be done in one sitting.

That idea is achievable without changing the module

I don't know what this sentence means. What is "the module"?

gridsystem commented 4 years ago

Cool! That's awesome that it's so achievable.

Sorry - module is a pretty generic term in this context I guess. I mean VCV eurorack module.

JerrySievert commented 4 years ago

jeez, try to sleep in one morning and you miss all sorts of conversations!

I'll try to jump in with a couple of comments:

Do you know if any progress has been made or planned with the current proposal?

I've had other projects (software and hardware, and other things that I make my living doing) going on, so I haven't given this that much priority, and it hasn't jumped up in priority due to a couple of things:

what's actually required: deciding whether to use commonjs or es6 modules and writing a loader.

and here's a module that I wrote for a similar module to this from 0.6 (see https://github.com/JerrySievert/ProtoTools) - an LFO that I am hereby contributing as GPLv3 code (or whatever specific license that this repo adopts in the future):

function LowFrequencyOscillator ( ) {
  this.phase = 0;
  this.pw = 0.5;
  this.freq = 1;
}

LowFrequencyOscillator.prototype.setPitch = function setPitch (pitch) {
  pitch = Math.min(pitch, 8.0);
  this.freq = 261.626 * Math.pow(2, pitch);
};

LowFrequencyOscillator.prototype.setFrequency = function setFrequency (frequency) {
  this.freq = frequency;
};

LowFrequencyOscillator.prototype.setPulseWidth = function setPulseWidth (pw) {
  this.pw = Math.max(Math.min(pw, 0.99), 0.01);
};

LowFrequencyOscillator.prototype.step = function step (dt) {
  var deltaPhase = Math.min(this.freq * dt, 0.5);
  this.phase += deltaPhase;

  if (this.phase >= 1) {
    this.phase -= 1;
  }
};

LowFrequencyOscillator.prototype.sin = function sin ( ) {
  return Math.sin(2 * Math.PI * this.phase);
};

LowFrequencyOscillator.prototype.tri = function tri ( ) {
  var x = this.phase - 0.75;

  return (4 * Math.abs(x - Math.round(x))) - 1;
};

LowFrequencyOscillator.prototype.saw = function saw ( ) {
  return 2 * (this.phase - Math.round(this.phase));
};

LowFrequencyOscillator.prototype.sqr = function sqr ( ) {
  return (this.phase < this.pw) ? 1 : -1;
};

LowFrequencyOscillator.prototype.noise = function noise ( ) {
  return (Math.random() - 0.5) * 2;
};

module.exports = exports = LowFrequencyOscillator;

note that this is in commonjs - I just haven't taken the time to write the 10-20 line loader, which would be a welcome addition if you wanted to take it on inside the realm of the proposal @gridsystem

gridsystem commented 4 years ago

Haha I hope the snooze was worth it!

I would love to help with this, but I don’t know a thing about C.

I could put together a js bundler as I described, but it wouldn’t be worth doing if a native module loader is going to be included. If it becomes a more realistic choice, let me know and I can try to find some time. I relate completely to you having other commitments.

Really cool to see the osc code! Thanks for sharing. Would be cool to get it published on npm, I would gladly set up a repo and do that if you’d want.