-
Notifications
You must be signed in to change notification settings - Fork 17
Public domain sw #188
Public domain sw #188
Changes from all commits
e88a5bb
4f97ff6
df5297f
f891977
2b3e7ba
70ca62a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,13 +21,18 @@ var promisify = require('promisify-node'); | |
|
|
||
| var fs = require('fs'); | ||
| 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')); | ||
| 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'); | ||
|
|
@@ -42,9 +47,10 @@ module.exports = function(config) { | |
| } | ||
|
|
||
| var fileGlobs = config.fileGlobs || ['**/*']; | ||
| var importScripts = config.importScripts || []; | ||
|
|
||
| // Remove the existing service worker, if any, so sw-precache doesn't include | ||
| // it in the list of files to cache. | ||
| // Remove the existing service worker, if any, so the worker doesn't include | ||
| // itself in the list of files to cache. | ||
| try { | ||
| fs.unlinkSync(path.join(rootDir, 'offline-worker.js')); | ||
| } catch (ex) { | ||
|
|
@@ -55,60 +61,116 @@ 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; | ||
| } catch (ex) { | ||
| return ''; | ||
| } | ||
| }).then(function(cacheId) { | ||
| var staticFileGlobs = fileGlobs.map(function(v) { | ||
| }) | ||
| .then(function(cacheId) { | ||
| checkImports(importScripts, rootDir); | ||
|
|
||
| var absoluteGlobs = fileGlobs.map(function (v) { | ||
| return path.join(rootDir, v); | ||
| }); | ||
| var files = flatGlobs(absoluteGlobs); | ||
| var filesAndHashes = getFilesAndHashes(files, rootDir, absoluteGlobs); | ||
|
|
||
| staticFileGlobs.forEach(function(globPattern) { | ||
| glob.sync(globPattern.replace(path.sep, '/')).forEach(function(file) { | ||
| var stat = fs.statSync(file); | ||
| var replacements = { | ||
| cacheId: cacheId, | ||
| cacheVersion: getHashOfHashes(pluckHashes(filesAndHashes)), | ||
| resources: filesAndHashes, | ||
| importScripts: importScripts, | ||
| }; | ||
|
|
||
| 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.')); | ||
| } | ||
| }); | ||
| var stream = gulp.src([__dirname + '/../templates/app/offline-worker.js']) | ||
| .pipe(template(replacements)) | ||
| .pipe(gulp.dest(rootDir)); | ||
|
|
||
| return new Promise(function (resolve, reject) { | ||
| stream.on('finish', resolve); | ||
| stream.on('error', reject); | ||
| }); | ||
| }) | ||
| .then(resolve, reject); | ||
|
|
||
| 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 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; | ||
| }, {})); | ||
| } | ||
|
|
||
| function getFilesAndHashes(files, rootDir, sizeWhiteList) { | ||
| var totalSize = 0; | ||
| var filesAndHashes = files.map(function (filepath) { | ||
| var size = countFileSize(filepath, sizeWhiteList); | ||
| totalSize += size; | ||
| var data = fs.readFileSync(filepath); | ||
| var hash = getHash(data); | ||
| console.log(chalk.bold.green('✓ ') + 'Caching ' + filepath + ' (' + prettyBytes(size) + ')'); | ||
| return { | ||
| path: filepath.replace(rootDir, function (match, offset) { | ||
| // The root must be the worker's directory | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd still like to understand what this comment is suggesting, but I'm not going to block landing the branch on this. |
||
| return offset === 0 ? './' : match; | ||
| }), | ||
| hash: hash, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: you could avoid defining these two variables ( |
||
| }; | ||
| }); | ||
|
|
||
| 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, | ||
| console.log('Total cache size is ' + prettyBytes(totalSize) + | ||
| ' for ' + files.length + ' files.\n'); | ||
|
|
||
| return filesAndHashes; | ||
| } | ||
|
|
||
| function checkImports(imports, rootDir) { | ||
| imports.forEach(function (filepath) { | ||
| assertExists(path.join(rootDir, filepath)); | ||
| }); | ||
| })); | ||
| } | ||
|
|
||
| function pluckHashes(entries) { | ||
| return entries.map(function (entry) { return entry.hash; }); | ||
| } | ||
|
|
||
| function assertExists(filepath) { | ||
| var stat; | ||
| try { | ||
| stat = fs.statSync(filepath); | ||
| } catch (ex) { | ||
| console.log(chalk.red.bold(filepath + ' doesn\'t exist.')); | ||
| throw ex; | ||
| } | ||
|
|
||
| if (!stat.isFile()) { | ||
| console.log(chalk.red.bold(filepath + ' is not a file.')); | ||
| throw new Error(filepath + ' is not a file.'); | ||
| } | ||
| } | ||
|
|
||
| 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(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 getHashOfHashes(hashArray) { | ||
| return getHash(new Buffer(hashArray.join(''), 'hex')); | ||
| } | ||
|
|
||
| function getHash(data) { | ||
| var sha1 = crypto.createHash('sha1'); | ||
| sha1.update(data); | ||
| return sha1.digest('hex'); | ||
| } | ||
| }); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| /* Any copyright is dedicated to the Public Domain. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder if this should live at the top level of the directory tree, since it's a template, but unlike the other files in templates/, this one doesn't get copied to a developer's app directory when they bootstrap an app. Eh, it probably doesn't matter.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As you want. If we don't like this way, we can open a follow up later. |
||
| * http://creativecommons.org/publicdomain/zero/1.0/ */ | ||
|
|
||
| <% importScripts.forEach(function (filepath) { | ||
| %>importScripts('<%- filepath %>'); | ||
| <% }); %> | ||
| (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) { | ||
| event.waitUntil(oghliner.cacheResources().then(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) { | ||
| event.waitUntil(oghliner.clearOtherCaches().then(function () { | ||
| return self.clients.claim(); | ||
| })); | ||
| }); | ||
|
|
||
| // Retrieves the request following oghliner strategy. | ||
| self.addEventListener('fetch', function (event) { | ||
| if (event.request.method === 'GET') { | ||
| event.respondWith(oghliner.get(event.request)); | ||
| } else { | ||
| event.respondWith(self.fetch(event.request)); | ||
| } | ||
| }); | ||
|
|
||
| var oghliner = self.oghliner = { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see self.oghliner being used anywhere. Is this to support imported scripts having access to the oghliner object?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, to expose the object and to allow better testing. |
||
|
|
||
| // 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: [ | ||
| './', // cache always the current root to make the default page available | ||
| <% resources.forEach(function (pathAndHash) { | ||
| %> '<%- pathAndHash.path %>', // <%- pathAndHash.hash %> | ||
| <% }); %> | ||
| ], | ||
|
|
||
| // Adds the resources to the cache controlled by this worker. | ||
| cacheResources: function () { | ||
| var now = Date.now(); | ||
| var baseUrl = self.location; | ||
| 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 not controlled by this worker. | ||
| clearOtherCaches: function () { | ||
| 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. | ||
| get: function (request) { | ||
| return this.openCache() | ||
| .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 | ||
| // serving from the offline-cache. | ||
| openCache: function () { | ||
| if (!this._cache) { | ||
| this._cache = self.caches.open(this.CACHE_NAME); | ||
| } | ||
| return this._cache; | ||
| } | ||
|
|
||
| }; | ||
| }(self)); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doing this with Gulp has the unfortunate side-effect of displaying a complex and confusing error message at the end of the process:
We shouldn't display this message, per the design proposal in #162 and its implementation in #175. (If we did decide that it made sense to tell the user we were creating that file, then it would look very different, something like
✓ Creating PATH/TO/offline-worker.js.) Can you suppress that output?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I simply removed the warning as the information about the sw being created is displayed before the caching messages.