UNPKG

@atomist/yaml-updater

Version:

Update YAML documents while ensuring clean diffs

278 lines 11 kB
"use strict"; /* * Copyright © 2019 Atomist, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ Object.defineProperty(exports, "__esModule", { value: true }); const deepEqual = require("fast-deep-equal"); const yaml = require("js-yaml"); /** * Parse the provides update string as JSON and use the keys in the * provided object to update, insert, or delete keys in the provided * YAML. * * @param updates string of updates * @param currentYaml YAML document to update * @param options format settings * @return updated YAML document as a string */ function updateYamlDocumentWithString(updatesString, currentYaml, options = { keepArrayIndent: false }) { let updates = {}; try { updates = JSON.parse(updatesString); } catch (e) { throw new Error(`failed to parse update JSON '${updatesString}': ${e.message}`); } return updateYamlDocument(updates, currentYaml, options); } exports.updateYamlDocumentWithString = updateYamlDocumentWithString; /** * Use the keys in the provided object to update, insert, or delete * keys in the provided YAML. The YAML can be multiple documents. * * The updating follows two possible strategies depending on the ̀updateAll` * option. When `false`, the default, if the keys are found in any of the * documents, they are updated in the first document the key exists in. If a key * is not found in any document, it is added to the first document. When * `̀updateAll` is `true`, the updates append on all documents. * * @param updates object of updates * @param currentYaml YAML document to update * @param options format settings * @return updated YAML document as a string */ function updateYamlDocuments(updates, yamlDocuments, options = { keepArrayIndent: false, updateAll: false }) { const yamlSepRegExp = /^(---(?:[ \t]+.*)?\n)/m; const yamlDocs = yamlDocuments.split(yamlSepRegExp); const insertIndex = (/\S/.test(yamlDocs[0]) || yamlDocs.length === 1) ? 0 : 2; const updateAll = options.updateAll || false; for (const k of Object.keys(updates)) { const v = updates[k]; let found = -1; for (let i = 0; i < yamlDocs.length; i += 2) { let current; try { current = yaml.safeLoad(yamlDocs[i]); } catch (e) { throw new Error(`failed to parse YAML document '${yamlDocs[i]}': ${e.message}`); } if (!current) { continue; } if (updateAll) { yamlDocs[i] = updateYamlKey(k, v, yamlDocs[i], options); } else { if (k in current) { found = i; break; } } } if (!updateAll) { const index = (found < 0) ? insertIndex : found; yamlDocs[index] = updateYamlKey(k, v, yamlDocs[index], options); } } return yamlDocs.join(""); } exports.updateYamlDocuments = updateYamlDocuments; /** * Use the keys in the provided object to update, insert, or delete * keys in the provided YAML document. * * @param updates object of updates * @param currentYaml YAML document to update * @param options format settings * @return updated YAML document as a string */ function updateYamlDocument(updates, currentYaml, options = { keepArrayIndent: false }) { let updatedYaml = currentYaml; for (const k of Object.keys(updates)) { const v = updates[k]; updatedYaml = updateYamlKey(k, v, updatedYaml, options); } return updatedYaml; } exports.updateYamlDocument = updateYamlDocument; /** * Update, insert, or delete the value of a key in `currentYml`, a * string containing valid YAML. It does its best to retain the same * formatting, but empty lines and trailing whitespace may disappear * if adjacent lines are edited and comments that are parsed as part * of a value that is deleted will be deleted. * * @param key the key whose value should be replaced * @param value the value to set the key to, set to `null` or `undefined` to remove the key * @param options settings for the formatting * @return updated YAML document as a string */ function updateYamlKey(key, value, currentYaml, options = { keepArrayIndent: false }) { // match index 01 2 const keyValRegExp = new RegExp(`(^|\\n)${key}[^\\S\\n]*:(?:[^\\S\\n]*?\\n)?([\\s\\S]*?(?:\\n(?![\\n\\- #])|$))`); let updatedYaml = (/\n$/.test(currentYaml)) ? currentYaml : currentYaml + "\n"; let current; try { current = yaml.safeLoad(updatedYaml); } catch (e) { throw new Error(`failed to parse current YAML '${updatedYaml}': ${e.message}`); } if (!current) { updatedYaml = (updatedYaml === "\n") ? "" : updatedYaml; updatedYaml += formatYamlKey(key, value, options); return updatedYaml; } if (value === null || value === undefined) { if (key in current) { updatedYaml = updatedYaml.replace(keyValRegExp, "$1"); } } else if (knownType(value)) { if (key in current) { if (deepEqual(current[key], value)) { return currentYaml; } else if (simpleType(value) || simpleType(current[key])) { const newKeyValue = formatYamlKey(key, value, options); updatedYaml = updatedYaml.replace(keyValRegExp, `\$1${newKeyValue}`); } else if (typeof current[key] === "object") { const keyMatches = keyValRegExp.exec(updatedYaml); if (!keyMatches) { throw new Error(`failed to match key ${key} in current YAML: ${updatedYaml}`); } const keyObject = keyMatches[2]; // find first properly indented line const indentationRegExp = /^( +)[^\-# ]/m; const indentMatches = indentationRegExp.exec(keyObject); if (!indentMatches) { throw new Error(`failed to match indentation for elements of key ${key}: ${keyObject}`); } const indentLevel = indentMatches[1]; const indentRegex = new RegExp(`^${indentLevel}`); const lines = keyObject.split("\n"); const indentation = []; const undentedLines = lines.map(l => { indentation.push(new YamlLine(l, indentRegex.test(l))); return l.replace(indentRegex, ""); }); let currentValueYaml = undentedLines.join("\n"); for (const k of Object.keys(value)) { const v = value[k]; currentValueYaml = updateYamlKey(k, v, currentValueYaml, options); } const currentLines = currentValueYaml.split("\n"); let nextToMatch = 0; const indentedLines = currentLines.map(l => { for (let j = nextToMatch; j < indentation.length; j++) { if (l.trim() === indentation[j].content.trim()) { nextToMatch = j + 1; if (indentation[j].indented) { return indentLevel + l; } else { return l; } } } return (/\S/.test(l)) ? indentLevel + l : l; }); const indentedYaml = indentedLines.join("\n"); // last line must be empty but indentation matching may erroneously indent the last // empty line if there were indented empty lines after a deleted key const trailerYaml = (/\n$/.test(indentedYaml)) ? indentedYaml : indentedYaml + "\n"; updatedYaml = updatedYaml.replace(keyValRegExp, `\$1${key}:\n${trailerYaml}`); } else { throw new Error(`cannot update current YAML key ${key} of type ${typeof current[key]}`); } } else { const tailMatches = /\n(\n*)$/.exec(updatedYaml); const tail = (tailMatches && tailMatches[1]) ? tailMatches[1] : ""; updatedYaml = updatedYaml.replace(/\n+$/, "\n") + formatYamlKey(key, value, options) + tail; } } else { throw new Error(`cannot update YAML with value (${value}) of type ${typeof value}`); } return updatedYaml; } exports.updateYamlKey = updateYamlKey; function simpleType(a) { const typ = typeof a; return typ === "string" || typ === "boolean" || typ === "number" || Array.isArray(a); } function knownType(a) { return simpleType(a) || typeof a === "object"; } class YamlLine { constructor(content, indented) { this.content = content; this.indented = indented; } } /** * Format a key and value into a YAML string. * * @param key key to serialize * @param value value to serialize * @param options settings for the formatting */ function formatYamlKey(key, value, options = { keepArrayIndent: false }) { const obj = {}; obj[key] = value; let y; try { y = yaml.safeDump(obj); } catch (e) { throw new Error(`failed to create YAML for {${key}: ${value}}: ${e.message}`); } y = arrayIndent(y, options.keepArrayIndent); return y; } exports.formatYamlKey = formatYamlKey; /** * Format object into a YAML string. * * @param obj object to serialize * @param options settings for the formatting */ function formatYaml(obj, options = { keepArrayIndent: false }) { let y; try { y = yaml.safeDump(obj); } catch (e) { throw new Error(`failed to create YAML for '${JSON.stringify(obj)}': ${e.message}`); } y = arrayIndent(y, options.keepArrayIndent); return y; } exports.formatYaml = formatYaml; /** * Remove superfluous indentation from array if `indentArray` is `false`. * * @param y YAML document as string * @param indentArray retain indented arrays if `true` * @return YAML document as string with arrays indented as desired. */ function arrayIndent(y, indentArray) { return (indentArray) ? y : y.replace(/^( *) - /gm, "$1- "); } //# sourceMappingURL=yaml.js.map