ruby2js / ruby2js

Ruby to JavaScript conversion
https://www.ruby2js.com/
427 stars 35 forks source link

ruby2js as an npm module? #103

Closed rubys closed 3 years ago

rubys commented 3 years ago

A byproduct of getting the demo to work with stimulus was the creation of a single source file that contains a JS implementation of ruby2js with a clean JS API. This source could be published to npm with both an ESM and CJS interface.

Examples of calling this API via node: nodetest. The ruby2js.com site also adds Ruby2JS as a property on the window object, so it can be accessed directly via the console for experimentation.

This would enable the webpack loader to be reduced to something like this:

const Ruby2JS = require('./ruby2js.js');
const path = require('path');

module.exports = function (source) {
  let file = path.relative(__dirname, this.resourcePath);
  let options = { ...this.getOptions(), file };

  try {
    let js = Ruby2JS.convert(source, options);
    this.callback(null, js.toString(), js.sourcemap)
  } catch(error) {
    let message = error.message;
    if (error.diagnostic) message += `\n\n${error.diagnostic}`;
    this.callback(new Error(message))
  }
}
rubys commented 3 years ago

0.0.1 version of an npm module published. The following webpack loader can self load:

import Ruby2JS, from: '@ruby2js/ruby2js'
import path, from: 'path'

export default def loader(source)
  file = path.relative(__dirname, self.resourcePath)
  options = { **self.getOptions(), file: file }

  begin
    js = Ruby2JS.convert(source, options)
    self.callback(nil, js.to_s, js.sourcemap)
  rescue => error
    message = error.message
    message += "\n\n#{error.diagnostic}" if error.diagnostic
    self.callback(Error.new(message))
  end
end
rubys commented 3 years ago

@jaredcwhite Or, perhaps:

import Ruby2JS, from: '@ruby2js/ruby2js'
import path, from: 'path'

export default def loader(source)
  file = path.relative(__dirname, self.resourcePath)
  options = { **self.getOptions(), file: file }

  begin
    js = Ruby2JS.convert(source, options)

    if options[:provide_source_maps] == false
      return js.to_s
    else
      self.callback(nil, js.to_s, js.sourcemap)
    end
  rescue => error
    message = error.message
    message += "\n\n#{error.diagnostic}" if error.diagnostic
    self.callback(Error.new(message))
  end
end
jaredcwhite commented 3 years ago

That sounds great! I'm assuming the list of options/filters/etc. would be similar to the Snowpack loader? (seems like feature parity there makes sense)

rubys commented 3 years ago

Identical. The snowpack loader doesn't currently recognize a provide_source_maps option, but that could be added. On the other hand, snowpack support for sourcemaps is experimental, and by default off. I'm not sure, but I think I read that snowpack rewrites some imports and that can make the sourcemap a bit off.

it does mean that one would configure the loader would be in the webpack.config.js, so this wouldn't be a backwards compatible change. Arguably, that's where people who use webpack would expect to place their configuration?

I'm actually optimistic that this would be a big performance win. Ruby2JS takes a fraction of a second to load and then runs real fast. With the current design, Ruby2JS is spawned once per file. With the new design, it will only be loaded once. My initial measurements are that the initial Opal/JS load is slower than Ruby, but as I said, sub-second even on an i3. It is when you have a project with dozens, hundreds, or more source files that you will see a difference.

jaredcwhite commented 3 years ago

Seems cool. I might miss the niceness of the config .rb file but placing it directly within the webpack config makes the most sense, and I'm sure it'll be a lot faster with tons of files than spawning Ruby processes (although for me that hasn't been a major issue).

rubys commented 3 years ago

Challenge accepted. The following script will extract the options and format it as JSON. I just need to figure out where to put this script so that it runs automatically at startup whenever a rb2js.config.rb file is present, and then merge the results with the options in the webpack config.

require_relative 'rb2js.config.rb'
require 'json'

puts({ filters: Ruby2JS::Filter::DEFAULTS.map {|mod|
  method = mod.instance_method(mod.instance_methods.first)
  File.basename(method.source_location.first, '.rb')
}, **Ruby2JS::Loader.options}.to_json)
jaredcwhite commented 3 years ago

Wowee…I wouldn't have even thought of that. Very nice!

rubys commented 3 years ago

I've now pushed a @ruby2js/ruby2js to npm with support for taking options from either rb2js.config.rb or a RUBY2JS_OPTIONS environment variable.

I've also pushed a 2.0.0 version of @ruby2js/webpack-loader that makes use of this.

Finally, I've published an example using this support: https://www.ruby2js.com/examples/rails/stimulus_webpacker

Please try it out, and let me know if there are any problems (either by reopening this issue or opening a new issue).