@r2abreu/path-tree
v2.0.3
Published
Build hierarchical trees from flat arrays of path-based objects. Zero dependencies, TypeScript-first, framework-agnostic.
Maintainers
Readme
@r2abreu/path-tree
Build hierarchical trees from flat arrays of path-based objects. TypeScript-first, framework-agnostic.
Why?
You have flat data with paths:
[
{ path: '/docs/api/users' },
{ path: '/docs/guides/intro' },
{ path: '/blog/2024/post-1' }
]You need a tree:
[
{
item: { path: '/docs' },
children: [
{
item: { path: '/docs/api' },
children: [
{ item: { path: '/docs/api/users' }, children: [] }
]
},
{
item: { path: '/docs/guides' },
children: [
{ item: { path: '/docs/guides/intro' }, children: [] }
]
}
]
},
{
item: { path: '/blog' },
children: [
{
item: { path: '/blog/2024' },
children: [
{ item: { path: '/blog/2024/post-1' }, children: [] }
]
}
]
}
]Features
- ✅ Zero dependencies - Lightweight and fast
- ✅ TypeScript-first - Full type safety with generics
- ✅ Framework-agnostic - Works with any JavaScript framework
- ✅ Automatic parent creation - Missing intermediate paths are created automatically
- ✅ Synthetic node marking - Clearly identifies generated vs. original nodes
- ✅ Vue Router adapter - First-class support for Vue Router
- ✅ Tree-shakeable - Only bundle what you use
Installation
npm install @r2abreu/path-treeQuick Start
Generic Usage
import { buildPathTree } from '@r2abreu/path-tree';
const items = [
{ path: '/a/b/c', data: 1 },
{ path: '/a/b/d', data: 2 },
{ path: '/x/y', data: 3 }
];
const tree = buildPathTree(items);Vue Router
import { useRouter } from 'vue-router';
import { buildRouteTree } from '@r2abreu/path-tree/vue-router';
const router = useRouter();
const tree = buildRouteTree(router.getRoutes());Use Cases
1. Navigation Menus
<script setup lang="ts">
import { useRouter } from 'vue-router';
import { buildRouteTree, isRealRoute } from '@r2abreu/path-tree/vue-router';
const router = useRouter();
const routeTree = buildRouteTree(router.getRoutes());
</script>
<template>
<nav>
<ul>
<li v-for="node in routeTree" :key="node.item.path">
<router-link v-if="isRealRoute(node)" :to="node.item.path">
{{ node.item.path }}
</router-link>
<span v-else class="parent">{{ node.item.path }}</span>
</li>
</ul>
</nav>
</template>2. File System Trees
import { buildPathTree } from '@r2abreu/path-tree';
const files = [
{ path: '/src/components/Button.vue', size: 1024 },
{ path: '/src/utils/helpers.ts', size: 512 },
{ path: '/tests/unit/Button.test.ts', size: 2048 }
];
const fileTree = buildPathTree(files);3. API Endpoint Documentation
import { buildPathTree } from '@r2abreu/path-tree';
const endpoints = [
{ path: '/api/v1/users', method: 'GET' },
{ path: '/api/v1/users/:id', method: 'GET' },
{ path: '/api/v1/posts', method: 'POST' }
];
const apiTree = buildPathTree(endpoints);4. Breadcrumbs
import { buildPathTree } from '@r2abreu/path-tree';
const pages = [
{ path: '/', title: 'Home' },
{ path: '/docs', title: 'Documentation' },
{ path: '/docs/api', title: 'API Reference' },
{ path: '/docs/api/users', title: 'Users API' }
];
const tree = buildPathTree(pages);
// Use tree to generate breadcrumb trailAPI
Core
buildPathTree<T>(items, options?)
Build a hierarchical tree from a flat array of path-based items.
Parameters:
items: T[]- Array of items with path propertyoptions?: BuildPathTreeOptions<T>- Optional configuration
Returns: TreeNode<T>[] - Array of root nodes
Example:
const tree = buildPathTree(items, {
getPath: (item) => item.url, // Custom path extractor
createNode: (path) => ({ ... }), // Custom node factory
enrichItem: (item) => ({ ...item }), // Transform items
getParentPath: (path) => dirname(path) // Custom parent resolver
});isRealNode<T>(node)
Check if a node is from the original input (not synthetic).
Example:
if (isRealNode(node)) {
// This node was in the original input
}filterRealNodes<T>(tree)
Filter tree to only include real nodes (removes synthetic parents).
Example:
const realOnly = filterRealNodes(tree);Vue Router Adapter
buildRouteTree(routes, options?)
Build a hierarchical tree from Vue Router routes.
Parameters:
routes: RouteRecordNormalized[]- Vue Router routesoptions?: BuildPathTreeOptions- Optional configuration
Returns: RouteTreeNode[] - Array of root route nodes
useRouteTree(routes, options?)
Vue composable for building route tree.
isRealRoute(node)
Check if a route node is real (not synthetic).
filterRealRoutes(tree)
Filter tree to only real routes.
Synthetic Nodes
The library automatically creates parent nodes for missing intermediate paths:
buildPathTree([{ path: '/a/b/c' }])
// Creates: /, /a, /a/b (all marked with synthetic: true)When synthetic nodes are useful:
- ✅ File systems (directories always exist)
- ✅ Breadcrumbs (need complete path)
- ✅ Navigation structure (visual hierarchy)
When to filter them out:
- ❌ API endpoints (only real endpoints should be callable)
- ❌ Strict routing (only defined routes should be clickable)
Identifying synthetic nodes:
import { isRealNode } from '@r2abreu/path-tree';
tree.forEach(node => {
if (node.synthetic) {
console.log('Synthetic:', node.item.path);
}
// or use the helper
if (!isRealNode(node)) {
console.log('Synthetic:', node.item.path);
}
});TypeScript
Fully typed with generics:
interface Page {
path: string;
title: string;
meta?: Record<string, any>;
}
const tree: TreeNode<Page>[] = buildPathTree(pages);Types
interface TreeNode<T> {
item: T;
children: TreeNode<T>[];
synthetic?: boolean;
}
interface BuildPathTreeOptions<T> {
getPath?: (item: T) => string;
createNode?: (path: string) => TreeNode<T>;
enrichItem?: (item: T) => T;
getParentPath?: (path: string) => string;
}Advanced Examples
Custom Path Property
interface CustomItem {
path: string;
url: string;
title: string;
}
const items: CustomItem[] = [
{ path: '/custom', url: '/custom/path', title: 'Custom' }
];
const tree = buildPathTree(items, {
getPath: (item) => item.url // Use 'url' instead of 'path'
});Enrich Items
const tree = buildPathTree(routes, {
enrichItem: (route) => ({
...route,
displayName: route.meta?.title || route.path,
icon: route.meta?.icon
})
});Custom Parent Resolution
import { dirname } from 'pathe';
const tree = buildPathTree(items, {
getParentPath: (path) => {
// Custom logic for determining parent
return dirname(path);
}
});Framework Examples
Vue 3
<script setup lang="ts">
import { buildRouteTree, isRealRoute } from 'path-tree/vue-router';
import { useRouter } from 'vue-router';
const router = useRouter();
const tree = buildRouteTree(router.getRoutes());
</script>
<template>
<nav>
<TreeNode :nodes="tree" />
</nav>
</template>React
import { buildPathTree } from '@r2abreu/path-tree';
function Navigation({ routes }) {
const tree = buildPathTree(routes);
return (
<nav>
{tree.map(node => (
<TreeNode key={node.item.path} node={node} />
))}
</nav>
);
}Svelte
<script>
import { buildPathTree } from '@r2abreu/path-tree';
export let routes;
const tree = buildPathTree(routes);
</script>
<nav>
{#each tree as node}
<TreeNode {node} />
{/each}
</nav>Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT © r2abreu
Links
Glory to God
