UNPKG

jsm-treeify

Version:

A library to display JavaScript objects as colorized, tree-like structures in the console.

352 lines (351 loc) 13.8 kB
"use strict"; 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; } }