Awesome
js
Tools for Working with JavaScript in R
A set of utility functions for working with JavaScript in R. Currently includes functions to compile, validate, reformat, optimize and analyze JavaScript code.
Documentation
- Vignette: Working with JavaScript in R
The js package implements bindings to several popular JavaScript libraries for validating, reformatting, optimizing and analyzing JavaScript code. It builds on the V8 package (a fully standalone JavaScript engine in R) to call out to these libraries.
Syntax Validation
Several R packages allow the user to supply JavaScript code to be used as callback function or configuration object within a visualization or web application. By validating in R that the JavaScript code is syntactically correct and of the right type before actually inserting it in the HTML, we can avoid many annoying bugs.
The js_typeof
function simply calls the typeof
operator on the given code. If the code is invalid, a SyntaxError will be raised.
callback <- 'function test(x, y){
var z = x*y ;
return z;
}'
js_typeof(callback)
[1] "function"
Same for objects:
conf <- '{
foo : function (){},
bar : 123
}'
js_typeof(conf)
[1] "object"
Catch JavaScript typos:
js_typeof('function(x,y){return x + y}}')
Error in context_eval(join(src), private$context): SyntaxError: Unexpected token }
Script Validation
A JavaScript program typically consists of script with a collection of JavaScript statements. The js_validate_script
function can be used to validate an entire script.
jscode <- readLines(system.file("js/uglify.min.js", package="js"), warn = FALSE)
js_validate_script(jscode)
[1] TRUE
Note that JavaScript does not allow for defining anonymous functions in the global scope:
js_validate_script('function(x, y){return x + y}', error = FALSE)
[1] FALSE
To validate individual functions or objects, use the js_typeof
function.
ESprima: Parsing
Esprima is a high performance, standard-compliant ECMAScript parser. It has full support for ECMAScript 2017 and returns a sensible syntax tree format as standardized by ESTree project.
esprima_tokenize(callback)
type value
1 Keyword function
2 Identifier test
3 Punctuator (
4 Identifier x
5 Punctuator ,
6 Identifier y
7 Punctuator )
8 Punctuator {
9 Keyword var
10 Identifier z
11 Punctuator =
12 Identifier x
13 Punctuator *
14 Identifier y
15 Punctuator ;
16 Keyword return
17 Identifier z
18 Punctuator ;
19 Punctuator }
esprima_parse(callback)
{
"type": "Program",
"body": [
{
"type": "FunctionDeclaration",
"id": {
"type": "Identifier",
"name": "test"
},
"params": [
{
"type": "Identifier",
"name": "x"
},
{
"type": "Identifier",
"name": "y"
}
],
"body": {
"type": "BlockStatement",
"body": [
{
"type": "VariableDeclaration",
"declarations": [
{
"type": "VariableDeclarator",
"id": {
"type": "Identifier",
"name": "z"
},
"init": {
"type": "BinaryExpression",
"operator": "*",
"left": {
"type": "Identifier",
"name": "x"
},
"right": {
"type": "Identifier",
"name": "y"
}
}
}
],
"kind": "var"
},
{
"type": "ReturnStatement",
"argument": {
"type": "Identifier",
"name": "z"
}
}
]
},
"generator": false,
"expression": false,
"async": false
}
],
"sourceType": "script"
}
Compiling CoffeeScript
CoffeeScript is a little language that compiles into JavaScript. It is an attempt to expose the good parts of JavaScript in a simple way. The coffee_compile
function binds to the coffee script compiler.
# Hello world
cat(coffee_compile("square = (x) -> x * x"))
(function() {
var square;
square = function(x) {
return x * x;
};
}).call(this);
cat(coffee_compile("square = (x) -> x * x", bare = TRUE))
var square;
square = function(x) {
return x * x;
};
The golden rule of CoffeeScript is: "It's just JavaScript". The code compiles one-to-one into the equivalent JS, and there is no interpretation at runtime. You can use any existing JavaScript library seamlessly from CoffeeScript (and vice-versa). The compiled output is readable and pretty-printed, will work in every JavaScript runtime, and tends to run as fast or faster than the equivalent handwritten JavaScript.
# Simple script
demo <- readLines(system.file("example/demo.coffee", package = "js"))
cat(demo, sep = "\n")
# Assignment:
number = 42
opposite = true
# Conditions:
number = -42 if opposite
# Functions:
square = (x) -> x * x
# Arrays:
list = [1, 2, 3, 4, 5]
# Objects:
math =
root: Math.sqrt
square: square
cube: (x) -> x * square x
# Splats:
race = (winner, runners...) ->
print winner, runners
# Existence:
alert "I knew it!" if elvis?
# Array comprehensions:
cubes = (math.cube num for num in list)
js <- coffee_compile(demo)
cat(js)
(function() {
var cubes, list, math, num, number, opposite, race, square,
slice = [].slice;
number = 42;
opposite = true;
if (opposite) {
number = -42;
}
square = function(x) {
return x * x;
};
list = [1, 2, 3, 4, 5];
math = {
root: Math.sqrt,
square: square,
cube: function(x) {
return x * square(x);
}
};
race = function() {
var runners, winner;
winner = arguments[0], runners = 2 <= arguments.length ? slice.call(arguments, 1) : [];
return print(winner, runners);
};
if (typeof elvis !== "undefined" && elvis !== null) {
alert("I knew it!");
}
cubes = (function() {
var i, len, results;
results = [];
for (i = 0, len = list.length; i < len; i++) {
num = list[i];
results.push(math.cube(num));
}
return results;
})();
}).call(this);
cat(uglify_optimize(js))
(function(){var cubes,list,math,num,number,opposite,race,square,slice=[].slice;number=42,opposite=!0,opposite&&(number=-42),square=function(x){return x*x},list=[1,2,3,4,5],math={root:Math.sqrt,square:square,cube:function(x){return x*square(x)}},race=function(){var runners,winner;return winner=arguments[0],runners=2<=arguments.length?slice.call(arguments,1):[],print(winner,runners)},"undefined"!=typeof elvis&&null!==elvis&&alert("I knew it!"),cubes=function(){var i,len,results;for(results=[],i=0,len=list.length;len>i;i++)num=list[i],results.push(math.cube(num));return results}()}).call(this);
Uglify: reformatting
One of the most popular and powerful libraries for working with JavaScript code is uglify-js. Uglify provides an extensive toolkit for manipulating the syntax tree of a piece of JavaScript code.
The uglify_reformat
binding parses a string with code and then feeds it to the uglify code generator which converts it back to JavaScript text, with custom formatting options. This is nice for fixing whitespace, semicolons, etc.
code <- "function test(x, y){ x = x || 1; y = y || 1; return x*y;}"
cat(uglify_reformat(code, beautify = TRUE, indent_level = 2))
function test(x, y) {
x = x || 1;
y = y || 1;
return x * y;
}
Uglify: optimization
The more impressive part of uglify-js is the compressor which refactors the entire syntax tree, effectively rewriting your code into a more compact but equivalent program. The uglify_optimize
function in R is a simple wrapper which parses code and then feeds it to the compressor.
cat(code)
function test(x, y){ x = x || 1; y = y || 1; return x*y;}
cat(uglify_optimize(code))
function test(x,y){return x=x||1,y=y||1,x*y}
You can pass compressor options to uglify_optimize
to control the various uglify optimization techniques.
JSHint: code analysis
JSHint will automatically detect errors and potential problems in JavaScript code. The jshint
function is R will return a data frame where each row is a problem detected by the library (type, line and reason of error):
code <- "var foo = 123"
jshint(code)
id raw code evidence line character scope reason
1 (error) Missing semicolon. W033 var foo = 123 1 14 (main) Missing semicolon.
JSHint has many configuration options to control which types of code propblems it will report on.