From b12cf557893163f3e839c8c8188c8399afeb1172 Mon Sep 17 00:00:00 2001
From: Robert Long <robert@robertlong.me>
Date: Mon, 4 Dec 2017 16:18:10 -0800
Subject: [PATCH] Add HandlebarsTemplatePlugin for rewriting asset urls. (#38)

* Add HandlebarsTemplatePlugin and asset helper.

* Recompile on template change.
---
 .gitignore                               |  5 +-
 README.md                                | 39 ++++++++++++--
 package.json                             |  6 ++-
 templates/HandlebarsTemplatePlugin.js    | 69 ++++++++++++++++++++++++
 public/index.html => templates/index.hbs |  2 +-
 public/room.html => templates/room.hbs   | 25 ++++-----
 webpack.common.js                        | 29 +++++++++-
 yarn.lock                                | 51 ++++++++++++++++--
 8 files changed, 199 insertions(+), 27 deletions(-)
 create mode 100644 templates/HandlebarsTemplatePlugin.js
 rename public/index.html => templates/index.hbs (90%)
 rename public/room.html => templates/room.hbs (83%)

diff --git a/.gitignore b/.gitignore
index 551f268cc..0f0a4c49b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -58,4 +58,7 @@ typings/
 .env
 
 # webpack bundle
-public/app.bundle.js*
\ No newline at end of file
+public/*.bundle.js*
+public/*.html
+
+.DS_Store
diff --git a/README.md b/README.md
index bdcc229e4..9a6a27740 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,44 @@
 # Mozilla Social Mixed Reality Client
 
-A prototype client demonstrating a multi-user experience in WebVR. Built with [A-Frame](https://github.com/aframevr/aframe/)
+A prototype client demonstrating a multi-user experience in WebVR. Built with
+[A-Frame](https://github.com/aframevr/aframe/)
 
 ## Getting Started
 
-To run the social client, type:
+To run the social client, run:
 
-```
+```sh
 git clone https://github.com/mozilla/mr-social-client.git
 yarn install
-yarn run dev
+yarn start
+```
+
+## Building Static Files
+
+To bundle javascript and generate the html templates, run:
+
+```sh
+yarn build
+```
+
+### Using CDN Assets
+
+If you are hosting your static assets at separate path from the html documents,
+the asset handlebars helper supports rewriting the base asset paths. To use it
+run:
+
+```sh
+BASE_ASSETS_PATH="https://cdn.mysite.com/assets/" yarn build
+```
+
+Ex.
+
+```hbs
+<img src="{{asset "asseturl.png"}}"/>
+```
+
+Will become:
+
+```html
+<img src="https://cdn.mysite.com/assets/asseturl.png?c=1512428142413"/>
 ```
diff --git a/package.json b/package.json
index 06a02941b..10d1e741c 100644
--- a/package.json
+++ b/package.json
@@ -4,8 +4,9 @@
   "main": "src/index.js",
   "license": "MPL-2.0",
   "scripts": {
+    "start": "npm run dev",
     "dev": "webpack-dev-server --https --host 0.0.0.0 --useLocalIp --open --config webpack.dev.js",
-    "build": "webpack --config webpack.prod.js",
+    "build": "NODE_ENV='production' webpack --config webpack.prod.js",
     "prettier": "prettier --write src/**/*.js"
   },
   "dependencies": {
@@ -30,10 +31,13 @@
     "babel-minify-webpack-plugin": "^0.2.0",
     "babel-preset-env": "^1.6.1",
     "babel-preset-react": "^6.24.1",
+    "chokidar": "^1.7.0",
     "css-loader": "^0.28.7",
     "eslint": "^4.10.0",
     "eslint-config-prettier": "^2.6.0",
     "eslint-plugin-prettier": "^2.3.1",
+    "fs-extra": "^4.0.2",
+    "handlebars": "^4.0.11",
     "prettier": "^1.7.0",
     "style-loader": "^0.19.0",
     "webpack": "^3.6.0",
diff --git a/templates/HandlebarsTemplatePlugin.js b/templates/HandlebarsTemplatePlugin.js
new file mode 100644
index 000000000..b80535744
--- /dev/null
+++ b/templates/HandlebarsTemplatePlugin.js
@@ -0,0 +1,69 @@
+const Handlebars = require("handlebars");
+const fs = require("fs-extra");
+const path = require("path");
+const chokidar = require("chokidar");
+
+class HandlebarsTemplatePlugin {
+  constructor(options) {
+    this.templatesPath = options.templatesPath;
+    this.templateExtension = options.templateExtension || ".hbs";
+    this.templateOptions = options.templateOptions || {};
+
+    if (options.helpers) {
+      Object.keys(options.helpers).forEach(helperName => {
+        Handlebars.registerHelper(helperName, options.helpers[helperName]);
+      });
+    }
+  }
+
+  apply(compiler) {
+    compiler.plugin("watch-run", (compilation, callback) => {
+      chokidar
+        .watch(path.join(this.templatesPath, "*" + this.templateExtension))
+        .on("change", () => {
+          compiler.run(err => {
+            if (err) {
+              throw err;
+            }
+          });
+        });
+
+      callback();
+    });
+
+    compiler.plugin("emit", (compilation, callback) => {
+      this.compileTemplates(compiler, compilation).then(callback);
+    });
+  }
+
+  // Compile all handlebars templates in the template directory and place them in the output directory.
+  async compileTemplates(compiler, compilation) {
+    const outputPath = compiler.options.output.path;
+    const templateFiles = await fs.readdir(this.templatesPath);
+
+    const templatePromises = templateFiles
+      .filter(filename => filename.indexOf(this.templateExtension) !== -1)
+      .map(fileName => {
+        const filePath = path.join(this.templatesPath, fileName);
+        const outputFileName = fileName.replace(
+          this.templateExtension,
+          ".html"
+        );
+        const outputFilePath = path.join(outputPath, outputFileName);
+
+        return this.compileTemplate(filePath, outputFilePath);
+      });
+
+    await Promise.all(templatePromises);
+  }
+
+  // Compile a single handlebars template given a file path and output file path.
+  async compileTemplate(filePath, outputFilePath) {
+    const templateStr = await fs.readFile(filePath);
+    const template = Handlebars.compile(templateStr.toString());
+    const compiledStr = template(this.templateOptions);
+    return fs.writeFile(outputFilePath, compiledStr);
+  }
+}
+
+module.exports = HandlebarsTemplatePlugin;
diff --git a/public/index.html b/templates/index.hbs
similarity index 90%
rename from public/index.html
rename to templates/index.hbs
index e0074d4ed..391f3f5b8 100644
--- a/public/index.html
+++ b/templates/index.hbs
@@ -12,6 +12,6 @@
   </head>
   <body>
       <div id="root"></div>
-      <script src="./lobby.bundle.js"></script>
+      <script src="{{asset "lobby.bundle.js"}}"></script>
   </body>
 </html>
diff --git a/public/room.html b/templates/room.hbs
similarity index 83%
rename from public/room.html
rename to templates/room.hbs
index 03c8dd6a3..01bab865a 100644
--- a/public/room.html
+++ b/templates/room.hbs
@@ -3,7 +3,7 @@
 <head>
     <title>Mozilla Mixed Reality Social Client</title>
     <script src="https://webrtc.github.io/adapter/adapter-6.0.2.js"></script>
-    <script src="./app.bundle.js"></script>
+    <script src="{{asset "app.bundle.js" }}"></script>
     <style>
         .a-enter-vr {
             top: 90px;
@@ -14,7 +14,7 @@
             width: 100vw;
             height: 100vh;
             z-index: 10001;
-            background: #eaeaea no-repeat url(assets/loading.gif) center center;
+            background: #eaeaea no-repeat url({{asset "assets/loading.gif" }}) center center;
             opacity: 0.9;
         }
     </style>
@@ -32,19 +32,16 @@
         light="defaultLightsEnabled: false">
 
         <a-assets>
-            <img id="grid" src="assets/grid.png" crossorigin="anonymous" />
-            <img id="sky" src="https://cdn.aframe.io/360-image-gallery-boilerplate/img/sechelt.jpg" crossorigin="anonymous" />
+            <a-asset-item id="bot-head-mesh" src="{{asset "assets/avatars/Bot_Head_Mesh.glb" }}"></a-asset-item>
+            <a-asset-item id="bot-body-mesh" src="{{asset "assets/avatars/Bot_Body_Mesh.glb" }}"></a-asset-item>
+            <a-asset-item id="bot-left-hand-mesh" src="{{asset "assets/avatars/Bot_LeftHand_Mesh.glb" }}"></a-asset-item>
+            <a-asset-item id="bot-right-hand-mesh" src="{{asset "assets/avatars/Bot_RightHand_Mesh.glb"}}"></a-asset-item>
 
-            <a-asset-item id="bot-head-mesh" src="assets/avatars/Bot_Head_Mesh.glb"></a-asset-item>
-            <a-asset-item id="bot-body-mesh" src="assets/avatars/Bot_Body_Mesh.glb"></a-asset-item>
-            <a-asset-item id="bot-left-hand-mesh" src="assets/avatars/Bot_LeftHand_Mesh.glb"></a-asset-item>
-            <a-asset-item id="bot-right-hand-mesh" src="assets/avatars/Bot_RightHand_Mesh.glb"></a-asset-item>
+            <a-asset-item id="watch-model" src="{{asset "assets/hud/watch.gltf"}}"></a-asset-item>
 
-            <a-asset-item id="watch-model" src="assets/hud/watch.gltf"></a-asset-item>
-
-            <a-asset-item id="meeting-space1-mesh" src="assets/environments/MeetingSpace1_mesh.glb"></a-asset-item>
-            <a-asset-item id="outdoor-facade-mesh" src="assets/environments/OutdoorFacade_mesh.glb"></a-asset-item>
-            <a-asset-item id="floor-nav-mesh" src="assets/environments/FloorNav_mesh.glb"></a-asset-item>
+            <a-asset-item id="meeting-space1-mesh" src="{{asset "assets/environments/MeetingSpace1_mesh.glb"}}"></a-asset-item>
+            <a-asset-item id="outdoor-facade-mesh" src="{{asset "assets/environments/OutdoorFacade_mesh.glb"}}"></a-asset-item>
+            <a-asset-item id="floor-nav-mesh" src="{{asset "assets/environments/FloorNav_mesh.glb"}}"></a-asset-item>
 
             <!-- Templates -->
             <script id="head-template" type="text/html">
@@ -144,7 +141,7 @@
             >
                 <a-entity
                     id="watch"
-                    cached-gltf-model="assets/hud/watch.gltf"
+                    cached-gltf-model="#watch-model"
                     position="0 0.0015 0.147"
                     rotation="3.5 0 0"
                 >
diff --git a/webpack.common.js b/webpack.common.js
index 4c2d6c040..93329fbc4 100644
--- a/webpack.common.js
+++ b/webpack.common.js
@@ -1,4 +1,6 @@
 const path = require("path");
+const HandlebarsTemplatePlugin = require("./templates/HandlebarsTemplatePlugin");
+const Handlebars = require("handlebars");
 
 module.exports = {
   entry: {
@@ -12,7 +14,7 @@ module.exports = {
   module: {
     rules: [
       {
-        test: /.js$/,
+        test: /\.js$/,
         include: [path.resolve(__dirname, "src")],
         exclude: [path.resolve(__dirname, "node_modules")],
         loader: "babel-loader"
@@ -22,5 +24,28 @@ module.exports = {
         use: ["style-loader", "css-loader"]
       }
     ]
-  }
+  },
+  plugins: [
+    new HandlebarsTemplatePlugin({
+      templatesPath: path.resolve(__dirname, "templates"),
+      helpers: {
+        /**
+         * Register a handlebars helper that prepends the base asset path.
+         * Useful for things like placing assets on a CDN and cache busting.
+         * Example:
+         * input: <img src="{{asset "asset.png"}}"/>
+         * output: <img src="https://cdn.mysite.com/asset.png?c="/>
+         */
+        asset: assetPath => {
+          const isProd = process.env.NODE_ENV === "production";
+          const baseAssetsPath = process.env.BASE_ASSETS_PATH || "/";
+          const cacheBustQueryString = isProd ? "?c=" + Date.now() : "";
+
+          const url = baseAssetsPath + assetPath + cacheBustQueryString;
+
+          return new Handlebars.SafeString(url);
+        }
+      }
+    })
+  ]
 };
diff --git a/yarn.lock b/yarn.lock
index 625d2b925..e3ccbc466 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -306,7 +306,7 @@ async@0.2.x:
   version "0.2.10"
   resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
 
-async@^1.5.2:
+async@^1.4.0, async@^1.5.2:
   version "1.5.2"
   resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
 
@@ -2620,6 +2620,14 @@ fs-exists-sync@^0.1.0:
   version "0.1.0"
   resolved "https://registry.yarnpkg.com/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz#982d6893af918e72d08dec9e8673ff2b5a8d6add"
 
+fs-extra@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b"
+  dependencies:
+    graceful-fs "^4.1.2"
+    jsonfile "^4.0.0"
+    universalify "^0.1.0"
+
 fs.realpath@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
@@ -2780,7 +2788,7 @@ globby@^6.1.0:
     pify "^2.0.0"
     pinkie-promise "^2.0.0"
 
-graceful-fs@^4.1.2:
+graceful-fs@^4.1.2, graceful-fs@^4.1.6:
   version "4.1.11"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.1.11.tgz#0e8bdfe4d1ddb8854d64e04ea7c00e2a026e5658"
 
@@ -2788,6 +2796,16 @@ handle-thing@^1.2.5:
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/handle-thing/-/handle-thing-1.2.5.tgz#fd7aad726bf1a5fd16dfc29b2f7a6601d27139c4"
 
+handlebars@^4.0.11:
+  version "4.0.11"
+  resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.0.11.tgz#630a35dfe0294bc281edae6ffc5d329fc7982dcc"
+  dependencies:
+    async "^1.4.0"
+    optimist "^0.6.1"
+    source-map "^0.4.4"
+  optionalDependencies:
+    uglify-js "^2.6"
+
 har-schema@^1.0.5:
   version "1.0.5"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e"
@@ -3343,6 +3361,12 @@ json5@^0.5.0, json5@^0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
 
+jsonfile@^4.0.0:
+  version "4.0.0"
+  resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-4.0.0.tgz#8771aae0799b64076b76640fca058f9c10e33ecb"
+  optionalDependencies:
+    graceful-fs "^4.1.6"
+
 jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
@@ -3678,6 +3702,10 @@ minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
+minimist@~0.0.1:
+  version "0.0.10"
+  resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.10.tgz#de3f98543dbf96082be48ad1a0c7cda836301dcf"
+
 mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
   version "0.5.1"
   resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
@@ -3945,6 +3973,13 @@ opn@^5.1.0:
   dependencies:
     is-wsl "^1.1.0"
 
+optimist@^0.6.1:
+  version "0.6.1"
+  resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686"
+  dependencies:
+    minimist "~0.0.1"
+    wordwrap "~0.0.2"
+
 optionator@^0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64"
@@ -5136,7 +5171,7 @@ source-map-support@^0.4.15:
   dependencies:
     source-map "^0.5.6"
 
-source-map@^0.4.2:
+source-map@^0.4.2, source-map@^0.4.4:
   version "0.4.4"
   resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.4.4.tgz#eba4f5da9c0dc999de68032d8b4f76173652036b"
   dependencies:
@@ -5530,7 +5565,7 @@ ua-parser-js@^0.7.9:
   version "0.7.17"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
 
-uglify-js@^2.8.29:
+uglify-js@^2.6, uglify-js@^2.8.29:
   version "2.8.29"
   resolved "https://registry.yarnpkg.com/uglify-js/-/uglify-js-2.8.29.tgz#29c5733148057bb4e1f75df35b7a9cb72e6a59dd"
   dependencies:
@@ -5577,6 +5612,10 @@ uniqs@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/uniqs/-/uniqs-2.0.0.tgz#ffede4b36b25290696e6e165d4a59edb998e6b02"
 
+universalify@^0.1.0:
+  version "0.1.1"
+  resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.1.1.tgz#fa71badd4437af4c148841e3b3b165f9e9e590b7"
+
 unpipe@1.0.0, unpipe@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
@@ -5812,6 +5851,10 @@ wordwrap@0.0.2:
   version "0.0.2"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
 
+wordwrap@~0.0.2:
+  version "0.0.3"
+  resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+
 wordwrap@~1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
-- 
GitLab