fast-tree-builder
Version:
Easily construct highly customizable bi-directional tree structures from iterable data.
219 lines (218 loc) • 8.09 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);
var index_exports = {};
__export(index_exports, {
default: () => buildTree
});
module.exports = __toCommonJS(index_exports);
function buildTree(items, options) {
if (!options) {
throw new Error(`Missing required 'options' parameter.`);
}
const {
id: idAccessor,
parentId: parentIdAccessor,
childIds: childIdsAccessor,
valueResolver,
valueKey = "value",
parentKey = "parent",
childrenKey = "children",
depthKey = false,
validateTree = false,
validateReferences = false,
includeEmptyChildrenArray = false
} = options;
if (!idAccessor) {
throw new Error(`Option 'id' is required.`);
}
if (!parentIdAccessor && !childIdsAccessor) {
throw new Error(`Either 'parentId' or 'childIds' must be provided.`);
}
if (parentIdAccessor && childIdsAccessor) {
throw new Error(`'parentId' and 'childIds' cannot be used together.`);
}
const roots = [];
const nodes = /* @__PURE__ */ new Map();
if (parentIdAccessor) {
const waitingForParent = /* @__PURE__ */ new Map();
for (const item of items) {
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
if (nodes.has(id)) {
throw new Error(`Duplicate identifier '${id}'.`);
}
const node = valueKey !== false ? { [valueKey]: valueResolver ? valueResolver(item) : item } : { ...valueResolver ? valueResolver(item) : item };
if (valueKey === false) {
if (parentKey !== false) {
delete node[parentKey];
}
delete node[childrenKey];
}
if (includeEmptyChildrenArray) {
node[childrenKey] = [];
}
nodes.set(id, node);
const parentId = typeof parentIdAccessor === "function" ? parentIdAccessor(item) : item[parentIdAccessor];
const parentNode = nodes.get(parentId);
if (parentNode) {
parentNode[childrenKey] ||= [];
parentNode[childrenKey].push(node);
if (parentKey !== false) {
node[parentKey] = parentNode;
}
} else {
const siblings = waitingForParent.get(parentId);
if (siblings) {
siblings.push(node);
} else {
waitingForParent.set(parentId, [node]);
}
}
const children = waitingForParent.get(id);
if (children) {
node[childrenKey] = children;
if (parentKey !== false) {
for (const child of children) {
child[parentKey] = node;
}
}
waitingForParent.delete(id);
}
}
for (const [parentId, nodes2] of waitingForParent.entries()) {
if (validateReferences && parentId != null) {
throw new Error(`Referential integrity violation: parentId '${parentId}' does not match any item in the input.`);
}
for (const node of nodes2) {
roots.push(node);
}
}
} else if (childIdsAccessor) {
const waitingChildren = /* @__PURE__ */ new Map();
const rootCandidates = /* @__PURE__ */ new Map();
for (const item of items) {
const id = typeof idAccessor === "function" ? idAccessor(item) : item[idAccessor];
if (nodes.has(id)) {
throw new Error(`Duplicate identifier '${id}'.`);
}
const node = valueKey !== false ? { [valueKey]: valueResolver ? valueResolver(item) : item } : { ...valueResolver ? valueResolver(item) : item };
if (valueKey === false) {
if (parentKey !== false) {
delete node[parentKey];
}
delete node[childrenKey];
}
if (includeEmptyChildrenArray) {
node[childrenKey] = [];
}
nodes.set(id, node);
const childIds = typeof childIdsAccessor === "function" ? childIdsAccessor(item) : item[childIdsAccessor];
if (childIds != null) {
if (typeof childIds[Symbol.iterator] !== "function") {
throw new Error(`Item '${id}' has invalid children: expected an iterable value.`);
}
node[childrenKey] = [];
for (const childId of childIds) {
const childNode = nodes.get(childId);
if (childNode) {
node[childrenKey].push(childNode);
if (parentKey !== false) {
if (childNode[parentKey] && childNode[parentKey] !== node) {
throw new Error(`Node '${childId}' already has a different parent, refusing to overwrite.`);
}
childNode[parentKey] = node;
}
rootCandidates.delete(childId);
} else {
if (waitingChildren.has(childId)) {
throw new Error(`Multiple parents reference the same unresolved child '${childId}'.`);
}
waitingChildren.set(childId, {
parentNode: node,
childIndex: node[childrenKey].length
});
node[childrenKey].push(null);
}
}
if (node[childrenKey].length === 0) {
delete node[childrenKey];
}
}
const parentDescriptor = waitingChildren.get(id);
if (parentDescriptor) {
const { parentNode, childIndex } = parentDescriptor;
parentNode[childrenKey][childIndex] = node;
if (parentKey !== false) {
if (node[parentKey] && node[parentKey] !== parentNode) {
throw new Error(`Node '${id}' already has a different parent, refusing to overwrite.`);
}
node[parentKey] = parentNode;
}
waitingChildren.delete(id);
} else {
rootCandidates.set(id, node);
}
}
if (waitingChildren.size > 0) {
if (validateReferences) {
const childId = waitingChildren.keys().next().value;
throw new Error(`Referential integrity violation: child reference '${childId}' does not match any item in the input.`);
}
const pending = Array.from(waitingChildren.values());
for (let i = pending.length - 1; i >= 0; i--) {
const { parentNode, childIndex } = pending[i];
parentNode[childrenKey].splice(childIndex, 1);
if (parentNode[childrenKey].length === 0) {
delete parentNode[childrenKey];
}
}
}
for (const node of rootCandidates.values()) {
roots.push(node);
}
}
const withDepth = typeof depthKey === "string" || typeof depthKey === "symbol" || typeof depthKey === "number";
if (validateTree || withDepth) {
if (roots.length === 0 && nodes.size > 0) {
throw new Error("Tree validation failed: detected a cycle.");
}
const stack = [...roots].map((node) => ({ node, depth: 0 }));
const visited = /* @__PURE__ */ new Set();
let processedCount = 0;
const MAX_NODES = nodes.size;
while (stack.length > 0 && processedCount++ <= MAX_NODES) {
const { node, depth } = stack.pop();
if (visited.has(node)) {
throw new Error("Tree validation failed: a node reachable via multiple paths.");
}
visited.add(node);
if (withDepth) {
node[depthKey] = depth;
}
if (node[childrenKey]) {
for (const child of node[childrenKey]) {
stack.push({ node: child, depth: depth + 1 });
}
}
}
if (nodes.size !== visited.size) {
throw new Error("Tree validation failed: detected a cycle.");
}
}
return { roots, nodes };
}