All files / lib/resource-types xml.js

100% Statements 179/179
100% Branches 30/30
100% Functions 8/8
100% Lines 179/179

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 1801x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 5x 5x 5x 5x 5x 1x 1x 1x 1x 1x 5x 3x 3x 3x 3x 3x 4x 1x 1x 5x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 3x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 7x 7x 7x 7x 6x 6x 5x 5x 5x 2x 2x 2x 2x 1x 1x 1x 1x 1x 1x 5x 1x 1x 1x 1x 5x 6x 6x 7x 1x 1x 1x 1x 1x 7x 6x 6x 7x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 13x 13x 13x 13x 13x 13x 13x 136x 136x 136x 136x 136x 95x 136x 10x 10x 10x 13x 13x 136x 136x 136x 14x 136x 10x 10x 10x 10x 9x 9x 9x 9x 10x 10x 136x 13x 13x 13x 13x 13x 1x 1x 1x 1x 1x 1x 1x 1x 1x  
"use strict";
/**
 * @module resource-types/xml
 */
import fs from "fs";
import sax from "sax";
 
/**
 * Set version value in the XML resource's element (defined in dot-notation
 * of element names, e.g. project.version in a Maven pom.xml).
 * Only the element's text is replaced, the rest of the file including its
 * formatting and comments is kept as-is.
 *
 * @param {String} version - version value to set
 * @param {Object} resource - resource configuration
 * @param {String} resource.path - path to the XML file
 * @param {String} resource.params.property - element names in dot-notation
 * @param {Object} opts - optional settings
 * @param {Boolean} [opts.dryRun] - when true, XML file won't be modified
 * @param {Function} cb - standard cb(err, result) callback
 */
function setVersion(version, resource, opts, cb) {
  const property = resource.params.property;
  const data = fs.readFileSync(resource.path, "UTF-8");
  const element = _findElement(data, property);
  if (!element) {
    cb(
      new Error(
        `Property ${property} does not exist in XML resource ${resource.path}`,
      ),
    );
  } else if (!opts.dryRun) {
    fs.writeFile(
      resource.path,
      data.slice(0, element.start) + version + data.slice(element.end),
      cb,
    );
  } else {
    cb();
  }
}
 
/**
 * Get version value from the XML resource's element (defined in
 * dot-notation of element names).
 *
 * @param {Object} resource - resource configuration
 * @param {String} resource.path - path to the XML file
 * @param {String} resource.params.property - element names in dot-notation
 * @param {Function} cb - standard cb(err, result) callback, result is the
 *   version value
 */
function getVersion(resource, cb) {
  const property = resource.params.property;
 
  function readCb(err, result) {
    let version;
    if (!err) {
      const element = _findElement(result, property);
      version = element ? element.value : undefined;
    }
    cb(err, version);
  }
  fs.readFile(resource.path, "UTF-8", readCb);
}
 
/**
 * Check that the XML resource is ready for release, i.e. the file can be
 * parsed and the element exists with a value.
 *
 * @param {Object} resource - resource configuration
 * @param {String} resource.path - path to the XML file
 * @param {String} resource.params.property - element names in dot-notation
 * @param {Function} cb - standard cb(err, result) callback, result is true
 *   when the resource is ready for release, an error is passed when the file
 *   is not configured, the file can't be parsed, or the element doesn't exist or has no value
 */
function isReadyForRelease(resource, cb) {
  const property = resource.params && resource.params.property;
 
  function readCb(err, result) {
    let ready;
    if (!err) {
      try {
        const element = _findElement(result, property);
        if (!element) {
          err = new Error(
            `Property ${property} does not exist in XML resource ${resource.path}`,
          );
        } else if (element.value === "") {
          err = new Error(
            `Property ${property} has no value in XML resource ${resource.path}`,
          );
        } else {
          ready = true;
        }
      } catch (parseErr) {
        err = new Error(
          `Failed to parse XML resource ${resource.path}. Error: ${parseErr.message}`,
        );
      }
    }
    cb(err, ready);
  }
  if (!property) {
    cb(
      new Error(
        `XML resource ${resource.path} has no params.property configured`,
      ),
    );
  } else {
    fs.readFile(resource.path, "UTF-8", readCb);
  }
}
 
/**
 * Find the first element matching the element names in dot-notation, which
 * only contains text.
 *
 * The XML is parsed with a SAX parser in order to get the position of the
 * element's text, so that it can be replaced without re-serialising (and
 * reformatting) the whole file.
 *
 * @private
 * @param {String} data - XML content
 * @param {String} property - element names in dot-notation
 * @return {Object} the element's trimmed text value along with its start
 *   and end positions within the content, undefined when there is no match
 * @throws {Error} when XML content can't be parsed
 */
function _findElement(data, property) {
  const parser = sax.parser(true);
  const names = [];
  let textStart;
  let element;
 
  parser.onopentag = function (node) {
    names.push(node.name);
    if (
      element === undefined &&
      textStart === undefined &&
      !node.isSelfClosing &&
      names.join(".") === property
    ) {
      // position is right after the opening tag's '>'
      textStart = parser.position;
    }
  };
  parser.onclosetag = function () {
    if (
      element === undefined &&
      textStart !== undefined &&
      names.join(".") === property
    ) {
      // startTagPosition is right after the closing tag's '<'
      const text = data.slice(textStart, parser.startTagPosition - 1);
      // skip element with child elements, comments, or CDATA
      if (!text.includes("<")) {
        const value = text.trim();
        const start = textStart + text.indexOf(value);
        element = { value: value, start: start, end: start + value.length };
      }
      textStart = undefined;
    }
    names.pop();
  };
  parser.write(data).close();
 
  return element;
}
 
const exports = {
  setReleaseVersion: setVersion,
  setPostReleaseVersion: setVersion,
  getVersion: getVersion,
  isReadyForRelease: isReadyForRelease,
};
 
export { exports as default };