mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
297 lines
9.1 KiB
JavaScript
Executable File
297 lines
9.1 KiB
JavaScript
Executable File
import { Meteor } from 'meteor/meteor'
|
|
import { isModern } from "meteor/modern-browsers";
|
|
import { WebApp } from "meteor/webapp";
|
|
import crypto from 'crypto';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
|
|
let _disableSizeCheck = false;
|
|
let disabledBrowsers = {};
|
|
|
|
Meteor.AppCache = {
|
|
config: options => {
|
|
Object.keys(options).forEach(option => {
|
|
value = options[option];
|
|
if (option === 'browsers') {
|
|
disabledBrowsers = {};
|
|
value.each(browser => disabledBrowsers[browser] = false);
|
|
}
|
|
else if (option === 'onlineOnly') {
|
|
value.forEach(urlPrefix =>
|
|
RoutePolicy.declare(urlPrefix, 'static-online')
|
|
);
|
|
}
|
|
// option to suppress warnings for tests.
|
|
else if (option === '_disableSizeCheck') {
|
|
_disableSizeCheck = value;
|
|
}
|
|
else if (value === false) {
|
|
disabledBrowsers[option] = true;
|
|
}
|
|
else if (value === true) {
|
|
disabledBrowsers[option] = false;
|
|
} else {
|
|
throw new Error('Invalid AppCache config option: ' + option);
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
const browserDisabled = request => disabledBrowsers[request.browser.name];
|
|
|
|
// Cache of previously computed app.manifest files.
|
|
const manifestCache = new Map;
|
|
|
|
const shouldSkip = resource =>
|
|
resource.type === 'dynamic js' ||
|
|
(resource.type === 'json' &&
|
|
(resource.url.endsWith('.map') ||
|
|
resource.url.endsWith('.stats.json?meteor_js_resource=true')));
|
|
|
|
WebApp.addHtmlAttributeHook(request =>
|
|
browserDisabled(request) ?
|
|
null :
|
|
{ manifest: "/app.manifest" }
|
|
);
|
|
|
|
WebApp.connectHandlers.use((req, res, next) => {
|
|
if (req.url !== '/app.manifest') {
|
|
return next();
|
|
}
|
|
|
|
const request = WebApp.categorizeRequest(req);
|
|
|
|
// Browsers will get confused if we unconditionally serve the
|
|
// manifest and then disable the app cache for that browser. If
|
|
// the app cache had previously been enabled for a browser, it
|
|
// will continue to fetch the manifest as long as it's available,
|
|
// even if we now are not including the manifest attribute in the
|
|
// app HTML. (Firefox for example will continue to display "this
|
|
// website is asking to store data on your computer for offline
|
|
// use"). Returning a 404 gets the browser to really turn off the
|
|
// app cache.
|
|
|
|
if (browserDisabled(request)) {
|
|
res.writeHead(404);
|
|
res.end();
|
|
return;
|
|
}
|
|
|
|
const cacheInfo = {
|
|
modern: isModern(request.browser),
|
|
};
|
|
|
|
cacheInfo.arch = cacheInfo.modern
|
|
? "web.browser"
|
|
: "web.browser.legacy";
|
|
|
|
// The true hash of the client manifest for this arch, regardless of
|
|
// AUTOUPDATE_VERSION or Autoupdate.autoupdateVersion.
|
|
cacheInfo.clientHash = WebApp.clientHash(cacheInfo.arch);
|
|
|
|
if (Package.autoupdate) {
|
|
const {
|
|
// New in Meteor 1.7.1 (autoupdate@1.5.0), this versions object maps
|
|
// client architectures (e.g. "web.browser") to client hashes that
|
|
// reflect AUTOUPDATE_VERSION and Autoupdate.autoupdateVersion.
|
|
versions,
|
|
// The legacy way of forcing a particular version, supported here
|
|
// just in case Autoupdate.versions is not defined.
|
|
autoupdateVersion,
|
|
} = Package.autoupdate.Autoupdate;
|
|
|
|
const version = versions
|
|
? versions[cacheInfo.arch].version
|
|
: autoupdateVersion;
|
|
|
|
if (typeof version === "string" &&
|
|
version !== cacheInfo.clientHash) {
|
|
cacheInfo.autoupdateVersion = version;
|
|
}
|
|
}
|
|
|
|
const cacheKey = JSON.stringify(cacheInfo);
|
|
|
|
if (! manifestCache.has(cacheKey)) {
|
|
manifestCache.set(cacheKey, computeManifest(cacheInfo));
|
|
}
|
|
|
|
const manifest = manifestCache.get(cacheKey);
|
|
|
|
res.setHeader('Content-Type', 'text/cache-manifest');
|
|
res.setHeader('Content-Length', manifest.length);
|
|
|
|
return res.end(manifest);
|
|
});
|
|
|
|
function computeManifest(cacheInfo) {
|
|
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
|
|
// *only* connect to the server and reload the application if the
|
|
// *contents* of the app manifest file has changed.
|
|
//
|
|
// So to ensure that the client updates if client resources change,
|
|
// include a hash of client resources in the manifest.
|
|
manifest += `# ${cacheInfo.clientHash}\n`;
|
|
|
|
// When using the autoupdate package, also include
|
|
// AUTOUPDATE_VERSION. Otherwise the client will get into an
|
|
// infinite loop of reloads when the browser doesn't fetch the new
|
|
// app HTML which contains the new version, and autoupdate will
|
|
// reload again trying to get the new code.
|
|
if (typeof cacheInfo.autoupdateVersion === "string") {
|
|
manifest += `# ${cacheInfo.autoupdateVersion}\n`;
|
|
}
|
|
|
|
manifest += "\n";
|
|
|
|
manifest += "CACHE:\n";
|
|
manifest += "/\n";
|
|
|
|
eachResource(cacheInfo, resource => {
|
|
const { url } = resource;
|
|
|
|
if (resource.where !== 'client' ||
|
|
RoutePolicy.classify(url) ||
|
|
shouldSkip(resource)) {
|
|
return;
|
|
}
|
|
|
|
manifest += url;
|
|
|
|
// If the resource is not already cacheable (has a query parameter,
|
|
// presumably with a hash or version of some sort), put a version with
|
|
// a hash in the cache.
|
|
//
|
|
// Avoid putting a non-cacheable asset into the cache, otherwise the
|
|
// user can't modify the asset until the cache headers expire.
|
|
if (! resource.cacheable) {
|
|
manifest += `?${resource.hash}`;
|
|
}
|
|
|
|
manifest += "\n";
|
|
});
|
|
manifest += "\n";
|
|
|
|
manifest += "FALLBACK:\n";
|
|
manifest += "/ /\n";
|
|
eachResource(cacheInfo, (resource, arch, prefix) => {
|
|
const { url } = resource;
|
|
|
|
if (resource.where !== 'client' ||
|
|
RoutePolicy.classify(url) ||
|
|
shouldSkip(resource)) {
|
|
return;
|
|
}
|
|
|
|
if (! resource.cacheable) {
|
|
// Add a fallback entry for each uncacheable asset we added above.
|
|
//
|
|
// This means requests for the bare url ("/image.png" instead of
|
|
// "/image.png?hash") will work offline. Online, however, the
|
|
// browser will send a request to the server. Users can remove this
|
|
// extra 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)
|
|
manifest += `${url} ${url}?${resource.hash}\n`;
|
|
}
|
|
|
|
if (resource.type === 'asset' &&
|
|
prefix.length > 0 &&
|
|
url.startsWith(prefix)) {
|
|
// If the URL has a prefix like /__browser.legacy or /__cordova, add
|
|
// a fallback from the un-prefixed URL to the fully prefixed URL, so
|
|
// that legacy/cordova browsers can load assets offline without
|
|
// using an explicit prefix. When the client is online, these assets
|
|
// will simply come from the modern web.browser bundle, which does
|
|
// not prefix its asset URLs. Using a fallback rather than just
|
|
// duplicating the resources in the manifest is important because of
|
|
// appcache size limits.
|
|
manifest += `${url.slice(prefix.length)} ${url}?${resource.hash}\n`;
|
|
}
|
|
});
|
|
|
|
manifest += "\n";
|
|
|
|
manifest += "NETWORK:\n";
|
|
// TODO adding the manifest file to NETWORK should be unnecessary?
|
|
// Want more testing to be sure.
|
|
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
|
|
return Buffer.from(manifest, "utf8");
|
|
}
|
|
|
|
function eachResource({
|
|
modern,
|
|
arch,
|
|
}, callback) {
|
|
const manifest = WebApp.clientPrograms[arch].manifest;
|
|
|
|
let prefix = "";
|
|
if (! modern) {
|
|
manifest.some(({ url }) => {
|
|
if (url && url.startsWith("/__")) {
|
|
prefix = url.split("/", 2).join("/");
|
|
return true;
|
|
}
|
|
});
|
|
}
|
|
|
|
manifest.forEach(resource => {
|
|
callback(resource, arch, prefix);
|
|
});
|
|
}
|
|
|
|
function sizeCheck() {
|
|
const sizes = [ // Check size of each known architecture independently.
|
|
"web.browser",
|
|
"web.browser.legacy",
|
|
].reduce((filt, arch) => {
|
|
let totalSize = 0;
|
|
|
|
WebApp.clientPrograms[arch].manifest.forEach(resource => {
|
|
if (resource.where === 'client' &&
|
|
! RoutePolicy.classify(resource.url) &&
|
|
! shouldSkip(resource)) {
|
|
totalSize += resource.size;
|
|
}
|
|
});
|
|
|
|
if (totalSize > 5 * 1024 * 1024) {
|
|
filt.push({
|
|
arch,
|
|
size: totalSize
|
|
});
|
|
}
|
|
return filt;
|
|
}, []);
|
|
if (sizes.length > 0) {
|
|
Meteor._debug([
|
|
"** You are using the appcache package, but the size of",
|
|
"** one or more of your cached resources is larger than",
|
|
"** the recommended maximum size of 5MB which may break",
|
|
"** your app in some browsers!",
|
|
"** ",
|
|
...sizes.map(data => `** ${data.arch}: ${(data.size / 1024 / 1024).toFixed(1)}MB`),
|
|
"** ",
|
|
"** See http://docs.meteor.com/#appcache for more",
|
|
"** information and fixes."
|
|
].join("\n"));
|
|
}
|
|
}
|
|
|
|
// Run the size check after user code has had a chance to run. That way,
|
|
// the size check can take into account files that the user does not
|
|
// 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(() => _disableSizeCheck || sizeCheck());
|