pwabuilder-lib
Version:
PWA Builder Core Library
360 lines (296 loc) • 12.3 kB
JavaScript
var fs = require('fs'),
url = require('url');
var AdmZip = require('adm-zip'),
cheerio = require('cheerio'),
request = require('request'),
path = require('path'),
Q = require('q'),
_url = require('url');
var constants = require('../constants'),
fileTools = require('../fileTools'),
manifestConverter = require('./manifestConverter'),
manifestTypeDetector = require('./manifestTypeDetector'),
log = require('../log'),
utils = require('../utils');
var manifestCreator = require('./manifestCreator');
// Request settings taken from https://github.com/InternetExplorer/modern.IE-static-code-scan/blob/master/app.js
// Then the user agent is updated to that of a PWA-capable browser
var request = request.defaults({
followAllRedirects: true,
encoding: null,
gzip: true,
jar: false,
rejectUnauthorized: false,
headers: {
'Accept': 'text/html, application/xhtml+xml, */*',
'Accept-Language': 'en-US,en;q=0.5',
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.61 Safari/537.36'
}
});
function listAvailableManifestFormats() {
return [constants.BASE_MANIFEST_FORMAT, constants.CHROME_MANIFEST_FORMAT, constants.EDGE_EXTENSION_MANIFEST_FORMAT];
}
function fetchManifestUrlFromSite (siteUrl, callback) {
var deferred = Q.defer();
request({ uri: siteUrl, followAllRedirects: true }, function (err, response, body) {
if (err || response.statusCode !== 200) {
return deferred.reject(new Error('Failed to retrieve manifest from site.'));
}
var $ = cheerio.load(body);
// Why this this marked as a space separated list of values
var manifestUrl = $('link[rel~="manifest"]').attr('href');
if (manifestUrl) {
var parsedManifestUrl = url.parse(manifestUrl);
if (!parsedManifestUrl.host) {
var searchOrEmpty = parsedManifestUrl.search || "";
manifestUrl = url.resolve(siteUrl, parsedManifestUrl.pathname + searchOrEmpty);
}
}
return deferred.resolve(manifestUrl);
});
return deferred.promise.nodeify(callback);
}
function processManifestContents (data, manifestFormat, callback) {
var manifestObj = utils.parseJSON(data);
if (!manifestObj) {
return callback(new Error('Invalid manifest format.'));
}
var detectedFormat = manifestTypeDetector.detect(manifestObj);
if (manifestFormat) {
log.warn('Forcing to format ' + manifestFormat + '...');
detectedFormat = manifestFormat;
} else if (!detectedFormat) {
detectedFormat = 'w3c';
}
log.info('Found a ' + detectedFormat + ' manifest...');
var manifestInfo = {
content: manifestObj,
format: detectedFormat
};
if (detectedFormat !== constants.CHROME_MANIFEST_FORMAT) {
return callback(null, manifestInfo);
}
// If the detected format is ChromeOS, convert it to W3C Manifest format.
log.info('Converting the Chrome OS manifest to W3C format...');
manifestConverter.convertTo(manifestInfo, function (err, w3cManifest) {
// Assuming conversion was successful, running the manifest JSON through the detector again will return the W3C format type.
detectedFormat = manifestTypeDetector.detect(w3cManifest.content);
if (detectedFormat === constants.BASE_MANIFEST_FORMAT) {
log.info('Conversion to W3C Manifest format successful.');
}
return callback(null, {
content: w3cManifest.content,
format: detectedFormat
});
});
}
function downloadManifestFromUrl (manifestUrl, manifestFormat, callback) {
if (arguments.length < 3) {
if (utils.isFunction(manifestFormat)) {
callback = manifestFormat;
manifestFormat = undefined;
}
}
var deferred = Q.defer();
request({ uri: manifestUrl, followAllRedirects: true }, function (err, response, data) {
if (err || response.statusCode !== 200) {
return deferred.reject(new Error('Failed to download manifest data.'));
}
Q.nfcall(processManifestContents, data, manifestFormat).then(function (manifestInfo) {
if (manifestInfo) {
manifestInfo.generatedUrl = manifestUrl;
}
return deferred.resolve(manifestInfo);
})
.catch(function (err) {
return deferred.reject(err);
});
});
return deferred.promise.nodeify(callback);
}
function getManifestFromSite (siteUrl, manifestFormat, callback) {
if (arguments.length < 3) {
if (utils.isFunction(manifestFormat)) {
callback = manifestFormat;
manifestFormat = undefined;
}
}
return fetchManifestUrlFromSite(siteUrl).then(function (manifestUrl) {
if (manifestUrl) {
return Q.nfcall(downloadManifestFromUrl, manifestUrl, manifestFormat);
} else {
log.warn('No manifest found. A new manifest will be created.');
return Q.nfcall(manifestCreator, { url: siteUrl })
.then(function(manifest) {
var manifestObj = {
content: manifest,
format: constants.BASE_MANIFEST_FORMAT,
generated:true
};
return manifestObj;
})
.catch(function (err) {
log.warn('An error ocurred while creating manifest - ' + err);
var manifestObj = {
content: {
'start_url': _url.parse(siteUrl).path,
'short_name': utils.getDefaultShortName(siteUrl)
},
format: constants.BASE_MANIFEST_FORMAT,
generated:true
};
return manifestObj;
});
}
})
.nodeify(callback);
}
function getManifestFromFile (filePath, manifestFormat, callback) {
if (arguments.length < 3) {
if (utils.isFunction(manifestFormat)) {
callback = manifestFormat;
manifestFormat = undefined;
}
}
return Q.nfcall(fs.readFile, filePath).then(function (data) {
return Q.nfcall(processManifestContents, data, manifestFormat);
})
.nodeify(callback);
}
/**
* Calls the image generation service to generate missing images and update manifest accordingly
*
* Arguments
* image - binary buffer with image data
* manifest - the w3c manifest that should be updated
* options.contentType - custom MIME type for image, otherwise will use default defined at constants
* options.generationSvcUrl - endpoint for the image service, otherwise will use default defined at constants
* options.outputFolder - to download files to local folder, otherwise will use data uri
* options.padding - padding for generated images, otherwise will use 0
* options.platforms - platforms to generate images for, otherwise will use default defined at constants
* options.rootFolder - when download files to local folder (this indicates the root for relative paths), otherwise full paths are used
*
* Returns the manifest updated (and writes images at local folder if options.outputFolder is available)
*/
function generateImagesForManifest(image, manifest, options, callback) {
if (!options) { options = { }; }
var formData = {
padding: options.padding || 0,
platform: options.platforms || constants.IMG_GEN_IN_PLATFORM,
image: {
value: image,
options: {
contentType: options.contentType || constants.IMG_GEN_IN_MIMETYPE,
filename: 'image'
}
}
};
var imgGenerationSvc = constants.IMG_GEN_SVC_URL || 'https://appimagegenerator-prod.azurewebsites.net';
log.debug('Generating images from app image generation service: ' + imgGenerationSvc);
return Q.nfcall(request.post, { url: _url.resolve(imgGenerationSvc, constants.IMG_GEN_SVC_API), formData: formData })
.then(function(response) {
if (!response || response.length < 0) {
return new Error('No incoming message available from response');
}
var incomingMessage = response[0];
if (incomingMessage.statusCode !== 201) {
return new Error(incomingMessage.statusCode + ' - ' + incomingMessage.statusMessage);
}
return JSON.parse(incomingMessage.body);
}).then(function(generatedInfo) {
var generatedImagesUri = _url.resolve(imgGenerationSvc, generatedInfo.Uri);
log.debug('Getting generated images from: ' + generatedImagesUri);
return Q.nfcall(request.get, { url: generatedImagesUri }).then(function(response) {
if (!response || response.length < 0) {
return new Error('No incoming message available from response');
}
var incomingMessage = response[0];
if (incomingMessage.statusCode !== 200) {
return new Error(incomingMessage.statusCode + ' - ' + incomingMessage.statusMessage);
}
return incomingMessage.body;
});
}).then(function(zipBuffer) {
log.debug('Generated images ZIP size: ' + zipBuffer.length);
var iconsEntry;
var zip = new AdmZip(zipBuffer);
zip.getEntries().forEach(function(zipEntry) {
if (!iconsEntry && zipEntry.name === constants.IMG_GEN_OUT_ICONSINFO) {
iconsEntry = zipEntry;
}
});
return { zip: zip, iconsEntry: iconsEntry };
}).then(function(zipData) {
var deferred = Q.defer();
zipData.iconsEntry.getDataAsync(function(data) {
var iconsInfo = JSON.parse(data).icons;
if (!manifest.icons) {
manifest.icons = iconsInfo;
} else {
iconsInfo.forEach(function(iconInfo) {
var found = false;
manifest.icons.forEach(function(manifestIconInfo) {
found = found || manifestIconInfo.sizes === iconInfo.sizes;
});
if (!found) {
log.debug('Adding missing icon: ' + iconInfo.sizes);
// adding known MIME type returned from generation service
iconInfo.type = constants.IMG_GEN_OUT_MIMETYPE;
manifest.icons.push(iconInfo);
}
});
}
return deferred.resolve(zipData);
});
return deferred.promise;
}).then(function(zipData) {
log.debug('Iterating icons from manifest: ' + manifest.icons.length);
return Q.allSettled(zipData.zip.getEntries().map(function(zipEntry) {
var deferred = Q.defer();
manifest.icons.forEach(function(icon) {
if (icon.src !== zipEntry.entryName) {
return deferred.resolve();
}
zipEntry.getDataAsync(function(data) {
if (options.outputFolder) {
var outputFilename = path.resolve(options.outputFolder, icon.src);
fileTools.mkdirp(path.dirname(outputFilename), function(err) {
if (err) { return deferred.reject(err); }
fs.writeFile(outputFilename, data, function(err) {
if (err) { return deferred.reject(err); }
icon.src = _url.parse(options.rootFolder ?
outputFilename.replace(options.rootFolder, '') :
outputFilename).path;
return deferred.resolve();
});
});
} else {
icon.fileName = icon.src;
icon.src = constants.IMG_GEN_OUT_DATAURI + data.toString('base64');
return deferred.resolve();
}
});
});
return deferred.promise;
}));
}).then(function() {
return manifest;
}).nodeify(callback);
}
function writeToFile (manifestInfo, filePath, callback) {
if (manifestInfo && manifestInfo.content) {
var jsonString = JSON.stringify(manifestInfo.content, undefined, 4);
return Q.nfcall(fs.writeFile, filePath, jsonString).nodeify(callback);
}
return Q.reject(new Error('Manifest content is empty or invalid.')).nodeify(callback);
}
module.exports = {
getManifestFromSite: getManifestFromSite,
getManifestFromFile: getManifestFromFile,
writeToFile: writeToFile,
fetchManifestUrlFromSite: fetchManifestUrlFromSite,
downloadManifestFromUrl: downloadManifestFromUrl,
listAvailableManifestFormats: listAvailableManifestFormats,
generateImagesForManifest: generateImagesForManifest
};
;