@sandboxed/diff
Version:
A zero dependency, high-performance, security-conscious JavaScript diffing library
711 lines (700 loc) • 19.3 kB
JavaScript
;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var index_exports = {};
__export(index_exports, {
ChangeType: () => ChangeType,
default: () => index_default
});
module.exports = __toCommonJS(index_exports);
// src/utils/constants.ts
var Iterables = /* @__PURE__ */ new Set([Object, Array, Set, Map]);
var ChangeType = /* @__PURE__ */ ((ChangeType2) => {
ChangeType2["ADD"] = "add";
ChangeType2["UPDATE"] = "update";
ChangeType2["REMOVE"] = "remove";
ChangeType2["NOOP"] = "noop";
return ChangeType2;
})(ChangeType || {});
var DefaultPathHints = {
map: "__MAP__",
set: "__SET__"
};
var MAX_DEPTH = 50;
var MAX_KEYS = 1e3;
var ITERATION_TIMEOUT_CHECK = 1e3;
var MAX_TIMEOUT_MS = 1e3;
var REDACTED = "*****";
// src/utils/fns.ts
function isPrimitive(value) {
return value !== Object(value);
}
function getRawValue(value) {
if (isPrimitive(value) && typeof value !== "symbol") return value;
return value.toString();
}
function isNullOrUndefined(value) {
return value === null || value === void 0;
}
function isCustomClassInstance(object) {
if (isNullOrUndefined(object) || typeof object !== "object" || !object.constructor) {
return false;
}
return !object.constructor?.toString?.().includes("[native code]");
}
function emptyShellClone(object) {
if (isCustomClassInstance(object)) {
return Object.create(Object.getPrototypeOf(object));
}
if (isObject(object)) {
return /* @__PURE__ */ Object.create(null);
}
return new object.constructor();
}
function getWrapper(obj) {
let wrapper = ["{", "}"];
if (Array.isArray(obj)) {
wrapper = ["[", "]"];
} else if (isCustomClassInstance(obj)) {
wrapper = [`${obj.constructor.name} {`, "}"];
} else if (obj instanceof Map) {
wrapper = [`Map (${obj.size}) {`, "}"];
} else if (obj instanceof Set) {
wrapper = ["Set [", "]"];
}
return wrapper;
}
function getRef(value) {
return `ref<${value?.constructor?.name || typeof value}>`;
}
function isObject(obj) {
return obj !== null && typeof obj === "object" && (Object.getPrototypeOf(obj) === Object.prototype || Object.getPrototypeOf(obj) === null);
}
function areObjects(a, b) {
return isObject(a) && isObject(b);
}
function getEnumerableKeys(obj) {
return [...Object.keys(obj), ...Object.getOwnPropertySymbols(obj)];
}
function isIterable(obj) {
return Iterables.has(obj?.constructor) || isObject(obj) || isCustomClassInstance(obj);
}
// src/diff/shared.ts
function lastPathValue(changeType, value) {
return { deleted: changeType === "remove" /* REMOVE */, value };
}
function includeDiffType(type, config) {
return config.include?.includes?.(type) && !config.exclude?.includes?.(type);
}
function shouldRedactValue(key, config) {
const rawKey = getRawValue(key);
return config.redactKeys?.includes?.(rawKey);
}
function createReplacer(config, obj) {
const seen = /* @__PURE__ */ new WeakSet();
return function replacer(k, v) {
if (shouldRedactValue(k, config)) return REDACTED;
if (k !== "" && v === obj) return "[Circular]";
if (v && typeof v === "object") {
if (seen.has(v)) return "[Circular]";
seen.add(v);
}
if (v instanceof Set) {
const stringified = JSON.stringify([...v.values()], replacer);
seen.delete(v);
return `Set ${stringified}`;
}
if (v instanceof Map) {
const entries = [...v.entries()];
const stringified = entries.map(([key, value]) => {
if (seen.has(key)) {
return `"[Circular]": ${JSON.stringify(value, replacer)}`;
}
if (!isPrimitive(key)) {
seen.add(key);
}
const serializedValue = shouldRedactValue(key, config) ? REDACTED : JSON.stringify(value, replacer);
const serialized = `${JSON.stringify(key, replacer)}: ${serializedValue}`;
if (!isPrimitive) {
seen.delete(key);
}
return serialized;
}).join(", ");
seen.delete(v);
return `Map (${entries.length}) { ${stringified} }`;
}
if (v instanceof Function) {
return `Function ${v.name || "(anonymous)"}`;
}
if (typeof v === "symbol") {
return v.toString();
}
if (typeof v === "bigint") {
return `BigInt(${v.toString()})`;
}
seen.delete(v);
return v;
};
}
function stringify(obj, config) {
return JSON.stringify(obj, createReplacer(config, obj));
}
function getObjectChangeResult(lhs, rhs, depth, key, parsedKey, config, path) {
const isLhsMap = lhs instanceof Map;
const isRhsMap = rhs instanceof Map;
let valueInLhs = lhs?.[key];
let valueInRhs = rhs?.[key];
let keyInLhs = Object.hasOwn(lhs, key);
let keyInRhs = Object.hasOwn(rhs, key);
if (isLhsMap) {
valueInLhs = lhs.get(key);
keyInLhs = lhs.has(key);
}
if (isRhsMap) {
valueInRhs = rhs.get(key);
keyInRhs = rhs.has(key);
}
const redactValue = shouldRedactValue(key, config);
const rawValueInLhs = getRawValue(valueInLhs);
const rawValueInRhs = getRawValue(valueInRhs);
const formattedValueInLhs = stringify(
redactValue ? REDACTED : rawValueInLhs,
config
);
const formattedValueInRhs = stringify(
redactValue ? REDACTED : rawValueInRhs,
config
);
let type = "noop" /* NOOP */;
let formattedValue = formattedValueInRhs;
let pathValue = valueInRhs;
if (!keyInLhs && keyInRhs) {
type = "add" /* ADD */;
} else if (keyInLhs && !keyInRhs) {
type = "remove" /* REMOVE */;
formattedValue = formattedValueInLhs;
pathValue = valueInLhs;
} else if (config.strict ? rawValueInLhs !== rawValueInRhs : rawValueInLhs != rawValueInRhs) {
type = "update" /* UPDATE */;
}
const result = [];
if (includeDiffType(type, config)) {
const stringifiedParsedKey = stringify(parsedKey, config);
if (type === "update" /* UPDATE */ && !config.showUpdatedOnly) {
result.push({
type: "remove" /* REMOVE */,
str: `${stringifiedParsedKey}: ${formattedValueInLhs},`,
depth,
path: [...path, lastPathValue("remove" /* REMOVE */, valueInLhs)]
});
}
result.push({
type,
str: `${stringifiedParsedKey}: ${formattedValue},`,
depth,
path: [...path, lastPathValue(type, pathValue)]
});
}
return result;
}
function getPathHint(config, type) {
if (typeof config.pathHints === "object") {
const hint = config.pathHints[type];
if (typeof hint === "string") return hint;
}
return null;
}
function buildResult(rhs, result, depth, initialChangeType, parent, path, config) {
if (!includeDiffType(initialChangeType, config)) return result;
const parentDepth = depth - 1;
const [open, close] = getWrapper(rhs);
return [
{
type: initialChangeType,
str: parentDepth > 0 ? `${stringify(parent, config)}: ${open}` : open,
depth: parentDepth,
path
},
...result,
{
type: initialChangeType,
str: parentDepth > 0 ? `${close},` : close,
depth: parentDepth,
path
}
];
}
function timeoutSecurityCheck(startedAt, config) {
if (Date.now() - startedAt > config.timeout) {
throw new Error("Diff took too much time! Aborting.");
}
}
function maxKeysSecurityCheck(size, config) {
if (size > config.maxKeys) {
throw new Error("Object is too big to continue! Aborting.");
}
}
// src/diff/diffObjects.ts
function diffObjects({
recursiveDiff: recursiveDiff2,
lhs,
rhs,
config,
depth,
parent,
seen,
initialChangeType,
path,
startedAt
}) {
const result = [];
const lhsKeys = getEnumerableKeys(lhs);
maxKeysSecurityCheck(lhsKeys.length, config);
const rhsKeys = getEnumerableKeys(rhs);
maxKeysSecurityCheck(rhsKeys.length, config);
const keys = /* @__PURE__ */ new Set([...lhsKeys, ...rhsKeys]);
let i = 0;
for (const key of keys) {
if (i++ % ITERATION_TIMEOUT_CHECK === 0) {
timeoutSecurityCheck(startedAt, config);
}
const lhsValue = Array.isArray(lhs) ? lhs[key] : lhs?.[key];
const rhsValue = Array.isArray(rhs) ? rhs[key] : rhs?.[key];
const numericKey = typeof key !== "symbol" ? Number(key) : NaN;
const parsedKey = isNaN(numericKey) ? key : numericKey;
const updatedPath = [...path, parsedKey];
if (isIterable(lhsValue) || isIterable(rhsValue)) {
result.push(
...recursiveDiff2({
lhs: lhsValue,
rhs: rhsValue,
config,
depth: depth + 1,
parent: parsedKey,
seen,
initialChangeType,
path: updatedPath,
startedAt
})
);
continue;
}
result.push(
...getObjectChangeResult(
lhs,
rhs,
depth,
key,
parsedKey,
config,
updatedPath
)
);
}
seen.delete(lhs);
seen.delete(rhs);
return buildResult(
rhs,
result,
depth,
initialChangeType,
parent,
path,
config
);
}
var diffObjects_default = diffObjects;
// src/diff/diffSets.ts
function diffSets({
recursiveDiff: recursiveDiff2,
lhs,
rhs,
config,
depth,
parent,
seen,
initialChangeType,
path,
startedAt
}) {
const result = [];
maxKeysSecurityCheck(lhs.size, config);
maxKeysSecurityCheck(rhs.size, config);
const mergedSet = /* @__PURE__ */ new Set([...lhs, ...rhs]);
let i = 0;
for (const value of mergedSet) {
if (i++ % ITERATION_TIMEOUT_CHECK === 0) {
timeoutSecurityCheck(startedAt, config);
}
const existsInLhs = lhs.has(value);
const existsInRhs = rhs.has(value);
const hint = getPathHint(config, "set");
const updatedPath = [...path];
if (hint) {
updatedPath.push(hint);
}
if (isIterable(value)) {
result.push(
...recursiveDiff2({
lhs: existsInLhs ? value : void 0,
rhs: existsInRhs ? value : void 0,
config,
depth: depth + 1,
parent: getRef(value),
seen,
initialChangeType,
path: updatedPath,
startedAt
})
);
continue;
}
let type = "noop" /* NOOP */;
if (existsInLhs && !existsInRhs) {
type = "remove" /* REMOVE */;
} else if (!existsInLhs && existsInRhs) {
type = "add" /* ADD */;
}
if (includeDiffType(type, config)) {
result.push({
type,
str: `${stringify(value, config)},`,
depth,
path: [...updatedPath, lastPathValue(type, value)]
});
}
}
seen.delete(lhs);
seen.delete(rhs);
return buildResult(
rhs,
result,
depth,
initialChangeType,
parent,
path,
config
);
}
var diffSets_default = diffSets;
// src/diff/diffMaps.ts
function diffMaps({
recursiveDiff: recursiveDiff2,
lhs,
rhs,
config,
depth,
parent,
seen,
initialChangeType,
path,
startedAt
}) {
const result = [];
maxKeysSecurityCheck(lhs.size, config);
maxKeysSecurityCheck(rhs.size, config);
const mergedMapKeys = /* @__PURE__ */ new Set([...lhs.keys(), ...rhs.keys()]);
let i = 0;
for (const key of mergedMapKeys) {
if (i++ % ITERATION_TIMEOUT_CHECK === 0) {
timeoutSecurityCheck(startedAt, config);
}
const keyInLhs = lhs.has(key);
const keyInRhs = rhs.has(key);
const lhsValue = keyInLhs ? lhs.get(key) : null;
const rhsValue = keyInRhs ? rhs.get(key) : null;
const hint = getPathHint(config, "map");
const pathUpdate = hint ? [hint, key] : [key];
const updatedPath = [...path, ...pathUpdate];
if (isIterable(lhsValue) || isIterable(rhsValue)) {
result.push(
...recursiveDiff2({
lhs: lhsValue,
rhs: rhsValue,
config,
depth: depth + 1,
parent: key,
seen,
initialChangeType,
path: updatedPath,
startedAt
})
);
continue;
}
result.push(
...getObjectChangeResult(lhs, rhs, depth, key, key, config, updatedPath)
);
}
seen.delete(lhs);
seen.delete(rhs);
return buildResult(
rhs,
result,
depth,
initialChangeType,
parent,
path,
config
);
}
var diffMaps_default = diffMaps;
// src/diff/diffConstructors.ts
function diffConstructors({
recursiveDiff: recursiveDiff2,
lhs,
rhs,
config,
depth,
parent,
seen,
initialChangeType,
path,
startedAt
}) {
let modLhs = lhs;
let modRhs = rhs;
let defaultChangeType = initialChangeType;
if (isObject(rhs) || rhs?.constructor) {
modLhs = emptyShellClone(rhs);
defaultChangeType = "add" /* ADD */;
} else if (isObject(lhs) || lhs?.constructor) {
modRhs = emptyShellClone(lhs);
defaultChangeType = "remove" /* REMOVE */;
} else {
throw new Error("Edge case raised, I don't know how to handle this input");
}
return recursiveDiff2({
lhs: modLhs,
rhs: modRhs,
config,
depth,
parent,
seen,
initialChangeType: defaultChangeType,
path,
startedAt
});
}
var diffConstructors_default = diffConstructors;
// src/diff/index.ts
function recursiveDiff({
lhs,
rhs,
config,
depth,
parent,
seen,
initialChangeType,
path,
startedAt
}) {
if (depth > config.maxDepth) {
throw new Error("Max depth exceeded!");
}
timeoutSecurityCheck(startedAt, config);
const lhsSeen = seen.get(lhs) ?? 0;
const rhsSeen = seen.get(rhs) ?? 0;
if (lhsSeen > 1 || rhsSeen > 1) {
if (!includeDiffType(initialChangeType, config)) return [];
return [
{
type: initialChangeType,
str: `${stringify(parent, config)}: [Circular],`,
depth: depth - 1,
path
}
];
}
if (typeof lhs === "object" && lhs !== null) seen.set(lhs, lhsSeen + 1);
if (typeof rhs === "object" && rhs !== null) seen.set(rhs, rhsSeen + 1);
const args = {
recursiveDiff,
lhs,
rhs,
config,
depth,
parent,
seen,
initialChangeType,
path,
startedAt
};
if (isPrimitive(lhs) && isPrimitive(rhs)) {
if (config.strict ? lhs === rhs : lhs == rhs) {
return [];
}
const parentDepth = depth - 1;
const lhsValue = stringify(lhs, config);
const rhsValue = stringify(rhs, config);
const result = [];
if (includeDiffType("update" /* UPDATE */, config)) {
if (!config.showUpdatedOnly) {
result.push({
type: "remove" /* REMOVE */,
str: `${lhsValue}`,
depth: parentDepth,
path: [...path, lastPathValue("remove" /* REMOVE */, lhs)]
});
}
result.push({
type: "update" /* UPDATE */,
str: `${rhsValue}`,
depth: parentDepth,
path: [...path, lastPathValue("update" /* UPDATE */, rhs)]
});
}
return result;
}
if (!areObjects(lhs, rhs) && // Skips for Object.create(null) vs {}
(lhs?.constructor !== rhs?.constructor || isNullOrUndefined(lhs) && rhs || lhs && isNullOrUndefined(rhs))) {
return diffConstructors_default(args);
}
if (lhs instanceof Set && rhs instanceof Set) {
return diffSets_default(args);
}
if (lhs instanceof Map && rhs instanceof Map) {
return diffMaps_default(args);
}
return diffObjects_default(args);
}
var diff_default = recursiveDiff;
// src/utils/toDiffString.ts
var ANSI_RESET = "\x1B[0m";
function colorWrapper(color) {
return (str) => `${color}${str}${ANSI_RESET}`;
}
var ansiColors = {
["remove" /* REMOVE */]: colorWrapper("\x1B[31m"),
["add" /* ADD */]: colorWrapper("\x1B[32m"),
["update" /* UPDATE */]: colorWrapper("\x1B[33m"),
["noop" /* NOOP */]: colorWrapper("")
};
function toDiffString(diff2, config) {
const defaultConfig = {
withColors: true,
colors: ansiColors,
wrapper: [],
indentSize: 2,
symbols: {
["add" /* ADD */]: "+",
["remove" /* REMOVE */]: "-",
["update" /* UPDATE */]: "!",
["noop" /* NOOP */]: ""
}
};
const mergedConfig = {
...defaultConfig,
...config,
colors: {
...defaultConfig.colors,
...config?.colors
},
symbols: {
...defaultConfig.symbols,
...config?.symbols
}
};
const diffString = diff2.map(({ type, str, depth }, index) => {
let symbolString = mergedConfig.symbols[type];
if (index > 0 && index < diff2.length - 1 && !symbolString.length) {
symbolString = ` ${mergedConfig.symbols[type]}`;
}
let buildStr = `${symbolString}${" ".repeat(depth * mergedConfig.indentSize)}${str}`;
if (mergedConfig.withColors) {
buildStr = mergedConfig.colors[type](buildStr);
}
return buildStr;
}).join("\n");
const [open = "", close = ""] = mergedConfig.wrapper || [];
return `${open ? `${open}
` : ""}${diffString}${close ? `
${close}` : ""}`;
}
// src/index.ts
function diff(lhs, rhs, config) {
const defaultConfig = {
include: [
"add" /* ADD */,
"remove" /* REMOVE */,
"update" /* UPDATE */,
"noop" /* NOOP */
],
exclude: [],
strict: true,
showUpdatedOnly: false,
pathHints: {
map: DefaultPathHints.map,
set: DefaultPathHints.set
},
maxDepth: MAX_DEPTH,
maxKeys: MAX_KEYS,
timeout: MAX_TIMEOUT_MS,
redactKeys: [
"password",
"secret",
"token",
"Symbol(password)",
"Symbol(secret)",
"Symbol(token)"
]
};
const mergedConfig = {
...defaultConfig,
...config
};
if (!mergedConfig.maxDepth) mergedConfig.maxDepth = MAX_DEPTH;
if (!mergedConfig.maxKeys) mergedConfig.maxKeys = MAX_KEYS;
if (!mergedConfig.timeout) mergedConfig.timeout = MAX_TIMEOUT_MS;
mergedConfig.include = Array.isArray(mergedConfig.include) ? mergedConfig.include : [mergedConfig.include];
mergedConfig.exclude = Array.isArray(mergedConfig.exclude) ? mergedConfig.exclude : [mergedConfig.exclude];
const diffResult = diff_default({
lhs,
rhs,
config: mergedConfig,
depth: 1,
parent: null,
seen: /* @__PURE__ */ new WeakMap(),
initialChangeType: "noop" /* NOOP */,
path: [],
startedAt: Date.now()
});
const areEqual = diffResult.every(
(result) => result.type === "noop" /* NOOP */
);
Object.defineProperty(diffResult, "toDiffString", {
value: (diffStringConfig) => toDiffString(diffResult, diffStringConfig),
enumerable: false,
writable: false,
configurable: false
});
Object.defineProperty(diffResult, "equal", {
value: areEqual,
enumerable: false,
writable: false,
configurable: false
});
return diffResult;
}
var index_default = diff;
// Annotate the CommonJS export names for ESM import in node:
0 && (module.exports = {
ChangeType
});
//# sourceMappingURL=index.cjs.map