Last active
June 27, 2016 05:03
-
-
Save PaulKinlan/ce7c707e4cb96c752816 to your computer and use it in GitHub Desktop.
Progressive Web App Checklist
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(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