// Based on original implementation from Tofandel’s answer: https://stackoverflow.com/a/59787588/247441 /* Aggregates parts (mapped to slash-separated paths) into a nested object. E.g.: { /some/path: A, /foo: B, /some/other/path: C } gets turned into: { foo: B, some: { path: A, other: { path: C } } } */ export function unflattenObject> (parts: Record): T { const result: Record = {}; // Ideally should be typed as Partial, but that causes problems down the line for (const partPath of Object.keys(parts)) { if (Object.prototype.hasOwnProperty.call(parts, partPath)) { const keys = partPath.match(/^\/+[^\/]*|[^\/]*\/+$|(?:\/{2,}|[^\/])+(?:\/+$)?/g); if (keys) { keys.reduce((accumulator, val, idx) => { return accumulator[val] ?? ( (accumulator[val] = isNaN(Number(keys[idx + 1])) ? (keys.length - 1 === idx ? parts[partPath] : {}) : []) ); }, result); } } } return result as T; } /* Recursively decomposes an arbitrarily nested object into a flat record of slash-separated part paths mapped to respective structures. E.g.: { foo: B, some: { path: A, other: { path: C } } } gets turned into: { /some/path: A, /foo: B, /some/other/path: C } */ export function flattenObject( obj: Record, _prefix: false | string = false, _result: Record | null = null, ): Record { const result: Record = _result ?? {}; // Preserve empty objects and arrays, they are lost otherwise if (_prefix !== false && typeof obj === 'object' && obj !== null && Object.keys(obj).length === 0) { result[_prefix] = Array.isArray(obj) ? [] : {}; return result; } const prefix = _prefix !== false ? (_prefix + path.posix.sep) : path.posix.sep; for (const i in obj) { if (Object.prototype.hasOwnProperty.call(obj, i)) { if (typeof obj[i] === 'object' && obj[i] !== null) { // Recursion on deeper objects flattenObject(obj[i], prefix + i, result); } else { result[prefix + i] = obj[i]; } } } return result; }