Skip to content

A composable router for TypeScript applications built on parser combinators. Define routes declaratively with type inference and flexible route matching.

License

Notifications You must be signed in to change notification settings

doeixd/combi-router

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

23 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

npm version TypeScript MIT License Build Status

Combi-Router πŸ›€οΈ

A composable, type-safe router built on my parser combinator library Combi Parse that thinks in trees. Routes are defined functionally and composed by reference, creating natural hierarchies that mirror your application structure.


πŸ“¦ Installation

npm install @doeixd/combi-router @doeixd/combi-parse zod

Combi-Router is built on @doeixd/combi-parse for robust URL parsing and uses zod for powerful, type-safe parameter validation.


✨ Key Features

Β Β πŸ”— Type-Safe & Composable
Β Β Β Β Β Β Β  Build routes functionally and compose them by reference for perfect type safety and effortless refactoring.

  🌳 Hierarchical & Introspective
Β Β Β Β Β Β Β  Routes create natural trees that mirror your app's structure, with built-in utilities to analyze the hierarchy.

  ⚑ Powerful Data Loading
Β Β Β Β Β Β Β  Run data loaders for nested routes in parallel, with an advanced resource system featuring Suspense, caching, retries, and invalidation.

  🧩 Composable Layer Architecture
Β Β Β Β Β Β Β  Build your ideal router by mixing and matching feature layers (data, performance, dev tools) or creating your own.

Β Β  πŸ›‘οΈ Advanced Navigation & Guards
Β Β Β Β Β Β Β  Navigate with detailed results, cancellation support, and robust, type-safe route guards for fine-grained access control.

Β Β πŸ”Ž Integrated SEO & Head Management
Β Β Β Β Β Β Β  Dynamically manage document head tags, including titles, meta descriptions, and social cards, directly from your route definitions.

Β Β  βœ‚οΈ Tree-Shakeable & Modular
Β Β Β Β Β Β Β  A modular design ensures you only bundle the features you use, keeping your app lean and fast.

Β Β  πŸ› οΈ Superior Developer Experience
Β Β Β Β Β Β Β  Get dev-mode warnings, advanced debugging utilities, and detailed route analysis right out of the box.


πŸš€ Quick Start

Let's start simple and build up your understanding step by step.

Understanding Routes

A route in Combi-Router is a blueprint that describes a URL's structure and behavior.

import { route, path } from '@doeixd/combi-router';

// This route matches the exact path "/users"
export const usersRoute = route(path('users'));

The route() function creates a new route from matchers. Matchers are small building blocks that each handle one part of a URL.

Why export routes? Routes are first-class objects you'll reference throughout your app for navigation, so treating them as exportable values makes them reusable and type-safe.

Basic Matchers

import { route, path, param } from '@doeixd/combi-router';
import { z } from 'zod';

// Static path segment
export const aboutRoute = route(path('about'));  // matches "/about"

// Dynamic parameter with validation
export const userRoute = route(
  path('users'),
  param('id', z.number())  // matches "/users/123" -> params.id is a number
);

Why validation? URLs are just strings. By validating during route matching, you catch errors early and get proper TypeScript types for your parameters.

Building Route Trees

The real power comes from composing routes by reference. Instead of redefining common parts, you extend existing routes:

import { extend } from '@doeixd/combi-router';

// Base route
export const dashboardRoute = route(path('dashboard'));

// Extend the base route
export const usersRoute = extend(dashboardRoute, path('users'));
export const userRoute = extend(usersRoute, param('id', z.number()));

// This creates a natural tree:
// /dashboard           <- dashboardRoute
// /dashboard/users     <- usersRoute  
// /dashboard/users/123 <- userRoute

Why extend? When you change the base route (e.g., to /admin), all extended routes automatically update. Your route structure mirrors your application structure.

Adding Behavior with Higher-Order Functions

Enhance routes with additional behavior using pipe() and higher-order functions:

import { meta, loader, layout, pipe } from '@doeixd/combi-router';

export const enhancedUserRoute = pipe(
  userRoute,
  meta({ title: 'User Profile' }),
  loader(async ({ params }) => {
    const user = await fetchUser(params.id);
    return { user };
  }),
  layout(ProfileLayout)
);

Why higher-order functions? They're composable and reusable. You can create your own enhancers and mix them with built-in ones.

Creating the Router

Once you have routes, create a router instance from an array of all your routes:

import { createRouter } from '@doeixd/combi-router';

const router = createRouter([
  dashboardRoute,
  usersRoute,
  enhancedUserRoute
]);

// Reference-based navigation with detailed results
const result = await router.navigate(enhancedUserRoute, { id: 123 });
if (result.success) {
  console.log('Navigation successful');
} else {
  console.error('Navigation failed:', result.error);
}

// Simple navigation for backward compatibility  
const success = await router.navigateSimple(enhancedUserRoute, { id: 123 });

// Type-safe URL building
const userUrl = router.build(enhancedUserRoute, { id: 123 }); // "/dashboard/users/123"

Why route references? Using actual route objects instead of string names provides perfect type inference and makes refactoring safe. TypeScript knows exactly what parameters each route needs.


πŸ—οΈ Core Concepts

Route Building Improvements

Route Introspection Utilities

Routes now provide powerful introspection capabilities to analyze their structure:

import { route, extend, path, param } from '@doeixd/combi-router';
import { z } from 'zod';

const dashboardRoute = route(path('dashboard'));
const usersRoute = extend(dashboardRoute, path('users'));
const userRoute = extend(usersRoute, param('id', z.number()));

// Analyze route structure
console.log(userRoute.depth);        // 2 (dashboard -> users -> user)
console.log(userRoute.ancestors);    // [dashboardRoute, usersRoute]
console.log(userRoute.staticPath);   // "/dashboard/users"
console.log(userRoute.paramNames);   // ["id"]
console.log(userRoute.isDynamic);    // true
console.log(userRoute.routeChain);   // [dashboardRoute, usersRoute, userRoute]

Route Validation at Creation Time

Routes are now validated when created, catching common configuration errors early:

import { RouteValidationError } from '@doeixd/combi-router';

try {
  // This will throw if there are duplicate parameter names
  const problematicRoute = extend(
    route(param('id', z.string())),
    param('id', z.number()) // Error: Duplicate parameter name 'id'
  );
} catch (error) {
  if (error instanceof RouteValidationError) {
    console.error('Route configuration error:', error.message);
  }
}

Parent-Child Relationships

Routes maintain explicit parent-child relationships for better debugging and tooling:

console.log(userRoute.parent === usersRoute);     // true
console.log(usersRoute.parent === dashboardRoute); // true
console.log(dashboardRoute.parent);               // null (root route)

// Walk up the hierarchy
let current = userRoute;
while (current) {
  console.log(current.staticPath);
  current = current.parent;
}
// Output: "/dashboard/users", "/dashboard", "/"

Route Matchers

Matchers are the building blocks of routes. Each matcher handles one aspect of URL parsing:

// Path segments
path('users')                    // matches "/users"
path.optional('category')        // matches "/category" or ""
path.wildcard('segments')        // matches "/any/number/of/segments"

// Parameters with validation
param('id', z.number())          // matches "/123" and validates as number
param('slug', z.string().min(3)) // matches "/hello" with minimum length

// Query parameters
query('page', z.number().default(1)) // matches "?page=5"
query.optional('search', z.string()) // matches "?search=term"

// Other components
end                              // ensures no remaining path segments
// subdomain(...) and hash(...) can be added with similar patterns

Route Composition

Routes are composed functionally using extend():

export const apiRoute = route(path('api'), path('v1'));
export const usersRoute = extend(apiRoute, path('users'));
export const userRoute = extend(usersRoute, param('id', z.number()));

// userRoute now matches /api/v1/users/123

Parameters from parent routes are automatically inherited and merged into a single params object.

Higher-Order Route Enhancers

Enhance routes with additional functionality:

import { pipe, meta, loader, guard, cache, lazy } from '@doeixd/combi-router';

export const userRoute = pipe(
  route(path('users'), param('id', z.number())),
  meta({ title: (params) => `User ${params.id}` }),
  loader(async ({ params }) => ({ user: await fetchUser(params.id) })),
  guard(async () => await isAuthenticated() || '/login'),
  cache({ ttl: 5 * 60 * 1000 }), // Cache for 5 minutes
  lazy(() => import('./UserProfile'))
);

πŸ”§ Modular Architecture

Combi-Router now features a modular architecture optimized for tree-shaking and selective feature adoption.

Import Paths

// Core routing functionality (always included)
import { route, extend, createRouter } from '@doeixd/combi-router';

// Advanced data loading and caching
import { createAdvancedResource, resourceState } from '@doeixd/combi-router/data';

// Production features and optimizations
import { 
  PerformanceManager,
  ScrollRestorationManager,
  TransitionManager 
} from '@doeixd/combi-router/features';

// Development tools and debugging
import { 
  createWarningSystem, 
  analyzeRoutes,
  DebugUtils 
} from '@doeixd/combi-router/dev';

// Framework-agnostic utilities
import { 
  createLink, 
  createActiveLink,
  createOutlet 
} from '@doeixd/combi-router/utils';

Module Breakdown

Core Module (@doeixd/combi-router)

Essential routing functionality including route definition, matching, navigation, and basic data loading.

import { 
  route, extend, path, param, query,
  createRouter, pipe, meta, loader, guard
} from '@doeixd/combi-router';

Data Module (@doeixd/combi-router/data)

Advanced resource management with caching, retry logic, and global state management.

import { 
  createAdvancedResource,
  resourceState,
  globalCache 
} from '@doeixd/combi-router/data';

// Enhanced resource with retry and caching
const userResource = createAdvancedResource(
  () => api.fetchUser(userId),
  {
    retry: { attempts: 3 },
    cache: { ttl: 300000, invalidateOn: ['user'] },
    staleTime: 60000,
    backgroundRefetch: true
  }
);

Features Module (@doeixd/combi-router/features)

Production-ready features for performance optimization and user experience.

import { 
  PerformanceManager,
  ScrollRestorationManager,
  TransitionManager,
  CodeSplittingManager 
} from '@doeixd/combi-router/features';

// Initialize performance monitoring
const performanceManager = new PerformanceManager({
  prefetchOnHover: true,
  prefetchViewport: true,
  enablePerformanceMonitoring: true,
  connectionAware: true
});

Dev Module (@doeixd/combi-router/dev)

Development tools for debugging and route analysis.

import { 
  createWarningSystem,
  analyzeRoutes,
  DebugUtils,
  ConflictDetector 
} from '@doeixd/combi-router/dev';

// Create warning system for development
const warningSystem = createWarningSystem(router, {
  runtimeWarnings: true,
  performanceWarnings: true
});

// Quick route analysis
analyzeRoutes(router);

Utils Module (@doeixd/combi-router/utils)

Framework-agnostic utilities for DOM integration.

import { 
  createLink,
  createActiveLink,
  createOutlet,
  createMatcher,
  createRouterStore 
} from '@doeixd/combi-router/utils';

Bundle Size Optimization

The modular architecture enables significant bundle size optimization:

// Minimal bundle - only core routing
import { route, extend, createRouter } from '@doeixd/combi-router';

// With advanced resources
import { createAdvancedResource } from '@doeixd/combi-router/data';

// With production features
import { PerformanceManager } from '@doeixd/combi-router/features';

// Development tools (excluded in production)
import { createWarningSystem } from '@doeixd/combi-router/dev';
// (dev only)

πŸ“Š Enhanced Resource System

The new resource system provides production-ready data loading with advanced features.

Basic Resources

import { createResource } from '@doeixd/combi-router';

// Simple suspense-based resource
const userRoute = pipe(
  route(path('users'), param('id', z.number())),
  loader(({ params }) => ({
    user: createResource(() => fetchUser(params.id)),
    posts: createResource(() => fetchUserPosts(params.id))
  }))
);

// In your component
function UserProfile() {
  const { user, posts } = router.currentMatch.data;
  
  // These will suspend until data is ready
  const userData = user.read();
  const postsData = posts.read();
  
  return <div>...</div>;
}

Advanced Resources

import { createAdvancedResource, resourceState } from '@doeixd/combi-router/data';

// Enhanced resource with all features
const userResource = createAdvancedResource(
  () => api.fetchUser(userId),
  {
    // Retry configuration with exponential backoff
    retry: {
      attempts: 3,
      delay: (attempt) => Math.min(1000 * Math.pow(2, attempt - 1), 10000),
      shouldRetry: (error) => error.status >= 500,
      onRetry: (error, attempt) => console.log(`Retry ${attempt}:`, error)
    },
    
    // Caching with tags for invalidation
    cache: {
      ttl: 300000, // 5 minutes
      invalidateOn: ['user', 'profile'],
      priority: 'high'
    },
    
    // Stale-while-revalidate behavior
    staleTime: 60000, // 1 minute
    backgroundRefetch: true
  }
);

// Check state without suspending
if (userResource.isLoading) {
  console.log('Loading user...');
}

// Non-suspending peek at cached data
const cachedUser = userResource.peek();
if (cachedUser) {
  console.log('Cached user:', cachedUser);
}

// Force refresh
await userResource.refetch();

// Invalidate resource
userResource.invalidate();

Cache Management

import { resourceState } from '@doeixd/combi-router/data';

// Global resource state monitoring
const globalState = resourceState.getGlobalState();
console.log('Loading resources:', globalState.loadingCount);

// Event system for observability
const unsubscribe = resourceState.onEvent((event) => {
  switch (event.type) {
    case 'fetch-start':
      console.log('Started loading:', event.resource);
      break;
    case 'fetch-success':
      console.log('Loaded successfully:', event.data);
      break;
    case 'fetch-error':
      console.error('Loading failed:', event.error);
      break;
    case 'retry':
      console.log(`Retry attempt ${event.attempt}:`, event.error);
      break;
  }
});

// Cache invalidation by tags
resourceState.invalidateByTags(['user', 'profile']);

πŸš€ Performance Features

Intelligent Prefetching

import { PerformanceManager } from '@doeixd/combi-router/features';

const performanceManager = new PerformanceManager({
  // Prefetch on hover with delay
  prefetchOnHover: true,
  
  // Prefetch when links enter viewport
  prefetchViewport: true,
  
  // Adjust behavior based on connection
  connectionAware: true,
  
  // Monitor performance metrics
  enablePerformanceMonitoring: true,
  
  // Preload critical routes immediately
  preloadCriticalRoutes: ['dashboard', 'user-profile'],
  
  // Memory management
  memoryManagement: {
    enabled: true,
    maxCacheSize: 50,
    maxCacheAge: 30 * 60 * 1000,
    cleanupInterval: 5 * 60 * 1000
  }
});

// Setup hover prefetching for a link
const cleanup = performanceManager.setupHoverPrefetch(linkElement, 'user-route');

// Setup viewport prefetching
const cleanupViewport = performanceManager.setupViewportPrefetch(linkElement, 'user-route');

// Get performance report
const report = performanceManager.getPerformanceReport();
console.log('Prefetch hit rate:', report.prefetchHitRate);

Scroll Restoration

import { ScrollRestorationManager } from '@doeixd/combi-router/features';

const scrollManager = new ScrollRestorationManager({
  enabled: true,
  restoreOnBack: true,
  restoreOnForward: true,
  saveScrollState: true,
  smoothScrolling: true,
  scrollBehavior: 'smooth',
  debounceTime: 100,
  
  // Advanced configuration
  customScrollContainer: '#main-content',
  excludeRoutes: ['modal-routes'],
  persistScrollState: true
});

// Manual scroll position management
scrollManager.saveScrollPosition(routeId);
scrollManager.restoreScrollPosition(routeId);
scrollManager.scrollToTop();
scrollManager.scrollToElement('#section');

Advanced Transitions

import { TransitionManager } from '@doeixd/combi-router/features';

const transitionManager = new TransitionManager({
  enabled: true,
  duration: 300,
  easing: 'ease-in-out',
  type: 'fade',
  
  // Per-route transition configuration
  routeTransitions: {
    'user-profile': { type: 'slide-left', duration: 400 },
    'settings': { type: 'fade', duration: 200 }
  },
  
  // Custom transition classes
  transitionClasses: {
    enter: 'page-enter',
    enterActive: 'page-enter-active',
    exit: 'page-exit',
    exitActive: 'page-exit-active'
  }
});

// Manual transition control
await transitionManager.performTransition(fromRoute, toRoute, {
  direction: 'forward',
  customData: { userId: 123 }
});

πŸ› οΈ Development Experience

Development Warnings

import { createWarningSystem, analyzeRoutes } from '@doeixd/combi-router/dev';

// Create comprehensive warning system
const warningSystem = createWarningSystem(router, {
  runtimeWarnings: true,
  staticWarnings: true,
  performanceWarnings: true,
  severityFilter: ['warning', 'error']
});

// Quick route analysis
analyzeRoutes(router);

// Get warnings programmatically
const warnings = warningSystem.getWarnings();
const conflictWarnings = warningSystem.getWarningsByType('conflicting-routes');
const errorWarnings = warningSystem.getWarningsBySeverity('error');

Debugging Tools

import { DebugUtils } from '@doeixd/combi-router/dev';

// Route structure debugging
DebugUtils.logRouteTree(router);
DebugUtils.analyzeRoutePerformance(router);
DebugUtils.checkRouteConflicts(router);

// Navigation debugging
DebugUtils.enableNavigationLogging(router);
DebugUtils.logMatchDetails(currentMatch);

// Performance debugging
DebugUtils.enablePerformanceMonitoring(router);
const metrics = DebugUtils.getPerformanceMetrics();

Enhanced Error Handling

import { NavigationErrorType } from '@doeixd/combi-router';

const result = await router.navigate(userRoute, { id: 123 });

if (!result.success) {
  switch (result.error?.type) {
    case NavigationErrorType.RouteNotFound:
      console.error('Route not found');
      break;
    case NavigationErrorType.GuardRejected:
      console.error('Navigation blocked:', result.error.message);
      break;
    case NavigationErrorType.LoaderFailed:
      console.error('Data loading failed:', result.error.originalError);
      break;
    case NavigationErrorType.ValidationFailed:
      console.error('Parameter validation failed');
      break;
    case NavigationErrorType.Cancelled:
      console.log('Navigation was cancelled');
      break;
  }
}

πŸ”„ Migration Guide

From v1.x to v2.x

Modular Imports

Before:

import { createRouter, createResource, createLink } from '@doeixd/combi-router';

After:

// Core functionality
import { createRouter } from '@doeixd/combi-router';

// Advanced resources (optional)
import { createAdvancedResource } from '@doeixd/combi-router/data';

// Utilities (optional)
import { createLink } from '@doeixd/combi-router/utils';

Enhanced Resources

Before:

const resource = createResource(() => fetchUser(id));

After:

// Simple resource (same API)
const resource = createResource(() => fetchUser(id));

// Or enhanced resource with more features
const resource = createAdvancedResource(
  () => fetchUser(id),
  {
    retry: { attempts: 3 },
    cache: { ttl: 300000 },
    staleTime: 60000
  }
);

Navigation API

The navigation API is fully backward compatible. Enhanced error handling is opt-in:

// Old way (still works)
const success = await router.navigateSimple(route, params);

// New way (detailed error information)
const result = await router.navigate(route, params);
if (result.success) {
  // Handle success
} else {
  // Handle specific error types
}

πŸ—‚οΈ Advanced Features

Document Head Management

The head management module provides comprehensive document head tag management with support for dynamic content, SEO optimization, and server-side rendering.

Basic Head Management

import { head, seoMeta } from '@doeixd/combi-router/features';

// Static head data
const aboutRoute = pipe(
  route(path('about')),
  head({
    title: 'About Us',
    meta: [
      { name: 'description', content: 'Learn more about our company' },
      { name: 'keywords', content: 'about, company, team' }
    ],
    link: [
      { rel: 'canonical', href: 'https://example.com/about' }
    ]
  })
);

// Dynamic head data based on route parameters
const userRoute = pipe(
  route(path('users'), param('id', z.number())),
  head(({ params }) => ({
    title: `User Profile - ${params.id}`,
    meta: [
      { name: 'description', content: `Profile page for user ${params.id}` }
    ]
  }))
);

SEO Optimization

// Complete SEO setup with Open Graph and Twitter Cards
const productRoute = pipe(
  route(path('products'), param('id', z.number())),
  head(({ params }) => ({
    title: `Product ${params.id}`,
    titleTemplate: 'Store | %s', // Results in: "Store | Product 123"
    
    // Basic SEO
    ...seoMeta.basic({
      description: `Amazing product ${params.id}`,
      keywords: ['product', 'store', 'shopping'],
      robots: 'index,follow'
    }),
    
    // Open Graph tags
    ...seoMeta.og({
      title: `Product ${params.id}`,
      description: 'The best product you will ever buy',
      image: `https://example.com/products/${params.id}/image.jpg`,
      url: `https://example.com/products/${params.id}`,
      type: 'product'
    }),
    
    // Twitter Cards
    ...seoMeta.twitter({
      card: 'summary_large_image',
      title: `Product ${params.id}`,
      description: 'An amazing product',
      image: `https://example.com/products/${params.id}/twitter.jpg`
    })
  }))
);

Advanced Features

// Scripts, styles, and HTML attributes
const dashboardRoute = pipe(
  route(path('dashboard')),
  head({
    title: 'Dashboard',
    script: [
      { src: 'https://analytics.example.com/track.js', async: true },
      { innerHTML: 'window.config = { theme: "dark" };' }
    ],
    style: [
      { innerHTML: 'body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); }' }
    ],
    htmlAttrs: { lang: 'en', 'data-theme': 'dark' },
    bodyAttrs: { class: 'dashboard dark-mode' }
  })
);

DOM Integration

import { HeadManager, resolveHeadData } from '@doeixd/combi-router/features';

// Initialize head manager
const headManager = new HeadManager(document);

// Update head tags on navigation
router.onNavigate((match) => {
  if (match?.route._head) {
    const resolvedHead = resolveHeadData(match.route._head, match);
    headManager.apply(resolvedHead);
  }
});

For complete documentation, see Head Management Guide.

Navigation Improvements

NavigationResult with Detailed Error Handling

The navigate() method now returns a NavigationResult object with comprehensive information about the navigation attempt:

import { NavigationErrorType } from '@doeixd/combi-router';

const result = await router.navigate(userRoute, { id: 123 });

if (result.success) {
  console.log('Navigation completed successfully');
  console.log('Active match:', result.match);
} else {
  // Handle different types of navigation errors
  switch (result.error?.type) {
    case NavigationErrorType.RouteNotFound:
      console.error('Route not found');
      break;
    case NavigationErrorType.GuardRejected:
      console.error('Navigation blocked by guard:', result.error.message);
      break;
    case NavigationErrorType.LoaderFailed:
      console.error('Data loading failed:', result.error.originalError);
      break;
    case NavigationErrorType.ValidationFailed:
      console.error('Parameter validation failed');
      break;
    case NavigationErrorType.Cancelled:
      console.log('Navigation was cancelled');
      break;
  }
}

Navigation Cancellation with NavigationController

Long-running navigations can now be cancelled, which is especially useful for preventing race conditions:

// Start a navigation and get a controller
const controller = router.currentNavigation;

if (controller) {
  console.log('Navigating to:', controller.route);
  
  // Cancel the navigation if needed
  setTimeout(() => {
    if (!controller.cancelled) {
      controller.cancel();
      console.log('Navigation cancelled');
    }
  }, 1000);
  
  // Wait for the result
  const result = await controller.promise;
  if (result.cancelled) {
    console.log('Navigation was cancelled');
  }
}

Backward Compatibility with navigateSimple()

For simple use cases, the navigateSimple() method provides the traditional boolean return value:

// Simple boolean result for straightforward cases
const success = await router.navigateSimple(userRoute, { id: 123 });
if (success) {
  console.log('Navigation successful');
} else {
  console.log('Navigation failed');
}

// Still get full details when needed
const detailedResult = await router.navigate(userRoute, { id: 123 });

Typed Guards

Enhanced Guard Context and Type Safety

The new typedGuard() function provides better type safety and more context for route protection:

import { typedGuard, GuardContext } from '@doeixd/combi-router';
import { z } from 'zod';

// Define a route with parameters
const adminUserRoute = route(
  path('admin'), 
  path('users'), 
  param('userId', z.string())
);

// Create a typed guard with full context access
const adminGuard = typedGuard<{ userId: string }>(({ params, to, from, searchParams }) => {
  // Full type safety on params
  const userId = params.userId; // TypeScript knows this is a string
  
  // Access to route context
  console.log('Navigating to:', to.url);
  console.log('Coming from:', from?.url || 'initial load');
  console.log('Search params:', searchParams.get('redirect'));
  
  // Return boolean for allow/deny or string for redirect
  if (!isCurrentUserAdmin()) {
    return '/login?redirect=' + encodeURIComponent(to.url);
  }
  
  // Additional validation based on the user ID
  if (!canAccessUser(userId)) {
    return false; // Block navigation
  }
  
  return true; // Allow navigation
});

// Apply the guard to the route
const protectedRoute = pipe(
  adminUserRoute,
  guard(adminGuard)
);

Nested Routes and Parallel Data Loading

When a nested route like /dashboard/users/123 is matched, Combi-Router builds a tree of match objects. If both dashboardRoute and userRoute have a loader, they are executed in parallel, and you can access data from any level of the hierarchy.

// dashboard-layout.ts
const dashboardRoute = pipe(
  route(path('dashboard')),
  loader(async () => ({ stats: await fetchDashboardStats() })),
  layout(DashboardLayout) // Layout component with <Outlet />
);

// user-profile.ts
const userRoute = pipe(
  extend(dashboardRoute, path('users'), param('id', z.number())),
  loader(async ({ params }) => ({ user: await fetchUser(params.id) }))
);

// In your view for the user route, you can access both sets of data:
const dashboardData = router.currentMatch.data; // { stats: ... }
const userData = router.currentMatch.child.data; // { user: ... }

Predictive Preloading

Improve perceived performance by loading a route's code and data before the user clicks a link. The router.peek() method is perfect for this.

// Preload on hover to make navigation feel instantaneous
myLink.addEventListener('mouseenter', () => {
  router.peek(userRoute, { id: 123 });
});

// Navigate as usual on click
myLink.addEventListener('click', (e) => {
  e.preventDefault();
  router.navigate(userRoute, { id: 123 });
});

View Transitions

Combi-Router automatically uses the browser's native View Transitions API for smooth, app-like page transitions. To enable it, simply add a CSS view-transition-name to elements that should animate between pages.

/* On a list page */
.product-thumbnail {
  view-transition-name: product-image-123;
}

/* On a detail page */
.product-hero-image {
  view-transition-name: product-image-123; /* Same name! */
}

The router handles the rest. No JavaScript changes are needed.


🧩 Vanilla JS Utilities

Combi-Router is framework-agnostic at its core. To help you integrate it into a vanilla JavaScript project, we provide a set of utility functions. These helpers bridge the gap between the router's state and the DOM, making it easy to create navigable links, render nested views, and react to route changes.

Link & Navigation Helpers

createLink(router, route, params, options)

Creates a fully functional <a> element that navigates using the router. It automatically sets the href and intercepts click events to trigger client-side navigation. Each created link comes with a destroy function to clean up its event listeners.

import { createLink } from '@doeixd/combi-router/utils';

const { element, destroy } = createLink(
  router,
  userRoute,
  { id: 123 },
  { children: 'View Profile', className: 'btn' }
);
document.body.appendChild(element);

// Later, when the element is removed from the DOM:
// destroy();

createActiveLink(router, route, params, options)

Builds on createLink to create an <a> element that automatically updates its CSS class when its route is active. This is perfect for navigation menus.

  • activeClassName: The CSS class to apply when the link is active.
  • exact: If true, the class is applied only on an exact route match. If false (default), it's also applied for any active child routes.
import { createActiveLink } from '@doeixd/combi-router/utils';

const { element } = createActiveLink(router, dashboardRoute, {}, {
  children: 'Dashboard',
  className: 'nav-link',
  activeClassName: 'font-bold' // Applied on /dashboard, /dashboard/users, etc.
});
document.querySelector('nav').appendChild(element);

attachNavigator(element, router, route, params)

Makes any existing HTML element navigable. This is useful for turning buttons, divs, or other non-anchor elements into type-safe navigation triggers.

import { attachNavigator } from '@doeixd/combi-router/utils';

const myButton = document.getElementById('home-button');
const { destroy } = attachNavigator(myButton, router, homeRoute, {});

Conditional Rendering

createOutlet(router, parentRoute, container, viewMap)

Provides a declarative "outlet" for nested routing, similar to <Outlet> in React Router or <router-view> in Vue. It listens for route changes and renders the correct child view into a specified container element.

  • parentRoute: The route of the component that contains the outlet.
  • container: The DOM element where child views will be rendered.
  • viewMap: An object mapping Route.id to an ElementFactory function (match) => Node.
// In your dashboard layout component
import { createOutlet } from '@doeixd/combi-router/utils';
import { dashboardRoute, usersRoute, settingsRoute } from './routes';
import { UserListPage, SettingsPage } from './views';

const outletContainer = document.querySelector('#outlet');
createOutlet(router, dashboardRoute, outletContainer, {
  [usersRoute.id]: (match) => new UserListPage(match.data), // Pass data to the view
  [settingsRoute.id]: () => new SettingsPage(),
});

createMatcher(router)

Creates a fluent, type-safe conditional tool that reacts to route changes. It's a powerful way to implement declarative logic that isn't tied directly to rendering.

import { createMatcher } from '@doeixd/combi-router/utils';

// Update the document title based on the active route
createMatcher(router)
  .when(homeRoute, () => {
    document.title = 'My App | Home';
  })
  .when(userRoute, (match) => {
    document.title = `Profile for User ${match.params.id}`;
  })
  .otherwise(() => {
    document.title = 'My App';
  });

State Management

createRouterStore(router)

Creates a minimal, framework-agnostic reactive store for the router's state (currentMatch, isNavigating, isFetching). This is useful for integrating with UI libraries or building your own reactive logic in vanilla JS.

import { createRouterStore } from '@doeixd/combi-router/utils';

const store = createRouterStore(router);

const unsubscribe = store.subscribe(() => {
  const { isNavigating } = store.getSnapshot();
  // Show a global loading indicator while navigating
  document.body.style.cursor = isNavigating ? 'wait' : 'default';
});

// To clean up:
// unsubscribe();

🎨 Web Components

For even simpler integration, Combi-Router provides ready-to-use Web Components that handle routing declaratively in your HTML:

<!DOCTYPE html>
<html>
<head>
    <script type="module">
        // Import standalone components (no setup required!)
        import '@doeixd/combi-router/components-standalone';
    </script>
</head>
<body>
    <!-- Define your routes declaratively -->
    <view-area match="/users/:id" view-id="user-detail"></view-area>
    <view-area match="/about" view-id="about-page"></view-area>

    <!-- Define your templates with automatic head management -->
    <template is="view-template" view-id="user-detail">
        <!-- Head automatically discovered and linked to view-area -->
        <view-head 
            title="User Profile"
            title-template="My App | %s"
            description="View user profile and details"
            og-title="User Profile"
            og-description="Comprehensive user profile page"
            og-type="profile">
        </view-head>
        
        <h1>User Details</h1>
        <p>User ID: <span class="user-id"></span></p>
    </template>

    <template is="view-template" view-id="about-page">
        <!-- Each template can have its own head configuration -->
        <view-head 
            title="About Us"
            description="Learn more about our company and mission"
            keywords="about, company, mission, team"
            canonical="https://myapp.com/about"
            og-title="About Our Company"
            og-description="Discover our story and values">
        </view-head>
        
        <h1>About</h1>
        <p>This is the about page.</p>
    </template>

    <!-- Navigation works automatically -->
    <nav>
        <a href="/users/123">User 123</a>
        <a href="/about">About</a>
    </nav>
</body>
</html>

Advanced Example with Nested Routes

<!-- Nested route structure -->
<view-area match="/dashboard" view-id="dashboard"></view-area>
<view-area match="/dashboard/users" view-id="users-list"></view-area>
<view-area match="/dashboard/users/:id" view-id="user-detail"></view-area>

<!-- Templates with automatic head discovery -->
<template is="view-template" view-id="dashboard">
    <!-- Parent template head - automatically merges with child heads -->
    <view-head 
        title="Dashboard"
        title-template="Admin | %s"
        description="Admin dashboard overview">
    </view-head>
    
    <h1>Dashboard</h1>
    <nav>
        <a href="/dashboard/users">Users</a>
        <a href="/dashboard/analytics">Analytics</a>
    </nav>
    <main class="dashboard-content"></main>
</template>

<template is="view-template" view-id="users-list">
    <!-- Child template head - merges with parent -->
    <view-head 
        title="Users"
        description="Manage users and permissions"
        robots="noindex">
    </view-head>
    
    <h2>Users</h2>
    <div class="users-grid"></div>
</template>

<!-- External template with dynamic head loading -->
<template is="view-template" view-id="user-detail" src="/views/user-detail.html"></template>

<!-- You can still use manual linking for external head configs -->
<view-head head-id="external-head" src="/head-configs/user-detail.js"></view-head>
<view-area match="/special/:id" view-id="special-view" head-id="external-head"></view-area>

Key Benefits

  • Zero JavaScript Configuration: Just import and use
  • Declarative Routing: Define routes in HTML attributes
  • Automatic Navigation: Links work out of the box
  • SEO-Ready: Built-in head management with Open Graph and Twitter Cards
  • Automatic Head Discovery: Place view-head inside templates - no manual linking needed
  • Nested Head Management: Head tags merge hierarchically for complex layouts
  • Dynamic Content: Load head configurations from external modules
  • Flexible Linking: Choose automatic discovery or manual head-id linking
  • Progressive Enhancement: Works with or without JavaScript
  • Dynamic Route Management: Add/remove routes programmatically when needed

Learn more β†’


βš™οΈ Configuration & API

🧰 Composable Layer Architecture

Combi-Router now features a revolutionary layer-based composition system using our custom makeLayered implementation, enabling true user extensibility while maintaining backwards compatibility.

Why Layers?

Traditional routers force you to choose between their built-in features or build everything from scratch. With layers, you can:

  • Mix and match built-in features exactly as needed
  • Create custom layers for your specific business logic
  • Compose layers conditionally based on environment or feature flags
  • Build orchestrated systems where layers can call each other's methods
  • Maintain type safety with full TypeScript inference across all layers

Basic Layer Composition

import { 
  createLayeredRouter, 
  createCoreNavigationLayer,
  withPerformance, 
  withScrollRestoration 
} from '@doeixd/combi-router';

// Compose exactly the router you need
const router = createLayeredRouter(routes)
  (createCoreNavigationLayer())           // Base navigation
  (withPerformance({ prefetchOnHover: true }))  // Performance optimizations
  (withScrollRestoration({ strategy: 'smooth' })) // Scroll management
  ();

// All layer methods are now available
router.navigate('/user/123');
router.prefetchRoute('about');
router.saveScrollPosition();

Custom Layer Creation

Create your own layers for analytics, authentication, or any business logic:

const withAnalytics = (config: { trackingId: string }) => (self: any) => {
  // Register lifecycle hooks
  if ('_registerLifecycleHook' in self) {
    self._registerLifecycleHook('onNavigationStart', (context: any) => {
      console.log(`[Analytics] Navigation started: ${context.to?.path}`);
    });

    self._registerLifecycleHook('onNavigationComplete', (match: any) => {
      console.log(`[Analytics] Page view: ${match.path}`);
    });
  }

  return {
    trackEvent: (event: string, data?: any) => {
      console.log(`[Analytics] Event: ${event}`, data);
    },
    
    trackError: (error: Error, context?: any) => {
      console.log(`[Analytics] Error: ${error.message}`, context);
    }
  };
};

// Use your custom layer
const router = createLayeredRouter(routes)
  (createCoreNavigationLayer())
  (withPerformance())
  (withAnalytics({ trackingId: 'GA-123456-7' }))
  ();

// Your custom methods are now available
router.trackEvent('button_click', { button: 'signup' });

Layer Orchestration

Layers can call methods from previously applied layers, enabling powerful composition patterns:

const withSmartNavigation = (self: any) => ({
  // Enhanced navigation that uses multiple layers
  smartNavigate: async (path: string, options: any = {}) => {
    // Track with analytics (if analytics layer is present)
    if ('trackEvent' in self) {
      self.trackEvent('navigation_intent', { path });
    }

    // Save scroll position (if scroll restoration layer is present)
    if ('saveScrollPosition' in self) {
      self.saveScrollPosition();
    }

    // Perform the navigation using core layer
    const result = await self.navigate(path, options);
    
    if (result && 'trackEvent' in self) {
      self.trackEvent('navigation_complete', { path });
    }
    
    return result;
  }
});

const router = createLayeredRouter(routes)
  (createCoreNavigationLayer())
  (withPerformance())
  (withScrollRestoration())
  (withAnalytics({ trackingId: 'GA-123' }))
  (withSmartNavigation)  // Orchestrates all previous layers
  ();

// One method that uses multiple layer capabilities
router.smartNavigate('/dashboard');

Conditional Layer Application

Apply layers based on environment, feature flags, or any condition:

import { conditionalLayer } from '@doeixd/combi-router';

const isDev = process.env.NODE_ENV === 'development';
const isProd = process.env.NODE_ENV === 'production';
const hasAnalytics = config.features.analytics;

const router = createLayeredRouter(routes)
  (createCoreNavigationLayer())
  
  // Only add performance layer in production
  (conditionalLayer(isProd, withPerformance({
    prefetchOnHover: true,
    enablePerformanceMonitoring: true
  })))
  
  // Only add debug layer in development
  (conditionalLayer(isDev, (self: any) => ({
    debug: () => console.log('Router state:', self.currentMatch),
    logAllNavigation: true
  })))
  
  // Conditional analytics
  (conditionalLayer(hasAnalytics, withAnalytics({ 
    trackingId: config.analytics.trackingId 
  })))
  ();

Built-in Layer Types

  • Core Navigation (createCoreNavigationLayer): Essential routing functionality
  • Performance (withPerformance): Prefetching, monitoring, memory management
  • Scroll Restoration (withScrollRestoration): Automatic scroll position management
  • Transitions (withTransitions): Smooth page transitions
  • Code Splitting (withCodeSplitting): Dynamic route loading

Migration from Configuration-Based Approach

⚠️ Deprecation Notice: The configuration-based feature system (RouterOptions.features) is deprecated in favor of the new layer system. The old API continues to work but will be removed in the next major version.

// ❌ Old way (deprecated)
const router = new CombiRouter(routes, {
  features: {
    performance: { prefetchOnHover: true },
    scrollRestoration: { strategy: 'smooth' }
  }
});

// βœ… New way (recommended)
const router = createLayeredRouter(routes)
  (createCoreNavigationLayer())
  (withPerformance({ prefetchOnHover: true }))
  (withScrollRestoration({ strategy: 'smooth' }))
  ();

The new layer system provides:

  • Better tree-shaking: Only bundle layers you use
  • User extensibility: Create custom layers for your needs
  • Better composition: Mix and match features freely
  • Type safety: Full TypeScript inference across layers
  • Self-aware layers: Layers can interact with each other

Router Creation (Legacy)

For backwards compatibility, the traditional configuration-based approach still works:

const router = createRouter(
  [homeRoute, usersRoute, userRoute], // An array of all routes
  {
    baseURL: 'https://myapp.com', // For running in a subdirectory
    hashMode: false, // Use `/#/path` style URLs
    features: { // ⚠️ Deprecated - use layer system instead
      performance: { prefetchOnHover: true }
    }
  }
);

Error Handling

// Define a fallback route for any URL that doesn't match
router.fallback(notFoundRoute);

// Define a global error handler for failures during navigation
router.onError(({ error, to, from }) => {
  console.error('Navigation error:', error);
  // Send to an error tracking service
});

🧰 Advanced: Creating Custom Matchers

While Combi-Router provides a comprehensive set of built-in matchers like path, param, and query, its true power lies in its composable foundation. The router is designed to be fully extensible, allowing you to create your own custom matchers using the full power of the underlying @doeixd/combi-parse library.

This is an advanced feature for when you need to parse complex URL structures that go beyond simple static or dynamic segments.

The RouteMatcher Contract

At its core, a matcher is an object that fulfills the RouteMatcher contract. It tells the router two things:

  1. How to parse a URL segment: This is done with a combi-parse parser. The parser's job is to recognize a part of the URL and, if it captures a value, return it as an object (e.g., { myParam: 'value' }).
  2. How to build a URL segment: This is the inverse operation, handled by a build function. Given a params object, it constructs the corresponding URL string.

Example: A Version Matcher (/v1/ or /v2/)

Imagine you have an API that can be versioned, and you want a single route definition to handle both /api/v1/posts and /api/v2/posts, capturing the version as a parameter.

You can create a custom version() matcher to handle this.

// in my-matchers.ts
import { str, choice } from '@doeixd/combi-parse';
import type { RouteMatcher } from '@doeixd/combi-router';

/**
 * A custom matcher that recognizes /v1 or /v2 and captures the result.
 * @param paramName The name for the captured version parameter.
 */
export function version(paramName: string): RouteMatcher {
  // 1. The Parser: Use `choice` to accept 'v1' or 'v2'.
  // It must return an object with the parameter name as the key.
  const versionParser = str('/')
    .keepRight(choice([str('v1'), str('v2')]))
    .map(parsedVersion => ({ [paramName]: parsedVersion }));

  // 2. The Builder: The inverse of the parser.
  const buildFn = (params: Record<string, any>): string | null => {
    const apiVersion = params[paramName];
    if (apiVersion === 'v1' || apiVersion === 'v2') {
      return `/${apiVersion}`;
    }
    // Return null if the required param is missing or invalid.
    return null;
  };

  // 3. The Contract: Return an object that fulfills the RouteMatcher interface.
  return {
    type: 'customVersion', // A unique type for debugging
    parser: versionParser,
    build: buildFn,
    paramName: paramName,
  };
}

Using Your Custom Matcher

Now, you can import and use version() in your route definitions just like any built-in matcher.

// in my-routes.ts
import { route, path, param, createRouter } from '@doeixd/combi-router';
import { version } from './my-matchers'; // Import your custom matcher

const postsRoute = route(
  path('api'),
  version('apiVersion'), // Your custom matcher in action!
  path('posts'),
  param('id', z.number())
);

const router = createRouter([postsRoute]);

// --- Matching ---
const matchV1 = router.match('/api/v1/posts/123');
// matchV1.params -> { apiVersion: 'v1', id: 123 }

const matchV2 = router.match('/api/v2/posts/456');
// matchV2.params -> { apiVersion: 'v2', id: 456 }

// --- Building ---
const urlV1 = router.build(postsRoute, { apiVersion: 'v1', id: 123 });
// -> "/api/v1/posts/123"

const urlV2 = router.build(postsRoute, { apiVersion: 'v2', id: 456 });
// -> "/api/v2/posts/456"

By creating your own domain-specific matchers, you can build highly expressive, reusable, and type-safe routing grammars that are perfectly tailored to your application's needs.

API Reference

Core Functions

  • route(...matchers): Creates a new base route.
  • extend(baseRoute, ...matchers): Creates a new child route from a base.
  • createRouter(routes, options?): Creates the router instance.
  • createResource(promiseFn): Wraps an async function in a suspense-ready resource.
  • createAdvancedResource(promiseFn, config?): Creates an enhanced resource with retry, caching, and state management.
  • typedGuard<TParams>(guardFn): Creates a type-safe guard function with enhanced context.

Route Matchers

  • path(segment): Matches a static path segment.
  • path.optional(segment): Matches an optional path segment.
  • path.wildcard(name?): Matches all remaining path segments into an array.
  • param(name, schema): Matches a dynamic parameter with Zod validation.
  • query(name, schema): Declares a required query parameter with Zod validation.
  • query.optional(name, schema): Declares an optional query parameter.
  • end: Ensures the path has no remaining segments.

Higher-Order Enhancers

  • pipe(route, ...enhancers): Applies a series of enhancers to a route.
  • meta(metadata): Attaches arbitrary metadata to a route.
  • loader(loaderFn): Adds a data-loading function to a route.
  • layout(component): Associates a layout component with a route.
  • guard(...guardFns): Protects a route with one or more guard functions.
  • cache(options): Adds caching behavior to a route's loader.
  • lazy(importFn): Makes a route's component lazy-loaded.

Router Methods

  • navigate(route, params): Programmatically navigates to a route, returns Promise<NavigationResult>.
  • navigateSimple(route, params): Simple navigation that returns Promise<boolean> for backward compatibility.
  • build(route, params): Generates a URL string for a route.
  • match(url): Matches a URL and returns the corresponding RouteMatch tree.
  • peek(route, params): Proactively loads a route's code and data.
  • subscribe(listener): Subscribes to route changes.
  • addRoute(route): Dynamically adds a route to the router.
  • removeRoute(route): Dynamically removes a route from the router.
  • cancelNavigation(): Cancels the current navigation if one is in progress.

Router Properties

  • currentMatch: The currently active RouteMatch object tree, or null.
  • currentNavigation: The active NavigationController if a navigation is in progress, or null.
  • isNavigating: A boolean indicating if a navigation is in progress.
  • isFetching: A boolean indicating if any route loaders are active.
  • routes: A flat array of all registered route objects.

Route Properties (Introspection)

  • route.depth: The depth of the route in the hierarchy (0 for root routes).
  • route.ancestors: Array of all ancestor routes from root to parent.
  • route.staticPath: The static path parts (non-parameter segments).
  • route.paramNames: Array of all parameter names defined by the route.
  • route.isDynamic: Boolean indicating if the route has dynamic parameters.
  • route.hasQuery: Boolean indicating if the route has query parameters.
  • route.routeChain: Array of routes from root to this route (including this route).
  • route.parent: The parent route, or null for root routes.

Error Types

  • RouteValidationError: Thrown when route validation fails during creation.
  • NavigationErrorType: Enum of possible navigation error types (RouteNotFound, GuardRejected, LoaderFailed, ValidationFailed, Cancelled, Unknown).
  • NavigationError: Interface describing detailed navigation error information.
  • NavigationResult: Interface describing the result of a navigation attempt.
  • NavigationController: Interface for managing ongoing navigation.
  • GuardContext<TParams>: Context object passed to typed guard functions.
  • TypedRouteGuard<TParams>: Type for typed guard functions.

πŸ—οΈ Layered Router Architecture

Creating Layered Routers

The layered router architecture allows you to compose routers from independent, reusable layers:

import { 
  createLayeredRouter, 
  dataLayer, 
  devLayer, 
  performanceLayer 
} from '@doeixd/combi-router';

// Basic layered router
const router = createLayeredRouter(routes)
  (dataLayer())     // Add data management capabilities
  (devLayer())      // Add development tools (dev mode only)
  ();               // Finalize the router

// Advanced configuration
const advancedRouter = createLayeredRouter(routes, {
  baseURL: '/app',
  hashMode: false
})
  (dataLayer({
    autoCleanup: true,
    cleanupInterval: 300000,
    logResourceEvents: true
  }))
  (devLayer({
    exposeToWindow: true,
    autoAnalyze: true,
    performanceMonitoring: true
  }))
  (performanceLayer({
    prefetchOnHover: true,
    prefetchViewport: true,
    connectionAware: true
  }))
  ();

Data Layer Features

The data layer provides advanced data management capabilities:

// Access data layer features
const router = createLayeredRouter(routes)(dataLayer())();

// Advanced caching with tags
router.cache.set('user:123', userData, {
  ttl: 300000,
  invalidateOn: ['user', 'profile'],
  priority: 'high'
});

// Create suspense-compatible resources
const userResource = router.createResource(() => 
  fetch(`/api/users/${params.id}`).then(r => r.json())
);

// Advanced resources with retry and caching
const advancedResource = router.createAdvancedResource(
  () => api.fetchUser(userId),
  {
    retry: { attempts: 3 },
    cache: { ttl: 300000, invalidateOn: ['user'] },
    staleTime: 60000,
    backgroundRefetch: true
  }
);

// Global resource monitoring
const globalState = router.getGlobalResourceState();
if (globalState.isLoading) {
  showLoadingSpinner();
}

// Cache invalidation
router.invalidateByTags(['user', 'profile']);

// Route preloading
router.preloadRoute('user-dashboard', { id: userId });

Development Layer Features

The development layer provides comprehensive debugging and development tools:

// Access dev tools (development mode only)
const router = createLayeredRouter(routes)(devLayer())();

// Run comprehensive analysis
router.runDevAnalysis();

// Get detailed development report
const report = router.getDevReport();
console.log(`Performance score: ${report.performance?.score}/100`);
console.log(`Found ${report.warnings.length} warnings`);

// Log formatted report
router.logDevReport();

// Export debug data
const debugData = router.exportDevData();
localStorage.setItem('router-debug', debugData);

// Access via window (if exposeToWindow: true)
window.combiRouterDev?.analyze();
window.combiRouterDev?.report();

Quick Setup Functions

For common use cases, use the quick setup functions:

import { quickDataLayer, quickDevLayer } from '@doeixd/combi-router';

// Production-ready setup
const router = createLayeredRouter(routes)
  (quickDataLayer())  // Optimized data management
  (quickDevLayer())   // All dev tools (dev mode only)
  ();

// Equivalent to full configuration
const router = createLayeredRouter(routes)
  (dataLayer({
    autoCleanup: true,
    cleanupInterval: 300000,
    logResourceEvents: process.env.NODE_ENV !== 'production'
  }))
  (devLayer({
    exposeToWindow: true,
    autoAnalyze: true,
    warnings: true,
    conflictDetection: true,
    performanceMonitoring: true,
    routeValidation: true,
    debugMode: true
  }))
  ();

Backwards Compatibility

The new layered system is fully backwards compatible:

// Original API still works
const router = new CombiRouter(routes, options);

// Automatically includes:
// - Data layer for resource management
// - Dev layer in development mode
// - All existing functionality

🎁 Benefits of Reference-Based Approach

  • Perfect Type Safety: Impossible to make typos in route names or pass incorrect parameter types.
  • Better IDE Support: Get autocompletion for routes and go-to-definition that works.
  • Confident Refactoring: Rename a route or change its parameters, and TypeScript will instantly show you everywhere that needs to be updated.
  • Functional Composition: Routes are first-class values that can be imported, exported, and composed with pure functions.
  • Framework Agnostic: The core logic is pure TypeScript, allowing for simple integration with any framework or vanilla JS.
  • Tree-Shakable: Import only the features you need for optimal bundle size.
  • Production Ready: Built-in performance optimizations, error handling, and monitoring.

πŸ“ˆ Performance

Combi-Router is designed for performance with several optimization strategies:

Bundle Size

  • Core: ~12KB gzipped (essential routing functionality)
  • +Data: ~4KB gzipped (advanced resources and caching)
  • +Features: ~6KB gzipped (performance optimizations)
  • +Utils: ~3KB gzipped (DOM utilities)
  • Dev Tools: ~3KB gzipped (excluded in production builds)

Runtime Performance

  • Tree-shaking optimized: Only bundle what you use
  • Lazy route loading: Code splitting at the route level
  • Intelligent prefetching: Connection-aware prefetching strategies
  • Memory management: Automatic cleanup of unused cache entries
  • Performance monitoring: Built-in Web Vitals tracking

Best Practices

  1. Use modular imports to minimize bundle size
  2. Enable connection-aware prefetching for mobile users
  3. Configure cache TTL based on data volatility
  4. Use scroll restoration for better UX
  5. Enable performance monitoring in development

🀝 Contributing

We welcome contributions! Please see our Contributing Guide for details.


πŸ“„ License

MIT License - see LICENSE file for details.

About

A composable router for TypeScript applications built on parser combinators. Define routes declaratively with type inference and flexible route matching.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published