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
packageOverrideswhich takes an argumentself,selfbeing the previous set of packages. with self;introduces all of the contained attributes ofselfinto the declaring set, i.e. we can reference regular packages while not having to specifyselfas a prefix attribute path each time, compareself.jdkto justjdk.- We use
letto introduce a functionmkJarAppwhich 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”.mkJarAppcreates a nix derivation around a shell script which relies on the depending attributejdkto run the jar file injarand 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
phasesinstdenv.mkDerivationto inform the standard builder to only runinstallPhase - We use let binding to introduce
appinto theinstallPhasescope.
- Finally we show the
mkJarAppusage using the jar file for raccoon. Since the argument set formkJarAppis closed, i.e. doesn’t specify an ellipsis(...), we use another let binding to introduce the name attribute for thefetchurlrhs clause.- We use
recto introducepnameandversioninto 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
loadfunction which can load another JavaScript file into its runtime. - It defines a
printfunction which outputs its argument to stdout. - It defines an array
argumentswhich is zero indexed but, unlike the shell arguments, doesn’t contain the executable name. - It defines a
readFilewhich 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:
beautypoints to the JavaScript pretty printer source file.loaderis a JavaScript loader file we usewriteTextFileto get into nix store. It purpose is to load the js-beautify library JavaScript file, a target JavaScript file and apply thejs_beautifyfunction to the target, printing the result to stdout.appis the target shell script which actually calls rhino with js-beautify and our target JavaScript.appandloadercould 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 jsprettyWhich 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