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