UNPKG

d2-manifest

Version:

App manifest generator for DHIS 2 Web Apps

418 lines (355 loc) 12.2 kB
'use strict'; require('colors'); const fs = require('fs'); const log = require('loglevel'); const getAuthorRegex = require('author-regex'); const isPlainObject = require('lodash.isplainobject'); class Manifest { /** * Manifest constructor * * @param data Object containing initial data for the manifest */ constructor(data) { Object.assign(this, data); this.write = this.write.bind(this); this.merge = this.merge.bind(this); this.setFieldValue = this.setFieldValue.bind(this); this.getFieldValue = this.getFieldValue.bind(this); this.isValid = this.isValid.bind(this); this.getMissingFields = this.getMissingFields.bind(this); this.getInvalidFields = this.getInvalidFields.bind(this); this.getJSON = this.getJSON.bind(this); } /** * Returns a copy of the provided data where any properties that don't have values are removed * * @param {Object} data * @returns {Object} * @private */ static cleanObject(data) { const out = Object.keys(data) .filter(field => isPlainObject(data[field]) || !!data[field]) .reduce((obj, field) => { if (isPlainObject(data[field])) { obj[field] = Manifest.cleanObject(data[field]); } else { obj[field] = data[field]; } return obj; }, {}); return Object.keys(out).length > 0 ? out : undefined; } /** * Merge the fields from the specified data with the current manifest * * @param {Object} data * @param {boolean} force If true, empty fields in the data will be removed from the manifest * @returns {Manifest} */ merge(data, force) { if (data instanceof Object) { Object.keys(data).map(key => { if (data[key] instanceof Object) { if (key === 'developer' && data[key].name) { Object.assign(data[key], Manifest.parseAuthor(data[key].name)); } const cleanData = force ? data[key] : Manifest.cleanObject(data[key]); this[key] = Object.assign({}, this[key], cleanData); } else if (data[key] || force) { this[key] = data[key]; } }); } return this; } /** * Helper method to recursively set the value of a field * * @param manifest The manifest object to operate on * @param fields An array of field names to recurse into * @param value * @private */ static _setFieldValue(manifest, fields, value) { if (!Array.isArray(fields) || fields.length === 0) { throw new Error('fields must be an array'); } if (fields.length > 1) { if (!manifest.hasOwnProperty(fields[0])) { manifest[fields[0]] = {}; Manifest._setFieldValue(manifest[fields[0]], fields.splice(1), value); } else { if (!isPlainObject(manifest[fields[0]])) { throw new Error('field is not an object'); } Manifest._setFieldValue(manifest[fields[0]], fields.splice(1), value); } } else { if (value) { manifest[fields[0]] = value; } else { delete manifest[fields[0]]; } } } /** * Set the value of the specified field * * @param fieldName The name of the field to add or modify, in dot notation (field.subfield.etc) * @param value */ setFieldValue(fieldName, value) { if (fieldName === 'developer.name') { value = Object.assign({}, this.developer, Manifest.parseAuthor(value)); Manifest._setFieldValue(this, ['developer'], value); } else { Manifest._setFieldValue(this, fieldName.split('.'), value); } } /** * Helper method to recursively get the value of a field * * @param object * @param fields Array of field names * @returns {string} * @private */ static _getFieldValue(object, fields) { if (!Array.isArray(fields) || fields.length === 0) { throw new Error('fields must be an array'); } if (fields.length > 1) { return object.hasOwnProperty(fields[0]) ? Manifest._getFieldValue(object[fields[0]], fields.splice(1)) : ''; } else { return object.hasOwnProperty(fields[0]) ? object[fields[0]] : ''; } } /** * Get the value of the specified field name * * @param fieldName A field name, in dot notation (field.subfield.etc) * @returns {*} */ getFieldValue(fieldName) { return Manifest._getFieldValue(this, fieldName.split('.')); } /** * Validate the current manifest * * @returns {boolean} */ isValid() { const invalidFields = this.getInvalidFields(); const missingFields = this.getMissingFields(); return invalidFields.length + missingFields.length === 0; } /** * Return a list of required fields * * @returns {string[]} */ static getRequiredFields() { return [ 'name', 'description', 'version', 'icons.48', 'developer.name', 'launch_path', 'default_locale', 'activities.dhis.href' ]; } /** * Return a list of optional fields * * @returns {string[]} */ static getOptionalFields() { return [ 'appType', 'icons.16', 'icons.128', 'developer.email', 'developer.url', 'developer.company' ]; } /** * Return an array of string options for the specified field name, or an empty array if there are no predefined * options * * @param fieldName * @returns {string[]} */ static getOptionsForField(fieldName) { switch (fieldName) { case 'appType': return [ 'APP', 'DASHBOARD_WIDGET', 'TRACKER_DASHBOARD_WIDGET', 'RESOURCE' ]; default: return []; } } /** * Return a list of all known and optional fields * * @returns {string[]} List of field names */ static getAllKnownFields() { return Manifest.getRequiredFields().concat(Manifest.getOptionalFields()); } /** * Recursively check if the specified field has a value * * @param target The target object to check * @param fields An array of field names to recurse into * @returns {boolean} True if the field exists and is not empty * @private */ static _fieldIsSet(target, fields) { if (Array.isArray(fields) && fields.length > 1) { const field = fields.shift(); return target && target.hasOwnProperty(field) && isPlainObject(target[field]) && Manifest._fieldIsSet(target[field], fields); } return target[fields[0]]; } /** * Check if the specified fields on the target object exist and are not empty * * @param target The target object to check * @param fieldNames A list of field names to check, in dot notation (field.subfield) * @returns {string[]} A list of fields that are not present or have no value * @private */ static _fieldsAreSet(target, fieldNames) { return fieldNames.filter(fieldName => { if (fieldName.indexOf('.') > 0) { const fields = fieldName.split('.'); const object = fields.shift(); return !(target.hasOwnProperty(object) && Manifest._fieldIsSet(target[object], fields)); } return !(target.hasOwnProperty(fieldName) && target[fieldName] !== undefined && target[fieldName] !== ''); }) } /** * Return a list of all available fields for the current manifest, with required fields sorted * before optional ones * * @returns {string[]} List of field names */ getAllEmptyFields() { return Manifest._fieldsAreSet(this, Manifest.getRequiredFields()) .concat(Manifest._fieldsAreSet(this, Manifest.getOptionalFields())); } /** * Return an array of fields that have values that don't pass validation * * @returns {string[]} */ getInvalidFields() { return Manifest .getAllKnownFields() .filter(field => { const opts = Manifest.getOptionsForField(field); return opts.length > 0 && this.getFieldValue(field) != '' && opts.indexOf(this.getFieldValue(field)) === -1; }); } /** * Checks the current manifest against the list of required fields * * @returns {string[]} List of required fields that are missing */ getMissingFields() { return Manifest._fieldsAreSet(this, Manifest.getRequiredFields()); } /** * Return a list of all known optional fields that aren't specified for the current manifest * * @returns {string[]} */ getEmptyOptionalFields() { return Manifest._fieldsAreSet(this, Manifest.getOptionalFields()); } /** * Return a JSON representation of the current manifest * * @param {boolean} ugly If true, no extra spaces or newlines will be returned */ getJSON(ugly) { return JSON.stringify(this, null, ugly == true ? 0 : 2); } /** * Write the JSON representation of the current manifest to a file * * @param {String} filename * @param {boolean} ugly */ write(filename, ugly) { try { fs.writeFileSync(filename, this.getJSON(ugly == true)); } catch (e) { log.error('Failed to write to file:'.red, e.message); throw e; } } /** * Read npm package data from the specified file, typically package.json * * @param filename * @returns {{}} */ static readPackageFile(filename) { try { const pkg = JSON.parse(fs.readFileSync(filename, 'utf8')); const out = {}; if (pkg.name) out.name = pkg.name; if (pkg.version) out.version = pkg.version; if (pkg.description) out.description = pkg.description; if (pkg.author) out.developer = Manifest.parseAuthor(pkg.author); // Additional fields to support manifests as source if (pkg.icons) out.icons = Object.assign({}, pkg.icons); if (!out.developer && pkg.developer) out.developer = Object.assign({}, pkg.developer); if (pkg['launch_path']) out['launch_path'] = pkg['launch_path']; if (pkg['default_locale']) out['default_locale'] = pkg['default_locale']; if (pkg.activities) out.activities = Object.assign({}, pkg.activities); if (pkg.hasOwnProperty('manifest.webapp')) { Object.assign(out, pkg['manifest.webapp']); } return out; } catch (e) { log.error('Failed to read package file:'.red, e.message); process.exit(1); } } /** * Parse a "person field" as used by npm into an object consisting of * name, email and url * * @param {String} str * @returns {{name: String, email: String, url: String}} */ static parseAuthor(str) { if (isPlainObject(str)) { return { name: str.name, email: str.email, url: str.url }; } const author = getAuthorRegex().exec(str); if (!author) return {}; const out = {name: author[1]}; if (author[2] && author[2] !== '') out.email = author[2]; if (author[3] && author[3] !== '') out.url = author[3]; return out; } } module.exports = Manifest;