UNPKG

pwabuilder-lib

Version:
428 lines (368 loc) 15.9 kB
'use strict'; var fs = require('fs'), path = require('path'), Q = require('q'), url = require('url'); var constants = require('./constants'), fileTools = require('./fileTools'), iconTools = require('./iconTools'), log = require('./log'), manifestTools = require('./manifestTools'), packageTools = require('./packageTools'), utils = require('./utils'); function PlatformBase (id, name, packageName, baseDir, extendedCfg) { var self = this; self.id = id; self.name = name; self.packageName = packageName; self.baseDir = baseDir; self.log = log; self.isPWA = false; self.imagesSubfolder = 'Images'; if (extendedCfg) { self.isPWA = extendedCfg.isPWA; self.targetFolder = extendedCfg.targetFolder; self.imagesSubfolder = extendedCfg.imagesSubfolder || self.imagesSubfolder; } /** * Creates a fully-functional hosted web application application for the platform. * * This function must be overridden by subclasses. */ self.create = function (w3cManifestInfo, rootDir, options, callback) { return Q.reject(new Error('The \'create\' operation is not implemented for platform: ' + self.id)) .nodeify(callback); }; /** * Runs the application created using the `create` operation. * * This function is optional. It can be overridden by subclasses. */ self.run = function (projectDir, options, callback) { self.warn('The \'run\' command is not implemented for platform: ' + self.id); return Q.resolve().nodeify(callback); }; /** * Packages the application for publication to the platform's store. * * This function is optional. It can be overridden by subclasses. */ self.package = function (projectDir, options, callback) { self.warn('The \'package\' command is not implemented for platform: ' + self.id); return Q.resolve().nodeify(callback); }; /** * Opens the source code for the platform. * * This function is optional. It can be overridden by subclasses. */ self.open = function (projectDir, options, callback) { self.warn('The \'open\' command is not implemented for platform: ' + self.id); return Q.resolve().nodeify(callback); }; self.getValidationRules = function (platforms, callback) { if (!self.baseDir) { self.warn('Missing base directory for platform: ' + self.id + '.'); return Q.resolve([]).nodeify(callback); } var validationRulesDir = path.join(self.baseDir, 'validationRules'); return Q.nfcall(fs.stat, validationRulesDir).then(function (stats) { if (stats.isDirectory()) { return manifestTools.loadValidationRules(validationRulesDir, platforms); } }) .catch(function () { var validationRulesFile = validationRulesDir + '.js'; return Q.nfcall(fs.stat, validationRulesFile).then(function (stats) { if (stats.isFile()) { return manifestTools.loadValidationRules(validationRulesFile, platforms); } self.warn('Failed to retrieve the validation rules for platform: ' + self.id + '. The validation rules folder is missing or invalid.'); return Q.resolve([]); }); }) .nodeify(callback); }; /** * Returns an array of icons files as defined in the manifest. The method assumes that all icons * are square and defined as properties of an 'icons' member, for example: * "icons": { * "30": "/images/smalllogo.png", * "50": "/images/storelogo.png", * "150": "/images/logo.png" * } * Platforms should override this method if the manifest icons use a different format. */ self.getManifestIcons = function (manifest) { return Object.keys(manifest.icons || {}).map(function (size) { return manifest.icons[size]; }); }; /** * Receives the size (e.g. '50x50') of an icon and returns the corresponding icon element * from the manifest or undefined if not found. The method assumes that all icons * are square and defined as properties of an 'icons' member, for example: * "icons": { * "30": "/images/smalllogo.png", * "50": "/images/storelogo.png", * "150": "/images/logo.png" * } * Platforms should override this method if the manifest icons use a different format. */ self.getManifestIcon = function (manifest, size) { // assumes icons are square var dimensions = size.toLowerCase().split('x'); return manifest.icons && manifest.icons[dimensions[0]]; }; /** * Receives an icon (got from getManifestIcons) and gets the corresponding uri element * from the manifest. The method assumes that all icons are square and defined as properties * of an 'icons' member, for example: * "icons": { * "30": "/images/smalllogo.png", * "50": "/images/storelogo.png", * "150": "/images/logo.png" * } * Platforms should override this method if the getManifestIcons fcn returns a different format. */ self.getEmbeddedIconUri = function(manifest, iconFromGetManifestIcons) { // in this case getManifestIcons() already returns the uri // url doesn't seem to be in use in most of the new manifest information return iconFromGetManifestIcons.url || iconFromGetManifestIcons.src || iconFromGetManifestIcons; }; /** * Receives an icon (got from getManifestIcons) and returns a filename for it. The method assumes * that all icons are square and defined as properties of an 'icons' member, for example: * "icons": { * "30": "/images/smalllogo.png", * "50": "/images/storelogo.png", * "150": "/images/logo.png" * } * Platforms should override this method if the getManifestIcons fcn returns a different format. */ self.getEmbeddedIconFilename = function(manifest, iconFromGetManifestIcons) { return iconFromGetManifestIcons.fileName || utils.newGuid() + '.png'; }; self.updateEmbeddedIconUriW3C = function(uri) { return uri; }; /** * Receives an icon (got from getManifestIcons) and updates the corresponding uri element * from the manifest. The method assumes that all icons are square and defined as properties * of an 'icons' member, for example: * "icons": { * "30": "/images/smalllogo.png", * "50": "/images/storelogo.png", * "150": "/images/logo.png" * } * Platforms should override this method if the manifest icons use a different format. */ self.updateEmbeddedIconUri = function(manifest, iconFromGetManifestIcons, uri) { uri = ('/' + uri).replace('//', '/'); var oldUri = iconFromGetManifestIcons.url || iconFromGetManifestIcons; if (iconFromGetManifestIcons.url) { iconFromGetManifestIcons.url = uri; } else { Object.keys(manifest.icons || { }).map(function(size) { if (manifest.icons[size] === oldUri) { manifest.icons[size] = uri; } }); } if (manifest.__w3cManifestInfo && manifest.__w3cManifestInfo.content) { (manifest.__w3cManifestInfo.content.icons || [ ]).forEach(function(icon) { if (icon.src === oldUri) { icon.src = self.updateEmbeddedIconUriW3C(uri); icon.type = icon.type || 'image/png'; delete(icon.fileName); } }); } }; /** * Adds an icon file with the specified size (e.g. '50x50') to the manifest. The method * assumes that all icons are square and defined as properties of an 'icons' member, for example: * "icons": { * "30": "/images/smalllogo.png", * "50": "/images/storelogo.png", * "150": "/images/logo.png" * } * Platforms should override this method if the manifest icons use a different format. */ self.addManifestIcon = function (manifest, fileName, size) { if (!manifest.icons) { manifest.icons = { }; } // assumes icons are square var dimensions = size.toLowerCase().split('x'); manifest.icons[dimensions[0]] = fileName; }; /** * Writes an embedded icon (as data uri) - if data uris for some icons are equal this function won't * write all the icons as helper functions and related caller logic uses the uris list as input */ self.resolveEmbeddedIcon = function(manifest, iconFromGetManifestIcons, rootDir, imagesDir, callback) { self.debug('Getting embedded icon from ' + self.getEmbeddedIconUri(manifest, iconFromGetManifestIcons).substr(0, 50) + '...'); var targetFilename = self.getEmbeddedIconFilename(manifest, iconFromGetManifestIcons); var outputFilename = path.join(imagesDir, targetFilename); return fileTools.mkdirp(path.dirname(outputFilename)).then(function() { var image = new Buffer( self.getEmbeddedIconUri(manifest, iconFromGetManifestIcons).replace(constants.IMG_GEN_OUT_DATAURI, ''), 'base64'); self.debug('Writing embedded icon file to ' + outputFilename); return Q.nfcall(fs.writeFile, outputFilename, image).then(function() { return self.updateEmbeddedIconUri(manifest, iconFromGetManifestIcons, url.parse(targetFilename).pathname); }); }).nodeify(callback); }; /** * Updates the icons path from absolute Url to relative Url */ self.updateManifestIconsPaths = function(manifest, imagesPath) { if (manifest.icons && manifest.icons.length > 0) { log.debug('Updating icons absolute path to relative path at manifest info...'); manifest.icons.forEach(function (icon) { if (icon.src) { icon.src = (imagesPath + url.parse(icon.src).pathname).replace('//', '/'); } }); } }; /** * Downloads the platform icons to the generated app's folder. */ self.downloadIcons = function (manifest, baseUrl, imagesOutputInfo, callback) { self.debug('Downloading the ' + self.id + ' icons...'); var rootDir = imagesOutputInfo; var imagesDir = imagesOutputInfo; var relativePath = ''; var updatePaths = false; if (imagesOutputInfo.rootFolder) { self.debug('Overriding defaults with custom images info: ' + JSON.stringify(imagesOutputInfo)); rootDir = imagesOutputInfo.rootFolder; imagesDir = imagesOutputInfo.outputFolder; relativePath = imagesOutputInfo.relativePath; updatePaths = imagesOutputInfo.updatePaths; } var iconList = manifest.icons; return Q.resolve().then(function () { if (iconList) { var icons = self.getManifestIcons(manifest); var downloadTasks = icons.map(function(icon) { var isDataUri = new RegExp('^' + constants.IMG_GEN_OUT_DATAURI); if (self.getEmbeddedIconUri(manifest, icon).match(isDataUri)) { return self.resolveEmbeddedIcon(manifest, icon, rootDir, imagesDir); } var iconPath = icon.url || icon.src || icon; var iconUrl = url.resolve(baseUrl, iconPath); var pathname = icon.fileName || url.parse(iconUrl).pathname; var iconFilePath = path.join(imagesDir, pathname); return iconTools.getIcon(iconUrl, iconFilePath); }); return Q.allSettled(downloadTasks).then(function (results) { results.forEach(function (result) { if (result.state === 'rejected') { self.warn('Error downloading an icon file. ' + result.reason.message); } }); }); } }).then(function () { delete(manifest.__w3cManifestInfo); var defaultImagesDir = path.join(self.baseDir, 'assets', 'images'); return fileTools.syncFiles(defaultImagesDir, imagesDir, { filter: function (file) { var size = path.basename(file, path.extname(file)); return !self.getManifestIcon(manifest, size); } }).then(function (files) { files.forEach(function (file) { var filePath = path.relative(imagesDir, file); var size = path.basename(file, path.extname(file)); self.addManifestIcon(manifest, filePath, size); }); }) .catch(function (err) { if (err.code !== 'ENOENT') { return Q.reject(err); } self.debug('No default icons were found to copy for the \'' + self.id + '\' platform.'); }); }).then(function() { if (!updatePaths) { return; } return self.updateManifestIconsPaths(manifest, relativePath); }).nodeify(callback); }; self.copyDocumentation = function (targetPath, platform, callback) { if (arguments.length > 1) { if (utils.isFunction(platform)) { callback = platform; platform = ''; } } var sourcePath = path.join(self.baseDir, 'docs', platform || ''); self.info('Copying documentation from \'' + sourcePath + '\' to \'' + targetPath + '\'...'); return fileTools.copyFolder(sourcePath, targetPath).catch (function (err) { self.warn('Failed to copy the documentation for the \'' + platform + '\' Cordova platform. ' + err.getMessage()); }) .nodeify(callback); }; self.writeGenerationInfo = function (manifestInfo, targetPath, callback) { var appModule = packageTools.getPackageInformation(); if (!appModule) { self.warn('Failed to retrieve the metadata for the app generation tool.'); appModule = { version: 'Unknown' }; } var platformModule = packageTools.getPackageInformation(self.packageName); if (!platformModule) { self.warn('Failed to retrieve the metadata for the \'' + self.id + '\' platform package.'); platformModule = { version: 'Unknown' }; } var timestamp = manifestInfo.timestamp || new Date().toISOString().replace(/T/, ' ').replace(/\.[0-9]+/, ' '); var generationInfo = { 'generationTool': appModule.name, 'generationToolVersion': appModule.version, 'platformId' : self.id, 'platformPackage' : self.packageName, 'platformVersion': platformModule.version, 'generatedFrom': manifestInfo.generatedFrom || 'API', 'generationDate': timestamp, }; if (manifestInfo.generatedUrl) { generationInfo['generatedURL'] = manifestInfo.generatedUrl; } var filePath = path.join(targetPath, constants.TELEMETRY_FILE_NAME); self.info('Writing the generation information for the \'' + self.name + '\' platform to \'' + filePath + '\'...'); return Q.nfcall(fs.writeFile, filePath, JSON.stringify(generationInfo, null, 4)) .nodeify(callback); }; self.isPWAPlatform = function() { return self.isPWA; }; self.getOutputFolder = function(rootDir) { var targetFolder = self.targetFolder ? self.targetFolder : self.id; return path.join(rootDir, targetFolder); }; self.getOutputImagesInfo = function(rootDir) { return { rootFolder: rootDir, outputFolder: path.join(rootDir, self.imagesSubfolder), relativePath: '/' + self.imagesSubfolder.toLowerCase() + '/', updatePaths: true }; }; self.debug = function (message, source) { self.log.debug(message, source || self.id); }; self.info = function (message, source) { self.log.info(message, source || self.id); }; self.warn = function (message, source) { self.log.warn(message, source || self.id); }; self.error = function (message, source) { self.log.error(message, source || self.id); }; self.write = function (message, source) { self.log.write(message, source || self.id); }; } module.exports = PlatformBase;