From e88a5bbc6ea9ea02fbde04d4c3eb90b111c61209 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20de=20la=20Puente=20Gonz=C3=A1lez?= Date: Thu, 12 Nov 2015 17:58:57 +0100 Subject: [PATCH 1/6] Worker skeleton --- templates/app/offline-worker.js | 85 +++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 templates/app/offline-worker.js diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js new file mode 100644 index 00000000..e90b7ae6 --- /dev/null +++ b/templates/app/offline-worker.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +(function (self) { + 'use strict'; + + // On install, cache resources and skip waiting so the worker won't + // wait for clients to be closed before becoming active. + self.addEventListener('install', function (event) { + var skipWaiting = self.skipWaiting.bind(self); + event.waitUntil(oghliner.cacheResources().then(skipWaiting)); + }); + + // On activation, delete old caches and start controlling the clients + // without waiting for them to reload. + self.addeventlistener('activate', function (event) { + var claim = self.clients.claim.bind(self.clients); + event.waituntil(oghliner.cleanothercaches().then(claim)); + }); + + // Retrieves the request following oghliner strategy. + self.addeventlistener('fetch', function (event) { + event.respondWith(oghliner.get(event.request)); + }); + + var oghliner = self.oghliner = { + + // This is the name for the cache controlled by this worker. + CACHE_NAME: 'offline-cache-<%= cacheVersion %>', + + // This is a list of resources that will be cached. + RESOURCES: [ +<% resources.forEach(function (pathAndHash) { + %>'<%- pathAndHash.path %>', /* <%- pathAndHash.hash %> */ +<% }); %> + ], + + // Adds the resources to the cache controlled by this worker. + cacheResources: function () { + var _this = this; + return _this.openCache() + .then(function (cache) { + return cache.addAll(_this.RESOURCES); + }); + }, + + // Remove the offline caches non controlled by this worker. + clearOtherCaches: function () { + var _this = this; + return self.caches.keys() + .then(function (cacheNames) { + return Promise.all(cacheNames.map(deleteIfNotCurrent)); + }); + + function deleteIfNotCurrent(cacheName) { + if (cacheName.indexOf('offline-cache-') !== 0 || cacheName === _this.CACHE_NAME) { + return Promise.resolve(); + } + return self.caches.delete(cacheName); + } + }, + + // Get a response from the current offline cache or from the network. + get: function (request) { + return this.openCache() + .then(function (cache) { + return cache.match(request); + }) + .then(function (response) { + if (response) { return response; } + return self.fetch(request); + }); + }, + + // Open and cache the offline cache promise to improve the performance when + // serving from the offline-cache. + openCache: function () { + if (!this._cache) { + this._cache = self.caches.open(this.CACHE_NAME); + } + return this._cache; + } + + }; +}(self)); From 4f97ff614401101af925b2d967447e43f6193a57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20de=20la=20Puente=20Gonz=C3=A1lez?= Date: Fri, 13 Nov 2015 10:02:33 +0100 Subject: [PATCH 2/6] Fixing template, adapting bootstrap --- lib/bootstrap.js | 4 +- lib/offline.js | 156 +++++++++++++++++++++++--------- templates/app/offline-worker.js | 13 ++- 3 files changed, 122 insertions(+), 51 deletions(-) diff --git a/lib/bootstrap.js b/lib/bootstrap.js index 7fdce116..287011b0 100644 --- a/lib/bootstrap.js +++ b/lib/bootstrap.js @@ -119,7 +119,9 @@ module.exports = function(config) { console.log('\nCreating files…'); return new Promise(function(resolve, reject) { - var stream = gulp.src([__dirname + '/../templates/**']) + var contents = __dirname + '/../templates/**'; + var workerTemplate = __dirname + '/../templates/app/offline-worker.js'; + var stream = gulp.src([contents, '!' + workerTemplate]) .pipe(rename(function (path) { // NPM can't include a .gitignore file so we have to rename it. if (path.basename === 'gitignore') { diff --git a/lib/offline.js b/lib/offline.js index f6d98c9f..a366cae0 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -20,11 +20,14 @@ var promisify = require('promisify-node'); var fs = require('fs'); +var conflict = require('gulp-conflict'); var path = require('path'); -var swPrecache = promisify(require('sw-precache')); var chalk = require('chalk'); -var ghslug = promisify(require('github-slug')); var glob = require('glob'); +var template = require('gulp-template'); +var crypto = require('crypto'); +var gulp = require('gulp'); +var ghslug = promisify(require('github-slug')); module.exports = function(config) { return new Promise(function(resolve, reject) { @@ -38,12 +41,13 @@ module.exports = function(config) { // directory for a custom domain (f.e. https://example.com/) or a subdirectory // of a GitHub Pages domain (f.e. https://mykmelez.github.io/example/). if (rootDir.lastIndexOf('/') !== rootDir.length -1) { - rootDir = rootDir + '/'; + rootDir = rootDir + "/"; } var fileGlobs = config.fileGlobs || ['**/*']; + var importGlobs = config.importScripts || []; - // Remove the existing service worker, if any, so sw-precache doesn't include + // Remove the existing service worker, if any, so the worker doesn't include // it in the list of files to cache. try { fs.unlinkSync(path.join(rootDir, 'offline-worker.js')); @@ -55,7 +59,7 @@ module.exports = function(config) { } } - resolve(ghslug('./').catch(function() { + ghslug('./').catch(function() { // Use the name from package.json if there's an error while fetching the GitHub slug. try { return JSON.parse(fs.readFileSync('package.json')).name; @@ -63,52 +67,114 @@ module.exports = function(config) { return ''; } }).then(function(cacheId) { - var staticFileGlobs = fileGlobs.map(function(v) { + var absoluteGlobs = fileGlobs.map(function (v) { return path.join(rootDir, v); }); + var files = flatGlobs(absoluteGlobs); + var filesAndHashes = getFilesAndHashes(files, rootDir, absoluteGlobs); + + var imports = flatGlobs(importGlobs); + var importsAndHashes = getImportsAndHashes(imports, + + var replacements = { + cacheId: cacheId, //TODO: Not implemented in template + ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], //TODO: Not implemented in template + cacheVersion: getVersion(filesAndHashes), + resources: filesAndHashes, + importScripts: importsAndHashes + }; - staticFileGlobs.forEach(function(globPattern) { - glob.sync(globPattern.replace(path.sep, '/')).forEach(function(file) { - var stat = fs.statSync(file); + var stream = gulp.src([__dirname + '/../templates/app/offline-worker.js']) + .pipe(template(replacements)) + .pipe(conflict(rootDir)) + .pipe(gulp.dest(rootDir)); - if (stat.isFile() && stat.size > 2 * 1024 * 1024 && staticFileGlobs.indexOf(file) === -1) { - console.log(chalk.yellow.bold(file + ' is bigger than 2 MiB. Are you sure you want to cache it? To suppress this warning, explicitly include the file in the fileGlobs list.')); - } + stream.on('finish', function () { resolve(); }); + stream.on('error', function (e) { reject(e); }); + }); + + function flatGlobs(fileGlobs) { + return Object.keys(fileGlobs.reduce(function (matches, fileGlob) { + fileGlob = fileGlob.replace(path.sep, '/'); + glob.sync(fileGlob, { nodir: true }).forEach(function (filepath) { + matches[filepath] = filepath; }); - }); + return matches; + }, {})); + } - var importScripts = config.importScripts || []; - importScripts.forEach(function(script) { - var stat; - try { - stat = fs.statSync(path.join(rootDir, script)); - } catch (ex) { - console.log(chalk.red.bold(script + ' doesn\'t exist.')); - throw ex; - } - - if (!stat.isFile()) { - console.log(chalk.red.bold(script + ' is not a file.')); - throw new Error(script + ' is not a file.'); - } - }); + function getFilesAndHashes(files, stripPrefix, sizeWhiteList) { + return files + .map(function (path) { + warnFileSize(path, fileGlobs); + logOk(path); + + var data = fs.readFileSync(path); + var hash = getHash(data); + return { + path: path.replace(stripPrefix, function (match, offset) { + return offset === 0 ? './' : match; + }), + hash: hash + }; + }); + } - return swPrecache.write(path.join(rootDir, 'offline-worker.js'), { - staticFileGlobs: staticFileGlobs, - stripPrefix: rootDir, - verbose: true, - logger: function(message) { - if (message.indexOf('Caching') === 0) { - message = chalk.bold.green('✓ ') + message.replace('static resource ', ''); - } - - console.log(message); - }, - importScripts: importScripts, - cacheId: cacheId, - ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], - maximumFileSizeToCacheInBytes: Infinity, - }); - })); + // TODO: Think about unifying this function and the previous one + function getImportsAndHashes(imports, stripPrefix, sizeWhiteList) { + return files + .map(function (path) { + // TODO: I think we should warn, not fail + assertExists(filepath); + + var data = fs.readFileSync(path); + var hash = getHash(data); + return { + path: path.replace(stripPrefix, function (match, offset) { + return offset === 0 ? './' : match; + }), + hash: hash + }; + }); + } + + function assertExists(path) { + var stat; + try { + stat = fs.statSync(path); + } catch (ex) { + console.log(chalk.red.bold(script + ' doesn\'t exist.')); + } + + if (!stat.isFile()) { + console.log(chalk.red.bold(script + ' is not a file.')); + throw new Error(script + ' is not a file.'); + } + } + + function getVersion(filesAndHashes) { + return getHashOfHashes(filesAndHashes.map((entry) => entry.hash)); + } + + function warnFileSize(filepath, whiteList) { + var stat = fs.statSync(filepath); + if (stat.isFile() && stat.size > 2 * 1024 * 1024 && whiteList.indexOf(filepath) === -1) { + console.log(chalk.yellow.bold(file + ' is bigger than 2 MiB. Are you sure you want to cache it? To suppress this warning, explicitly include the file in the fileGlobs list.')); + } + } + + function logOk(filepath) { + console.log(chalk.bold.green('✓ ') + 'Caching \'' + filepath + '\''); + } + + function getHashOfHashes(hashArray) { + return getHash(new Buffer(hashArray.join(''), 'hex')); + } + + function getHash(data) { + var sha1 = crypto.createHash('sha1'); + sha1.update(data); + return sha1.digest('hex'); + } }); }; diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js index e90b7ae6..9d9a7356 100644 --- a/templates/app/offline-worker.js +++ b/templates/app/offline-worker.js @@ -13,13 +13,13 @@ // On activation, delete old caches and start controlling the clients // without waiting for them to reload. - self.addeventlistener('activate', function (event) { + self.addEventListener('activate', function (event) { var claim = self.clients.claim.bind(self.clients); - event.waituntil(oghliner.cleanothercaches().then(claim)); + event.waitUntil(oghliner.clearOtherCaches().then(claim)); }); // Retrieves the request following oghliner strategy. - self.addeventlistener('fetch', function (event) { + self.addEventListener('fetch', function (event) { event.respondWith(oghliner.get(event.request)); }); @@ -30,8 +30,9 @@ // This is a list of resources that will be cached. RESOURCES: [ + '/', <% resources.forEach(function (pathAndHash) { - %>'<%- pathAndHash.path %>', /* <%- pathAndHash.hash %> */ +%> '<%- pathAndHash.path %>', /* <%- pathAndHash.hash %> */ <% }); %> ], @@ -67,7 +68,9 @@ return cache.match(request); }) .then(function (response) { - if (response) { return response; } + if (response) { + return response; + } return self.fetch(request); }); }, From df5297feba5caed166d440a6ed135f57bcada0f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20de=20la=20Puente=20Gonz=C3=A1lez?= Date: Fri, 13 Nov 2015 16:23:07 +0100 Subject: [PATCH 3/6] Passing tests --- lib/offline.js | 101 ++++++++++++++++++-------------- package.json | 1 + templates/app/offline-worker.js | 28 ++++++--- test/testOffline.js | 4 +- 4 files changed, 81 insertions(+), 53 deletions(-) diff --git a/lib/offline.js b/lib/offline.js index a366cae0..1ac48bc5 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -28,9 +28,12 @@ var template = require('gulp-template'); var crypto = require('crypto'); var gulp = require('gulp'); var ghslug = promisify(require('github-slug')); +var prettyBytes = require('pretty-bytes'); module.exports = function(config) { return new Promise(function(resolve, reject) { + 'use strict'; + var rootDir = config.rootDir || './'; console.log('Offlining ' + chalk.bold(rootDir) + ' to ' + chalk.bold(path.join(rootDir, 'offline-worker.js')) + '…\n'); @@ -41,11 +44,11 @@ module.exports = function(config) { // directory for a custom domain (f.e. https://example.com/) or a subdirectory // of a GitHub Pages domain (f.e. https://mykmelez.github.io/example/). if (rootDir.lastIndexOf('/') !== rootDir.length -1) { - rootDir = rootDir + "/"; + rootDir = rootDir + '/'; } var fileGlobs = config.fileGlobs || ['**/*']; - var importGlobs = config.importScripts || []; + var importScripts = config.importScripts || []; // Remove the existing service worker, if any, so the worker doesn't include // it in the list of files to cache. @@ -67,21 +70,20 @@ module.exports = function(config) { return ''; } }).then(function(cacheId) { + var importsAndHashes = getImportsAndHashes(importScripts, rootDir); + var absoluteGlobs = fileGlobs.map(function (v) { return path.join(rootDir, v); }); var files = flatGlobs(absoluteGlobs); var filesAndHashes = getFilesAndHashes(files, rootDir, absoluteGlobs); - var imports = flatGlobs(importGlobs); - var importsAndHashes = getImportsAndHashes(imports, - var replacements = { - cacheId: cacheId, //TODO: Not implemented in template + cacheId: cacheId, ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], //TODO: Not implemented in template cacheVersion: getVersion(filesAndHashes), resources: filesAndHashes, - importScripts: importsAndHashes + importScripts: importsAndHashes, }; var stream = gulp.src([__dirname + '/../templates/app/offline-worker.js']) @@ -89,9 +91,11 @@ module.exports = function(config) { .pipe(conflict(rootDir)) .pipe(gulp.dest(rootDir)); - stream.on('finish', function () { resolve(); }); - stream.on('error', function (e) { reject(e); }); - }); + return new Promise(function (resolve, reject) { + stream.on('finish', function () { resolve(); }); + stream.on('error', function (e) { reject(e); }); + }); + }).then(resolve, reject); function flatGlobs(fileGlobs) { return Object.keys(fileGlobs.reduce(function (matches, fileGlob) { @@ -103,68 +107,77 @@ module.exports = function(config) { }, {})); } - function getFilesAndHashes(files, stripPrefix, sizeWhiteList) { - return files - .map(function (path) { - warnFileSize(path, fileGlobs); - logOk(path); - - var data = fs.readFileSync(path); - var hash = getHash(data); - return { - path: path.replace(stripPrefix, function (match, offset) { - return offset === 0 ? './' : match; - }), - hash: hash - }; - }); + function getFilesAndHashes(files, rootDir, sizeWhiteList) { + var totalSize = 0; + var filesAndHashes = files.map(function (filepath) { + totalSize += countFileSize(filepath, sizeWhiteList); + var data = fs.readFileSync(filepath); + var hash = getHash(data); + logOk(filepath); + return { + path: filepath.replace(rootDir, function (match, offset) { + //XXX: The root must be the worker's directory + return offset === 0 ? './' : match; + }), + hash: hash, + }; + }); + + console.log('Total precache size is about ' + prettyBytes(totalSize)); + return filesAndHashes; } - // TODO: Think about unifying this function and the previous one - function getImportsAndHashes(imports, stripPrefix, sizeWhiteList) { - return files - .map(function (path) { - // TODO: I think we should warn, not fail + function getImportsAndHashes(imports, rootDir) { + return imports + .map(function (filepath) { + + filepath = path.join(rootDir, filepath); + //TODO: I think we should warn, not fail assertExists(filepath); - - var data = fs.readFileSync(path); + + var data = fs.readFileSync(filepath); var hash = getHash(data); return { - path: path.replace(stripPrefix, function (match, offset) { - return offset === 0 ? './' : match; + path: filepath.replace(rootDir, function (match, offset) { + //TODO: Are we sure about this replacement is not relative to the worker directory + return offset === 0 ? '' : match; }), - hash: hash + hash: hash, }; }); } - function assertExists(path) { + function assertExists(filepath) { var stat; try { - stat = fs.statSync(path); + stat = fs.statSync(filepath); } catch (ex) { - console.log(chalk.red.bold(script + ' doesn\'t exist.')); + console.log(chalk.red.bold(filepath + ' doesn\'t exist.')); + throw ex; } if (!stat.isFile()) { - console.log(chalk.red.bold(script + ' is not a file.')); - throw new Error(script + ' is not a file.'); + console.log(chalk.red.bold(filepath + ' is not a file.')); + throw new Error(filepath + ' is not a file.'); } } function getVersion(filesAndHashes) { - return getHashOfHashes(filesAndHashes.map((entry) => entry.hash)); + return getHashOfHashes(filesAndHashes.map(function(entry) { + return entry.hash; + })); } - function warnFileSize(filepath, whiteList) { + function countFileSize(filepath, whiteList) { var stat = fs.statSync(filepath); if (stat.isFile() && stat.size > 2 * 1024 * 1024 && whiteList.indexOf(filepath) === -1) { - console.log(chalk.yellow.bold(file + ' is bigger than 2 MiB. Are you sure you want to cache it? To suppress this warning, explicitly include the file in the fileGlobs list.')); + console.log(chalk.yellow.bold(filepath + ' is bigger than 2 MiB. Are you sure you want to cache it? To suppress this warning, explicitly include the file in the fileGlobs list.')); } + return stat.size; } function logOk(filepath) { - console.log(chalk.bold.green('✓ ') + 'Caching \'' + filepath + '\''); + console.log(chalk.bold.green('✓ ') + 'Caching ' + filepath); } function getHashOfHashes(hashArray) { diff --git a/package.json b/package.json index 9512b7c5..8af4b6d0 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "gulp-rename": "^1.2.2", "gulp-template": "^3.0.0", "mozilla-tabzilla": "^0.5.1", + "pretty-bytes": "^2.0.1", "promisified-promptly": "^1.0.0", "promisify-node": "^0.2.1", "read-yaml": "^1.0.0", diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js index 9d9a7356..e29bc9fa 100644 --- a/templates/app/offline-worker.js +++ b/templates/app/offline-worker.js @@ -1,21 +1,30 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ +<% importScripts.forEach(function (importAndHash) { +%>importScripts('<%- importAndHash.path %>'); /* <%- importAndHash.hash %> */; +<% }); %> (function (self) { 'use strict'; // On install, cache resources and skip waiting so the worker won't // wait for clients to be closed before becoming active. self.addEventListener('install', function (event) { - var skipWaiting = self.skipWaiting.bind(self); - event.waitUntil(oghliner.cacheResources().then(skipWaiting)); + event.waitUntil(oghliner.cacheResources().then(function () { + if (typeof self.skipWaiting === 'function') { + return self.skipWaiting(); + } + })); }); // On activation, delete old caches and start controlling the clients // without waiting for them to reload. self.addEventListener('activate', function (event) { - var claim = self.clients.claim.bind(self.clients); - event.waitUntil(oghliner.clearOtherCaches().then(claim)); + event.waitUntil(oghliner.clearOtherCaches().then(function () { + if (self.clients && typeof self.clients.claim === "function") { + return self.clients.claim(); + } + })); }); // Retrieves the request following oghliner strategy. @@ -25,8 +34,13 @@ var oghliner = self.oghliner = { - // This is the name for the cache controlled by this worker. - CACHE_NAME: 'offline-cache-<%= cacheVersion %>', + // This is the unique prefix for all the caches controlled by this worker. + CACHE_PREFIX: 'offline-cache:<%= cacheId %>:' + (self.registration ? self.registration.scope : '') + ':', + + // This is the unique name for the cache controlled by this version of the worker. + get CACHE_NAME() { + return this.CACHE_PREFIX + '<%= cacheVersion %>'; + }, // This is a list of resources that will be cached. RESOURCES: [ @@ -54,7 +68,7 @@ }); function deleteIfNotCurrent(cacheName) { - if (cacheName.indexOf('offline-cache-') !== 0 || cacheName === _this.CACHE_NAME) { + if (cacheName.indexOf(_this.CACHE_PREFIX) !== 0 || cacheName === _this.CACHE_NAME) { return Promise.resolve(); } return self.caches.delete(cacheName); diff --git a/test/testOffline.js b/test/testOffline.js index 00b9e91e..8de34b77 100644 --- a/test/testOffline.js +++ b/test/testOffline.js @@ -71,7 +71,7 @@ describe('Offline', function() { }); }); - it('should use importScript in the service worker if the importScripts option is defined', function() { + it('should use importScripts in the service worker if the importScripts option is defined', function() { var dir = temp.mkdirSync('oghliner'); fs.writeFileSync(path.join(dir, 'a-script.js'), 'data'); @@ -80,7 +80,7 @@ describe('Offline', function() { importScripts: [ 'a-script.js', ], }).then(function() { var content = fs.readFileSync(path.join(dir, 'offline-worker.js'), 'utf8'); - assert.notEqual(content.indexOf('importScripts("a-script.js");'), -1); + assert.notEqual(content.indexOf('importScripts(\'a-script.js\');'), -1); }); }); From f891977f03446809ea7c66b79e2020e037318ae5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20de=20la=20Puente=20Gonz=C3=A1lez?= Date: Fri, 13 Nov 2015 21:16:25 +0100 Subject: [PATCH 4/6] Simplifying the worker --- lib/offline.js | 43 ++++++++++++--------------------- templates/app/offline-worker.js | 34 ++++++++++++++++++++++---- 2 files changed, 44 insertions(+), 33 deletions(-) diff --git a/lib/offline.js b/lib/offline.js index 1ac48bc5..f545dafe 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -70,7 +70,7 @@ module.exports = function(config) { return ''; } }).then(function(cacheId) { - var importsAndHashes = getImportsAndHashes(importScripts, rootDir); + checkImports(importScripts, rootDir); var absoluteGlobs = fileGlobs.map(function (v) { return path.join(rootDir, v); @@ -81,9 +81,9 @@ module.exports = function(config) { var replacements = { cacheId: cacheId, ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], //TODO: Not implemented in template - cacheVersion: getVersion(filesAndHashes), + cacheVersion: getHashOfHashes(pluckHashes(filesAndHashes)), resources: filesAndHashes, - importScripts: importsAndHashes, + importScripts: importScripts, }; var stream = gulp.src([__dirname + '/../templates/app/offline-worker.js']) @@ -123,28 +123,21 @@ module.exports = function(config) { }; }); - console.log('Total precache size is about ' + prettyBytes(totalSize)); + console.log('Total precache size is about ' + prettyBytes(totalSize) + + ' for ' + files.length + ' resources.'); + return filesAndHashes; } - function getImportsAndHashes(imports, rootDir) { - return imports - .map(function (filepath) { - - filepath = path.join(rootDir, filepath); - //TODO: I think we should warn, not fail - assertExists(filepath); - - var data = fs.readFileSync(filepath); - var hash = getHash(data); - return { - path: filepath.replace(rootDir, function (match, offset) { - //TODO: Are we sure about this replacement is not relative to the worker directory - return offset === 0 ? '' : match; - }), - hash: hash, - }; - }); + function checkImports(imports, rootDir) { + imports.forEach(function (filepath) { + //TODO: I think we should warn, not fail + assertExists(path.join(rootDir, filepath)); + }); + } + + function pluckHashes(entries) { + return entries.map(function (entry) { return entry.hash; }); } function assertExists(filepath) { @@ -162,12 +155,6 @@ module.exports = function(config) { } } - function getVersion(filesAndHashes) { - return getHashOfHashes(filesAndHashes.map(function(entry) { - return entry.hash; - })); - } - function countFileSize(filepath, whiteList) { var stat = fs.statSync(filepath); if (stat.isFile() && stat.size > 2 * 1024 * 1024 && whiteList.indexOf(filepath) === -1) { diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js index e29bc9fa..c0b8e27d 100644 --- a/templates/app/offline-worker.js +++ b/templates/app/offline-worker.js @@ -1,8 +1,8 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ -<% importScripts.forEach(function (importAndHash) { -%>importScripts('<%- importAndHash.path %>'); /* <%- importAndHash.hash %> */; +<% importScripts.forEach(function (filepath) { +%>importScripts('<%- filepath %>'); <% }); %> (function (self) { 'use strict'; @@ -29,7 +29,12 @@ // Retrieves the request following oghliner strategy. self.addEventListener('fetch', function (event) { - event.respondWith(oghliner.get(event.request)); + if (event.request.method === 'GET') { + event.respondWith(oghliner.get(event.request)); + } + else { + event.respondWith(self.fetch(event.request)); + } }); var oghliner = self.oghliner = { @@ -46,16 +51,35 @@ RESOURCES: [ '/', <% resources.forEach(function (pathAndHash) { -%> '<%- pathAndHash.path %>', /* <%- pathAndHash.hash %> */ +%> '<%- pathAndHash.path %>', // <%- pathAndHash.hash %> <% }); %> ], // Adds the resources to the cache controlled by this worker. cacheResources: function () { var _this = this; + var now = Date.now(); + var baseUrl = self.location; return _this.openCache() .then(function (cache) { - return cache.addAll(_this.RESOURCES); + return Promise.all(_this.RESOURCES.map(function (resource) { + // Bust the request to get a fresh response + var url = new URL(resource, baseUrl); + var bustParameter = (url.search ? '&' : '') + '__bust=' + now; + var bustedUrl = new URL(url.toString()); + bustedUrl.search += bustParameter; + + // But cache the response for the original request + var requestConfig = { credentials: 'same-origin' }; + var originalRequest = new Request(url.toString(), requestConfig); + var bustedRequest = new Request(bustedUrl.toString(), requestConfig); + return fetch(bustedRequest).then(function (response) { + if (response.ok) { + return cache.put(originalRequest, response); + } + console.error('Error fetching ' + url + ', status was ' + response.status); + }); + })); }); }, From 2b3e7ba0614594620119e7eb42a0bc02b8b293dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20de=20la=20Puente=20Gonz=C3=A1lez?= Date: Sat, 14 Nov 2015 01:50:36 +0100 Subject: [PATCH 5/6] Fixing nits --- lib/offline.js | 29 ++++++------ templates/app/offline-worker.js | 79 +++++++++++++++++---------------- test/testOffline.js | 6 +-- 3 files changed, 59 insertions(+), 55 deletions(-) diff --git a/lib/offline.js b/lib/offline.js index f545dafe..7e0416e3 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -20,7 +20,6 @@ var promisify = require('promisify-node'); var fs = require('fs'); -var conflict = require('gulp-conflict'); var path = require('path'); var chalk = require('chalk'); var glob = require('glob'); @@ -51,7 +50,7 @@ module.exports = function(config) { var importScripts = config.importScripts || []; // Remove the existing service worker, if any, so the worker doesn't include - // it in the list of files to cache. + // itself in the list of files to cache. try { fs.unlinkSync(path.join(rootDir, 'offline-worker.js')); } catch (ex) { @@ -62,14 +61,16 @@ module.exports = function(config) { } } - ghslug('./').catch(function() { + ghslug('./') + .catch(function() { // Use the name from package.json if there's an error while fetching the GitHub slug. try { return JSON.parse(fs.readFileSync('package.json')).name; } catch (ex) { return ''; } - }).then(function(cacheId) { + }) + .then(function(cacheId) { checkImports(importScripts, rootDir); var absoluteGlobs = fileGlobs.map(function (v) { @@ -80,7 +81,7 @@ module.exports = function(config) { var replacements = { cacheId: cacheId, - ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], //TODO: Not implemented in template + ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], cacheVersion: getHashOfHashes(pluckHashes(filesAndHashes)), resources: filesAndHashes, importScripts: importScripts, @@ -88,14 +89,14 @@ module.exports = function(config) { var stream = gulp.src([__dirname + '/../templates/app/offline-worker.js']) .pipe(template(replacements)) - .pipe(conflict(rootDir)) .pipe(gulp.dest(rootDir)); return new Promise(function (resolve, reject) { stream.on('finish', function () { resolve(); }); stream.on('error', function (e) { reject(e); }); }); - }).then(resolve, reject); + }) + .then(resolve, reject); function flatGlobs(fileGlobs) { return Object.keys(fileGlobs.reduce(function (matches, fileGlob) { @@ -110,10 +111,11 @@ module.exports = function(config) { function getFilesAndHashes(files, rootDir, sizeWhiteList) { var totalSize = 0; var filesAndHashes = files.map(function (filepath) { - totalSize += countFileSize(filepath, sizeWhiteList); + var size = countFileSize(filepath, sizeWhiteList); + totalSize += size; var data = fs.readFileSync(filepath); var hash = getHash(data); - logOk(filepath); + logOk(filepath, size); return { path: filepath.replace(rootDir, function (match, offset) { //XXX: The root must be the worker's directory @@ -123,15 +125,14 @@ module.exports = function(config) { }; }); - console.log('Total precache size is about ' + prettyBytes(totalSize) + - ' for ' + files.length + ' resources.'); + console.log('Total cache size is ' + prettyBytes(totalSize) + + ' for ' + files.length + ' files.\n'); return filesAndHashes; } function checkImports(imports, rootDir) { imports.forEach(function (filepath) { - //TODO: I think we should warn, not fail assertExists(path.join(rootDir, filepath)); }); } @@ -163,8 +164,8 @@ module.exports = function(config) { return stat.size; } - function logOk(filepath) { - console.log(chalk.bold.green('✓ ') + 'Caching ' + filepath); + function logOk(filepath, size) { + console.log(chalk.bold.green('✓ ') + 'Caching ' + filepath + ' (' + prettyBytes(size) + ')'); } function getHashOfHashes(hashArray) { diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js index c0b8e27d..61c976f4 100644 --- a/templates/app/offline-worker.js +++ b/templates/app/offline-worker.js @@ -49,7 +49,7 @@ // This is a list of resources that will be cached. RESOURCES: [ - '/', + './', // cache always the current root to make the default page available <% resources.forEach(function (pathAndHash) { %> '<%- pathAndHash.path %>', // <%- pathAndHash.hash %> <% }); %> @@ -57,42 +57,40 @@ // Adds the resources to the cache controlled by this worker. cacheResources: function () { - var _this = this; var now = Date.now(); var baseUrl = self.location; - return _this.openCache() - .then(function (cache) { - return Promise.all(_this.RESOURCES.map(function (resource) { - // Bust the request to get a fresh response - var url = new URL(resource, baseUrl); - var bustParameter = (url.search ? '&' : '') + '__bust=' + now; - var bustedUrl = new URL(url.toString()); - bustedUrl.search += bustParameter; - - // But cache the response for the original request - var requestConfig = { credentials: 'same-origin' }; - var originalRequest = new Request(url.toString(), requestConfig); - var bustedRequest = new Request(bustedUrl.toString(), requestConfig); - return fetch(bustedRequest).then(function (response) { - if (response.ok) { - return cache.put(originalRequest, response); - } - console.error('Error fetching ' + url + ', status was ' + response.status); - }); - })); - }); + return this.prepareCache() + .then(function (cache) { + return Promise.all(this.RESOURCES.map(function (resource) { + // Bust the request to get a fresh response + var url = new URL(resource, baseUrl); + var bustParameter = (url.search ? '&' : '') + '__bust=' + now; + var bustedUrl = new URL(url.toString()); + bustedUrl.search += bustParameter; + + // But cache the response for the original request + var requestConfig = { credentials: 'same-origin' }; + var originalRequest = new Request(url.toString(), requestConfig); + var bustedRequest = new Request(bustedUrl.toString(), requestConfig); + return fetch(bustedRequest).then(function (response) { + if (response.ok) { + return cache.put(originalRequest, response); + } + console.error('Error fetching ' + url + ', status was ' + response.status); + }); + })); + }.bind(this)); }, - // Remove the offline caches non controlled by this worker. + // Remove the offline caches not controlled by this worker. clearOtherCaches: function () { - var _this = this; return self.caches.keys() - .then(function (cacheNames) { - return Promise.all(cacheNames.map(deleteIfNotCurrent)); - }); + .then(function (cacheNames) { + return Promise.all(cacheNames.map(deleteIfNotCurrent.bind(this))); + }.bind(this)); function deleteIfNotCurrent(cacheName) { - if (cacheName.indexOf(_this.CACHE_PREFIX) !== 0 || cacheName === _this.CACHE_NAME) { + if (cacheName.indexOf(this.CACHE_PREFIX) !== 0 || cacheName === this.CACHE_NAME) { return Promise.resolve(); } return self.caches.delete(cacheName); @@ -102,15 +100,20 @@ // Get a response from the current offline cache or from the network. get: function (request) { return this.openCache() - .then(function (cache) { - return cache.match(request); - }) - .then(function (response) { - if (response) { - return response; - } - return self.fetch(request); - }); + .then(function (cache) { + return cache.match(request); + }) + .then(function (response) { + if (response) { + return response; + } + return self.fetch(request); + }); + }, + + // Prepare the cache for installation, deleting it before if it already exists. + prepareCache: function () { + return self.caches.delete(this.CACHE_NAME).then(this.openCache.bind(this)); }, // Open and cache the offline cache promise to improve the performance when diff --git a/test/testOffline.js b/test/testOffline.js index 8de34b77..07fb52a7 100644 --- a/test/testOffline.js +++ b/test/testOffline.js @@ -251,7 +251,7 @@ describe('Offline', function() { 'test_file_1.js is bigger than 2 MiB', 'test_file_2.js is bigger than 2 MiB', 'test_file_3.js is bigger than 2 MiB', - ], [], 'Total precache size'); + ], [], 'Total cache size'); var offlinePromise = offline({ rootDir: dir, @@ -283,7 +283,7 @@ describe('Offline', function() { 'test_file_3.js is bigger than 2 MiB', ], [ 'test_file_1.js is bigger than 2 MiB', - ], 'Total precache size'); + ], 'Total cache size'); var offlinePromise = offline({ rootDir: dir, @@ -318,7 +318,7 @@ describe('Offline', function() { 'test_file_3.js is bigger than 2 MiB', ], [ 'test_file_1.js is bigger than 2 MiB', - ], 'Total precache size'); + ], 'Total cache size'); var offlinePromise = offline({ rootDir: dir, From 70ca62ad606d04f38d2ecc7e920d01e4d87d0a87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Salvador=20de=20la=20Puente=20Gonz=C3=A1lez?= Date: Thu, 19 Nov 2015 02:32:00 +0100 Subject: [PATCH 6/6] More nits --- lib/offline.js | 13 ++++--------- templates/app/offline-worker.js | 26 +++++++++++--------------- 2 files changed, 15 insertions(+), 24 deletions(-) diff --git a/lib/offline.js b/lib/offline.js index 7e0416e3..779dbfc2 100644 --- a/lib/offline.js +++ b/lib/offline.js @@ -81,7 +81,6 @@ module.exports = function(config) { var replacements = { cacheId: cacheId, - ignoreUrlParametersMatching: config.ignoreUrlParametersMatching || [/./], cacheVersion: getHashOfHashes(pluckHashes(filesAndHashes)), resources: filesAndHashes, importScripts: importScripts, @@ -92,8 +91,8 @@ module.exports = function(config) { .pipe(gulp.dest(rootDir)); return new Promise(function (resolve, reject) { - stream.on('finish', function () { resolve(); }); - stream.on('error', function (e) { reject(e); }); + stream.on('finish', resolve); + stream.on('error', reject); }); }) .then(resolve, reject); @@ -115,10 +114,10 @@ module.exports = function(config) { totalSize += size; var data = fs.readFileSync(filepath); var hash = getHash(data); - logOk(filepath, size); + console.log(chalk.bold.green('✓ ') + 'Caching ' + filepath + ' (' + prettyBytes(size) + ')'); return { path: filepath.replace(rootDir, function (match, offset) { - //XXX: The root must be the worker's directory + // The root must be the worker's directory return offset === 0 ? './' : match; }), hash: hash, @@ -164,10 +163,6 @@ module.exports = function(config) { return stat.size; } - function logOk(filepath, size) { - console.log(chalk.bold.green('✓ ') + 'Caching ' + filepath + ' (' + prettyBytes(size) + ')'); - } - function getHashOfHashes(hashArray) { return getHash(new Buffer(hashArray.join(''), 'hex')); } diff --git a/templates/app/offline-worker.js b/templates/app/offline-worker.js index 61c976f4..81157f9b 100644 --- a/templates/app/offline-worker.js +++ b/templates/app/offline-worker.js @@ -11,9 +11,7 @@ // wait for clients to be closed before becoming active. self.addEventListener('install', function (event) { event.waitUntil(oghliner.cacheResources().then(function () { - if (typeof self.skipWaiting === 'function') { - return self.skipWaiting(); - } + return self.skipWaiting(); })); }); @@ -21,9 +19,7 @@ // without waiting for them to reload. self.addEventListener('activate', function (event) { event.waitUntil(oghliner.clearOtherCaches().then(function () { - if (self.clients && typeof self.clients.claim === "function") { - return self.clients.claim(); - } + return self.clients.claim(); })); }); @@ -31,8 +27,7 @@ self.addEventListener('fetch', function (event) { if (event.request.method === 'GET') { event.respondWith(oghliner.get(event.request)); - } - else { + } else { event.respondWith(self.fetch(event.request)); } }); @@ -84,17 +79,18 @@ // Remove the offline caches not controlled by this worker. clearOtherCaches: function () { - return self.caches.keys() - .then(function (cacheNames) { - return Promise.all(cacheNames.map(deleteIfNotCurrent.bind(this))); - }.bind(this)); - - function deleteIfNotCurrent(cacheName) { + var deleteIfNotCurrent = function (cacheName) { if (cacheName.indexOf(this.CACHE_PREFIX) !== 0 || cacheName === this.CACHE_NAME) { return Promise.resolve(); } return self.caches.delete(cacheName); - } + }.bind(self); + + return self.caches.keys() + .then(function (cacheNames) { + return Promise.all(cacheNames.map(deleteIfNotCurrent)); + }); + }, // Get a response from the current offline cache or from the network.