Skip to content
This repository was archived by the owner on Nov 27, 2018. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion lib/bootstrap.js
Original file line number Diff line number Diff line change
Expand Up @@ -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') {
Expand Down
150 changes: 106 additions & 44 deletions lib/offline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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) {
Expand All @@ -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));
Copy link
Contributor

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:

[13:39:15] [conflict] Creating offline-worker.js

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?

Copy link
Contributor Author

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.


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
Copy link
Contributor

Choose a reason for hiding this comment

The 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,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: you could avoid defining these two variables (data and hash) and directly do `hash: getHash(fs.readFileSync(filepath));', to make the function cleaner.

};
});

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');
}
});
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
125 changes: 125 additions & 0 deletions templates/app/offline-worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/* Any copyright is dedicated to the Public Domain.
Copy link
Contributor

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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 = {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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));
10 changes: 5 additions & 5 deletions test/testOffline.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand All @@ -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);
});
});

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down