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
JavaScript
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;
;