playground: switch to using pgbundle tool

Change-Id: Iee60e95a34e2ad533d4bdd678abce9839c62c30e
diff --git a/tools/playground/builder/Dockerfile b/tools/playground/builder/Dockerfile
index eb7f08f..cb9bf3a 100644
--- a/tools/playground/builder/Dockerfile
+++ b/tools/playground/builder/Dockerfile
@@ -44,7 +44,7 @@
 # your local version of the code. This is useful when developing and testing
 # local changes to the builder tool.
 # RUN rm $VEYRON_ROOT/veyron/go/bin/builder
-# ADD builder.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/builder.go
+# ADD main.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/main.go
 # ADD identity.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/identity.go
 # ADD services.go /usr/local/veyron/veyron/go/src/veyron/tools/playground/builder/services.go
 # RUN veyron go install veyron/tools/playground/builder
diff --git a/tools/playground/builder/builder.go b/tools/playground/builder/main.go
similarity index 100%
rename from tools/playground/builder/builder.go
rename to tools/playground/builder/main.go
diff --git a/tools/playground/pgbundle/.gitignore b/tools/playground/pgbundle/.gitignore
new file mode 100644
index 0000000..93f1361
--- /dev/null
+++ b/tools/playground/pgbundle/.gitignore
@@ -0,0 +1,2 @@
+node_modules
+npm-debug.log
diff --git a/tools/playground/pgbundle/.jshintignore b/tools/playground/pgbundle/.jshintignore
new file mode 100644
index 0000000..3c3629e
--- /dev/null
+++ b/tools/playground/pgbundle/.jshintignore
@@ -0,0 +1 @@
+node_modules
diff --git a/tools/playground/pgbundle/.jshintrc b/tools/playground/pgbundle/.jshintrc
new file mode 100644
index 0000000..b264540
--- /dev/null
+++ b/tools/playground/pgbundle/.jshintrc
@@ -0,0 +1,27 @@
+{
+  "camelcase": true,
+  "eqeqeq": true,
+  "expr": true,
+  "forin": true,
+  "freeze": true,
+  "immed": true,
+  "indent": 2,
+  "latedef": "nofunc",
+  "maxlen": 80,
+  "newcap": true,
+  "noarg": true,
+  "nonbsp": true,
+  "nonew": true,
+  "quotmark": "single",
+  "sub": true,
+  "trailing": true,
+  "undef": true,
+  "unused": "vars",
+
+  "browser": true,
+  "devel": true,
+  "node": true,
+
+  "globals": {
+  }
+}
diff --git a/tools/playground/pgbundle/Makefile b/tools/playground/pgbundle/Makefile
new file mode 100644
index 0000000..43cd1c5
--- /dev/null
+++ b/tools/playground/pgbundle/Makefile
@@ -0,0 +1,11 @@
+export SHELL := /bin/bash -euo pipefail
+
+node_modules: package.json
+	npm prune
+	npm install
+	touch node_modules
+
+lint:
+	jshint .
+
+.PHONY: lint
diff --git a/tools/playground/pgbundle/bin/pgbundle b/tools/playground/pgbundle/bin/pgbundle
new file mode 100755
index 0000000..599c379
--- /dev/null
+++ b/tools/playground/pgbundle/bin/pgbundle
@@ -0,0 +1,2 @@
+#!/usr/bin/env node
+require('../index.js').run();
diff --git a/tools/playground/pgbundle/index.js b/tools/playground/pgbundle/index.js
new file mode 100644
index 0000000..a05099a
--- /dev/null
+++ b/tools/playground/pgbundle/index.js
@@ -0,0 +1,117 @@
+var _ = require('lodash');
+var fs = require('fs');
+var glob = require('glob');
+var path = require('path');
+
+// Filename to write the data to.
+var BUNDLE_NAME = 'bundle.json';
+
+module.exports = {run: run};
+
+// TODO(nlacasse): improve this.
+function usage() {
+  console.log('Usage: pgbundle [options] <path> [<path> <path> ...]');
+  console.log('Options: --verbose: Enable verbose output.');
+  process.exit(1);
+}
+
+// If the first line is "// +build OMIT", strip the line and return the
+// remaining lines.
+function stripBuildOmit(lines) {
+  if (lines[0] === '// +build OMIT') {
+    return _.rest(lines);
+  }
+  return lines;
+}
+
+// If the first line is an index comment, strip the line and return the index
+// and remaining lines.
+function getIndex(lines) {
+  var index = null;
+  var match = lines[0].match(/^\/\/\s*index=(\d+)/);
+  if (match && match[1]) {
+    index = match[1];
+    lines = _.rest(lines);
+  }
+  return {
+    index: index,
+    lines: lines
+  };
+}
+
+function shouldIgnore(fileName) {
+  // Ignore directories.
+  if (_.last(fileName) === '/') {
+    return true;
+  }
+  // Ignore bundle files.
+  if (fileName === BUNDLE_NAME) {
+    return true;
+  }
+  // Ignore generated .vdl.go files.
+  if ((/\.vdl\.go$/i).test(fileName)) {
+    return true;
+  }
+  return false;
+}
+
+// Main function.
+function run() {
+  // Get the paths from process.argv.
+  var argv = require('minimist')(process.argv.slice(2));
+  var dirs = argv._;
+
+  // Make sure there is at least one path.
+  if (!dirs || dirs.length === 0) {
+    return usage();
+  }
+
+  // Loop over each path.
+  _.each(dirs, function(dir) {
+    var subFiles = glob.sync('**', {
+      cwd: dir,
+      mark: true  // Add a '/' char to directory matches.
+    });
+
+    if (subFiles.length === 0) {
+      return usage();
+    }
+
+    var out = {files: []};
+
+    // Loop over each subfile in the path.
+    _.each(subFiles, function(fileName) {
+      if (shouldIgnore(fileName)) {
+        return;
+      }
+
+      var fullFilePath = path.resolve(dir, fileName);
+      var text = fs.readFileSync(fullFilePath, {encoding: 'utf8'});
+
+      var lines = text.split('\n');
+      lines = stripBuildOmit(lines);
+      var indexAndLines = getIndex(lines);
+      var index = indexAndLines.index;
+      lines = indexAndLines.lines;
+
+      // TODO(sadovsky): Should we put the index in the bundle? Note that we
+      // already order files by index below. The playground client currently
+      // does not use the index.
+      out.files.push({
+        name: path.basename(fileName),
+        body: lines.join('\n'),
+        index: index
+      });
+    });
+
+    out.files = _.sortBy(out.files, 'index');
+
+    // Write the bundle.json.
+    var outFile = path.resolve(dir, BUNDLE_NAME);
+    fs.writeFileSync(outFile, JSON.stringify(out));
+
+    if (argv.verbose) {
+      console.log('Wrote ' + outFile);
+    }
+  });
+}
diff --git a/tools/playground/pgbundle/package.json b/tools/playground/pgbundle/package.json
new file mode 100644
index 0000000..71ef866
--- /dev/null
+++ b/tools/playground/pgbundle/package.json
@@ -0,0 +1,20 @@
+{
+  "name": "pgbundle",
+  "description": "Playground file bundler",
+  "version": "0.0.1",
+  "bin": "./bin/pgbundle",
+  "bugs": {
+    "url": "https://github.com/veyron/pgbundle/issues"
+  },
+  "dependencies": {
+    "glob": "^4.0.6",
+    "lodash": "^2.4.1",
+    "minimist": "^1.1.0"
+  },
+  "homepage": "https://github.com/veyron/pgbundle",
+  "private": true,
+  "repository": {
+    "type": "git",
+    "url": "https://github.com/veyron/pgbundle.git"
+  }
+}
diff --git a/tools/playground/test.sh b/tools/playground/test.sh
index 2d99198..04f5236 100755
--- a/tools/playground/test.sh
+++ b/tools/playground/test.sh
@@ -4,54 +4,55 @@
 
 source "${VEYRON_ROOT}/scripts/lib/shell_test.sh"
 
+# Installs the veyron.js library and makes it accessible to javascript files in
+# the veyron playground test folder under the module name 'veyron'.
 install_veyron_js() {
-  # This installs the veyron.js library, and makes it accessable to javascript
-  # files in the veyron playground test folder under the module name 'veyron'.
-  #
-  # TODO(nlacasse): Once veyron.js is installed in a public npm registry, this
-  # should be replaced with just "npm install veyron".
+  # TODO(nlacasse): Once veyron.js is publicly available in npm, replace this
+  # with "npm install veyron".
   pushd "${VEYRON_ROOT}/veyron.js"
   "${VEYRON_ROOT}/environment/cout/node/bin/npm" link
   popd
   "${VEYRON_ROOT}/environment/cout/node/bin/npm" link veyron
 }
 
+# Installs the pgbundle tool.
+install_pgbundle() {
+  pushd "${VEYRON_ROOT}/veyron/go/src/veyron.io/veyron/veyron/tools/playground/pgbundle"
+  "${VEYRON_ROOT}/environment/cout/node/bin/npm" link
+  popd
+  "${VEYRON_ROOT}/environment/cout/node/bin/npm" link pgbundle
+}
+
 build() {
-  veyron go build veyron.io/veyron/veyron/tools/identity || shell_test::fail "line ${LINENO}: failed to build 'identity'"
-  veyron go build veyron.io/veyron/veyron/services/proxy/proxyd || shell_test::fail "line ${LINENO}: failed to build 'proxyd'"
-  veyron go build veyron.io/veyron/veyron/services/mounttable/mounttabled || shell_test::fail "line ${LINENO}: failed to build 'mounttabled'"
+  # Note that "go build" puts built binaries in $(pwd), but only if they are
+  # built one at a time. So much for the principle of least surprise...
+  local -r V="veyron.io/veyron/veyron"
+  veyron go build $V/tools/identity || shell_test::fail "line ${LINENO}: failed to build 'identity'"
+  veyron go build $V/services/proxy/proxyd || shell_test::fail "line ${LINENO}: failed to build 'proxyd'"
+  veyron go build $V/services/mounttable/mounttabled || shell_test::fail "line ${LINENO}: failed to build 'mounttabled'"
+  veyron go build $V/tools/playground/builder || shell_test::fail "line ${LINENO}: failed to build 'builder'"
   veyron go build veyron.io/veyron/veyron2/vdl/vdl || shell_test::fail "line ${LINENO}: failed to build 'vdl'"
-  veyron go build veyron.io/veyron/veyron/tools/playground/builder || shell_test::fail "line ${LINENO}: failed to build 'builder'"
-  veyron go build veyron.io/veyron/veyron/tools/playground/testdata/escaper || shell_test::fail "line ${LINENO}: failed to build 'escaper'"
   veyron go build veyron.io/wspr/veyron/services/wsprd || shell_test::fail "line ${LINENO}: failed to build 'wsprd'"
 }
 
 test_with_files() {
-  echo '{"Files":[' > request.json
-  while [[ "$#" > 0 ]]; do
-    echo '{"Name":"'"$(basename $1)"'","Body":' >>request.json
-    grep -v OMIT "$1" | ./escaper >>request.json
-    shift
-    if [[ "$#" > 0 ]]; then
-      echo '},' >>request.json
-    else
-      echo '}' >>request.json
-    fi
-  done
-  echo ']}' >>request.json
+  local -r TESTDIR=$(shell::tmp_dir)
+  cp "$@" ${TESTDIR}
+  ./node_modules/.bin/pgbundle ${TESTDIR}
   rm -f builder.out
-  ./builder <request.json 2>&1 | tee builder.out
+  ./builder < "${TESTDIR}/bundle.json" 2>&1 | tee builder.out
 }
 
 main() {
   cd $(shell::tmp_dir)
   build
   install_veyron_js
+  install_pgbundle
 
   local -r DIR="${VEYRON_ROOT}/veyron/go/src/veyron.io/veyron/veyron/tools/playground/testdata"
 
-  export GOPATH="$(pwd)":"$(veyron env GOPATH)"
-  export VDLPATH="$(pwd)":"$(veyron env VDLPATH)"
+  export GOPATH="$(pwd):$(veyron env GOPATH)"
+  export VDLPATH="$(pwd):$(veyron env VDLPATH)"
   export PATH="$(pwd):${PATH}"
 
   # Test without identities
@@ -88,9 +89,7 @@
   grep -q "ipc: not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded"
 
   test_with_files "${DIR}/pong/pong.js" "${DIR}/ping/ping.js" "${DIR}/ids/expired.id" || shell_test::fail  "line ${LINENO}: failed to build with expired id (js -> js)"
-  # TODO(nlacasse): The error message in this case is very bad. Clean up the
-  # veyron.js errors and change this to something reasonable.
-  grep -q "vError.InternalError" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded"
+  grep -q "ipc: not authorized" builder.out || shell_test::fail "line ${LINENO}: rpc with expired id succeeded"
 
   # Test with unauthorized identities
 
diff --git a/tools/playground/testdata/escaper/main.go b/tools/playground/testdata/escaper/main.go
deleted file mode 100644
index 9f867e1..0000000
--- a/tools/playground/testdata/escaper/main.go
+++ /dev/null
@@ -1,19 +0,0 @@
-package main
-
-import (
-	"encoding/json"
-	"io/ioutil"
-	"os"
-)
-
-func main() {
-	data, err := ioutil.ReadAll(os.Stdin)
-	if err != nil {
-		panic(err)
-	}
-	out, err := json.Marshal(string(data))
-	if err != nil {
-		panic(err)
-	}
-	os.Stdout.Write(out)
-}