Convoluted JavaScript Pretty Printing
Published on 2016-05-07
Let’s put together a command line JavaScript pretty printer by relying on nixpkgs! But first something completely different.
The following .nixpkgs/config.nix
snippet is useful for creating command line programs from jar files which contains a Main-Class refered by MANIFEST.MF. We will go through it shortly.
pkgs: {
packageOverrides = self: with self; let
mkJarApp = { pname, version, jar }:
stdenv.mkDerivation rec {
name = pname + "-" + version;
src = null;
phases = [ "installPhase" ];
installPhase = let
app = writeScriptBin "${pname}" ''
#!${stdenv.shell}
${jdk}/bin/java -jar ${jdk} $@
'';
in
''
mkdir -p $out/bin
ln -s ${app}/bin/${pname} $out/bin/${pname}
'';
};
in
{
raccoon = mkJarApp rec {
pname = "raccoon";
version = "3.5";
jar = let
name = pname + "-" + version;
in fetchurl {
url = "http://www.onyxbits.de/sites/default/files/download/25/${name}.jar";
sha256 = "15m0nmvyyqkg3y0bspxzz3chkq5lw2l0lbzlnjyxbjw23nxfxqc9";
};
};
}
}
In this example we have:
- A function
packageOverrides
which takes an argumentself
,self
being the previous set of packages. with self;
introduces all of the contained attributes ofself
into the declaring set, i.e. we can reference regular packages while not having to specifyself
as a prefix attribute path each time, compareself.jdk
to justjdk
.- We use
let
to introduce a functionmkJarApp
which takes a set of attributes, in this case the set is final and the values in the set are bound to the specific ariates names, like “pname”.mkJarApp
creates a nix derivation around a shell script which relies on the depending attributejdk
to run the jar file injar
and pass any command line arguments along to the java main class.- We rely on the standard environment’s shell(
${stdenv.shell}
) as our interpreter. - We use the
phases
instdenv.mkDerivation
to inform the standard builder to only runinstallPhase
- We use let binding to introduce
app
into theinstallPhase
scope.
- Finally we show the
mkJarApp
usage using the jar file for raccoon. Since the argument set formkJarApp
is closed, i.e. doesn’t specify an ellipsis(...
), we use another let binding to introduce the name attribute for thefetchurl
rhs clause.- We use
rec
to introducepname
andversion
into the right hand side of the raccoon set.
- We use
Ok, that is great! We can now wrap BigJars, but what has this to do with JavaScript pretty printing? Well.. just for the heck of mixing different technologies we’re going to rely on the rhino JavaScript engine.
Fortunately for us the rhino
attribute is already in nixpkgs, sadly the derivation bound at the attribute doesn’t contain a shell wrapper. So we need to fix that by wrapping the package jar file. So we add the following snippet to our .nixpkgs/config.nix
overridePackages
set:
rhinosh = mkJarApp {
pname = "rhinosh";
version = "1.7R2";
jar = "${rhino}/share/java/js.jar";
};
And we have a shell wrapper for rhino. Now we need a bit of domain knowledge about the rhino runtime.
- It defines a
load
function which can load another JavaScript file into its runtime. - It defines a
print
function which outputs its argument to stdout. - It defines an array
arguments
which is zero indexed but, unlike the shell arguments, doesn’t contain the executable name. - It defines a
readFile
which takes a path and returns the contents into a string.
Still no way to actually do the pretty printing… Again we’re fortunate that there exists a project called js-beautify which helps us with exactly that. We just need to fetch its pretty printer from the js-beautify repo in our derivation. Now we can define a jspretty
package attribute which wraps rhinosh
with a JavaScript loader and js-beautify
. The only domain knowledge we need about js-beautify is that it attaches its js_beautify
function to global
in the rhino runtime.
jspretty = stdenv.mkDerivation rec {
name = pname + "-" + version;
pname = "jspretty";
version = "0.0.1";
src = null;
phases = [ "installPhase" ];
installPhase = let
beauty = fetchurl {
url = https://raw.githubusercontent.com/beautify-web/js-beautify/bdb91a2c307cc94c6860cedd4c0149c28762e82d/js/lib/beautify.js;
sha256 = "1gwf3s37h5a35b2vx33hz26wahbsa84n5sgb5nlwj2g0dnicl96j";
};
loader = writeTextFile { name = "loader.js"; text = ''
load(arguments[0])
print(global.js_beautify(readFile(arguments[1])));
'';};
app = writeScriptBin "${pname}" ''
#!${stdenv.shell}
${rhinosh}/bin/rhinosh ${loader} ${beauty} $@
'';
in
''
mkdir -p $out/bin
ln -s ${app}/bin/${pname} $out/bin/${pname}
'';
};
We give our jspretty
script a version and a name.. We have no sources so we set the derivation src
attribute to null
. Again we only care about the installPhase
and we introduce some attributes into its rhs scope:
beauty
points to the JavaScript pretty printer source file.loader
is a JavaScript loader file we usewriteTextFile
to get into nix store. It purpose is to load the js-beautify library JavaScript file, a target JavaScript file and apply thejs_beautify
function to the target, printing the result to stdout.app
is the target shell script which actually calls rhino with js-beautify and our target JavaScript.app
andloader
could of course be parametrised in order to create different JavaScript pretty printers for different conventions, intentionally left as an exercise for the reader :)
Finally we can install jspretty
into a profile:
usr@lcl $ nix-env -iA jspretty
Which just says that we define an command to be run just before the buffer is written to disk, that command being our jspretty shell script fed with the file. Activating the profile, gives us access to the jspretty shell script.
usr@lcl $ jspretty ugly.js > pretty.js