playground/client: Fortune example: generate all go/js combinations.

pgbundle now bundles files matching globs from a config file
and writes the bundle to stdout.
Added glob files for all combinations of go/js for fortune
client/server.

Change-Id: Id871efa6a39eac6e02898ffa27eda80d29ad104f
diff --git a/client/.gitignore b/client/.gitignore
index f587daf..709f25d 100644
--- a/client/.gitignore
+++ b/client/.gitignore
@@ -1,7 +1,7 @@
 bundles/*/*/bin
 bundles/*/*/pkg
 src/example_bundles
-bundle.json
+bundle_*.json
 node_modules
 npm-debug.log
 build
diff --git a/client/Makefile b/client/Makefile
index d4fdc65..494248a 100644
--- a/client/Makefile
+++ b/client/Makefile
@@ -31,18 +31,29 @@
 	mkdir -p $(@D)
 	browserify -d $< -p [ minifyify --map $(@F).map --output $@.map ] -o $@
 
-# All paths of the form bundles/<project>/<example>.
-example_code_bundle_dirs := $(shell find bundles -maxdepth 2 -mindepth 2)
-example_code_files := $(shell find bundles -mindepth 2)
+# Each profile (glob file with file patterns to include) from
+# `bundles/<profile>.bundle` is applied to each example folder of the
+# form `bundles/<example>/`.
+example_profiles := $(shell find bundles -maxdepth 1 -name "*.bundle")
+example_code_bundle_dirs := $(shell find bundles -maxdepth 1 -mindepth 1 -type d)
+example_files := $(shell find bundles -mindepth 1)
+bundle_temp_file := build/bundle.json
 
-# Builds the playground bundles for the examples.
+# Builds the playground bundles for each folder and profile combination.
 # This is an empty target.
 # See http://www.gnu.org/software/make/manual/html_node/Empty-Targets.html
-# This task depends on example_code_files because we want to re-bundle if any of
-# those change. However, the bundle tool works on directories, so we pass in
-# example_code_bundle_dirs as the argument.
-src/example_bundles: $(example_code_files) node_modules
-	pgbundle $(example_code_bundle_dirs)
+# This task depends on example_profiles and example_files because we want
+# to re-bundle if any of those change. However, the bundle tool works on
+# directories, so we pass in example_code_bundle_dirs as the path argument.
+src/example_bundles: $(example_profiles) $(example_files) node_modules
+	mkdir -p $(dir $(bundle_temp_file))
+	@for profile in $(example_profiles); do \
+			echo "Bundling with profile \"$${profile}\""; \
+				for folder in $(example_code_bundle_dirs); do \
+				pgbundle --verbose "$${profile}" "$${folder}" > $(bundle_temp_file); \
+				mv -f $(bundle_temp_file) "$${folder}/bundle_$$(basename $${profile} .bundle).json"; \
+			done; \
+		done
 	touch $@
 
 node_modules: package.json
@@ -70,7 +81,7 @@
 distclean: clean
 	@npm cache clean
 	@$(RM) -rf src/example_bundles
-	@$(RM) -rf $(shell find bundles -name "bundle.json")
+	@$(RM) -rf $(shell find bundles -name "bundle_*.json")
 
 .PHONY: lint
 lint:
diff --git a/client/README.md b/client/README.md
index 84d9816..304406d 100644
--- a/client/README.md
+++ b/client/README.md
@@ -3,7 +3,7 @@
 Source code for the Vanadium playground web client.
 
 * `build` - Temporary directory used for building the client.
-* `bundles` - Default playground examples. Organized as `bundles/<group>/<example>/`.
+* `bundles` - Default playground examples. Each combination of directory and `.bundle` file forms a bundle.
 * _Makefile_ - Targets for building the client (browserifying Javascript, etc.)
 * `node_modules` - Disposable directory created by `npm install` - dependency modules.
 * _package.json_ - Used by `npm install` to grab playground dependencies.
diff --git a/client/bundles/fortune/ex0_go/src/fortune/fortune.vdl b/client/bundles/fortune/ex0_go/src/fortune/fortune.vdl
deleted file mode 100644
index a823cab..0000000
--- a/client/bundles/fortune/ex0_go/src/fortune/fortune.vdl
+++ /dev/null
@@ -1,10 +0,0 @@
-// index=3
-package fortune
-
-type Fortune interface {
-	// Returns a random fortune.
-	Get() (Fortune string | error)
-
-	// Adds a fortune to the set used by Get().
-	Add(Fortune string) error
-}
diff --git a/client/bundles/fortune/ex0_go/src/client/client.go b/client/bundles/fortune/src/client/client.go
similarity index 83%
rename from client/bundles/fortune/ex0_go/src/client/client.go
rename to client/bundles/fortune/src/client/client.go
index e2087ac..32407a2 100644
--- a/client/bundles/fortune/ex0_go/src/client/client.go
+++ b/client/bundles/fortune/src/client/client.go
@@ -20,7 +20,7 @@
 
 	// Create a new stub that binds to address without
 	// using the name service.
-	stub := fortune.FortuneClient("fortune")
+	stub := fortune.FortuneClient("bakery/cookie/fortune")
 
 	// Issue a Get() RPC.
 	// We do this in a loop to give the server time to start up.
@@ -28,7 +28,7 @@
 	var fortune string
 	for {
 		var err error
-		if fortune, err = stub.Get(ctx); err == nil {
+		if fortune, err = stub.GetRandomFortune(ctx); err == nil {
 			break
 		}
 		fmt.Printf("%v\nRetrying in 100ms...\n", err)
diff --git a/client/bundles/fortune/ex0_js/src/client/client.js b/client/bundles/fortune/src/client/client.js
similarity index 100%
rename from client/bundles/fortune/ex0_js/src/client/client.js
rename to client/bundles/fortune/src/client/client.js
diff --git a/client/bundles/fortune/ex0_js/src/fortune/fortune.vdl b/client/bundles/fortune/src/fortune/fortune.vdl
similarity index 100%
rename from client/bundles/fortune/ex0_js/src/fortune/fortune.vdl
rename to client/bundles/fortune/src/fortune/fortune.vdl
diff --git a/client/bundles/fortune/ex0_go/src/server/server.go b/client/bundles/fortune/src/server/server.go
similarity index 83%
rename from client/bundles/fortune/ex0_go/src/server/server.go
rename to client/bundles/fortune/src/server/server.go
index 96620ee..93d906c 100644
--- a/client/bundles/fortune/ex0_go/src/server/server.go
+++ b/client/bundles/fortune/src/server/server.go
@@ -39,13 +39,13 @@
 }
 
 // Methods that get called by RPC requests.
-func (f *fortuned) Get(_ ipc.ServerCall) (Fortune string, err error) {
+func (f *fortuned) GetRandomFortune(_ ipc.ServerCall) (Fortune string, err error) {
 	Fortune = f.fortunes[f.random.Intn(len(f.fortunes))]
 	fmt.Printf("Serving: %s\n", Fortune)
 	return Fortune, nil
 }
 
-func (f *fortuned) Add(_ ipc.ServerCall, Fortune string) error {
+func (f *fortuned) AddNewFortune(_ ipc.ServerCall, Fortune string) error {
 	fmt.Printf("Adding: %s\n", Fortune)
 	f.fortunes = append(f.fortunes, Fortune)
 	return nil
@@ -74,8 +74,8 @@
 	}
 
 	// Start the fortune server at "fortune".
-	if err := server.Serve("fortune", fortuneServer, vflag.NewAuthorizerOrDie()); err == nil {
-		fmt.Printf("Fortune server serving under: fortune\n")
+	if err := server.Serve("bakery/cookie/fortune", fortuneServer, vflag.NewAuthorizerOrDie()); err == nil {
+		fmt.Printf("Fortune server serving under: bakery/cookie/fortune\n")
 	} else {
 		vlog.Panic("error serving fortune server: ", err)
 	}
diff --git a/client/bundles/fortune/ex0_js/src/server/server.js b/client/bundles/fortune/src/server/server.js
similarity index 100%
rename from client/bundles/fortune/ex0_js/src/server/server.js
rename to client/bundles/fortune/src/server/server.js
diff --git a/client/bundles/go.bundle b/client/bundles/go.bundle
new file mode 100644
index 0000000..93e083b
--- /dev/null
+++ b/client/bundles/go.bundle
@@ -0,0 +1,3 @@
+*.vdl
+client/**/*.go
+server/**/*.go
diff --git a/client/bundles/go_js.bundle b/client/bundles/go_js.bundle
new file mode 100644
index 0000000..dd4fb3e
--- /dev/null
+++ b/client/bundles/go_js.bundle
@@ -0,0 +1,3 @@
+*.vdl
+client/**/*.go
+server/**/*.js
diff --git a/client/bundles/js.bundle b/client/bundles/js.bundle
new file mode 100644
index 0000000..ae990f5
--- /dev/null
+++ b/client/bundles/js.bundle
@@ -0,0 +1,3 @@
+*.vdl
+client/**/*.js
+server/**/*.js
diff --git a/client/bundles/js_go.bundle b/client/bundles/js_go.bundle
new file mode 100644
index 0000000..33edac3
--- /dev/null
+++ b/client/bundles/js_go.bundle
@@ -0,0 +1,3 @@
+*.vdl
+client/**/*.js
+server/**/*.go
diff --git a/client/lib/shell/pg_test_util.sh b/client/lib/shell/pg_test_util.sh
index ee8de06..73f5ac3 100755
--- a/client/lib/shell/pg_test_util.sh
+++ b/client/lib/shell/pg_test_util.sh
@@ -55,19 +55,25 @@
 
 # Bundles a playground example and tests it using builder.
 # $1: root directory of example to test
-# $2: arguments to call builder with
+# $2: glob file with file patterns to bundle from $1
+# $3: arguments to call builder with
 test_pg_example() {
   local -r PGBUNDLE_DIR="$1"
-  local -r BUILDER_ARGS="$2"
+  local -r PATTERN_FILE="$2"
+  local -r BUILDER_ARGS="$3"
 
-  ./node_modules/.bin/pgbundle "${PGBUNDLE_DIR}"
+  # Create a fresh dir to save the bundle and run builder in.
+  local -r TEMP_DIR=$(shell::tmp_dir)
 
-  # Create a fresh dir to run builder in.
+  ./node_modules/.bin/pgbundle --verbose "${PATTERN_FILE}" "${PGBUNDLE_DIR}" > "${TEMP_DIR}/test.json" || return
+
   local -r ORIG_DIR=$(pwd)
-  pushd $(shell::tmp_dir)
+  pushd "${TEMP_DIR}"
+
   ln -s "${ORIG_DIR}/node_modules" ./  # for release/javascript/core
-  "${shell_test_BIN_DIR}/builder" ${BUILDER_ARGS} < "${PGBUNDLE_DIR}/bundle.json" 2>&1 | tee builder.out
+  "${shell_test_BIN_DIR}/builder" ${BUILDER_ARGS} < "test.json" 2>&1 | tee builder.out
   # Move builder output to original dir for verification.
   mv builder.out "${ORIG_DIR}"
+
   popd
 }
diff --git a/client/src/javascript/index.js b/client/src/javascript/index.js
index 7fb0f38..e40017e 100644
--- a/client/src/javascript/index.js
+++ b/client/src/javascript/index.js
@@ -5,16 +5,16 @@
 var Playground = require('./playground');
 
 _.forEach(document.querySelectorAll('.playground'), function(el) {
-  var srcdir = el.getAttribute('data-srcdir');
-  console.log('Creating playground', srcdir);
+  var src = el.getAttribute('data-src');
+  console.log('Creating playground', src);
 
-  fetchBundle(srcdir, function(err, bundle) {
+  fetchBundle(src, function(err, bundle) {
     if (err) {
       el.innerHTML = '<div class="error"><p>Playground error.' +
-        '<br>Bundle not found: <strong>' + srcdir + '</strong></p></div>';
+        '<br>Bundle not found: <strong>' + src + '</strong></p></div>';
       return;
     }
-    new Playground(el, srcdir, bundle);  // jshint ignore:line
+    new Playground(el, src, bundle);  // jshint ignore:line
   });
 });
 
@@ -22,7 +22,7 @@
   var basePath = '/bundles';
   console.log('Fetching bundle', loc);
   superagent
-    .get(path.join(basePath, loc, 'bundle.json'))
+    .get(path.join(basePath, loc))
     .accept('json')
     .end(function(err, res) {
       if (err) {
diff --git a/client/src/static/go/index.html b/client/src/static/go/index.html
index c529f7c..1eea88b 100644
--- a/client/src/static/go/index.html
+++ b/client/src/static/go/index.html
@@ -8,7 +8,7 @@
   <body>
     <main>
       <h1>Hello, go playground!</h1>
-      <div class="lang-go playground" data-srcdir="/fortune/ex0_go"></div>
+      <div class="lang-go playground" data-src="/fortune/bundle_go.json"></div>
     </main>
   </body>
 </html>
diff --git a/client/src/static/go_js/index.html b/client/src/static/go_js/index.html
new file mode 100644
index 0000000..a632f91
--- /dev/null
+++ b/client/src/static/go_js/index.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Playground</title>
+    <link rel="stylesheet" href="/stylesheets/playground.css">
+    <script src="/playground.js" async></script>
+  </head>
+  <body>
+    <main>
+      <h1>Hello, go-js playground!</h1>
+      <div class="lang-go lang-js playground" data-src="/fortune/bundle_go_js.json"></div>
+    </main>
+  </body>
+</html>
diff --git a/client/src/static/index.html b/client/src/static/index.html
index 8b9752e..ac3bad8 100644
--- a/client/src/static/index.html
+++ b/client/src/static/index.html
@@ -10,6 +10,11 @@
         <li><a href="./go">Go</a></li>
         <li><a href="./js">Javascript</a></li>
       </ul>
+      <h3>More challenge?</h3>
+      <ul>
+        <li><a href="./go_js">Go client, Javascript server</a></li>
+        <li><a href="./js_go">Javascript client, Go server</a></li>
+      </ul>
     </main>
   </body>
 </html>
diff --git a/client/src/static/js/index.html b/client/src/static/js/index.html
index 2592f82..4a3b9f6 100644
--- a/client/src/static/js/index.html
+++ b/client/src/static/js/index.html
@@ -8,7 +8,8 @@
   <body>
     <main>
       <h1>Hello, js playground!</h1>
-      <div class="lang-js playground" data-srcdir="/fortune/ex0_js"></div>
+      <!-- lang-go needed for vdl -->
+      <div class="lang-js lang-go playground" data-src="/fortune/bundle_js.json"></div>
     </main>
   </body>
 </html>
diff --git a/client/src/static/js_go/index.html b/client/src/static/js_go/index.html
new file mode 100644
index 0000000..4b97867
--- /dev/null
+++ b/client/src/static/js_go/index.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+  <head>
+    <title>Playground</title>
+    <link rel="stylesheet" href="/stylesheets/playground.css">
+    <script src="/playground.js" async></script>
+  </head>
+  <body>
+    <main>
+      <h1>Hello, js-go playground!</h1>
+      <div class="lang-js lang-go playground" data-src="/fortune/bundle_js_go.json"></div>
+    </main>
+  </body>
+</html>
diff --git a/client/test.sh b/client/test.sh
index 5ebb503..d66b476 100755
--- a/client/test.sh
+++ b/client/test.sh
@@ -4,7 +4,7 @@
 # If any new examples are added, they should be appended to $EXAMPLES below.
 
 # To debug playground compile errors you can build examples locally, e.g.:
-# $ cd bundles/fortune/ex0_go/src
+# $ cd bundles/fortune/src
 # $ GOPATH=$(dirname $(pwd)) VDLPATH=$(dirname $(pwd)) v23 go install ./...
 
 # v.io/core/shell/lib/shell_test.sh sourced via playground/lib/pg_test_util.sh
@@ -22,15 +22,18 @@
 
   local -r PG_BUNDLES_DIR="${PLAYGROUND_ROOT}/client/bundles"
 
-  local -r EXAMPLES="fortune/ex0_go fortune/ex0_js"
+  local -r EXAMPLES="fortune"
 
-  for e in $EXAMPLES; do
-    local d="${PG_BUNDLES_DIR}/${e}"
-    echo -e "\n\n>>>>> Test ${d}\n\n"
-    test_pg_example "${d}" "-v=true --runTimeout=5s" || shell_test::fail "${d}: failed to run"
-    # TODO(sadovsky): Make this "clean exit" check more robust.
-    grep -q "\"Exited cleanly.\"" builder.out || shell_test::fail "${d}: did not exit cleanly"
-    rm -f builder.out
+  for e in ${EXAMPLES}; do
+    for p in "${PG_BUNDLES_DIR}"/*.bundle; do
+      local d="${PG_BUNDLES_DIR}/${e}"
+      local description="${e} with $(basename ${p})"
+      echo -e "\n\n>>>>> Test ${description}\n\n"
+      test_pg_example "${d}" "${p}" "-v=true --runTimeout=5s" || shell_test::fail "${description}: failed to run"
+      # TODO(sadovsky): Make this "clean exit" check more robust.
+      grep -q "\"Exited cleanly.\"" builder.out || shell_test::fail "${description}: did not exit cleanly"
+      rm -f builder.out
+    done
   done
 
   shell_test::pass
diff --git a/go/src/playground/playground_v23_test.go b/go/src/playground/playground_v23_test.go
index 6150d3e..7b4057a 100644
--- a/go/src/playground/playground_v23_test.go
+++ b/go/src/playground/playground_v23_test.go
@@ -1,8 +1,11 @@
 package playground_test
 
 import (
+	"bytes"
+	"io/ioutil"
 	"os"
 	"path/filepath"
+	"strings"
 
 	_ "v.io/x/ref/profiles"
 	"v.io/x/ref/test/v23tests"
@@ -36,21 +39,19 @@
 
 // Bundles a playground example and tests it using builder.
 // - dir is the root directory of example to test
+// - globFile is the path to the glob file with file patterns to use from dir
 // - args are the arguments to call builder with
-func runPGExample(i *v23tests.T, dir string, args ...string) *v23tests.Invocation {
-	i.Run("./node_modules/.bin/pgbundle", dir)
+func runPGExample(i *v23tests.T, globFile, dir string, args ...string) *v23tests.Invocation {
+	bundle := i.Run("./node_modules/.bin/pgbundle", "--verbose", globFile, dir)
+
 	tmp := i.NewTempDir()
 	cwd := i.Pushd(tmp)
+	defer i.Popd()
 	old := filepath.Join(cwd, "node_modules")
 	if err := os.Symlink(old, filepath.Join(".", filepath.Base(old))); err != nil {
 		i.Fatalf("%s: symlink: failed: %v", i.Caller(2), err)
 	}
-	bundleName := filepath.Join(dir, "bundle.json")
 
-	stdin, err := os.Open(bundleName)
-	if err != nil {
-		i.Fatalf("%s: open(%s) failed: %v", i.Caller(2), bundleName, err)
-	}
 	// TODO(ivanpi): move this out so it only gets invoked once even though
 	// the binary is cached.
 	builderBin := i.BuildGoPkg("playground/builder")
@@ -59,22 +60,18 @@
 	if path := os.Getenv("PATH"); len(path) > 0 {
 		PATH += ":" + path
 	}
-	defer i.Popd()
+	stdin := bytes.NewBufferString(bundle)
 	return builderBin.WithEnv(PATH).WithStdin(stdin).Start(args...)
 }
 
-// Sets up a directory with the given files, then runs builder.
+// Sets up a glob file with the given files, then runs builder.
 func testWithFiles(i *v23tests.T, pgRoot string, files ...string) *v23tests.Invocation {
 	testdataDir := filepath.Join(pgRoot, "testdata")
-	pgBundleDir := i.NewTempDir()
-	for _, f := range files {
-		fdir := filepath.Join(pgBundleDir, filepath.Dir(f))
-		if err := os.MkdirAll(fdir, 0755); err != nil {
-			i.Fatalf("%s: mkdir(%q): failed: %v", i.Caller(1), fdir, err)
-		}
-		i.Run("/bin/cp", filepath.Join(testdataDir, f), fdir)
+	globFile := filepath.Join(i.NewTempDir(), "test.bundle")
+	if err := ioutil.WriteFile(globFile, []byte(strings.Join(files, "\n")+"\n"), 0644); err != nil {
+		i.Fatalf("%s: write(%q): failed: %v", i.Caller(1), globFile, err)
 	}
-	return runPGExample(i, pgBundleDir, "-v=true", "--includeV23Env=true", "--runTimeout=5s")
+	return runPGExample(i, globFile, testdataDir, "-v=true", "--includeV23Env=true", "--runTimeout=5s")
 }
 
 func V23TestPlayground(i *v23tests.T) {
diff --git a/go/src/playground/test.sh b/go/src/playground/test.sh
index 043efb6..4bd6378 100755
--- a/go/src/playground/test.sh
+++ b/go/src/playground/test.sh
@@ -6,19 +6,15 @@
 # (shell_test.sh has side effects, should not be sourced again)
 source "$(go list -f {{.Dir}} playground)/../../../client/lib/shell/pg_test_util.sh"
 
-# Sets up a directory with the given files, then runs builder.
+# Sets up a glob file with the given files, then runs builder.
 test_with_files() {
   local -r TESTDATA_DIR="$(go list -f {{.Dir}} playground)/testdata"
 
-  # Write input files to a fresh dir before bundling and running them.
-  local -r PGBUNDLE_DIR=$(shell::tmp_dir)
-  for f in $@; do
-    fdir="${PGBUNDLE_DIR}/$(dirname ${f})"
-    mkdir -p "${fdir}"
-    cp "${TESTDATA_DIR}/${f}" "${fdir}/"
-  done
+  # Write input file paths to the glob file.
+  local -r CONFIG_FILE="$(shell::tmp_dir)/test.bundle"
+  echo "$*" | tr ' ' '\n' > "${CONFIG_FILE}"
 
-  test_pg_example "${PGBUNDLE_DIR}" "-v=true --includeV23Env=true --runTimeout=5s"
+  test_pg_example "${TESTDATA_DIR}" "${CONFIG_FILE}" "-v=true --includeV23Env=true --runTimeout=5s"
 }
 
 main() {
diff --git a/pgbundle/index.js b/pgbundle/index.js
index bf8428a..4b9bf98 100644
--- a/pgbundle/index.js
+++ b/pgbundle/index.js
@@ -3,15 +3,19 @@
 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.');
+  console.error('Usage: pgbundle [options] <glob_file> <root_path>');
+  console.error('Arguments: <glob_file>: Path to file containing a list of' +
+    ' glob patterns, one per line. The bundle includes only files with path' +
+    ' suffixes matching one of the globs. Each glob must match at least one' +
+    ' file, otherwise bundling fails with a non-zero exit code.');
+  console.error('           <root_path>: Path to directory where files' +
+    ' matching glob patterns are taken from.');
+  console.error('Options: --verbose: Enable verbose output.');
+  console.error('         --empty: Omit file contents in bundle, include only' +
+    ' paths and metadata.');
   process.exit(1);
 }
 
@@ -47,87 +51,80 @@
   };
 }
 
-function shouldIgnore(fileName) {
-  // Ignore directories.
-  if (_.last(fileName) === '/') {
-    return true;
-  }
-  // Ignore bundle files.
-  if (fileName === BUNDLE_NAME) {
-    return true;
-  }
-  // Ignore generated .vdl.go and .vdl.js files.
-  if ((/\.vdl\.(go|js)$/i).test(fileName)) {
-    return true;
-  }
-  // Ignore files inside "bin" and "pkg" directories.
-  if (fileName.indexOf('bin/') === 0 ||
-      fileName.indexOf('pkg/') === 0) {
-    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._;
+  // Get the flags and positional arguments from process.argv.
+  var argv = require('minimist')(process.argv.slice(2), {
+    boolean: ['verbose', 'empty']
+  });
 
-  // Make sure there is at least one path.
-  if (!dirs || dirs.length === 0) {
+  // Make sure the glob file and the root path path are specified.
+  if (!argv._ || argv._.length !== 2) {
     return usage();
   }
 
-  // Loop over each path.
-  _.each(dirs, function(dir) {
-    var relpaths = glob.sync('**', {
+  var globFile = argv._[0];
+  var dir = argv._[1];
+  // Read glob file, filtering out empty lines.
+  var patterns = _.filter(
+    fs.readFileSync(globFile, {encoding: 'utf8'}).split('\n'));
+  // The root path must be a directory.
+  if (!fs.lstatSync(dir).isDirectory()) {
+    return usage();
+  }
+
+  var unmatched = [];
+
+  // Apply each glob pattern to the directory.
+  var relpaths = _.flatten(_.map(patterns, function(pattern) {
+    var match = glob.sync('**/' + pattern, {
       cwd: dir,
-      mark: true  // Add a '/' char to directory matches.
+      nodir: true
     });
-
-    if (relpaths.length === 0) {
-      return usage();
+    if (match.length === 0) {
+      unmatched.push(pattern);
     }
+    return match;
+  }));
 
-    var out = {files: []};
+  // If any pattern matched zero files, halt with a non-zero exit code.
+  // TODO(ivanpi): Allow optional patterns, e.g. prefixed by '?'?
+  if (unmatched.length > 0) {
+    console.warn('Error bundling "%s": unmatched patterns %j', dir, unmatched);
+    process.exit(2);
+  }
 
-    // Loop over each file.
-    _.each(relpaths, function(relpath) {
-      if (shouldIgnore(relpath)) {
-        return;
-      }
+  var out = {files: []};
 
-      var abspath = path.resolve(dir, relpath);
-      var text = fs.readFileSync(abspath, {encoding: 'utf8'});
+  // Loop over each file.
+  _.each(relpaths, function(relpath) {
+    var abspath = path.resolve(dir, relpath);
+    var lines = fs.readFileSync(abspath, {encoding: 'utf8'}).split('\n');
 
-      var lines = text.split('\n');
-      lines = stripBuildIgnore(lines);
-      lines = stripLeadingBlankLines(lines);
-      var indexAndLines = getIndex(lines);
-      var index = indexAndLines.index;
-      lines = indexAndLines.lines;
+    lines = stripBuildIgnore(lines);
+    lines = stripLeadingBlankLines(lines);
+    var indexAndLines = getIndex(lines);
+    var index = indexAndLines.index;
+    lines = indexAndLines.lines;
 
-      out.files.push({
-        name: relpath,
-        body: lines.join('\n'),
-        index: index
-      });
+    out.files.push({
+      name: relpath,
+      body: argv.empty ? '' : lines.join('\n'),
+      index: index
     });
-
-    out.files = _.sortBy(out.files, 'index');
-
-    // Drop the index fields -- we don't need them anymore.
-    out.files = _.map(out.files, function(f) {
-      return _.omit(f, '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);
-    }
   });
+
+  out.files = _.sortBy(out.files, 'index');
+
+  // Drop the index fields -- we don't need them anymore.
+  out.files = _.map(out.files, function(f) {
+    return _.omit(f, 'index');
+  });
+
+  // Write the bundle to stdout.
+  process.stdout.write(JSON.stringify(out) + '\n');
+
+  if (argv.verbose) {
+    console.warn('Bundled "%s" using "%s"', dir, globFile);
+  }
 }