UNPKG

yaml-diff-patch

Version:

Apply a JSON diff/patch to YAML while preserving whitespace, comments and overall structure

189 lines (188 loc) 8.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.yamlOverwrite = exports.yamlDiffPatch = exports.yamlPatch = void 0; const fast_json_patch_1 = require("fast-json-patch"); const yaml_1 = require("yaml"); const yaml_2 = require("yaml"); const helpers_1 = require("./helpers"); function traverse(root, op, path) { const segments = path.split('/').slice(1); if (segments.length === 0) throw new Error(`Invalid patch path: ${path}`); const pathTo = (index) => '/' + segments.slice(0, index === -1 ? undefined : index).join('/'); const last = (0, fast_json_patch_1.unescapePathComponent)(segments.pop()); let parent = root; segments.forEach((segment, index) => { if ((0, helpers_1.isYamlSeq)(parent)) parent = parent.get(parseInt(segment), true); else parent = parent.get(segment, true); if (!(0, helpers_1.isYamlMap)(parent) && !(0, helpers_1.isYamlSeq)(parent)) { throw new Error(`Can't ${op} node at path ${path}. ` + `${pathTo(index)} is ${parent === null || parent === void 0 ? void 0 : parent.type}.`); } }); return { segments, last, parent: parent, pathTo }; } function applySinglePatch(root, operation) { const { last, parent, pathTo } = traverse(root, operation.op, operation.path); if (operation.op === 'add') { if ((0, helpers_1.isYamlMap)(parent)) { // Object parent.delete(last); parent.add(new yaml_2.Pair(last, (0, helpers_1.toAstValue)(operation.value))); } else { // Array const index = (0, helpers_1.parseSafeIndex)(last, parent.items.length); if (index === parent.items.length) parent.add((0, helpers_1.toAstValue)(operation.value)); else if (isNaN(index)) throw RangeError(`Can't ${operation.op} index ${last} ` + `to array at ${pathTo(-1)}`); else parent.items[index] = (0, helpers_1.toAstValue)(operation.value); } } else if (operation.op === 'replace') { if ((0, helpers_1.isYamlMap)(parent)) { // Object if (!parent.has(last)) throw new ReferenceError(`Can't ${operation.op} ${operation.path} ` + `since it doesn't already exist`); parent.set(last, (0, helpers_1.toAstValue)(operation.value)); } else { // Array const index = (0, helpers_1.parseSafeIndex)(last, parent.items.length - 1); if (isNaN(index)) throw RangeError(`Can't ${operation.op} index ${last} ` + `to array at ${pathTo(-1)}`); parent.items[index] = (0, helpers_1.toAstValue)(operation.value); } } else if (operation.op === 'remove') { if ((0, helpers_1.isYamlMap)(parent)) { // Object parent.delete(last); } else { // Array const index = (0, helpers_1.parseSafeIndex)(last, parent.items.length - 1); if (isNaN(index)) throw RangeError(`Can't ${operation.op} index ${last} ` + `to array at ${pathTo(-1)}`); parent.items.splice(index, 1); } } else if (operation.op === 'move' || operation.op === 'copy') { const { last: lastFrom, parent: parentFrom, pathTo: pathToFrom } = traverse(root, operation.op, operation.from); const sourceNode = (() => { var _a, _b, _c; if ((0, helpers_1.isYamlMap)(parentFrom)) { // Object const node = (_a = parentFrom.get(lastFrom, true)) !== null && _a !== void 0 ? _a : null; if (lastFrom === undefined || node === null) throw ReferenceError(`Can't ${operation.op} key ${last} to object at ` + pathToFrom(-1)); const indexOfChild = parentFrom.items.findIndex(item => item.value === node); let { commentBefore } = (_b = parentFrom.items[indexOfChild].key) !== null && _b !== void 0 ? _b : {}; if (operation.op === 'move') parentFrom.delete(lastFrom); if (indexOfChild === 0) { // Copy (or move) commentBefore from parent object, // since this is the first property and the comment is on // the parent object in the CST. commentBefore = commentBefore == null ? parentFrom.commentBefore : parentFrom.commentBefore + '\n' + commentBefore; if (operation.op === 'move') parentFrom.commentBefore = null; } return { node, commentBefore }; } else if ((0, helpers_1.isYamlSeq)(parentFrom)) { // Array const index = (0, helpers_1.parseSafeIndex)(lastFrom, parentFrom.items.length - 1); if (isNaN(index)) throw RangeError(`Can't ${operation.op} index ${lastFrom} ` + `to array at ${pathToFrom(-1)}`); const node = (_c = parentFrom.get(index, true)) !== null && _c !== void 0 ? _c : null; if (operation.op === 'move') parentFrom.delete(index); return { node }; } else throw new ReferenceError(`Invalid type at ${operation.from}`); })(); const { node, commentBefore } = operation.op === 'move' ? sourceNode : { node: (0, helpers_1.cloneNode)(sourceNode.node) }; if ((0, helpers_1.isYamlMap)(parent)) { // Object parent.delete(last); // Add item with custom pair where the key is a scalar, hence can // have a commentBefore const lastScalar = new yaml_1.Scalar(last); lastScalar.commentBefore = commentBefore; const pair = new yaml_2.Pair(lastScalar, node); parent.add(pair); } else { // Array const index = (0, helpers_1.parseSafeIndex)(last, parent.items.length); if (isNaN(index)) throw RangeError(`Can't ${operation.op} index ${last} to array at ` + pathTo(-1)); else if (index === parent.items.length) parent.add(node); else parent.items[index] = node; } } else if (operation.op === 'test') { if (JSON.stringify(parent.toJSON()) !== JSON.stringify(operation.value)) throw TypeError(`Patch test failed at ${operation.path}`); } else { throw new Error(`Operation ${operation.op} not supported`); } } function yamlPatch(yaml, rfc6902) { const doc = (0, yaml_1.parseDocument)(yaml, { keepSourceTokens: true, strict: false, prettyErrors: true, }); // An empty yaml doc produces a null document. Replace with an empty map. if (!doc.contents) { const root = new yaml_2.YAMLMap(); root.range = [0, 0, 0]; doc.contents = root; doc.contents.flow = false; } rfc6902.forEach((operation, index) => { try { applySinglePatch(doc.contents, operation); } catch (err) { const newErr = new err.constructor(`Patch #${index + 1} failed: ${err.message}`); newErr.stack = err.stack; throw newErr; } }); return doc.toString(); } exports.yamlPatch = yamlPatch; function yamlDiffPatch(yaml, oldJson, newJson) { return yamlPatch(yaml, (0, fast_json_patch_1.compare)(oldJson, newJson)); } exports.yamlDiffPatch = yamlDiffPatch; function yamlOverwrite(yaml, newJson) { const old = (0, yaml_1.parse)(yaml); return yamlDiffPatch(yaml, old, newJson); } exports.yamlOverwrite = yamlOverwrite;