├── .gitignore ├── examples ├── app1 │ ├── styles │ │ └── app.css │ ├── js │ │ └── app.js │ ├── package.json │ ├── index.html │ └── gulpfile.js └── app2 │ ├── styles │ └── app.css │ ├── js │ └── app.js │ ├── package.json │ ├── index.html │ ├── gulpfile.js │ └── swExtra.js ├── templates ├── unminified │ ├── cache.tpl.html │ ├── sw.tpl.js │ ├── README.md │ ├── install.tpl.js │ └── cache.tpl.js └── index.js ├── package.json ├── README.md └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules 3 | -------------------------------------------------------------------------------- /examples/app1/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #FFF; 3 | background-color: #000; 4 | } 5 | -------------------------------------------------------------------------------- /examples/app2/styles/app.css: -------------------------------------------------------------------------------- 1 | body { 2 | color: #FFF; 3 | background-color: #000; 4 | } 5 | -------------------------------------------------------------------------------- /examples/app1/js/app.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', function() { 2 | console.log('We are ready to rock!'); 3 | }); 4 | -------------------------------------------------------------------------------- /examples/app2/js/app.js: -------------------------------------------------------------------------------- 1 | window.addEventListener('DOMContentLoaded', function() { 2 | console.log('We are ready to rock!'); 3 | }); 4 | -------------------------------------------------------------------------------- /templates/unminified/cache.tpl.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | c 5 | -------------------------------------------------------------------------------- /examples/app1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-sww-app1", 3 | "description": "An example that shows how to use the sww gulp plugin", 4 | "version": "0.0.0", 5 | "main": "gulpfile.js", 6 | "devDependencies": { 7 | "gulp": "^3.9.0" 8 | }, 9 | "engines": { 10 | "node": ">=4.2.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/app2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-sww-app2", 3 | "description": "An example that shows how to use the sww gulp plugin", 4 | "version": "0.0.0", 5 | "main": "gulpfile.js", 6 | "devDependencies": { 7 | "gulp": "^3.9.0" 8 | }, 9 | "engines": { 10 | "node": ">=4.2.0" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/app1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | App1 6 | 7 | 8 | 9 | 10 |
11 |
I want to be offline
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /templates/unminified/sw.tpl.js: -------------------------------------------------------------------------------- 1 | importScripts('sww.js'); 2 | 3 | var version = '$VERSION'; 4 | var worker = new self.ServiceWorkerWare(); 5 | worker.use(new self.StaticCacher($FILES_TO_LOAD)); 6 | worker.use(new self.SimpleOfflineCache()); 7 | 8 | var extraFiles = $HOOK; 9 | if (extraFiles.length) { 10 | importScripts(...extraFiles); 11 | } 12 | 13 | worker.init(); 14 | -------------------------------------------------------------------------------- /examples/app2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | App2 6 | 7 | 8 | 9 | 10 |
11 |
I am offline and given extra output in the console
12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/app1/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gulpsww = require('../../'); 3 | var package = require('./package.json'); 4 | 5 | 6 | gulp.task('offline', function() { 7 | return gulp.src([ 8 | '**/*', 9 | '!gulpfile.js', 10 | '!package.json' 11 | ], { cwd: './' }) 12 | .pipe(gulpsww({ 'version': package.version })) 13 | .pipe(gulp.dest('./')); 14 | }); 15 | 16 | gulp.task('default', ['offline']); 17 | -------------------------------------------------------------------------------- /examples/app2/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var gulpsww = require('../../'); 3 | var package = require('./package.json'); 4 | 5 | 6 | gulp.task('offline', function() { 7 | return gulp.src([ 8 | '**/*', 9 | '!gulpfile.js', 10 | '!package.json' 11 | ], { cwd: './' }) 12 | .pipe(gulpsww({ 'version': package.version, 'hookSW': 'swExtra.js' })) 13 | .pipe(gulp.dest('./')); 14 | }); 15 | 16 | gulp.task('default', ['offline']); 17 | -------------------------------------------------------------------------------- /examples/app2/swExtra.js: -------------------------------------------------------------------------------- 1 | // This file will be injected in the middle of the sw. 2 | // We can use the sww library or directly write code 3 | // for the worker here. 4 | 5 | worker.use({ 6 | onActivate: function(evt) { 7 | console.log('I passed through the onActivate event!'); 8 | } 9 | }) 10 | 11 | worker.get('*', function(request, response) { 12 | console.log('---------> Logging a get request for ', request.url); 13 | 14 | return Promise.resolve(response); 15 | }); -------------------------------------------------------------------------------- /templates/unminified/README.md: -------------------------------------------------------------------------------- 1 | # JavaScript template files 2 | 3 | Here are located the original, unminified JavaScript and HTML files used by the 4 | plugin. 5 | 6 | If you need to modify `/templates/templates.js`, please do the changes on the 7 | corresponding file in this folder and minify it (You can use the [online Closure 8 | Compiler UI](https://closure-compiler.appspot.com/home) for JavaScript files, 9 | `Advanced` optimisation mode is preferred). 10 | Then copy and paste the minified content in `/templates/templates.js`. 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gulp-sww", 3 | "version": "0.1.4", 4 | "description": "A gulp plugin to make your web app work offline.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "keywords": [ 10 | "gulp", 11 | "gulpplugin", 12 | "sww", 13 | "offline", 14 | "serviceworker", 15 | "serviceworkers", 16 | "service worker", 17 | "service workers", 18 | "appcache" 19 | ], 20 | "author": "Francisco Jordano ", 21 | "homepage": "https://github.com/arcturus/gulp-sww/", 22 | "repository": "git@github.com:arcturus/gulp-sww.git", 23 | "license": "MPL-2.0", 24 | "dependencies": { 25 | "gulp-util": "^3.0.7", 26 | "node-appcache-generator": "0.0.2", 27 | "serviceworkers-ware": "^0.3.2", 28 | "through": "^2.3.8", 29 | "vinyl": "^1.1.0" 30 | }, 31 | "engines": { 32 | "node": ">=4.2.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /templates/unminified/install.tpl.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | if (location.protocol !== 'https:' && location.hostname !== 'localhost') { 5 | // navigator.serviceWorker and window.applicationCache are both defined in 6 | // http pages but will not work, so we need to check the URL scheme. 7 | // As well we allow the hostname localhost for development pourpuses 8 | return; 9 | } 10 | 11 | if ('serviceWorker' in navigator && !navigator.serviceWorker.controller) { 12 | navigator.serviceWorker.register('sw.js'); 13 | } else if ('applicationCache' in window) { 14 | if (localStorage !== null) { 15 | if (localStorage.getItem('cached-by-gulp-sww') !== '1') { 16 | redirect(); 17 | } 18 | } else if (location.hash !== '#no-redirect') { 19 | redirect(); 20 | } 21 | } 22 | 23 | // Redirect to the page where the assets are being cached. 24 | function redirect() { 25 | var redirectUrl = encodeURIComponent(location.href); 26 | location.href = 'cache.html?redirect_url=' + redirectUrl; 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /templates/index.js: -------------------------------------------------------------------------------- 1 | // Look at /templates/unminified/README.md before changing content here. 2 | module.exports = { 3 | INSTALL_TPL_JS: `(function(){function a(){var a=encodeURIComponent(location.href);location.href="cache.html?redirect_url="+a}if("https:"===location.protocol||"localhost"===location.hostname)"serviceWorker"in navigator&&!navigator.serviceWorker.controller?navigator.serviceWorker.register("sw.js"):"applicationCache"in window&&(null!==localStorage?"1"!==localStorage.getItem("cached-by-gulp-sww")&&a():"#no-redirect"!==location.hash&&a())})();`, 4 | SW_TPL_JS: `importScripts("sww.js");var version="$VERSION",worker=new self.ServiceWorkerWare;worker.use(new self.StaticCacher($FILES_TO_LOAD)),worker.use(new self.SimpleOfflineCache);var extraFiles=$HOOK;extraFiles.length&&importScripts(...extraFiles),worker.init();`, 5 | CACHE_TPL_JS: `(function(){function e(){2!==applicationCache.status&&3!==applicationCache.status&&4!==applicationCache.status&&5!==applicationCache.status||c();g--?(d&&clearTimeout(d),d=setTimeout(e,1E3)):c()}function c(){var b=new URLSearchParams(location.search.substring(1));if(!b.has("debug")){var a=location.href.split("/");a.pop();a=a.join("/");b.has("redirect_url")&&(a=b.get("redirect_url")||a);null!==localStorage?localStorage.setItem("cached-by-gulp-sww","1"):a+="#no-redirect";location.href=a}}if(!("URLSearchParams"in 6 | window)){var f=function(b){this.a=b.split("&").map(function(a){var b=a.indexOf("=");return-1===b?[a,""]:[a.substring(0,b),a.substring(b+1)]})};f.prototype.has=function(b){return this.a.some(function(a){return a[0]===b})};f.prototype.get=function(b){var a=null;this.a.some(function(c){return c[0]===b?(a=c[1],!0):!1});return null===a?null:decodeURIComponent(a)};window.URLSearchParams=f}if("https:"!==location.protocol||"serviceWorker"in navigator)return c();var d=null,g=10;applicationCache.addEventListener("cached", 7 | c);applicationCache.addEventListener("checking",e);applicationCache.addEventListener("downloading",e);applicationCache.addEventListener("error",c);applicationCache.addEventListener("noupdate",c);applicationCache.addEventListener("obsolete",c);applicationCache.addEventListener("progress",e);applicationCache.addEventListener("updateready",c);d&&clearTimeout(d);d=setTimeout(e,250)})()`, 8 | CACHE_TPL_HTML: `c` 9 | }; 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | gulp-sww 2 | ======== 3 | 4 | A [gulp](http://gulpjs.com/) plugin to make your web app work offline. 5 | 6 | How it works 7 | ------------ 8 | Once applied to your app, this plugin code caches all your frontend assets using 9 | service workers if supported. 10 | 11 | If not, AppCache is used. In this mode, the main page is redirected to an empty 12 | page that uses appcache to cache your assets. When all the caching operations 13 | are done, the user is redirected back to the main page. 14 | This redirection only happens once. 15 | 16 | Please note that it will only work on web app served over https. 17 | 18 | Details 19 | ------- 20 | Make sure to serve the web app over `https`. 21 | 22 | This plugin uses the library [ServiceWorkerWare](https://github.com/fxos-components/serviceworkerware) to provide the integration with Service Worker. 23 | The AppCache manifest is generated by [node-appcache-generator](https://github.com/arcturus/node-appcache-generator). 24 | 25 | The steps this plugin provide are the following: 26 | 27 | + Receives a stream with your final set of assets. 28 | + Generates the files to create the worker and support the offline mode. 29 | + Modifies your `index.html` page to install the caching logic. 30 | 31 | Usage 32 | ----- 33 | ```javascript 34 | var sww = require('gulp-sww'); 35 | 36 | gulp.task('offline', function() { 37 | return gulp.src('**/*', { cwd : '' } ) 38 | .pipe(sww()) 39 | .pipe(gulp.dest('')); 40 | }); 41 | ``` 42 | 43 | By default, the `index.html` page is modified but you can specify a different 44 | entry point: 45 | ```javascript 46 | gulp.task('offline', function() { 47 | return gulp.src('**/*', { cwd : '' } ) 48 | .pipe(sww({ entryPoint: 'main.html' })) 49 | .pipe(gulp.dest('')); 50 | }); 51 | ``` 52 | 53 | Examples 54 | -------- 55 | You can find a simple example in the `examples/app1` and `examples/app2` 56 | folders, just execute `gulp` from these directories to see how the content is 57 | modified. 58 | 59 | Also [here](https://github.com/arcturus/ldjam-32/commit/4de02e5325136c78adced58a833f742e89c2452f) 60 | you can find how this plugin has been used to add offline support to a 61 | [HTML5 game](https://github.com/belen-albeza/ldjam-32) by [LadyBenko](http://www.belenalbeza.com/). 62 | 63 | Notes 64 | ----- 65 | This plugin requires node version >= 4.2.0. 66 | -------------------------------------------------------------------------------- /templates/unminified/cache.tpl.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | if (!('URLSearchParams' in window)) { 5 | // Simple polyfill for URLSearchParams#has() and URLSearchParams#get(). 6 | class URLSearchParamsPolyfill { 7 | /** 8 | * @param {(string|!URLSearchParams)=} queryString 9 | */ 10 | constructor(queryString) { 11 | this._queryString = queryString 12 | // Split on all `&`. 13 | .split('&') 14 | // Split each string on the first `=` sign. 15 | .map((string) => { 16 | const i = string.indexOf('='); 17 | 18 | if (i === -1) { 19 | return [string, '']; 20 | } 21 | 22 | return [string.substring(0, i), string.substring(i + 1)]; 23 | }); 24 | } 25 | 26 | has(key) { 27 | return this._queryString.some((string) => string[0] === key); 28 | } 29 | 30 | get(key) { 31 | let val = null; 32 | 33 | this._queryString.some((string) => { 34 | if (string[0] === key) { 35 | val = string[1]; 36 | return true; 37 | } 38 | return false; 39 | }); 40 | 41 | if (val === null) { 42 | return null; 43 | } 44 | 45 | return decodeURIComponent(val); 46 | } 47 | } 48 | 49 | window.URLSearchParams = URLSearchParamsPolyfill; 50 | } 51 | 52 | if (location.protocol !== 'https:' || 'serviceWorker' in navigator) { 53 | return redirect(); 54 | } 55 | 56 | // @see https://github.com/matthew-andrews/workshop-making-it-work-offline/tree/master/05-offline-news/04-more-hacking-appcache 57 | let checkTimer = null; 58 | let loopMax = 10; 59 | 60 | function checkNow() { 61 | /* 62 | 0 UNCACHED 63 | 1 IDLE 64 | 2 CHECKING 65 | 3 DOWNLOADING 66 | 4 UPDATEREADY 67 | 5 OBSOLETE 68 | */ 69 | if (applicationCache.status === 2 /* CHECKING */ 70 | || applicationCache.status === 3 /* DOWNLOADING */ 71 | || applicationCache.status === 4 /* UPDATEREADY */ 72 | || applicationCache.status === 5 /* OBSOLETE */) { 73 | redirect(); 74 | } 75 | 76 | if (loopMax--) { 77 | checkIn(1000); 78 | } else { 79 | redirect(); 80 | } 81 | } 82 | 83 | function checkIn(ms) { 84 | if (checkTimer) { 85 | clearTimeout(checkTimer); 86 | } 87 | 88 | checkTimer = setTimeout(checkNow, ms); 89 | } 90 | 91 | function redirect() { 92 | const searchParams = new URLSearchParams(location.search.substring(1)); 93 | 94 | if (searchParams.has('debug')) { 95 | return; 96 | } 97 | 98 | // By default, go to the default page in the current folder. 99 | // pathname 100 | let url = location.href.split('/'); 101 | url.pop(); 102 | url = url.join('/'); 103 | 104 | if (searchParams.has('redirect_url')) { 105 | // A param can be an empty string, so defaulting to the value of url. 106 | url = (searchParams.get('redirect_url') || url); 107 | } 108 | 109 | if (localStorage !== null) { 110 | // Use localStorage to avoid infinite redirections. 111 | localStorage.setItem('cached-by-gulp-sww', '1'); 112 | } else { 113 | // If localStorage can't be used, we fallback to a hash in the URL. 114 | url += '#no-redirect'; 115 | } 116 | 117 | location.href = url; 118 | } 119 | 120 | applicationCache.addEventListener('cached', redirect); 121 | applicationCache.addEventListener('checking', checkNow); 122 | applicationCache.addEventListener('downloading', checkNow); 123 | applicationCache.addEventListener('error', redirect); 124 | applicationCache.addEventListener('noupdate', redirect); 125 | applicationCache.addEventListener('obsolete', redirect); 126 | applicationCache.addEventListener('progress', checkNow); 127 | applicationCache.addEventListener('updateready', redirect); 128 | 129 | checkIn(250); 130 | }()); 131 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var through = require('through'); 2 | var File = require('vinyl'); 3 | var templates = require('./templates'); 4 | var fs = require('fs'); 5 | var AppCache = require('node-appcache-generator'); 6 | var gutil = require('gulp-util'); 7 | var PluginError = gutil.PluginError; 8 | 9 | var PLUGIN_NAME = 'gulp-sww'; 10 | 11 | module.exports = function(options) { 12 | options = options || {}; 13 | 14 | var entryPoint = options.entryPoint || 'index.html'; 15 | var version = options.version || Date.now(); 16 | var hookSW = options.hookSW || []; 17 | if (!Array.isArray(hookSW)) { 18 | hookSW = [hookSW]; 19 | } 20 | var paths = []; 21 | var hasAppCacheManifest = false; 22 | 23 | var onFile = function(file) { 24 | var path = file.path.substr(file.base.length); 25 | 26 | if (path === entryPoint 27 | && String(file.contents).match(/'); 45 | file.contents = new Buffer(content); 46 | this.push(file); 47 | } 48 | }; 49 | 50 | var onEnd = function() { 51 | var FILES_TO_REMOVE = [ 52 | 'install-sw.js', 53 | 'sw.js', 54 | 'sww.js', 55 | 'cache.html', 56 | 'cache.js', 57 | 'manifest.appcache' 58 | ]; 59 | FILES_TO_REMOVE.push.apply(FILES_TO_REMOVE, hookSW); 60 | 61 | var filesToLoad = paths 62 | .filter(function(file) { 63 | // Remove the files related to SW or appCache. 64 | return FILES_TO_REMOVE.indexOf(file) === -1; 65 | }) 66 | .sort(function(a, b) { 67 | return a - b; 68 | }); 69 | 70 | // Assets used by the service worker. 71 | var swFiles = filesToLoad.slice(0); // Clone array. 72 | swFiles.push('install-sw.js', 'sw.js', 'sww.js'); 73 | if (hookSW.length) { 74 | swFiles.push.apply(swFiles, hookSW); 75 | } 76 | 77 | var swContent = templates.SW_TPL_JS 78 | .replace('$VERSION', version) 79 | .replace('$FILES_TO_LOAD', JSON.stringify(swFiles)) 80 | .replace('$HOOK', JSON.stringify(hookSW)); 81 | var swFile = new File({ 82 | path: 'sw.js', 83 | contents: new Buffer(swContent) 84 | }); 85 | this.emit('data', swFile); 86 | 87 | // Copy library, check in different paths to support the example 88 | var swwPath = __dirname + 89 | '/node_modules/serviceworkers-ware/dist/sww.js'; 90 | try { 91 | fs.accessSync(swwPath); 92 | } catch (e1) { 93 | // This is for the example 94 | swwPath = '../../node_modules/serviceworkers-ware/dist/sww.js'; 95 | try { 96 | fs.accessSync(swwPath); 97 | } catch (e2) { 98 | // Check npm3 paths 99 | swwPath = __dirname + '/../serviceworkers-ware/dist/sww.js'; 100 | try { 101 | fs.accessSync(swwPath); 102 | } catch (e3) { 103 | // This is a proper error 104 | throw new PluginError(PLUGIN_NAME, 'Cannot find SWW library'); 105 | } 106 | } 107 | } 108 | 109 | var swwContent = fs.readFileSync(swwPath); 110 | var swwFile = new File({ 111 | path: 'sww.js', 112 | contents: new Buffer(swwContent) 113 | }); 114 | this.emit('data', swwFile); 115 | 116 | // Assets used by AppCache fall back. 117 | if (!hasAppCacheManifest) { 118 | var appCacheFiles = filesToLoad.slice(0); // Clone array. 119 | appCacheFiles.push('install-sw.js', 'cache.html', 'cache.js'); 120 | 121 | var appCache = new AppCache.Generator(appCacheFiles); 122 | var appCacheContent = appCache.generate(); 123 | var appCacheManifest = new File({ 124 | path: 'manifest.appcache', 125 | contents: new Buffer(appCacheContent) 126 | }); 127 | this.emit('data', appCacheManifest); 128 | 129 | var iframeJSContent = templates.CACHE_TPL_JS; 130 | var iframeJSFile = new File({ 131 | path: 'cache.js', 132 | contents: new Buffer(iframeJSContent) 133 | }); 134 | this.emit('data', iframeJSFile); 135 | 136 | var appCacheHtmlContent = templates.CACHE_TPL_HTML; 137 | var appCacheHtmlFile = new File({ 138 | path: 'cache.html', 139 | contents: new Buffer(appCacheHtmlContent) 140 | }); 141 | this.emit('data', appCacheHtmlFile); 142 | } 143 | 144 | this.emit('end'); 145 | }; 146 | 147 | return through(onFile, onEnd); 148 | }; 149 | --------------------------------------------------------------------------------