Opal 1.2

After about 11 years of development we are proud to announce the Opal 1.2 release!

Opal is a Ruby to JavaScript compiler, Ruby runtime and an idiomatic Ruby API for interfacing Browser/Node/anything that uses JS.

Notable new features

Full Ruby 3.0 support! (almost)

We missed a blog post about Opal 1.1, which was released with a preliminary support for Ruby 3.0 features, but Opal 1.2 finalizes this support. Let me update you about them!

An interesting thing is that you can still run older versions of Ruby on the backend, but use the new Ruby 3.0 features when writing Opal code for your frontend.

End-less method definition (Opal 1.1)

No more writing code like this:

def ten
  10
end

Write it in one line!

def ten = 10

Beginless and endless ranges (Opal 1.2)

While those were introduced in earlier Ruby versions (2.6, 2.7), we didn't have those, and now we do! You can absolutely call a[1..] instead of a[1..-1] now!

Numblocks (Opal 1.1)

Also a Ruby 2.7 feature. Personally, I don't like this syntax. Nevertheless, you can use it in Opal now!

[1,2,3].map { _1 ** 2 }

Argument forwarding support def a(...); b(...); end (Opal 1.1)

We support 3.0 level of argument forwarding. No more writing code like

def example(arg1, arg2, *args, **kwargs, &block)
  @arg1, @arg2 = arg1, arg2
  Klass.new(:begin, *args, **kwargs, &block)
end

...when you can write it like this:

def example(arg1, arg2, ...)
  @arg1, @arg2 = arg1, arg2
  Klass.new(:begin, ...)
end

Pattern matching (Opal 1.2)

We support 3.0 level of pattern matching. All kinds of patterns have been fully implemented, including the find pattern.

Pattern matching is optional, if you want to use it, you will need to add require "corelib/pattern_matching" to your application.

Do note, that the find pattern ([1, 2, 3, "needle", 4] => [*, String => a, *] to _find String and save it to a local variable a) and one-line 1 in a syntax are deemed experimental in Ruby 3.0 and may change their behavior in the future_

Methods

There are quite a lot of new methods, some of which got introduced in Ruby 3.0, some earlier.

Opal 1.2: - {Random,SecureRandom}#{hex,base64,urlsafe_base64,uuid,random_float,random_number,alphanumeric} - String#{+@,-@} - Hash#except! - {Array,Hash,Struct}#{deconstruct,deconstruct_keys}

Opal 1.1: - Array#{difference,intersection,union} - Enumerable#{filter_map,tally} - Kernel#then - {Proc,Method}#{<<,>>} - Integer#to_d

Breaking changes

Since Opal 1.2, we now follow the Ruby 3.0 semantics for Array#{to_a,slice,[],uniq,*,difference,-,intersection,&,union,|} and therefore those don't return an Array subclass anymore, but simply an Array.

What isn't there

We are still using keyword arguments in the Ruby 2.6 style. We want the future releases to warn on the incompatibilities, just like Ruby 2.7 did, to provide a smooth migration curve.

Ractor isn't implemented and likely won't be anytime soon. JavaScript has a very different threading model to Ruby.

ObjectSpace finalizer support and ObjectSpace::WeakMap implementation (Opal 1.2)

A destructor in Ruby? And also in a niche JavaScript Ruby implementation? Yes!

You need to do `require "corelib/objectspace"` to use those features._

begin ... end while ... (Opal 1.2)

Matz doesn't like this feature as it is inconsistent with how other Ruby postfix flow control constructs work. But it looks similar to do { ... } while(...) - and this is how it works. Now, also in Opal!

Native Promises (Opal 1.2 - experimental)

When Opal was first conceived, JavaScript didn't have Promises (or they weren't so universally supported), so we made our own. Now, that Promises in JavaScript are a big thing, well integrated into the language, we made a new kind of Promise (PromiseV2), that bridges the native JavaScript promise and is (mostly) compatible with the current Opal Promise (later called PromiseV1).

By bridges I mean that conversion between JavaScript and Opal values is seamless. Just as you can pass numbers, strings or arrays between those two ecosystems, Promises (only PromiseV2) will join that pack now.

There are some slight incompatibilities which is why we include both versions, and PromiseV2 is defined as experimental. This allows us to state, that it's behavior may change in the future (until deemed un-experimental). This is so also to foster a discussion about how to best support async/await in the next release.

There's a (mostly done) CoffeeScript approach to async/await available as a patch (not in the 1.2 release though), but we may as well take a different approach.

To use PromiseV2, require "promise/v2" and call PromiseV2.

Promise will become aliased to PromiseV2 instead of how it's currently aliased to PromiseV1 in Opal 2.0. You can require "promise/v1" and call PromiseV1 if you are certain that you want to stay with the old Promise. We will take a similar approach to redesigning other APIs (like NativeV2, BufferV2, etc.) in the upcoming minor releases.

The libraries in our ecosystem will soon be updated to use PromiseV2 if it's loaded instead of the PromiseV1. Do note, that PromiseV1 and PromiseV2 aren't to be mixed together, in your application you should pick one or the other. We suggest using Promises in your libraries this way:

module YourApp
  Promise = defined?(PromiseV2) ? PromiseV2 : Promise

  def using_promise
    Promise.value(true).then do
      ...
    end
  end
end

Other changes and statistics

Performance

Based on the statistics we made, Opal (both the runtime and the compiler) used to decrease in performance the more compatible with Ruby it became. This is not the case this time, we made quite a lot of performance improvements in this release. Opal 1.2 is more performant than 1.1 for many real world uses, and similar in performance to Opal 1.0. We will try to continue this trend in the future releases.

Issues

Opal 1.2 is mostly a bugfix release. We fixed a lot of long-standing bugs. Maybe it's time to try Opal again? Here we have a chart:

Issues chart

Roadmap

We didn't fully meet the expectations of the roadmap as described in the Opal 1.0 post. Better than postpone the Opal 1.1 release, we decided to release what we have (and we had quite a lot).

We have partially accomplished the code minification with an external (and experimental) opal-optimizer gem.

There are solutions for Webpack integration, but those aren't upstream. We want to gladly invite anyone interested to contribute during the Opal 1.3 development phase which we mainly want to dedicate towards accomplishment of that goal.

Opal 2.0 will hide Ruby methods behind a (JavaScript) symbol wall increasing compatibility with JavaScript libraries.

String are still immutable, (Regexps are still somewhat incompatible)[https://opalrb.com/blog/2021/06/26/webassembly-and-advanced-regexp-with-opal/], asynchronous methods can't be implemented in a Ruby synchronous style. Those features we plan for Opal 2.0 or later on, as they will require a lot of changes.

Ecosystem

All Gems from the opal-* namespace are compatible with the Opal 1.2 release. The new opal-browser version is up for a release soon, until then, we recommend you use the master branch!

From the outside of the core ecosystem, we have seen quite a lot of development, there are at least a few very interesting things that were done using Opal:

  • Glimmer DSL for Opal - an (experimental) platform-agnostic (web with Opal, desktop with SWT or Tk) toolkit for graphical applications. Write once, run anywhere!
  • 18xx.games - a website where you can play async or real-time 18xx games
  • Hyperstack - a well documented and stable Ruby DSL for wrapping React and ReactRouter as well as any JavaScript library or component. No need to learn JavaScript!

Conclusion

If you are unsure yet, Opal doesn't just feel like Ruby, it IS Ruby. It is absolutely possible to create a website with pure Ruby and not a single line of JavaScript (and a good performance!). In the advent of solutions like Hotwire, Opal goes a step higher and allows you to create fully isomorphic websites (with a fully fledged support for human language alike DSLs). Forget context switches from JavaScript, TypeScript, or even CoffeeScript to Ruby when you develop a feature, it's all Ruby!

If you are daring, you can even hook up the Opal compiler - written in a very clean Ruby - to add macro facilities to accommodate your application!