ExecJS::Xtrn
Drop-in replacement for ExecJS. The latter spawns separate process for every JavaScript compilation (when using external runtime), while ExecJS::Xtrn spawn long running JavaScript process and communicates with it via stdin & stderr.
This is just proof of concept, not suitable for production.
When not on MS Windows, one definitely should use ExecJS with excellent therubyracer
gem instead.
Installation
Add this line to your application's Gemfile:
gem 'execjs-xtrn'
And then execute:
$ bundle
Or install it yourself as:
$ gem install execjs-xtrn
Usage
Just add/require this gem after/instead (implicit) execjs
gem.
The latter will be monkey-patched.
Engines
ExecJS::Xtrn uses two external JavaScript runners:
- Windows Script Host
- Via external program
cscript.exe
- Direct call to
ScriptControl
ActiveX
- Via external program
- Node.js
In addition, both runners allow two usage modes:
- Simple (1 execution context = 1 external process)
- Virtual machines (all execution contexts share single external process)
So, there exist six engines:
- Engine - abstract engine (smart enough to execute blank lines)
- Wsh - WSH/CScript in simple mode
- Wvm - WSH/CScript with virtual machines (via
ScriptControl
ActiveX again) - Ole - WSH via ActiveX (no external processes at all)
- Node - Node.js in simple mode
- Nvm - Node.js' vm API
Nvm engine has nothing common with nvm.
All engines autodetect their availability at startup (on require 'execjs/xtrn'
) and sets Valid
constants.
Eg on MS Windows ExecJS::Xtrn::Wsh::Valid = true, on Linux - false
Ole engine is unavailable for 64-bit Ruby
(since ScriptControl
ActiveX is 32 bit).
Wvm engine meets the same trouble,
but it has been reenabled with some workaround.
One of available engines is made default engine for ExecJS. If Node.js is available it is Nvm. Else, if running on Windows it is Wsh. Else it is Engine, so ExecJS is made almost unusable.
Default engine can be shown/changed at any moment with
ExecJS::Xtrn.engine
accessor, eg
ExecJS::Xtrn.engine=ExecJS::Xtrn::Node
API
ExecJS::Xtrn is primarily designed to power other gems that use popular ExecJS.
But it has his own API (similar to ExecJS' API) and can be used itself.
In general one should create instance of an Engine and then feed it with JavaScript code:
ctx=ExecJS::Xtrn::Wsh.new
ctx.exec 'fact = function(n){return n>1 ? n*fact(n-1) : 1}'
puts "10! = #{ctx.call 'fact', 10}"
Every execution context has four methods:
- exec(
code
) - executes arbitrary JavaScript code. To get resultreturn
must be called - load(
code
) or load(path
) - exec that can load its code from file - eval(
expression
) - evaluate JavaScript expression.return
is not needed - call(
function
, arguments...) - special form of eval for function call
There are exec
and eval
methods in Engine class,
they just create brand new execution context,
pass argument to it, destroy that context and return its result.
Using these class methods is not recommended, since it's just what ExecJS does
(except for Nvm engine).
Engine class also has compile
method that combines new
and exec
and returns execution context.
This is how ExecJS is used in most cases.
ctx=ExecJS::Xtrn::Wsh.compile 'fact = function(n){return n>1 ? n*fact(n-1) : 1}'
puts "10! = #{ctx.call 'fact', 10}"
And load
methods is likewise combination of new
+load
,
it is compile
that can load its code from file.
load
method (class' or instance's) detects whether its argument
is code or path by first symbols of it. So, start path with /
, ./
or ../
(but not from //
). On Windows \
and C:
can be also used.
Finally ExecJS::Xtrn patches ExecJS and installs those 4 class methods
(exec
, eval
, compile
and load
) in it.
So, ExecJS.compile
is ExecJS::Xtrn::Nvm.compile
if Nvm engine is available.
Preloading
Sometimes it's neccesary to initialize all execution contexts before passing code to them. For instance, add some standard JavaScript methods missing in Wsh engine.
It can be done by setting Preload constant on engine class.
ExecJS::Xtrn::Wsh::Preload='./lib/js/map.js'
or maybe
ExecJS::Xtrn::Wsh::Preload=[
'./lib/js/map.js',
'./lib/js/keys.js',
'console={log: function(){WScript.Echo([].slice.call(arguments).join(" "))}}'
]
# Yes, console.log is avaialable in Wsh now!
# And yes, console.log can be used in ExecJS::Xtrn!
You can add preload scripts to any engine or to Engine base class. They will be loaded according to inheritance: Engine::Preload will be used by all engines, Node::Preload is for Node and Nvm, while Nvm::Preload is for Nvm only.
Overriding ExecJS
Sometimes ExecJS is required after ExecJS::Xtrn. In that case you can call ExecJS::Xtrn.init and it will overwrite ExecJS' methods again.
To test whether JavaScript is served by ExecJS::Xtrn, it's convenient to look at ExecJS::Xtrn statistics.
Statistics
Every engine gathers it's own usage statistics. Eg:
> ExecJS::Xtrn::Node.stats # or ExecJS::Xtrn::Nvm.stats or ExecJS::Xtrn::Wsh.stats
=> {:c=>2, :n=>2, :o=>8, :i=>6, :t=>0.131013}
Here:
- c = number of child processes spawned (for Nvm c should always be 1)
- n = number of request made
- o = bytes sent to child process(es)
- i = bytes got from child process(es)
- t = seconds spent communicating with child process(es)
- m = number of VMs created (Nvm only)
- x = number of VMs destroyed (Nvm only)
ExecJS::Xtrn.stats combines statistics for all its engines, even unused.
ExecJS.stats shows statistics for current engine only.
Every execution context has his own statistics too. Eg
s=ExecJS::Xtrn::Nvm.compile '...'
s.exec '...'
s.stats
but c (and m, x) fields are omitted there.
If ExecJS::Xtrn detects it is run under Ruby on Rails, it installs additional path `/rails/jsx' to display its statistics (you can see that route in sextant, for example).
It is one more reason not to use ExecJS::Xtrn in production mode ;-)
By default statistics is output in YAML format, but you can
get /rails/jsx.json
or /rails/jsx.html
.
Compatibilty
Not every JavaScript code behaves identically in ExecJS and ExecJS::Xtrn. In most cases it depends on how global JavaScript variables are used. For most modern code it is the same though.
As a rule of thumb, JavaScript code must survive after wrapping in anonymous function ((function(){...})()
).
For instance, old versions of handlebars_assets
didn't work
in ExecJS::Xtrn (and worked in ExecJS).
The following packages have been tested to run under ExecJS::Xtrn out-of-box:
- CoffeeScript via coffee-script and coffee-rails gems
- UglifyJS2 via uglifier
- Handlebars via handlebars_assets gem
CoffeeScript since v1.9.0 introduces new incompatibility:
it uses Object.create
that is missing from WSH.
To fix it, Object.create
was manually defined in ExecJS::Xtrn::Wsh
(sort of ExecJS::Xtrn::Wsh::Preload).
Path to this polyfill is available as
ExecJS::Xtrn::Wsh::ES5
constant.
Gem coffee-script since v2.4.0 introduces another incompatibility:
it silently creates global function. This approach works in regular ExecJS but fails in ExecJS::Xtrn.
As a workaround pin coffee-script
gem version to 2.3.0.
Later uglifier from v3 started to use globals either. Pin it to '~> 2'.
Testing
After git checkout, required NPM modules must be installed. Simply run:
bundle install
bundle exec rake npm
The testing itself is
bundle exec rake
And bundle exec
may be ommited in most cases.