jsm-treeify
Version:
A library to display JavaScript objects as colorized, tree-like structures in the console.
352 lines (351 loc) • 13.8 kB
JavaScript
Object.defineProperty(exports, "__esModule", { value: true });
exports.bufferToStringIfSmall = bufferToStringIfSmall;
exports.sanitize = sanitize;
exports.default = colorizeTree;
const ansi_code_1 = require("ansi-code");
/* -------------------------------------------------------------------------- */
/* TYPES */
/* -------------------------------------------------------------------------- */
var Color;
(function (Color) {
Color["Gray"] = "gray";
Color["Green"] = "green";
Color["Red"] = "red";
Color["Yellow"] = "yellow";
Color["Cyan"] = "cyan";
Color["Blue"] = "blue";
Color["Reset"] = "reset";
Color["Default"] = "default";
})(Color || (Color = {}));
/* -------------------------------------------------------------------------- */
/* BUFFER UTILS */
/* -------------------------------------------------------------------------- */
/**
* Universal function to detect if a value is a Buffer
* Works in both Node.js and browser environments
*
* @param value - The value to check
* @returns true if the value is a Buffer, false otherwise
*/
function isBuffer(value) {
return (value != null &&
typeof value === 'object' &&
// Check if Buffer exists and has isBuffer method (Node.js)
typeof Buffer !== 'undefined' &&
typeof Buffer.isBuffer === 'function' &&
Buffer.isBuffer(value)) || (
// Fallback for browser environments or when Buffer.isBuffer is not available
value != null &&
typeof value === 'object' &&
// Check for Buffer-like properties
typeof value.constructor === 'function' &&
value.constructor.name === 'Buffer' &&
typeof value.readUInt8 === 'function' &&
typeof value.writeUInt8 === 'function' &&
typeof value.slice === 'function' &&
// Ensure it's not a regular array
!Array.isArray(value) &&
// Check if it has length property like a buffer
typeof value.length === 'number');
}
/* -------------------------------------------------------------------------- */
/* MongoDB ObjectId Buffer to String conversion utilities */
/* -------------------------------------------------------------------------- */
/**
* Converts a Buffer to string if its length is less than specified limit
* @param buffer - The buffer to convert
* @param encoding - The encoding to use for conversion (default: 'hex')
* @param maxLength - Maximum allowed length (default: 256)
* @returns The string representation of the buffer, or null if buffer is too large
*/
function bufferToStringIfSmall(buffer, encoding = 'hex', maxLength = 256) {
if (!isBuffer(buffer)) {
return null;
}
if (buffer.length >= maxLength) {
return null;
}
return buffer.toString(encoding);
}
/**
* Converts MongoDB ObjectId buffer to string
* MongoDB ObjectIds are always 12 bytes, so they're well under the 256 byte limit
* @param objectIdBuffer - The ObjectId buffer from MongoDB
* @returns The ObjectId as a hex string, or null if invalid
*/
function mongoObjectIdToString(objectIdBuffer) {
// Check if it's a buffer and has the correct ObjectId length (12 bytes)
if (!isBuffer(objectIdBuffer)) {
return null;
}
// MongoDB ObjectIds are always exactly 12 bytes
if (objectIdBuffer.length !== 12 && objectIdBuffer.length !== 24) {
return null;
}
// Convert to hex string (this will be 24 characters)
return objectIdBuffer.toString('hex');
}
/**
* Safe converter for any potential ObjectId value
* Handles buffers, strings, and ObjectId objects
* @param value - Any value that might be an ObjectId
* @returns The ObjectId as a string, or null if invalid
*/
function safeObjectIdToString(value) {
var _a;
// If it's already a string and looks like an ObjectId (24 hex chars)
if (typeof value === 'string') {
return /^[0-9a-fA-F]{24}$/.test(value) ? value : null;
}
// If it's a MongoDB ObjectId object with toString method
if (value && typeof value.toString === 'function' && ((_a = value.constructor) === null || _a === void 0 ? void 0 : _a.name) === 'ObjectId') {
return value.toString();
}
// If it's a buffer (your case)
return mongoObjectIdToString(value);
}
/* -------------------------------------------------------------------------- */
/* UTILITIES */
/* -------------------------------------------------------------------------- */
/**
* Sanitizes an object by removing circular references and handling edge cases
* @param obj - The object to sanitize
* @param maxDepth - Maximum depth to traverse (prevents infinite recursion)
* @returns Sanitized object safe for JSON operations
*/
function sanitize(obj, maxDepth = 10, maxObjectItems = 30) {
const seen = new WeakSet();
function sanitizeRecursive(value, depth) {
// Prevent infinite recursion
if (depth >= maxDepth) {
return '[Max Depth Reached]';
}
if (isBuffer(value)) {
return safeObjectIdToString(value) || '[Buffer]';
}
// Handle primitives
if (value === null || value === undefined) {
return value;
}
// Handle non-object types
if (typeof value !== 'object') {
return value;
}
// Handle functions
if (typeof value === 'function') {
return `[Function: ${value.name || 'anonymous'}]`;
}
// Handle dates
if (value instanceof Date) {
return value;
}
// Handle circular references
if (seen.has(value)) {
return '[Circular Reference]';
}
seen.add(value);
try {
if (Array.isArray(value)) {
return value.slice(maxObjectItems).map(item => sanitizeRecursive(item, depth + 1));
}
// Handle regular objects
const sanitized = {};
let propCount = 0;
for (const key in value) {
if (propCount > maxObjectItems)
break;
if (value.hasOwnProperty(key)) {
sanitized[key] = sanitizeRecursive(value[key], depth + 1);
propCount++;
}
}
return sanitized;
}
finally {
seen.delete(value);
}
}
return sanitizeRecursive(obj, 0);
}
/**
* Determines if a value is a simple (primitive) type
* @param value - The value to check
* @returns True if the value is a simple type
*/
function isSimpleType(value) {
return (typeof value === 'string' ||
typeof value === 'number' ||
typeof value === 'boolean' ||
typeof value === 'undefined' ||
value === null ||
value instanceof Date);
}
/**
* Determines the appropriate color for a value based on its type
* @param value - The value to determine color for
* @returns The color name to use
*/
function getValueColor(value) {
if (value === null || value === undefined) {
return Color.Gray;
}
switch (typeof value) {
case 'string':
return Color.Cyan;
case 'number':
return Color.Yellow;
case 'boolean':
return value ? Color.Green : Color.Red;
default:
if (value instanceof Date) {
return Color.Blue;
}
return Color.Gray;
}
}
/**
* Formats a value for display in the tree
* @param value - The value to format
* @returns Formatted string representation
*/
function formatValue(value) {
if (value === null) {
return '(null)';
}
if (value === undefined) {
return '(undefined)';
}
if (value instanceof Date) {
return value.toISOString();
}
return String(value);
}
/**
* Transforms an object into a colored tree-like string representation.
*
* @param obj - The object to transform.
* @param prefix - The prefix string for the current level (used for indentation).
* @param isLast - Boolean indicating if the current element is the last in its level.
* @param isRoot - Boolean indicating if the current element the root element.
* @param options - Optional configuration for tree generation.
*
* @returns An array of strings representing the lines of the colored tree.
*/
function colorizeTree(obj, options = {}) {
const { maxDepth = 10, maxObjectItems = 30, showArrayIndices = true, sortKeys = false, prefix = '', isLast = false, isRoot = true } = options;
// ANSI color codes
const colors = {
[Color.Gray]: ansi_code_1.font.color.gray,
[Color.Green]: ansi_code_1.font.color.green,
[Color.Red]: ansi_code_1.font.color.red,
[Color.Yellow]: ansi_code_1.font.color.yellow,
[Color.Cyan]: ansi_code_1.font.color.cyan,
[Color.Blue]: ansi_code_1.font.color.blue,
[Color.Reset]: ansi_code_1.font.reset,
};
function colorize(text, color) {
if (color === Color.Default) {
return String(text);
}
return colors[color] + String(text) + colors[Color.Reset];
}
// Check for max depth to prevent infinite recursion
const currentDepth = (prefix.match(/[│├└]/g) || []).length;
if (currentDepth >= maxDepth) {
return [prefix + colorize('[Max Depth Reached]', Color.Gray)];
}
// Handle null and undefined at the top level
if (obj === null) {
return [prefix + colorize('(null)', Color.Gray)];
}
if (obj === undefined) {
return [prefix + colorize('(undefined)', Color.Gray)];
}
// Handle simple types
if (isSimpleType(obj)) {
const color = getValueColor(obj);
const value = formatValue(obj);
return [prefix + colorize(value, color)];
}
// Handle objects and arrays
if (typeof obj === 'object' && obj !== null) {
if (Array.isArray(obj)) {
return renderArray(obj, prefix, showArrayIndices);
}
else {
// Sanitize object to handle circular references
const sanitizedObj = sanitize(obj, maxDepth);
return renderObject(sanitizedObj, prefix, sortKeys);
}
}
// Fallback for unknown types
return [prefix + colorize(String(obj), Color.Gray)];
/**
* Renders an array as tree lines
*/
function renderArray(arr, prefix, showIndices) {
const lines = [];
if (arr.length === 0) {
lines.push(prefix + colorize("(empty array)", Color.Gray));
return lines;
}
for (let i = 0; i < arr.length; i++) {
const isLastItem = i === arr.length - 1;
const connector = isLastItem ? colorize('└─ ', Color.Gray) : colorize('├─ ', Color.Gray);
const nextPrefix = prefix + (isLastItem ? ' ' : colorize('│ ', Color.Gray));
const indexLabel = showIndices ? colorize(`[${i}] `, Color.Gray) : '';
if (isSimpleType(arr[i])) {
const color = getValueColor(arr[i]);
const value = formatValue(arr[i]);
lines.push(prefix + connector + indexLabel + colorize(value, color));
}
else {
lines.push(prefix + connector + indexLabel);
lines.push(...colorizeTree(arr[i], Object.assign(Object.assign({}, options), { prefix: nextPrefix, isLast: isLastItem, isRoot: false, maxObjectItems })));
}
}
return lines;
}
/**
* Renders an object as tree lines
*/
function renderObject(obj, prefix, shouldSortKeys) {
const lines = [];
let keys = Object.keys(obj);
if (shouldSortKeys) {
keys = keys.sort();
}
if (keys.length === 0) {
lines.push(prefix + colorize("(empty object)", Color.Gray));
return lines;
}
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
const value = obj[key];
const isLastKey = i === keys.length - 1;
const connector = isLastKey ? colorize('└─ ', Color.Gray) : colorize('├─ ', Color.Gray);
const nextPrefix = prefix + (isLastKey ? ' ' : colorize('│ ', Color.Gray));
if (value === null || value === undefined) {
const displayValue = formatValue(value);
lines.push(prefix + connector + colorize(key, Color.Default) + ': ' + colorize(displayValue, Color.Gray));
}
else if (isSimpleType(value)) {
const color = getValueColor(value);
const displayValue = formatValue(value);
lines.push(prefix + connector + colorize(key, Color.Default) + ': ' + colorize(displayValue, color));
}
else if (Array.isArray(value) && value.length === 0) {
lines.push(prefix + connector + colorize(key, Color.Default) + ': ' + colorize("(empty array)", Color.Gray));
}
else if (typeof value === 'object' && Object.keys(value).length === 0) {
lines.push(prefix + connector + colorize(key, Color.Default) + ': ' + colorize("(empty object)", Color.Gray));
}
else {
lines.push(prefix + connector + colorize(key, Color.Default) + ':');
lines.push(...colorizeTree(value, Object.assign(Object.assign({}, options), { prefix: nextPrefix, isLast: isLastKey, isRoot: false, maxObjectItems })));
}
}
return lines;
}
}
;