From 6fe9ddb17b3c57c0f6d7298515a19da7a1fb1041 Mon Sep 17 00:00:00 2001 From: ZauberNerd Date: Tue, 13 Feb 2018 21:58:54 +0100 Subject: [PATCH] feat(pwa): initial ServiceWorker and Web App Manifest support This package adds ServiceWorker and Web App Manifest capabilities to your Hops application. To use it: - add `hops-pwa` as a dependency to your project - create a Web App Manifest and load it in your app: ```jsx ``` - specify the path to the Service Worker file in your Hops config: ```json "hops": { "workerFile": "hops-pwa/worker.js" } ``` You can either use the default worker provided by the `hops-pwa` package or you can specify a path to your own worker implementation. --- packages/build-config/configs/build.js | 2 + packages/build-config/configs/develop.js | 3 + packages/build-config/package.json | 2 + .../plugins/webpack-service-worker.js | 56 +++++++++++++++++++ .../build-config/sections/module-rules.js | 1 + .../sections/module-rules/webmanifest.js | 36 ++++++++++++ packages/build-config/sections/resolve.js | 11 +++- packages/build-config/shims/worker-shim.js | 19 +++++++ packages/config/index.js | 2 +- packages/pwa/dom.js | 22 ++++++++ packages/pwa/node.js | 7 +++ packages/pwa/package.json | 13 +++++ packages/pwa/worker.js | 7 +++ yarn.lock | 56 +++++++++++++++++++ 14 files changed, 233 insertions(+), 4 deletions(-) create mode 100644 packages/build-config/plugins/webpack-service-worker.js create mode 100644 packages/build-config/sections/module-rules/webmanifest.js create mode 100644 packages/build-config/shims/worker-shim.js create mode 100644 packages/pwa/dom.js create mode 100644 packages/pwa/node.js create mode 100644 packages/pwa/package.json create mode 100644 packages/pwa/worker.js diff --git a/packages/build-config/configs/build.js b/packages/build-config/configs/build.js index 229fce9c8..753e50298 100644 --- a/packages/build-config/configs/build.js +++ b/packages/build-config/configs/build.js @@ -7,6 +7,7 @@ var ExtractTextPlugin = require('extract-text-webpack-plugin'); var UglifyPlugin = require('uglifyjs-webpack-plugin'); var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin; var WriteFilePlugin = require('../plugins/write-file'); +var ServiceWorkerPlugin = require('../plugins/webpack-service-worker'); var hopsConfig = require('hops-config'); @@ -29,6 +30,7 @@ module.exports = { plugins: [ new WriteFilePlugin(/^manifest\.js(\.map)?$/), new StatsWriterPlugin({ fields: null }), + new ServiceWorkerPlugin(), new webpack.optimize.CommonsChunkPlugin({ name: 'vendor', minChunks: function(module) { diff --git a/packages/build-config/configs/develop.js b/packages/build-config/configs/develop.js index 98c386318..c13f5fab4 100644 --- a/packages/build-config/configs/develop.js +++ b/packages/build-config/configs/develop.js @@ -4,6 +4,8 @@ var path = require('path'); var webpack = require('webpack'); +var ServiceWorkerPlugin = require('../plugins/webpack-service-worker'); + var hopsConfig = require('hops-config'); var getAssetPath = path.join.bind(path, hopsConfig.assetPath); @@ -25,6 +27,7 @@ module.exports = { rules: require('../sections/module-rules')('develop'), }, plugins: [ + new ServiceWorkerPlugin(), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), new webpack.EnvironmentPlugin( diff --git a/packages/build-config/package.json b/packages/build-config/package.json index e123f1a3c..74ccdbbbe 100644 --- a/packages/build-config/package.json +++ b/packages/build-config/package.json @@ -38,8 +38,10 @@ "style-loader": "^0.20.0", "uglifyjs-webpack-plugin": "^1.1.6", "url-loader": "^0.6.2", + "web-app-manifest-loader": "^0.1.1", "webpack": "^3.6.0", "webpack-node-externals": "^1.6.0", + "webpack-sources": "^1.1.0", "webpack-stats-plugin": "^0.1.5" } } diff --git a/packages/build-config/plugins/webpack-service-worker.js b/packages/build-config/plugins/webpack-service-worker.js new file mode 100644 index 000000000..0c8360bcf --- /dev/null +++ b/packages/build-config/plugins/webpack-service-worker.js @@ -0,0 +1,56 @@ +'use strict'; + +var SingleEntryPlugin = require('webpack/lib/SingleEntryPlugin'); +var RawSource = require('webpack-sources').RawSource; + +var hopsConfig = require('hops-config'); + +var PLUGIN_NAME = 'hops-service-worker-plugin'; + +module.exports = function ServiceWorkerPlugin() { + var assetPath = hopsConfig.workerPath.replace(/^\/+/, ''); + + this.apply = function(compiler) { + if (!hopsConfig.workerFile) { + return; + } + + function onMake(compilation, callback) { + compilation + .createChildCompiler(PLUGIN_NAME, { filename: assetPath }, [ + new SingleEntryPlugin( + compiler.context, + require.resolve('../shims/worker-shim'), + 'worker' + ), + ]) + .runAsChild(callback); + } + + function onEmit(compilation, callback) { + compilation.assets[assetPath] = new RawSource( + 'HOPS_ASSETS = ' + + JSON.stringify( + Object.keys(compilation.assets).filter(function(item) { + return !item.match(/\.map|stats\.json$/) && item !== assetPath; + }) + ) + + ';\n' + + compilation.assets[assetPath].source() + ); + callback(); + } + + if (typeof compiler.hooks !== 'undefined') { + compiler.hooks.make.tapAsync(PLUGIN_NAME, onMake); + } else { + compiler.plugin('make', onMake); + } + + if (typeof compiler.hooks !== 'undefined') { + compiler.hooks.emit.tap(PLUGIN_NAME, onEmit); + } else { + compiler.plugin('emit', onEmit); + } + }; +}; diff --git a/packages/build-config/sections/module-rules.js b/packages/build-config/sections/module-rules.js index 16cdad294..0547261b1 100644 --- a/packages/build-config/sections/module-rules.js +++ b/packages/build-config/sections/module-rules.js @@ -8,6 +8,7 @@ module.exports = function getModuleRules(target) { require('./module-rules/graphql'), require('./module-rules/postcss'), require('./module-rules/config'), + require('./module-rules/webmanifest'), require('./module-rules/tpl'), require('./module-rules/url'), require('./module-rules/file'), diff --git a/packages/build-config/sections/module-rules/webmanifest.js b/packages/build-config/sections/module-rules/webmanifest.js new file mode 100644 index 000000000..d45eb34d1 --- /dev/null +++ b/packages/build-config/sections/module-rules/webmanifest.js @@ -0,0 +1,36 @@ +'use strict'; + +var path = require('path'); + +var hopsConfig = require('hops-config'); + +exports.default = { + test: /\.webmanifest$/, + use: [ + { + loader: require.resolve('file-loader'), + options: { + name: path.join(hopsConfig.assetPath, '[name]-[hash:16].[ext]'), + }, + }, + { + loader: require.resolve('web-app-manifest-loader'), + }, + ], +}; + +exports.node = { + test: /\.webmanifest$/, + use: [ + { + loader: require.resolve('file-loader'), + options: { + name: path.join(hopsConfig.assetPath, '[name]-[hash:16].[ext]'), + emitFile: false, + }, + }, + { + loader: require.resolve('web-app-manifest-loader'), + }, + ], +}; diff --git a/packages/build-config/sections/resolve.js b/packages/build-config/sections/resolve.js index 4a6844c8c..cf2fb7145 100644 --- a/packages/build-config/sections/resolve.js +++ b/packages/build-config/sections/resolve.js @@ -5,9 +5,14 @@ var hopsConfig = require('hops-config'); module.exports = function getResolveConfig(target) { var platform = target === 'node' ? 'server' : 'browser'; return { - alias: { - 'hops-entry-point': hopsConfig.appDir, - }, + alias: Object.assign( + { + 'hops-entry-point': hopsConfig.appDir, + }, + hopsConfig.workerFile && { + 'hops-worker-entry-point': hopsConfig.workerFile, + } + ), mainFields: [ 'esnext:' + platform, 'jsnext:' + platform, diff --git a/packages/build-config/shims/worker-shim.js b/packages/build-config/shims/worker-shim.js new file mode 100644 index 000000000..9204232ad --- /dev/null +++ b/packages/build-config/shims/worker-shim.js @@ -0,0 +1,19 @@ +'use strict'; + +require('babel-polyfill'); + +(function execute() { + var entryPoint = require('hops-worker-entry-point'); + if ( + typeof entryPoint !== 'function' && + typeof entryPoint.default === 'function' + ) { + entryPoint = entryPoint.default; + } + entryPoint(HOPS_ASSETS); // eslint-disable-line no-undef + if (module.hot) { + module.hot.accept(require.resolve('hops-worker-entry-point'), function() { + setTimeout(execute); + }); + } +})(); diff --git a/packages/config/index.js b/packages/config/index.js index f6c847eb6..704559c6c 100644 --- a/packages/config/index.js +++ b/packages/config/index.js @@ -29,7 +29,7 @@ function applyDefaultConfig(config) { locations: [], basePath: '', assetPath: '', - workerPath: '/sw.js', + workerPath: '/sw.js', workerFile: null, browsers: '> 1%, last 2 versions, Firefox ESR', node: 'current', diff --git a/packages/pwa/dom.js b/packages/pwa/dom.js new file mode 100644 index 000000000..39cfb3b0e --- /dev/null +++ b/packages/pwa/dom.js @@ -0,0 +1,22 @@ +'use strict'; + +var hopsConfig = require('hops-config'); + +module.exports = function installServiceWorker() { + return new Promise(function(resolve, reject) { + if (!('serviceWorker' in navigator)) { + return reject( + new Error('ServiceWorkers are not supported in this browser') + ); + } + + if ( + window.location.protocol === 'https' || + window.location.hostname === 'localhost' + ) { + window.addEventListener('load', function() { + resolve(navigator.serviceWorker.register('/' + hopsConfig.workerPath)); + }); + } + }); +}; diff --git a/packages/pwa/node.js b/packages/pwa/node.js new file mode 100644 index 000000000..8c87fb603 --- /dev/null +++ b/packages/pwa/node.js @@ -0,0 +1,7 @@ +'use strict'; + +module.exports = function installServiceWorke() { + return new Promise(function() { + /* intentionally left empty */ + }); +}; diff --git a/packages/pwa/package.json b/packages/pwa/package.json new file mode 100644 index 000000000..b61140433 --- /dev/null +++ b/packages/pwa/package.json @@ -0,0 +1,13 @@ +{ + "name": "hops-pwa", + "version": "10.1.0", + "description": "ServiceWorker and Web App Manifest support for Hops", + "keywords": ["hops", "serviceworker", "pwa", "web app manifest"], + "license": "MIT", + "main": "node.js", + "browser": "dom.js", + "files": ["dom.js", "node.js", "worker.js"], + "dependencies": { + "hops-config": "^10.0.1" + } +} diff --git a/packages/pwa/worker.js b/packages/pwa/worker.js new file mode 100644 index 000000000..7cf209dbe --- /dev/null +++ b/packages/pwa/worker.js @@ -0,0 +1,7 @@ +'use strict'; + +var hopsConfig = require('hops-config'); + +module.exports = function(assets) { + console.log('hello from worker', assets, hopsConfig); +}; diff --git a/yarn.lock b/yarn.lock index f3ea89e93..9b340b4b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2789,10 +2789,36 @@ fast-levenshtein@~2.0.4: version "2.0.6" resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" +fastfall@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/fastfall/-/fastfall-1.5.1.tgz#3fee03331a49d1d39b3cdf7a5e9cd66f475e7b94" + dependencies: + reusify "^1.0.0" + +fastparallel@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/fastparallel/-/fastparallel-2.3.0.tgz#1e709bfb6a03993f3857e3ce7f01311ce7602613" + dependencies: + reusify "^1.0.0" + xtend "^4.0.1" + fastparse@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8" +fastq@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.5.0.tgz#05e32ffb999ec2d945dda27461bf08941436448b" + dependencies: + reusify "^1.0.0" + +fastseries@^1.7.0: + version "1.7.2" + resolved "https://registry.yarnpkg.com/fastseries/-/fastseries-1.7.2.tgz#d22ce13b9433dff3388d91dbd6b8bda9b21a0f4b" + dependencies: + reusify "^1.0.0" + xtend "^4.0.0" + fb-watchman@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.0.tgz#54e9abf7dfa2f26cd9b1636c588c1afc05de5d58" @@ -4501,6 +4527,15 @@ loader-runner@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" +loader-utils@^0.2.12: + version "0.2.17" + resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" + dependencies: + big.js "^3.1.3" + emojis-list "^2.0.0" + json5 "^0.5.0" + object-assign "^4.0.1" + loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" @@ -6551,6 +6586,10 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +reusify@^1.0.0: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + revalidator@0.1.x: version "0.1.8" resolved "https://registry.yarnpkg.com/revalidator/-/revalidator-0.1.8.tgz#fece61bfa0c1b52a206bd6b18198184bdd523a3b" @@ -6893,6 +6932,16 @@ stealthy-require@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/stealthy-require/-/stealthy-require-1.1.1.tgz#35b09875b4ff49f26a777e509b3090a3226bf24b" +steed@^1.1.2: + version "1.1.3" + resolved "https://registry.yarnpkg.com/steed/-/steed-1.1.3.tgz#f1525dd5adb12eb21bf74749537668d625b9abc5" + dependencies: + fastfall "^1.5.0" + fastparallel "^2.2.0" + fastq "^1.3.0" + fastseries "^1.7.0" + reusify "^1.0.0" + stream-browserify@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/stream-browserify/-/stream-browserify-2.0.1.tgz#66266ee5f9bdb9940a4e4514cafb43bb71e5c9db" @@ -7534,6 +7583,13 @@ wcwidth@^1.0.0: dependencies: defaults "^1.0.3" +web-app-manifest-loader@^0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/web-app-manifest-loader/-/web-app-manifest-loader-0.1.1.tgz#861186a70f37b69ee10496b7ed0353d56fbdd11a" + dependencies: + loader-utils "^0.2.12" + steed "^1.1.2" + webidl-conversions@^4.0.1, webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad"