@oaklean/profiler-core
Version:
Part of the @oaklean suite. It provides all basic functions to work with the `.oak` file format. It allows parsing the `.oak` file format as well as tools for analyzing the measurement values. It also provides all necessary capabilities required for prec
1,031 lines • 81.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.parsePath = parsePath;
exports.parse = parse;
exports.get = get;
exports.has = has;
exports.getComment = getComment;
exports.getTrailingComment = getTrailingComment;
exports.set = set;
exports.remove = remove;
exports.merge = merge;
exports.replace = replace;
exports.patch = patch;
exports.rename = rename;
exports.move = move;
exports.setComment = setComment;
exports.setTrailingComment = setTrailingComment;
exports.removeTrailingComment = removeTrailingComment;
exports.removeComment = removeComment;
exports.sort = sort;
exports.format = format;
exports.modify = modify;
const jsonc_parser_1 = require("jsonc-parser");
// =============================================================================
// Path Parsing
// =============================================================================
/**
* Parse a dot-notation path string into a JSONPath array
* Examples:
* "config.database.host" -> ["config", "database", "host"]
* "items.0" -> ["items", 0]
* "items[0]" -> ["items", 0]
*/
function parsePath(pathStr) {
if (!pathStr)
return [];
const result = [];
let current = '';
let i = 0;
while (i < pathStr.length) {
const char = pathStr[i];
if (char === '.') {
if (current) {
result.push(parseSegment(current));
current = '';
}
i++;
}
else if (char === '[') {
if (current) {
result.push(parseSegment(current));
current = '';
}
// Find closing bracket
const closeBracket = pathStr.indexOf(']', i);
if (closeBracket === -1) {
throw new Error(`Unclosed bracket in path: ${pathStr}`);
}
const indexStr = pathStr.slice(i + 1, closeBracket);
result.push(parseSegment(indexStr));
i = closeBracket + 1;
}
else {
current += char;
i++;
}
}
if (current) {
result.push(parseSegment(current));
}
return result;
}
function parseSegment(segment) {
const num = Number(segment);
if (!Number.isNaN(num) && Number.isInteger(num) && num >= 0) {
return num;
}
return segment;
}
/**
* Normalize a path to an array format
* If the path is already an array, return it as-is
* If it's a string, parse it using parsePath
*/
function normalizePath(path) {
return typeof path === 'string' ? parsePath(path) : path;
}
// =============================================================================
// Shared Helpers
// =============================================================================
/**
* Get all property keys from an object node in the JSON tree
*/
function getObjectKeys(node) {
if (!node || node.type !== 'object' || !node.children) {
return [];
}
const keys = [];
for (const child of node.children) {
if (child.type === 'property' && child.children && child.children[0]) {
keys.push(child.children[0].value);
}
}
return keys;
}
/**
* Find the property node at a given path
*/
function findProperty(json, path) {
const normalizedPath = normalizePath(path);
const tree = (0, jsonc_parser_1.parseTree)(json);
if (!tree)
return null;
const node = (0, jsonc_parser_1.findNodeAtLocation)(tree, normalizedPath);
if (!node || !node.parent)
return null;
return { node, propertyNode: node.parent, tree };
}
/**
* Get the range of a property including its line context
*/
function getPropertyRange(json, propertyNode) {
let lineStart = propertyNode.offset;
while (lineStart > 0 && json[lineStart - 1] !== '\n') {
lineStart--;
}
const textBeforeOnLine = json.slice(lineStart, propertyNode.offset);
const isSingleLine = textBeforeOnLine.includes('{');
return { lineStart, isSingleLine };
}
/**
* Look backwards from propertyOffset for a comment.
* Returns the start position of the comment line if it should be deleted with the field.
* Comments starting with "**" are preserved (returns null).
*/
function findAssociatedCommentStart(json, propertyOffset) {
let pos = propertyOffset - 1;
// Skip whitespace (spaces and tabs)
while (pos >= 0 && (json[pos] === ' ' || json[pos] === '\t')) {
pos--;
}
// Must have a newline before the property for there to be a comment above
if (pos >= 0 && json[pos] === '\n') {
pos--;
if (pos >= 0 && json[pos] === '\r') {
pos--;
}
}
else {
return null;
}
// Skip trailing whitespace on previous line
while (pos >= 0 && (json[pos] === ' ' || json[pos] === '\t')) {
pos--;
}
if (pos < 0)
return null;
// Check for block comment end */
if (pos >= 1 && json[pos] === '/' && json[pos - 1] === '*') {
let i = pos - 2;
while (i >= 1) {
if (json[i - 1] === '/' && json[i] === '*') {
const commentContent = json.slice(i + 1, pos - 1).trim();
// Comments starting with ** are preserved
if (commentContent.startsWith('**')) {
return null;
}
let commentLineStart = i - 1;
while (commentLineStart > 0 && json[commentLineStart - 1] !== '\n') {
commentLineStart--;
}
return commentLineStart;
}
i--;
}
return null;
}
// Check for line comment
const lineEnd = pos;
while (pos >= 0 && json[pos] !== '\n') {
pos--;
}
const lineStart = pos + 1;
const line = json.slice(lineStart, lineEnd + 1).trim();
if (line.startsWith('//')) {
const commentContent = line.slice(2).trim();
// Comments starting with ** are preserved
if (commentContent.startsWith('**')) {
return null;
}
return lineStart;
}
return null;
}
/**
* Find any comment above a property (not just associated ones)
*/
function findCommentAbove(json, propertyOffset) {
let pos = propertyOffset - 1;
// Skip whitespace
while (pos >= 0 && (json[pos] === ' ' || json[pos] === '\t')) {
pos--;
}
// Must have a newline
if (pos >= 0 && json[pos] === '\n') {
pos--;
if (pos >= 0 && json[pos] === '\r') {
pos--;
}
}
else {
return null;
}
// Skip trailing whitespace on previous line
while (pos >= 0 && (json[pos] === ' ' || json[pos] === '\t')) {
pos--;
}
if (pos < 0)
return null;
// Check for block comment end */
if (pos >= 1 && json[pos] === '/' && json[pos - 1] === '*') {
let i = pos - 2;
while (i >= 1) {
if (json[i - 1] === '/' && json[i] === '*') {
const commentStart = i - 1;
const commentEnd = pos + 1;
const content = json.slice(commentStart + 2, commentEnd - 2).trim();
return { start: commentStart, end: commentEnd, content };
}
i--;
}
return null;
}
// Check for line comment
const lineEnd = pos;
while (pos >= 0 && json[pos] !== '\n') {
pos--;
}
const lineStart = pos + 1;
const line = json.slice(lineStart, lineEnd + 1).trim();
if (line.startsWith('//')) {
const content = line.slice(2).trim();
return { start: lineStart, end: lineEnd + 1, content };
}
return null;
}
/**
* Find any trailing comment after a property (on the same line)
*/
function findTrailingComment(json, propertyEnd) {
let pos = propertyEnd;
// Skip comma if present
if (pos < json.length && json[pos] === ',') {
pos++;
}
// Skip whitespace (but not newlines)
while (pos < json.length && (json[pos] === ' ' || json[pos] === '\t')) {
pos++;
}
// If we hit a newline or end, no trailing comment
if (pos >= json.length || json[pos] === '\n' || json[pos] === '\r') {
return null;
}
// Check for line comment //
if (pos < json.length - 1 && json[pos] === '/' && json[pos + 1] === '/') {
const commentStart = pos;
pos += 2;
// Find end of line
while (pos < json.length && json[pos] !== '\n' && json[pos] !== '\r') {
pos++;
}
const content = json.slice(commentStart + 2, pos).trim();
return { start: commentStart, end: pos, content };
}
// Check for block comment /* */
if (pos < json.length - 1 && json[pos] === '/' && json[pos + 1] === '*') {
const commentStart = pos;
pos += 2;
// Find closing */
while (pos < json.length - 1) {
if (json[pos] === '*' && json[pos + 1] === '/') {
pos += 2;
const content = json.slice(commentStart + 2, pos - 2).trim();
return { start: commentStart, end: pos, content };
}
pos++;
}
// Unclosed block comment
return null;
}
return null;
}
/**
* Flatten a nested changes object into individual path-value pairs
*/
function flattenChanges(obj, prefix = []) {
// contains paths to delete (explicitly set to undefined)
const explicitDeletions = [];
const flatChanges = [];
for (const [key, value] of Object.entries(obj)) {
const currentPath = [...prefix, key];
if (value === undefined) {
explicitDeletions.push(currentPath);
}
else if (value !== null &&
typeof value === 'object' &&
!Array.isArray(value)) {
const childChanges = flattenChanges(value, currentPath);
if (childChanges.flatChanges.length === 0) {
// the object only contains undefined values, so we should set it to an empty object rather than deleting it
flatChanges.push({ path: currentPath, value: {} });
}
else {
flatChanges.push(...childChanges.flatChanges);
explicitDeletions.push(...childChanges.explicitDeletions);
}
}
else {
flatChanges.push({ path: currentPath, value });
}
}
return { explicitDeletions, flatChanges };
}
/**
* Compute deletions for fields that exist in currentJson but not in changes (replace semantics)
*/
function computeDeletions(currentJson, changes, prefix = []) {
const result = [];
const tree = (0, jsonc_parser_1.parseTree)(currentJson);
if (!tree)
return result;
const currentNode = prefix.length === 0 ? tree : (0, jsonc_parser_1.findNodeAtLocation)(tree, prefix);
const currentKeys = getObjectKeys(currentNode);
for (const key of currentKeys) {
if (!(key in changes)) {
result.push({ path: [...prefix, key], value: undefined });
}
else {
const changeValue = changes[key];
if (changeValue !== null &&
changeValue !== undefined &&
typeof changeValue === 'object' &&
!Array.isArray(changeValue)) {
result.push(...computeDeletions(currentJson, changeValue, [...prefix, key]));
}
}
}
return result;
}
// =============================================================================
// Parse Operations
// =============================================================================
/**
* Parse a JSONC string into a JavaScript value.
* Unlike JSON.parse(), this handles comments and trailing commas.
*/
function parse(json) {
const tree = (0, jsonc_parser_1.parseTree)(json);
if (!tree) {
throw new SyntaxError('Failed to parse JSON');
}
return (0, jsonc_parser_1.getNodeValue)(tree);
}
// =============================================================================
// Path-based Operations
// =============================================================================
/**
* Get the value at a path in the JSON
*/
function get(json, path) {
const normalizedPath = normalizePath(path);
const tree = (0, jsonc_parser_1.parseTree)(json);
if (!tree)
return undefined;
const node = (0, jsonc_parser_1.findNodeAtLocation)(tree, normalizedPath);
if (!node)
return undefined;
return (0, jsonc_parser_1.getNodeValue)(node);
}
/**
* Check if a path exists in the JSON
*/
function has(json, path) {
const normalizedPath = normalizePath(path);
const tree = (0, jsonc_parser_1.parseTree)(json);
if (!tree)
return false;
const node = (0, jsonc_parser_1.findNodeAtLocation)(tree, normalizedPath);
return node !== undefined;
}
/**
* Get the comment associated with a field at a path.
* First checks for a comment above the field, then falls back to a trailing comment.
*/
function getComment(json, path) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return null;
const { propertyNode } = found;
// First check for comment above
const commentAbove = findCommentAbove(json, propertyNode.offset);
if (commentAbove) {
return commentAbove.content;
}
// Fall back to trailing comment
const propertyEnd = propertyNode.offset + propertyNode.length;
const trailingComment = findTrailingComment(json, propertyEnd);
return trailingComment ? trailingComment.content : null;
}
/**
* Get the trailing comment after a field at a path (on the same line)
*/
function getTrailingComment(json, path) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return null;
const { propertyNode } = found;
const propertyEnd = propertyNode.offset + propertyNode.length;
const trailingComment = findTrailingComment(json, propertyEnd);
return trailingComment ? trailingComment.content : null;
}
/**
* Set a value at a path in the JSON, optionally with a comment
*/
function set(json, path, value, comment) {
const normalizedPath = normalizePath(path);
const edits = (0, jsonc_parser_1.modify)(json, normalizedPath, value, {});
let result = (0, jsonc_parser_1.applyEdits)(json, edits);
// Add comment if provided
if (comment !== undefined) {
result = setComment(result, normalizedPath, comment);
}
return result;
}
/**
* Remove a field at a path (with associated comment handling)
*/
function remove(json, path) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return json;
const { propertyNode } = found;
const { lineStart, isSingleLine } = getPropertyRange(json, propertyNode);
// Check for associated comment above (comments starting with ** are preserved)
const commentStart = !isSingleLine
? findAssociatedCommentStart(json, propertyNode.offset)
: null;
// Determine delete start
let deleteStart;
if (commentStart !== null) {
deleteStart = commentStart;
}
else if (isSingleLine) {
deleteStart = propertyNode.offset;
}
else {
deleteStart = lineStart;
}
// Find the end of the property
let deleteEnd = propertyNode.offset + propertyNode.length;
// Check for trailing comment (unless it starts with **)
const propertyEnd = propertyNode.offset + propertyNode.length;
const trailingComment = findTrailingComment(json, propertyEnd);
const hasPreservedTrailingComment = trailingComment === null || trailingComment === void 0 ? void 0 : trailingComment.content.startsWith('**');
// If there's a trailing comment that should be deleted, include it in the deletion
if (trailingComment && !hasPreservedTrailingComment) {
deleteEnd = trailingComment.end;
}
// Skip trailing comma (if not already past it due to trailing comment)
if (deleteEnd < json.length && json[deleteEnd] === ',') {
deleteEnd++;
}
// Skip trailing whitespace
while (deleteEnd < json.length &&
(json[deleteEnd] === ' ' || json[deleteEnd] === '\t')) {
deleteEnd++;
}
// For multi-line, include the trailing newline
if (!isSingleLine) {
if (deleteEnd < json.length && json[deleteEnd] === '\n') {
deleteEnd++;
}
else if (deleteEnd < json.length && json[deleteEnd] === '\r') {
deleteEnd++;
if (deleteEnd < json.length && json[deleteEnd] === '\n') {
deleteEnd++;
}
}
}
const beforeDelete = json.slice(0, deleteStart);
const afterDelete = json.slice(deleteEnd);
let result = beforeDelete + afterDelete;
// Handle trailing comma issues
const trimmedBefore = beforeDelete.trimEnd();
const afterTrimmed = afterDelete.trimStart();
if (trimmedBefore.endsWith(',') && afterTrimmed.startsWith('}')) {
const commaPos = beforeDelete.lastIndexOf(',');
result =
beforeDelete.slice(0, commaPos) +
beforeDelete.slice(commaPos + 1) +
afterDelete;
}
return result;
}
// =============================================================================
// Merge Strategies
// =============================================================================
/**
* Merge changes into JSON (update/add only, never delete)
*/
function merge(json, changes) {
let result = json;
const { explicitDeletions, flatChanges } = flattenChanges(changes);
// remove fields explicitly set to undefined in changes
for (const path of explicitDeletions) {
result = remove(result, path);
}
for (const { path, value } of flatChanges) {
const edits = (0, jsonc_parser_1.modify)(result, path, value, {});
result = (0, jsonc_parser_1.applyEdits)(result, edits);
}
return result;
}
/**
* Replace JSON with changes (delete fields not in changes)
*/
function replace(json, changes) {
let result = json;
// Compute deletions for fields not in changes
const deletions = computeDeletions(json, changes);
const { explicitDeletions, flatChanges } = flattenChanges(changes);
// Process deletions first (from deepest to shallowest)
const sortedDeletions = deletions.sort((a, b) => b.path.length - a.path.length);
for (const { path } of sortedDeletions) {
result = remove(result, path);
}
// remove fields explicitly set to undefined in changes
for (const path of explicitDeletions) {
result = remove(result, path);
}
// Then apply updates
for (const { path, value } of flatChanges) {
const edits = (0, jsonc_parser_1.modify)(result, path, value, {});
result = (0, jsonc_parser_1.applyEdits)(result, edits);
}
return result;
}
/**
* Patch JSON with explicit deletes via undefined
*/
function patch(json, changes) {
return merge(json, changes);
}
// =============================================================================
// Key Operations
// =============================================================================
/**
* Rename a key while preserving its value and associated comment
*/
function rename(json, path, newKey) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return json;
const { node, propertyNode } = found;
// Get the value
const value = (0, jsonc_parser_1.getNodeValue)(node);
// Check for associated comment
const { isSingleLine } = getPropertyRange(json, propertyNode);
const commentInfo = !isSingleLine
? findCommentAbove(json, propertyNode.offset)
: null;
// Build new path
const newPath = [...normalizedPath.slice(0, -1), newKey];
// Remove old key
let result = remove(json, normalizedPath);
// Add new key with the value
result = set(result, newPath, value);
// If there was a comment, preserve it
if (commentInfo) {
result = setComment(result, newPath, commentInfo.content);
}
return result;
}
/**
* Move a field to a different location
*/
function move(json, fromPath, toPath) {
const normalizedFromPath = normalizePath(fromPath);
const normalizedToPath = normalizePath(toPath);
const found = findProperty(json, normalizedFromPath);
if (!found)
return json;
const { node } = found;
const value = (0, jsonc_parser_1.getNodeValue)(node);
// Remove from old location
let result = remove(json, normalizedFromPath);
// Add to new location
result = set(result, normalizedToPath, value);
return result;
}
// =============================================================================
// Comment Operations
// =============================================================================
/**
* Set or update a comment above a field
*/
function setComment(json, path, comment) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return json;
const { propertyNode } = found;
const { lineStart, isSingleLine } = getPropertyRange(json, propertyNode);
if (isSingleLine) {
// Can't add comment to single-line JSON
return json;
}
// Check for existing comment
const existingComment = findCommentAbove(json, propertyNode.offset);
// Determine indentation
const indentation = json.slice(lineStart, propertyNode.offset);
const commentLine = `${indentation}// ${comment}\n`;
if (existingComment) {
// Find the line start of the existing comment
let commentLineStart = existingComment.start;
while (commentLineStart > 0 && json[commentLineStart - 1] !== '\n') {
commentLineStart--;
}
// Find the end of the comment line (including newline)
let commentLineEnd = existingComment.end;
while (commentLineEnd < json.length && json[commentLineEnd] !== '\n') {
commentLineEnd++;
}
if (commentLineEnd < json.length) {
commentLineEnd++; // Include the newline
}
// Replace the existing comment
return (json.slice(0, commentLineStart) + commentLine + json.slice(commentLineEnd));
}
else {
// Insert new comment before the property
return json.slice(0, lineStart) + commentLine + json.slice(lineStart);
}
}
/**
* Set or update a trailing comment after a field (on the same line)
*/
function setTrailingComment(json, path, comment) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return json;
const { propertyNode } = found;
const propertyEnd = propertyNode.offset + propertyNode.length;
// Check for existing trailing comment
const existingComment = findTrailingComment(json, propertyEnd);
// Find where to insert the comment (after the value, before any comma)
const insertPos = propertyEnd;
// Check if there's a comma after the property
let pos = propertyEnd;
while (pos < json.length && (json[pos] === ' ' || json[pos] === '\t')) {
pos++;
}
const hasComma = pos < json.length && json[pos] === ',';
if (existingComment) {
// Replace existing trailing comment
return `${json.slice(0, existingComment.start)}// ${comment}${json.slice(existingComment.end)}`;
}
// Insert new trailing comment
// If there's a comma, insert before it; otherwise insert after value
if (hasComma) {
// Insert before the comma
return `${json.slice(0, pos)} // ${comment}${json.slice(pos)}`;
}
// Insert after the value
return `${json.slice(0, insertPos)} // ${comment}${json.slice(insertPos)}`;
}
/**
* Remove the trailing comment after a field
*/
function removeTrailingComment(json, path) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return json;
const { propertyNode } = found;
const propertyEnd = propertyNode.offset + propertyNode.length;
const existingComment = findTrailingComment(json, propertyEnd);
if (!existingComment)
return json;
// Remove the comment and any whitespace before it
let removeStart = existingComment.start;
while (removeStart > propertyEnd &&
(json[removeStart - 1] === ' ' || json[removeStart - 1] === '\t')) {
removeStart--;
}
return json.slice(0, removeStart) + json.slice(existingComment.end);
}
/**
* Remove the comment above a field
*/
function removeComment(json, path) {
const normalizedPath = normalizePath(path);
const found = findProperty(json, normalizedPath);
if (!found)
return json;
const { propertyNode } = found;
const existingComment = findCommentAbove(json, propertyNode.offset);
if (!existingComment)
return json;
// Find the full line range of the comment
let commentLineStart = existingComment.start;
while (commentLineStart > 0 && json[commentLineStart - 1] !== '\n') {
commentLineStart--;
}
let commentLineEnd = existingComment.end;
while (commentLineEnd < json.length && json[commentLineEnd] !== '\n') {
commentLineEnd++;
}
if (commentLineEnd < json.length) {
commentLineEnd++; // Include the newline
}
return json.slice(0, commentLineStart) + json.slice(commentLineEnd);
}
/**
* Extract information about all properties in an object node
*/
function getPropertyInfos(json, objectNode) {
if (!objectNode.children)
return [];
const infos = [];
for (const child of objectNode.children) {
if (child.type !== 'property' || !child.children || !child.children[0]) {
continue;
}
const keyNode = child.children[0];
const key = keyNode.value;
// Find line start for indentation
let lineStart = child.offset;
while (lineStart > 0 && json[lineStart - 1] !== '\n') {
lineStart--;
}
const indentation = json.slice(lineStart, child.offset);
// Check for associated comment above
const commentStart = findAssociatedCommentStart(json, child.offset);
const fullStart = commentStart !== null ? commentStart : lineStart;
// Check for trailing comment
const propertyEnd = child.offset + child.length;
const trailingComment = findTrailingComment(json, propertyEnd);
const fullEnd = trailingComment ? trailingComment.end : propertyEnd;
const info = {
key,
keyNode,
propertyNode: child,
fullStart,
propertyEnd,
fullEnd,
indentation
};
if (trailingComment) {
info.trailingComment = trailingComment;
}
infos.push(info);
}
return infos;
}
/**
* Sort object keys while preserving comments
*/
function sort(json, path = [], options = {}) {
const { comparator = (a, b) => a.localeCompare(b), deep = true } = options;
const normalizedPath = normalizePath(path);
const tree = (0, jsonc_parser_1.parseTree)(json);
if (!tree)
return json;
// Find the target node at the given path
const targetNode = normalizedPath.length === 0
? tree
: (0, jsonc_parser_1.findNodeAtLocation)(tree, normalizedPath);
if (!targetNode || targetNode.type !== 'object')
return json;
// Collect all objects that need sorting within the target (process deepest first)
const objectsToSort = [];
function collectObjects(node, depth) {
if (node.type === 'object' && node.children && node.children.length > 0) {
objectsToSort.push({ node, depth });
}
if (node.children) {
for (const child of node.children) {
collectObjects(child, depth + 1);
}
}
}
collectObjects(targetNode, 0);
// Sort deepest objects first so offsets remain valid
objectsToSort.sort((a, b) => b.depth - a.depth);
// If not deep, only process the target object itself
const objectsToProcess = deep
? objectsToSort
: objectsToSort.filter((o) => o.depth === 0);
let result = json;
for (const { node: objectNode } of objectsToProcess) {
result = sortSingleObject(result, objectNode, comparator);
// Re-parse after each modification to get updated offsets
if (objectsToProcess.length > 1) {
const newTree = (0, jsonc_parser_1.parseTree)(result);
if (!newTree)
break;
}
}
// If we processed multiple objects, we need to re-process with fresh offsets
// since sorting one object invalidates offsets for others
if (deep && objectsToSort.length > 1) {
// Re-parse and process again from scratch with updated positions
result = sortDeepAtPath(result, normalizedPath, comparator);
}
return result;
}
/**
* Recursively sort all objects in the JSON starting at a path
*/
function sortDeepAtPath(json, path, comparator) {
const tree = (0, jsonc_parser_1.parseTree)(json);
if (!tree)
return json;
const targetNode = path.length === 0 ? tree : (0, jsonc_parser_1.findNodeAtLocation)(tree, path);
if (!targetNode || targetNode.type !== 'object')
return json;
// Find the deepest object and sort it, repeat until no changes
let result = json;
let changed = true;
while (changed) {
changed = false;
const currentTree = (0, jsonc_parser_1.parseTree)(result);
if (!currentTree)
break;
const currentTarget = path.length === 0 ? currentTree : (0, jsonc_parser_1.findNodeAtLocation)(currentTree, path);
if (!currentTarget)
break;
// Find deepest object within target
let deepestObject = null;
let maxDepth = -1;
function findDeepest(node, depth) {
if (node.type === 'object' && node.children && node.children.length > 1) {
// Check if this object needs sorting
const infos = getPropertyInfos(result, node);
const keys = infos.map((i) => i.key);
const sortedKeys = [...keys].sort(comparator);
const needsSort = keys.some((k, i) => k !== sortedKeys[i]);
if (needsSort && depth > maxDepth) {
deepestObject = node;
maxDepth = depth;
}
}
if (node.children) {
for (const child of node.children) {
findDeepest(child, depth + 1);
}
}
}
findDeepest(currentTarget, 0);
if (deepestObject) {
result = sortSingleObject(result, deepestObject, comparator);
changed = true;
}
}
return result;
}
/**
* Sort a single object's keys
*/
function sortSingleObject(json, objectNode, comparator) {
const infos = getPropertyInfos(json, objectNode);
if (infos.length <= 1)
return json;
// Check if already sorted
const keys = infos.map((i) => i.key);
const sortedKeys = [...keys].sort(comparator);
if (keys.every((k, i) => k === sortedKeys[i])) {
return json;
}
// Sort the property infos
const sortedInfos = [...infos].sort((a, b) => comparator(a.key, b.key));
// Find the object's content boundaries (inside the braces)
const objectStart = objectNode.offset;
const objectEnd = objectNode.offset + objectNode.length;
// Find where content starts (after opening brace and any whitespace)
let contentStart = objectStart + 1;
while (contentStart < objectEnd &&
(json[contentStart] === ' ' ||
json[contentStart] === '\t' ||
json[contentStart] === '\n' ||
json[contentStart] === '\r')) {
contentStart++;
}
// Find where content ends (before closing brace)
let contentEnd = objectEnd - 1;
while (contentEnd > contentStart &&
(json[contentEnd - 1] === ' ' ||
json[contentEnd - 1] === '\t' ||
json[contentEnd - 1] === '\n' ||
json[contentEnd - 1] === '\r')) {
contentEnd--;
}
// Check if it's a single-line object
const objectContent = json.slice(objectStart, objectEnd);
const isSingleLine = !objectContent.includes('\n');
// Detect if original has trailing comma (check after last property, accounting for trailing comments)
const lastInfo = infos[infos.length - 1];
let hasTrailingComma = false;
if (lastInfo) {
// Start after the property (or trailing comment if present)
let pos = lastInfo.fullEnd;
while (pos < objectEnd && (json[pos] === ' ' || json[pos] === '\t')) {
pos++;
}
// Check for comma
if (pos < objectEnd && json[pos] === ',') {
hasTrailingComma = true;
}
else {
// Also check if comma comes before trailing comment
pos = lastInfo.propertyEnd;
while (pos < objectEnd && (json[pos] === ' ' || json[pos] === '\t')) {
pos++;
}
hasTrailingComma = pos < objectEnd && json[pos] === ',';
}
}
if (isSingleLine) {
// For single-line objects, rebuild simply but preserve trailing comments
const sortedPairs = sortedInfos.map((info) => {
var _a;
const valueNode = (_a = info.propertyNode.children) === null || _a === void 0 ? void 0 : _a[1];
const valueStr = valueNode
? json.slice(valueNode.offset, valueNode.offset + valueNode.length)
: 'null';
// Include trailing comment if present
const trailingCommentStr = info.trailingComment
? ` // ${info.trailingComment.content}`
: '';
return `"${info.key}": ${valueStr}${trailingCommentStr}`;
});
const trailingComma = hasTrailingComma ? ',' : '';
return (json.slice(0, objectStart) +
'{ ' +
sortedPairs.join(', ') +
trailingComma +
' }' +
json.slice(objectEnd));
}
// For multi-line objects, preserve formatting
// Extract each property with its full content (including comments above and trailing)
const propertyTexts = [];
for (const sortedInfo of sortedInfos) {
// Find the original info to get proper text boundaries
const originalInfo = infos.find((x) => x.key === sortedInfo.key);
if (!originalInfo)
continue;
// Build the property text including comment above and trailing comment
let propText;
// Start with comment above if present
if (originalInfo.fullStart < originalInfo.propertyNode.offset) {
// There's a comment before this property
const commentText = json.slice(originalInfo.fullStart, originalInfo.propertyNode.offset);
// Get just the property part (without trailing comment)
const propOnlyText = json.slice(originalInfo.propertyNode.offset, originalInfo.propertyEnd);
// Reconstruct with consistent indentation
propText = commentText + propOnlyText;
}
else {
// No comment above, just the property with indentation
propText =
sortedInfo.indentation +
json.slice(originalInfo.propertyNode.offset, originalInfo.propertyEnd);
}
// Add trailing comment if present
if (originalInfo.trailingComment) {
propText += ` // ${originalInfo.trailingComment.content}`;
}
propertyTexts.push(propText);
}
// Find closing brace indentation
const closingBracePos = objectEnd - 1;
let closingIndentStart = closingBracePos;
while (closingIndentStart > 0 && json[closingIndentStart - 1] !== '\n') {
closingIndentStart--;
}
const closingIndent = json.slice(closingIndentStart, closingBracePos);
// Build the new object content
const newContent = propertyTexts
.map((text, i) => {
// Add comma except for last item (unless trailing comma is preserved)
const isLast = i === propertyTexts.length - 1;
if (isLast) {
return hasTrailingComma ? `${text},` : text;
}
return `${text},`;
})
.join('\n');
// Reconstruct the object
const beforeObject = json.slice(0, objectStart);
const afterObject = json.slice(objectEnd);
return `${beforeObject}{\n${newContent}\n${closingIndent}}${afterObject}`;
}
// =============================================================================
// Format Operations
// =============================================================================
/**
* Format a JSONC document with consistent indentation.
* Preserves comments while normalizing whitespace.
*/
function format(json, options = {}) {
const { tabSize = 2, insertSpaces = true, eol = '\n' } = options;
const edits = (0, jsonc_parser_1.format)(json, undefined, { tabSize, insertSpaces, eol });
return (0, jsonc_parser_1.applyEdits)(json, edits);
}
// =============================================================================
// High-level API
// =============================================================================
/**
* Modify JSON with replace semantics — fields not in changes are deleted
*/
function modify(json, changes) {
return replace(json, changes);
}
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi9saWIvYXl3c29uL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7O0FBMkJBLDhCQXdDQztBQXlXRCxzQkFNQztBQVNELGtCQVNDO0FBS0Qsa0JBT0M7QUFNRCxnQ0FpQkM7QUFLRCxnREFhQztBQUtELGtCQWdCQztBQUtELHdCQStFQztBQVNELHNCQWVDO0FBS0QsMEJBK0JDO0FBS0Qsc0JBRUM7QUFTRCx3QkErQkM7QUFLRCxvQkFvQkM7QUFTRCxnQ0ErQ0M7QUFLRCxnREF1Q0M7QUFLRCxzREFxQkM7QUFLRCxzQ0F5QkM7QUFzRkQsb0JBNkRDO0FBa1BELHdCQWVDO0FBU0Qsd0JBRUM7QUFqeUNELCtDQVFxQjtBQVFyQixnRkFBZ0Y7QUFDaEYsZUFBZTtBQUNmLGdGQUFnRjtBQUVoRjs7Ozs7O0dBTUc7QUFDSCxTQUFnQixTQUFTLENBQUMsT0FBZTtJQUN4QyxJQUFJLENBQUMsT0FBTztRQUFFLE9BQU8sRUFBRSxDQUFBO0lBRXZCLE1BQU0sTUFBTSxHQUF3QixFQUFFLENBQUE7SUFDdEMsSUFBSSxPQUFPLEdBQUcsRUFBRSxDQUFBO0lBQ2hCLElBQUksQ0FBQyxHQUFHLENBQUMsQ0FBQTtJQUVULE9BQU8sQ0FBQyxHQUFHLE9BQU8sQ0FBQyxNQUFNLEVBQUUsQ0FBQztRQUMzQixNQUFNLElBQUksR0FBRyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUE7UUFFdkIsSUFBSSxJQUFJLEtBQUssR0FBRyxFQUFFLENBQUM7WUFDbEIsSUFBSSxPQUFPLEVBQUUsQ0FBQztnQkFDYixNQUFNLENBQUMsSUFBSSxDQUFDLFlBQVksQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFBO2dCQUNsQyxPQUFPLEdBQUcsRUFBRSxDQUFBO1lBQ2IsQ0FBQztZQUNELENBQUMsRUFBRSxDQUFBO1FBQ0osQ0FBQzthQUFNLElBQUksSUFBSSxLQUFLLEdBQUcsRUFBRSxDQUFDO1lBQ3pCLElBQUks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