Skip to content

Instantly share code, notes, and snippets.

@PaulKinlan
Last active June 27, 2016 05:03
Show Gist options
  • Save PaulKinlan/ce7c707e4cb96c752816 to your computer and use it in GitHub Desktop.
Save PaulKinlan/ce7c707e4cb96c752816 to your computer and use it in GitHub Desktop.
Progressive Web App Checklist
(function() {
var ManifestParser = (function() {
'use strict';
var _jsonInput = {};
var _manifest = {};
var _logs = [];
var _tips = [];
var _success = true;
var ALLOWED_DISPLAY_VALUES = ['fullscreen',
'standalone',
'minimal-ui',
'browser'];
var ALLOWED_ORIENTATION_VALUES = ['any',
'natural',
'landscape',
'portrait',
'portrait-primary',
'portrait-secondary',
'landscape-primary',
'landscape-secondary'];
function _parseString(args) {
var object = args.object;
var property = args.property;
if (!(property in object)) {
return undefined;
}
if (typeof object[property] != 'string') {
_logs.push('ERROR: \'' + property +
'\' expected to be a string but is not.');
return undefined;
}
if (args.trim) {
return object[property].trim();
}
return object[property];
}
function _parseBoolean(args) {
var object = args.object;
var property = args.property;
var defaultValue = args.defaultValue;
if (!(property in object)) {
return defaultValue;
}
if (typeof object[property] != 'boolean') {
_logs.push('ERROR: \'' + property +
'\' expected to be a boolean but is not.');
return defaultValue;
}
return object[property];
}
function _parseURL(args) {
var object = args.object;
var property = args.property;
var baseURL = args.baseURL;
var str = _parseString({object: object, property: property, trim: false});
if (str === undefined) {
return undefined;
}
// TODO: resolve url using baseURL
// new URL(object[property], baseURL);
return object[property];
}
function _parseColor(args) {
var object = args.object;
var property = args.property;
if (!(property in object)) {
return undefined;
}
if (typeof object[property] != 'string') {
_logs.push('ERROR: \'' + property +
'\' expected to be a string but is not.');
return undefined;
}
// If style.color changes when set to the given color, it is valid. Testing
// against 'white' and 'black' in case of the given color is one of them.
var dummy = document.createElement('div');
dummy.style.color = 'white';
dummy.style.color = object[property];
if (dummy.style.color != 'white') {
return object[property];
}
dummy.style.color = 'black';
dummy.style.color = object[property];
if (dummy.style.color != 'black') {
return object[property];
}
return undefined;
}
function _parseName() {
return _parseString({object: _jsonInput, property: 'name', trim: true});
}
function _parseShortName() {
return _parseString({object: _jsonInput,
property: 'short_name',
trim: true});
}
function _parseStartUrl() {
// TODO: parse url using manifest_url as a base (missing).
return _parseURL({object: _jsonInput, property: 'start_url'});
}
function _parseDisplay() {
var display = _parseString({object: _jsonInput,
property: 'display',
trim: true});
if (display === undefined) {
return display;
}
if (ALLOWED_DISPLAY_VALUES.indexOf(display.toLowerCase()) == -1) {
_logs.push('ERROR: \'display\' has an invalid value, will be ignored.');
return undefined;
}
return display;
}
function _parseOrientation() {
var orientation = _parseString({object: _jsonInput,
property: 'orientation',
trim: true});
if (orientation === undefined) {
return orientation;
}
if (ALLOWED_ORIENTATION_VALUES.indexOf(orientation.toLowerCase()) == -1) {
_logs.push('ERROR: \'orientation\' has an invalid value' +
', will be ignored.');
return undefined;
}
return orientation;
}
function _parseIcons() {
var property = 'icons';
var icons = [];
if (!(property in _jsonInput)) {
return icons;
}
if (!Array.isArray(_jsonInput[property])) {
_logs.push('ERROR: \'' + property +
'\' expected to be an array but is not.');
return icons;
}
_jsonInput[property].forEach(function(object) {
var icon = {};
if (!('src' in object)) {
return;
}
// TODO: pass manifest url as base.
icon.src = _parseURL({object: object, property: 'src'});
icon.type = _parseString({object: object,
property: 'type',
trim: true});
icon.density = parseFloat(object.density);
if (isNaN(icon.density) || !isFinite(icon.density) || icon.density <= 0) {
icon.density = 1.0;
}
if ('sizes' in object) {
var set = new Set();
var link = document.createElement('link');
link.sizes = object.sizes;
for (var i = 0; i < link.sizes.length; ++i) {
set.add(link.sizes.item(i).toLowerCase());
}
if (set.size != 0) {
icon.sizes = set;
}
}
icons.push(icon);
});
return icons;
}
function _parseRelatedApplications() {
var property = 'related_applications';
var applications = [];
if (!(property in _jsonInput)) {
return applications;
}
if (!Array.isArray(_jsonInput[property])) {
_logs.push('ERROR: \'' + property +
'\' expected to be an array but is not.');
return applications;
}
_jsonInput[property].forEach(function(object) {
var application = {};
application.platform = _parseString({object: object,
property: 'platform',
trim: true});
application.id = _parseString({object: object,
property: 'id',
trim: true});
// TODO: pass manfiest url as base.
application.url = _parseURL({object: object, property: 'url'});
applications.push(application);
});
return applications;
}
function _parsePreferRelatedApplications() {
return _parseBoolean({object: _jsonInput,
property: 'prefer_related_applications',
defaultValue: false});
}
function _parseThemeColor() {
return _parseColor({object: _jsonInput, property: 'theme_color'});
}
function _parseBackgroundColor() {
return _parseColor({object: _jsonInput, property: 'background_color'});
}
function _parse(string) {
// TODO: temporary while ManifestParser is a collection of static methods.
_logs = [];
_tips = [];
_success = true;
try {
_jsonInput = JSON.parse(string);
} catch (e) {
_logs.push('File isn\'t valid JSON: ' + e);
_tips.push('Your JSON failed to parse, these are the main reasons why ' +
'JSON parsing usually fails:\n' +
'- Double quotes should be used around property names and for ' +
'strings. Single quotes are not valid.\n' +
'- JSON specification disallow trailing comma after the last ' +
'property even if some implementations allow it.');
_success = false;
return;
}
_logs.push('JSON parsed successfully.');
_manifest.name = _parseName();
//jscs:disable requireCamelCaseOrUpperCaseIdentifiers
_manifest.short_name = _parseShortName();
_manifest.start_url = _parseStartUrl();
_manifest.display = _parseDisplay();
_manifest.orientation = _parseOrientation();
_manifest.icons = _parseIcons();
_manifest.related_applications = _parseRelatedApplications();
_manifest.prefer_related_applications = _parsePreferRelatedApplications();
_manifest.theme_color = _parseThemeColor();
_manifest.background_color = _parseBackgroundColor();
_logs.push('Parsed `name` property is: ' +
_manifest.name);
_logs.push('Parsed `short_name` property is: ' +
_manifest.short_name);
_logs.push('Parsed `start_url` property is: ' +
_manifest.start_url);
_logs.push('Parsed `display` property is: ' +
_manifest.display);
_logs.push('Parsed `orientation` property is: ' +
_manifest.orientation);
_logs.push('Parsed `icons` property is: ' +
JSON.stringify(_manifest.icons, null, 4));
_logs.push('Parsed `related_applications` property is: ' +
JSON.stringify(_manifest.related_applications, null, 4));
_logs.push('Parsed `prefer_related_applications` property is: ' +
JSON.stringify(_manifest.prefer_related_applications, null, 4));
_logs.push('Parsed `theme_color` property is: ' +
_manifest.theme_color);
_logs.push('Parsed `background_color` property is: ' +
_manifest.background_color);
//jscs:enable
}
return {
parse: _parse,
manifest: function() { return _manifest; },
logs: function() { return _logs; },
tips: function() { return _tips; },
success: function() { return _success; }
};
})();
var d = document;
const parseManifest = Promise.resolve().then(() => {
var link = d.querySelector('link[rel=manifest]');
return fetch(link.href);
})
.then(r => r.text())
.then(manifestText => {
ManifestParser.parse(manifestText);
return ManifestParser.manifest();
})
.catch(er => undefined);
var hasManifestDefined = () => !!d.querySelector('link[rel=manifest]');
var hasManfiestAvailable = () => parseManifest.then(manifest => !!manifest);
var hasManifestThemeColor = () => parseManifest.then(manifest => !!manifest.theme_color);
var hasManifestBackgroundColor = () => parseManifest.then(manifest => !!manifest.background_color);
var hasManifestIcons = () => parseManifest.then(manifest => !!manifest.icons);
var hasManifestIcons192 = () => parseManifest.then(manifest => !!manifest.icons.find((i) => i.sizes.has("192x192")));
var hasManifestShortName = () => parseManifest.then(manifest => !!manifest.short_name);
var hasManifestName = () => parseManifest.then(manifest => !!manifest.name);
var hasManifestStartUrl = () => parseManifest.then(manifest => !!manifest.start_url);
var hasCanonicalUrl = () => !!d.querySelector('link[rel=canonical]');
var isControlledByServiceWorker = () => !!(navigator.serviceWorker.controller);
var hasServiceWorkerRegistration = () => navigator.serviceWorker.getRegistration().then(r => !!r);
var hasCachedData = () => window.caches.keys().then(keys => keys.length > 0);
var isOnHTTPS = () => location.protocol == 'https:';
// This might fail if requests are served on localhost
var noMixedModeRequests = () => window.performance.getEntriesByType("resource").every(r => r.name.indexOf('https:') == 0);
var allRequestsInCache = () => window.performance.getEntriesByType("resource").every(r => caches.match(new Request(r.name)));
var tests = [
[hasManifestDefined, "Has a manifest"],
[hasManfiestAvailable, "Manifest has been fetched"],
[isOnHTTPS, "Site is on HTTPS"],
[noMixedModeRequests, "All assets are on https"],
[hasCanonicalUrl, "Site has a canonical URL"],
[hasManifestThemeColor, "Site manifest has theme_color"],
[hasManifestBackgroundColor, "Site manifest has background_color"],
[hasManifestStartUrl, "Site manifest has start_url"],
[hasManifestShortName, "Site manifest has short_name"],
[hasManifestName, "Site manifest has name"],
[hasManifestIcons, "Site manifest has icons defined"],
[hasManifestIcons192, "Site manifest has 192px icon"],
[isControlledByServiceWorker, "Site is currently controlled by a service worker"],
[hasServiceWorkerRegistration, "Site is has a service worker registration"],
[hasCachedData, "Site has cached data. Might work offline"],
[allRequestsInCache, "All Requests made from the page are in the cache"]
];
var results = tests.map((t) => {
// put the call to the function in a promise.
return Promise.resolve().then(() => t[0]()).then(r => `${t[1]}: ${r}`).catch(r => `${t[1]}: false`);
});
// Some voodoo by Jake.
results.reduce((chain, item) => {
return chain.then(() => item).then(r => console.log(r));
}, Promise.resolve());
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment