Modernize appcache package (#9600)

This commit is contained in:
James Burgess
2018-02-08 16:35:13 +01:00
committed by Ben Newman
parent b5a5935b09
commit 552764635e
5 changed files with 165 additions and 182 deletions

View File

@@ -1,67 +1,69 @@
import { Meteor } from 'meteor/meteor';
if (window.applicationCache) {
var appCacheStatuses = [
'uncached',
'idle',
'checking',
'downloading',
'updateready',
'obsolete'
];
const appCacheStatuses = [
'uncached',
'idle',
'checking',
'downloading',
'updateready',
'obsolete'
];
var updatingAppcache = false;
var reloadRetry = null;
var appcacheUpdated = false;
let updatingAppcache = false;
let reloadRetry = null;
let appcacheUpdated = false;
Reload._onMigrate('appcache', function (retry) {
if (appcacheUpdated)
return [true];
// An uncached application (one that does not have a manifest) cannot
// be updated.
if (window.applicationCache.status === window.applicationCache.UNCACHED)
return [true];
if (!updatingAppcache) {
try {
window.applicationCache.update();
} catch (e) {
Meteor._debug('applicationCache update error', e);
// There's no point in delaying the reload if we can't update the cache.
Reload._onMigrate('appcache', retry => {
if (appcacheUpdated)
return [true];
// An uncached application (one that does not have a manifest) cannot
// be updated.
if (window.applicationCache.status === window.applicationCache.UNCACHED)
return [true];
if (!updatingAppcache) {
try {
window.applicationCache.update();
} catch (e) {
Meteor._debug('applicationCache update error', e);
// There's no point in delaying the reload if we can't update the cache.
return [true];
}
updatingAppcache = true;
}
updatingAppcache = true;
}
// Delay migration until the app cache has been updated.
reloadRetry = retry;
return false;
});
// Delay migration until the app cache has been updated.
reloadRetry = retry;
return false;
});
// If we're migrating and the app cache is now up to date, signal that
// we're now ready to migrate.
var cacheIsNowUpToDate = function () {
if (!updatingAppcache)
return;
appcacheUpdated = true;
reloadRetry();
};
window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false);
window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false);
// We'll get the obsolete event on a 404 fetching the app.manifest:
// we had previously been running with an app cache, but the app
// cache has now been disabled or the appcache package removed.
// Reload to get the new non-cached code.
window.applicationCache.addEventListener('obsolete', (function () {
if (reloadRetry) {
cacheIsNowUpToDate();
} else {
// If we're migrating and the app cache is now up to date, signal that
// we're now ready to migrate.
const cacheIsNowUpToDate = () => {
if (!updatingAppcache)
return;
appcacheUpdated = true;
Reload._reload();
}
}), false);
reloadRetry();
};
window.applicationCache.addEventListener('updateready', cacheIsNowUpToDate, false);
window.applicationCache.addEventListener('noupdate', cacheIsNowUpToDate, false);
// We'll get the obsolete event on a 404 fetching the app.manifest:
// we had previously been running with an app cache, but the app
// cache has now been disabled or the appcache package removed.
// Reload to get the new non-cached code.
window.applicationCache.addEventListener('obsolete', () => {
if (reloadRetry) {
cacheIsNowUpToDate();
} else {
appcacheUpdated = true;
Reload._reload();
}
}, false);
} // if window.applicationCache

View File

@@ -1,22 +1,23 @@
var crypto = Npm.require('crypto');
var fs = Npm.require('fs');
var path = Npm.require('path');
import { Meteor } from 'meteor/meteor'
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
var _disableSizeCheck = false;
let _disableSizeCheck = false;
let disabledBrowsers = {};
Meteor.AppCache = {
config: function (options) {
_.each(options, function (value, option) {
config: options => {
Object.keys(options).forEach(option => {
value = options[option];
if (option === 'browsers') {
disabledBrowsers = {};
_.each(value, function (browser) {
disabledBrowsers[browser] = false;
});
value.each(browser => disabledBrowsers[browser] = false);
}
else if (option === 'onlineOnly') {
_.each(value, function (urlPrefix) {
RoutePolicy.declare(urlPrefix, 'static-online');
});
value.forEach(urlPrefix =>
RoutePolicy.declare(urlPrefix, 'static-online')
);
}
// option to suppress warnings for tests.
else if (option === '_disableSizeCheck') {
@@ -34,27 +35,22 @@ Meteor.AppCache = {
}
};
var disabledBrowsers = {};
var browserDisabled = function (request) {
return disabledBrowsers[request.browser.name];
};
const browserDisabled = request => disabledBrowsers[request.browser.name];
function isDynamic(resource) {
return resource.type === 'dynamic js' ||
const isDynamic = resource =>
resource.type === 'dynamic js' ||
(resource.type === 'json' &&
// TODO Update this test with PR #9439.
resource.url.startsWith('/dynamic/') &&
resource.url.endsWith('.map'))
}
resource.url.endsWith('.map'));
WebApp.addHtmlAttributeHook(function (request) {
if (browserDisabled(request))
return null;
else
return { manifest: "/app.manifest" };
});
WebApp.addHtmlAttributeHook(request =>
browserDisabled(request) ?
null :
{ manifest: "/app.manifest" }
);
WebApp.connectHandlers.use(function (req, res, next) {
WebApp.connectHandlers.use((req, res, next) => {
if (req.url !== '/app.manifest') {
return next();
}
@@ -75,7 +71,7 @@ WebApp.connectHandlers.use(function (req, res, next) {
return;
}
var manifest = "CACHE MANIFEST\n\n";
let manifest = "CACHE MANIFEST\n\n";
// After the browser has downloaded the app files from the server and
// has populated the browser's application cache, the browser will
@@ -85,7 +81,7 @@ WebApp.connectHandlers.use(function (req, res, next) {
// So to ensure that the client updates if client resources change,
// include a hash of client resources in the manifest.
manifest += "# " + WebApp.clientHash() + "\n";
manifest += `# ${WebApp.clientHash()}\n`;
// When using the autoupdate package, also include
// AUTOUPDATE_VERSION. Otherwise the client will get into an
@@ -94,16 +90,16 @@ WebApp.connectHandlers.use(function (req, res, next) {
// reload again trying to get the new code.
if (Package.autoupdate) {
var version = Package.autoupdate.Autoupdate.autoupdateVersion;
const version = Package.autoupdate.Autoupdate.autoupdateVersion;
if (version !== WebApp.clientHash())
manifest += "# " + version + "\n";
manifest += `# ${version}\n`;
}
manifest += "\n";
manifest += "CACHE:" + "\n";
manifest += "/" + "\n";
_.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
manifest += "CACHE:\n";
manifest += "/\n";
WebApp.clientPrograms[WebApp.defaultArch].manifest.forEach(resource => {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url) &&
! isDynamic(resource)) {
@@ -116,7 +112,7 @@ WebApp.connectHandlers.use(function (req, res, next) {
// the user can't modify the asset until the cache headers
// expire.
if (!resource.cacheable)
manifest += "?" + resource.hash;
manifest += `?${resource.hash}`;
manifest += "\n";
}
@@ -124,7 +120,7 @@ WebApp.connectHandlers.use(function (req, res, next) {
manifest += "\n";
manifest += "FALLBACK:\n";
manifest += "/ /" + "\n";
manifest += "/ /\n";
// Add a fallback entry for each uncacheable asset we added above.
//
// This means requests for the bare url ("/image.png" instead of
@@ -133,13 +129,12 @@ WebApp.connectHandlers.use(function (req, res, next) {
// request to the server and have the asset served from cache by
// specifying the full URL with hash in their code (manually, with
// some sort of URL rewriting helper)
_.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
WebApp.clientPrograms[WebApp.defaultArch].manifest.forEach(resource => {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url) &&
! resource.cacheable &&
! isDynamic(resource)) {
manifest += resource.url + " " + resource.url +
"?" + resource.hash + "\n";
manifest += `${resource.url} ${resource.url}?${resource.hash}\n`;
}
});
@@ -148,29 +143,24 @@ WebApp.connectHandlers.use(function (req, res, next) {
manifest += "NETWORK:\n";
// TODO adding the manifest file to NETWORK should be unnecessary?
// Want more testing to be sure.
manifest += "/app.manifest" + "\n";
_.each(
[].concat(
RoutePolicy.urlPrefixesFor('network'),
RoutePolicy.urlPrefixesFor('static-online')
),
function (urlPrefix) {
manifest += urlPrefix + "\n";
}
);
manifest += "*" + "\n";
manifest += "/app.manifest\n";
[
...RoutePolicy.urlPrefixesFor('network'),
...RoutePolicy.urlPrefixesFor('static-online')
].forEach(urlPrefix => manifest += `${urlPrefix}\n`);
manifest += "*\n";
// content length needs to be based on bytes
var body = Buffer.from(manifest);
const body = Buffer.from(manifest);
res.setHeader('Content-Type', 'text/cache-manifest');
res.setHeader('Content-Length', body.length);
return res.end(body);
});
var sizeCheck = function () {
var totalSize = 0;
_.each(WebApp.clientPrograms[WebApp.defaultArch].manifest, function (resource) {
const sizeCheck = () => {
let totalSize = 0;
WebApp.clientPrograms[WebApp.defaultArch].manifest.forEach(resource => {
if (resource.where === 'client' &&
! RoutePolicy.classify(resource.url) &&
! isDynamic(resource)) {
@@ -181,7 +171,7 @@ var sizeCheck = function () {
Meteor._debug(
"** You are using the appcache package but the total size of the\n" +
"** cached resources is " +
(totalSize / 1024 / 1024).toFixed(1) + "MB.\n" +
`${(totalSize / 1024 / 1024).toFixed(1)}MB.\n` +
"**\n" +
"** This is over the recommended maximum of 5 MB and may break your\n" +
"** app in some browsers! See http://docs.meteor.com/#appcache\n" +
@@ -195,7 +185,4 @@ var sizeCheck = function () {
// want cached. Otherwise, the size check warning will still print even
// if the user excludes their large files with
// `Meteor.AppCache.config({onlineOnly: files})`.
Meteor.startup(function () {
if (! _disableSizeCheck)
sizeCheck();
});
Meteor.startup(() => ! _disableSizeCheck ? sizeCheck() : null);

View File

@@ -1,13 +1,9 @@
var manifestUrl = '/app.manifest';
const manifestUrl = '/app.manifest';
var appcacheTest = function (name, cb) {
Tinytest.addAsync('appcache - ' + name, function (test, next) {
HTTP.get(manifestUrl, function (err, res) {
if (err) {
test.fail(err);
} else {
cb(test, res);
}
const appcacheTest = (name, cb) => {
Tinytest.addAsync(`appcache - ${name}`, (test, next) => {
HTTP.get(manifestUrl, (err, res) => {
err ? test.fail(err) : cb(test, res);
next();
});
});
@@ -15,31 +11,34 @@ var appcacheTest = function (name, cb) {
// Verify that the code status of the HTTP response is "OK"
appcacheTest('presence', function (test, manifest) {
test.equal(manifest.statusCode, 200, 'manifest not served');
});
appcacheTest('presence', (test, manifest) =>
test.equal(manifest.statusCode, 200, 'manifest not served'));
// Verify the content-type HTTP header
appcacheTest('content type', function (test, manifest) {
test.equal(manifest.headers['content-type'], 'text/cache-manifest');
});
appcacheTest('content type', (test, manifest) =>
test.equal(manifest.headers['content-type'], 'text/cache-manifest'));
// Verify that each section header is only set once.
appcacheTest('sections uniqueness', function (test, manifest) {
var content = manifest.content;
var mandatorySectionHeaders = ['CACHE:', 'NETWORK:', 'FALLBACK:'];
var optionalSectionHeaders = ['SETTINGS'];
_.each(_.union(mandatorySectionHeaders, optionalSectionHeaders),
function (sectionHeader) {
var globalSearch = new RegExp(sectionHeader, "g");
var matches = content.match(globalSearch) || [];
test.isTrue(matches.length <= 1, sectionHeader + ' is set twice');
if (_.contains(mandatorySectionHeaders, sectionHeader)) {
test.isTrue(matches.length == 1, sectionHeader + ' is not set');
}
});
appcacheTest('sections uniqueness', (test, manifest) => {
const { content } = manifest;
const mandatorySectionHeaders = ['CACHE:', 'NETWORK:', 'FALLBACK:'];
const optionalSectionHeaders = ['SETTINGS'];
const allSectionHeaders = [
...mandatorySectionHeaders,
...optionalSectionHeaders.filter(
header => !mandatorySectionHeaders.includes(header)
),
];
allSectionHeaders.forEach(sectionHeader => {
const globalSearch = new RegExp(sectionHeader, "g");
const matches = content.match(globalSearch) || [];
test.isTrue(matches.length <= 1, `${sectionHeader} is set twice`);
if (mandatorySectionHeaders.includes(sectionHeader)) {
test.isTrue(matches.length == 1, `${sectionHeader} is not set`);
}
});
});
@@ -47,25 +46,24 @@ appcacheTest('sections uniqueness', function (test, manifest) {
// regular expressions. Regular expressions matches malformed URIs but that's
// not what we're trying to catch here (the user is free to add its own content
// in the manifest -- even malformed).
appcacheTest('sections validity', function (test, manifest) {
var lines = manifest.content.split('\n');
var i = 0;
var currentRegex = null, line = null;
appcacheTest('sections validity', (test, manifest) => {
const lines = manifest.content.split('\n');
let i = 0;
let currentRegex = null;
let line = null;
var nextLine = function () {
return lines[i++];
};
const nextLine = () => lines[i++];
var eof = function () {
return i >= lines.length;
};
const eof = () => i >= lines.length;
var nextLineMatches = function (expected, n) {
const nextLineMatches = (expected, n) => {
n = n || 1;
_.times(n, function () {
var testFunc = _.isRegExp(expected) ? 'matches' : 'equal';
for(let j = 0; j < n; j++) {
const testFunc = toString.call(expected) === '[object RegExp]' ?
'matches' :
'equal';
test[testFunc](nextLine(), expected);
});
}
};
// Verify header validity
@@ -96,7 +94,7 @@ appcacheTest('sections validity', function (test, manifest) {
// Outside sections, only blanks lines and comments are valid
else if (currentRegex === null)
test.fail('Invalid line ' + i + ': ' + line);
test.fail(`Invalid line ${i}: ${line}`);
// Inside a section, a star is a valid expression
else if (line === '*')
@@ -105,7 +103,7 @@ appcacheTest('sections validity', function (test, manifest) {
// If it is not a blank line, not a comment, and not a header it must
// match the current section format
else
test.matches(line, currentRegex, 'line ' + i);
test.matches(line, currentRegex, `line ${i}`);
}
});
@@ -114,30 +112,30 @@ appcacheTest('sections validity', function (test, manifest) {
// are present in the network section of the manifest. The `appcache` package
// also automatically add the manifest (`app.manifest`) add the star symbol to
// this list and therefore we also check the presence of these two elements.
appcacheTest('network section content', function (test, manifest) {
var shouldBePresentInNetworkSection = [
appcacheTest('network section content', (test, manifest) => {
const shouldBePresentInNetworkSection = [
"/app.manifest",
"/online/",
"/bigimage.jpg",
"/largedata.json",
"*"
];
var lines = manifest.content.split('\n');
var startNetworkSection = lines.indexOf('NETWORK:');
const lines = manifest.content.split('\n');
const startNetworkSection = lines.indexOf('NETWORK:');
// We search the end of the 'NETWORK:' section by looking at the beginning
// of any potential other section. By default we set this value to
// `lines.length - 1` which is the index of the last line.
var otherSections = ['CACHE:', 'FALLBACK:', 'SETTINGS'];
var endNetworkSection = _.reduce(otherSections, function (min, sectionName) {
var position = lines.indexOf(sectionName);
const otherSections = ['CACHE:', 'FALLBACK:', 'SETTINGS'];
const endNetworkSection = otherSections.reduce((min, sectionName) => {
const position = lines.indexOf(sectionName);
return position > startNetworkSection && position < min ? position : min;
}, lines.length - 1);
// We remove the first line because it's the 'NETWORK:' header line.
var networkLines = lines.slice(startNetworkSection + 1, endNetworkSection);
const networkLines = lines.slice(startNetworkSection + 1, endNetworkSection);
_.each(shouldBePresentInNetworkSection, function (item) {
test.include(networkLines, item);
});
shouldBePresentInNetworkSection.forEach(
item => test.include(networkLines, item)
);
});

View File

@@ -8,9 +8,7 @@
// real hook. We point to a non-existent file to clear the appcache in
// case there was previously a site running with appcache on
// localhost:3000.
WebApp.addHtmlAttributeHook(function (request) {
return { manifest: "/no-such-file" };
});
WebApp.addHtmlAttributeHook(request => ({ manifest: "/no-such-file" }));
// Let's add some resources in the 'NETWORK' section

View File

@@ -1,23 +1,21 @@
Package.describe({
summary: "Enable the application cache in the browser",
version: "1.1.1"
version: "1.1.2",
});
Package.onUse(function (api) {
api.use('webapp', 'server');
Package.onUse(api => {
api.use('ecmascript', ['client', 'server']);
api.use(['webapp', 'routepolicy'], 'server');
api.use('reload', 'client');
api.use('routepolicy', 'server');
api.use('underscore', 'server');
api.use('autoupdate', 'server', {weak: true});
api.addFiles('appcache-client.js', 'client');
api.addFiles('appcache-server.js', 'server');
api.mainModule('appcache-client.js', 'client');
api.mainModule('appcache-server.js', 'server');
});
Package.onTest(function (api) {
Package.onTest(api => {
api.use('tinytest');
api.use('appcache');
api.use('http', 'client');
api.use('underscore', 'client');
api.use('webapp', 'server');
api.addFiles('appcache_tests-server.js', 'server');
api.addFiles('appcache_tests-client.js', 'client');