@mapomodule/utils
Pure TypeScript utility helpers shared across all @mapomodule/* packages. No Vue, no Nuxt, no side effects — safe to import from any context including server-only code and tests.
Installation
pnpm add @mapomodule/utils@next # @next: Mapo v2 is in beta, latest is still v1normalizeEndpoint(endpoint)
Ensures an API endpoint path has exactly one leading slash and one trailing slash. Query strings (anything after ?) are preserved unchanged — the slash is inserted before the ?, never after it. Used internally by useCrud to normalize user-provided paths.
import { normalizeEndpoint } from "@mapomodule/utils";
normalizeEndpoint("api/articles"); // '/api/articles/'
normalizeEndpoint("/api/articles"); // '/api/articles/'
normalizeEndpoint("/api/articles/"); // '/api/articles/'
normalizeEndpoint("///api/articles"); // '/api/articles/'
normalizeEndpoint("/api/articles?fields=id"); // '/api/articles/?fields=id'
normalizeEndpoint("/api/articles/?ordering=-date"); // '/api/articles/?ordering=-date'splitEndpointParams(endpoint)
Splits an endpoint string into its clean path and the parsed query params. Useful when an endpoint carries baked-in params (e.g. ?fields=id,title) that must be merged into request params while CRUD mutations target the bare path. Used by MapoListTable to keep the list endpoint and mutation endpoints in sync.
import { splitEndpointParams } from "@mapomodule/utils";
splitEndpointParams("/api/articles/?fields=id,title");
// → { path: '/api/articles/', params: { fields: 'id,title' } }
splitEndpointParams("/api/articles/");
// → { path: '/api/articles/', params: {} }deepMerge(target, source)
Recursively merges two plain objects. Arrays and primitives in source overwrite those in target; nested objects are merged recursively.
import { deepMerge } from "@mapomodule/utils";
deepMerge({ a: 1, b: { c: 2 } }, { b: { d: 3 } });
// → { a: 1, b: { c: 2, d: 3 } }getNestedValue(obj, path) / setNestedValue(obj, path, value)
Read or write a value at a dotted path string.
import { getNestedValue, setNestedValue } from "@mapomodule/utils";
const obj = { user: { address: { city: "Rome" } } };
getNestedValue(obj, "user.address.city"); // 'Rome'
setNestedValue(obj, "user.address.city", "Milan");
// → { user: { address: { city: 'Milan' } } }objectDiff(original, updated)
Returns only the keys whose values differ between two objects. Used by useCrud.partialUpdate to send minimal PATCH payloads.
import { objectDiff } from "@mapomodule/utils";
objectDiff({ title: "Old", body: "Text" }, { title: "New", body: "Text" });
// → { title: 'New' }Arrays are compared by content (deep recursive equality), not by reference. This means a cloned array with identical elements is treated as unchanged — which prevents false-positive isDirty flags when the form baseline is built from deepClone(model).
formatDate(date, format?)
Formats a Date object or ISO string using a token-based format string.
| Token | Value |
|---|---|
YYYY | 4-digit year |
MM | 2-digit month |
DD | 2-digit day |
HH | 2-digit hours (24h) |
mm | 2-digit minutes |
ss | 2-digit seconds |
import { formatDate } from "@mapomodule/utils";
formatDate(new Date("2026-04-28T10:30:00"), "YYYY-MM-DD"); // '2026-04-28'
formatDate("2026-04-28T10:30:00", "DD/MM/YYYY HH:mm"); // '28/04/2026 10:30'humanFileSize(bytes, si?, dp?)
Formats a byte count as a human-readable string. si selects metric units (powers of 1000, kB/MB) over binary (powers of 1024, KiB/MiB, default); dp sets decimal places (default 1). Used across the Media Manager for file sizes.
import { humanFileSize } from "@mapomodule/utils";
humanFileSize(1536); // '1.5 KiB'
humanFileSize(1500000, true); // '1.5 MB'slugify(str)
Converts a string into a URL-safe slug: lowercase, accents stripped, non-alphanumerics collapsed into single dashes. The Camomilla media adapter uses it to derive a folder slug from its title.
import { slugify } from "@mapomodule/utils";
slugify("Càffé & Co."); // 'caffe-co'debounce(fn, delay)
Returns a debounced version of fn that delays invocation until delay ms have elapsed since the last call.
import { debounce } from "@mapomodule/utils";
const onSearch = debounce((query: string) => fetchResults(query), 300);
// Drop a call still pending when the component goes away.
onBeforeUnmount(() => onSearch.cancel());The returned function has the type Debounced<A>: it takes the arguments of fn and has a cancel() method that drops the pending call, if any.
slotNamespace(slots, prefix)
Filters a Vue Slots object to only those whose names start with prefix, and strips the prefix from each key. Used to forward a slot subset to a child component.
import { slotNamespace } from "@mapomodule/utils";
// Parent has slots: 'header', 'list:empty', 'list:item'
const listSlots = slotNamespace(slots, "list:");
// → { empty: ..., item: ... }buildRouteTree(routes)
Converts a flat RouteRecordNormalized[] array into a nested MenuNode[] tree, using route.meta.parent to establish parent–child relationships. At each level, finite numeric route.meta.sidebarOrder values sort ascending before routes without an order; equal values and unordered siblings preserve router order. This applies recursively. Used by the sidebar to build the navigation tree.
import { buildRouteTree } from "@mapomodule/utils";
import type { MenuNode } from "@mapomodule/utils";
const tree: MenuNode[] = buildRouteTree(router.getRoutes());calcMaxMenuNestDepth(nodes)
Returns the maximum nesting depth of a MenuNode[] tree. Root level = 1.
import { calcMaxMenuNestDepth } from "@mapomodule/utils";
calcMaxMenuNestDepth([]); // 0
calcMaxMenuNestDepth([{ children: [] }]); // 1
calcMaxMenuNestDepth([{ children: [{ children: [] }] }]); // 2deepClone(value)
Deep-clones plain objects and arrays. Does not handle class instances, Maps, Sets, or circular references.
import { deepClone } from "@mapomodule/utils";
const copy = deepClone({ user: { name: "Alice", tags: ["admin"] } });
copy.user.tags.push("editor");
// original is unchangedisFile(value) / isBlob(value) / isFileOrBlob(value)
Type-safe guards for File and Blob instances. Safe to call server-side (returns false if the global is unavailable).
import { isFile, isBlob, isFileOrBlob } from "@mapomodule/utils";
isFile(new File([], "doc.pdf")); // true
isBlob(new Blob(["data"])); // true
isFileOrBlob(42); // falsefindPropPaths(obj, predicate)
Returns all dot-notation paths in obj where predicate(value) is truthy.
import { findPropPaths, isFile } from "@mapomodule/utils";
const obj = { title: "Hello", meta: { thumb: new File([], "img.png") } };
findPropPaths(obj, isFile); // ["meta.thumb"]filesInObject(obj)
Shorthand for findPropPaths(obj, isFileOrBlob). Returns paths of all File / Blob values — used internally by useCrud multipart detection.
import { filesInObject } from "@mapomodule/utils";
filesInObject({ title: "Doc", file: new File([], "a.pdf") }); // ["file"]filterObj(obj, keys)
Returns a shallow copy of obj containing only the specified top-level keys.
import { filterObj } from "@mapomodule/utils";
filterObj({ a: 1, b: 2, c: 3 }, ["a", "c"]); // { a: 1, c: 3 }