cfn-changeset-viewer
Version:
View the details of a CloudFormation ChangeSet (including nested ones!) in a human-friendly way
144 lines (137 loc) • 4.29 kB
JavaScript
import chalk from "chalk";
import assert from "assert";
/**
* @typedef {import('@aws-sdk/client-cloudformation').ChangeAction | "Default"} ChangeAction
*/
/** @type {Record<ChangeAction, { icon: string; color: chalk.Chalk }>} */
const actionMap = {
Add: {
icon: "+",
color: chalk.green,
},
Modify: {
icon: "~",
color: chalk.yellow,
},
Remove: {
icon: "-",
color: chalk.red,
},
Import: {
icon: "↓",
color: chalk.cyan,
},
Dynamic: {
icon: "?",
color: chalk.magenta,
},
Default: {
icon: " ",
color: chalk.white,
},
};
/**
* @param {unknown} value
*/
function typeOf(value) {
if (Array.isArray(value)) return "array";
if (value === null) return "null";
return typeof value;
}
/**
* @param {unknown} obj
* @param {ChangeAction} changeType
* @param {string} key
* @param {string} indent
*/
function logObject(obj, changeType, key, indent = "") {
if (obj === undefined) return;
const color = actionMap[changeType ?? "Default"].color;
const icon = actionMap[changeType ?? "Default"].icon;
if (Array.isArray(obj)) {
if (key) {
console.log(color(`${icon} ${indent}${key}:`));
indent = indent.replace(/-/g, " ");
}
const newIndent = key ? ` ` : "";
for (let i = 0; i < obj.length; i++) {
logObject(obj[i], changeType, "", `${indent}${newIndent}- `);
// ensure hyphen only shows up once per array element
indent = indent.replace(/-/g, " ");
}
} else if (typeOf(obj) === "object") {
if (key) {
console.log(color(`${icon} ${indent}${key}:`));
indent = indent.replace(/-/g, " ");
}
const newIndent = key ? ` ` : "";
for (const objKey in obj) {
logObject(obj[/** @type {keyof obj} */ (objKey)], changeType, objKey, `${indent}${newIndent}`);
// ensure hyphen only shows up once per array element
indent = indent.replace(/-/g, " ");
}
} else {
if (key) {
console.log(color(`${icon} ${indent}${key}: ${JSON.stringify(obj)}`));
} else {
console.log(color(`${icon} ${indent}${JSON.stringify(obj)}`));
}
}
}
/**
* @param {unknown} before
* @param {unknown} after
* @param {ChangeAction} action
* @param {string} [currentKey]
* @param {string} [indent]
* @param {boolean} [showUnchangedProperties]
*/
export function logDiff(before, after, action, currentKey = "", indent = "", showUnchangedProperties = false) {
// handle not showing unchanged properties if not requested
try {
assert.deepStrictEqual(before, after);
if (!showUnchangedProperties) return;
} catch (e) {}
if (typeOf(before) !== typeOf(after)) {
logObject(before, "Remove", currentKey, indent);
logObject(after, action, currentKey, indent);
} else if (Array.isArray(before) && Array.isArray(after)) {
const maxLength = Math.max(before?.length || 0, after?.length || 0);
const newIndent = currentKey ? ` ` : "";
if (currentKey) {
console.log(` ${indent}${currentKey}:`);
indent = indent.replace(/-/g, " ");
}
for (let i = 0; i < maxLength; i++) {
logDiff(before[i], after[i], action, "", `${indent}${newIndent}- `, showUnchangedProperties);
// ensure hyphen only shows up once per array element
indent = indent.replace(/-/g, " ");
}
} else if (typeof before === "object") {
const keys = new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]).keys();
const newIndent = currentKey ? ` ` : "";
if (currentKey) {
console.log(` ${indent}${currentKey}:`);
indent = indent.replace(/-/g, " ");
}
for (const key of keys) {
logDiff(
before?.[/** @type {keyof before} */ (key)],
after?.[/** @type {keyof after} */ (key)],
action,
key,
`${indent}${newIndent}`,
showUnchangedProperties,
);
// ensure hyphen only shows up once per array element
indent = indent.replace(/-/g, " ");
}
} else if (before !== after) {
logObject(before, "Remove", currentKey, indent);
// ensure that hyphen only shows up once when logging the before object, not after
indent = indent.replace(/-/g, " ");
logObject(after, action, currentKey, indent);
} else {
logObject(after, "Default", currentKey, indent);
}
}