UNPKG

@keenmate/svelte-spa-router

Version:

Router for SPAs using Svelte 5 with runes, dual-mode routing, permissions, and error handling

714 lines (595 loc) 37.7 kB
# Changelog All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] ## [5.0.0-rc12] - 2025-02-12 ### Fixed - **Critical:** Fixed missing TypeScript source files in published npm package - Added `src/lib/**/*.ts` to `package.json` files field - Fixes build error: `Rollup failed to resolve import "@keenmate/svelte-spa-router/logger"` - The `logger.ts` file was missing from rc11 npm package, breaking production builds - Removed deprecated `setDebugLoggingEnabled()` and `getDebugLoggingEnabled()` type declarations from `utils.d.ts` - These functions were removed in rc11 but TypeScript definitions remained, causing confusion - Use new API: `import { enableLogging, disableLogging } from '@keenmate/svelte-spa-router/logger'` - Added missing TypeScript definitions for `helpers/hierarchy` module - Created `src/lib/helpers/hierarchy.d.ts` with full type support for `createHierarchy()` - Includes `HierarchyNode`, `HierarchyTree`, and `CreateHierarchyOptions` interfaces - Fixed logger type resolution in package.json exports - Changed `"types": "./src/lib/logger.d.ts"` to `"types": "./src/lib/logger.ts"` - TypeScript now correctly resolves types from the source file ## [5.0.0-rc11] - 2025-02-01 ### Changed #### BREAKING: Logging System Migration to loglevel - **Migrated from custom logging to loglevel library** (~1KB) with hierarchical categories - Consistent with `@keenmate/web-multiselect` logging implementation - Uses vendored `loglevel` and `loglevel-plugin-prefix` libraries (ESM versions) - **Breaking API change**: `setDebugLoggingEnabled()` replaced with new API - Old: `import { setDebugLoggingEnabled } from '@keenmate/svelte-spa-router/utils'` - New: `import { enableLogging, disableLogging, setLogLevel, enableCategory } from '@keenmate/svelte-spa-router/logger'` - **12 hierarchical categories** for granular control: - `ROUTER` - Core routing pipeline, route matching - `ROUTER:NAVIGATION` - push, pop, replace, goBack - `ROUTER:SCROLL` - Scroll restoration - `ROUTER:GUARDS` - Navigation guards - `ROUTER:CONDITIONS` - Route condition checks - `ROUTER:HIERARCHY` - Hierarchical route inheritance - `ROUTER:PERMISSIONS` - Permission checking - `ROUTER:ROUTES` - Named routes and URL building - `ROUTER:ZONES` - Multi-zone routing - `ROUTER:METADATA` - Breadcrumbs and route metadata - `ROUTER:ERROR_HANDLER` - Global error handling - `ROUTER:FILTERS` - Filter parsing - **Enhanced output format**: \`[HH:MM:SS.mmm] [LEVEL] [CATEGORY] message\` - **Color-coded by log level**: Blue (debug), Green (info), Orange (warn), Red (error) - **Per-category control**: Enable specific categories at different log levels \`\`\`javascript disableLogging() // Disable all enableCategory('ROUTER:SCROLL', 'debug') // Enable only scroll logs enableCategory('ROUTER:NAVIGATION', 'info') // Navigation at info level \`\`\` - **Global level control**: \`setLogLevel('warn')\` to set all categories at once - Removed \`src/lib/internal/logging.js\` (custom implementation) - Added \`src/lib/logger.ts\` (loglevel-based implementation) - Added \`src/lib/vendor/loglevel/\` with ESM library files ### Fixed - Fixed ESM import error with loglevel library (\`Cannot set properties of undefined\`) - Switched from UMD to ESM versions of vendored libraries - Added wrapper files (\`index.js\`, \`prefix.js\`) for proper ES module imports ## [5.0.0-rc10] - 2025-02-01 ### Added #### Debug Logging System - **Category-based debug logging** - Built-in debug logging to troubleshoot routing issues during development - **`setDebugLoggingEnabled()` function** - Enable/disable debug logging with a single call - Simple on/off control - no complex configuration needed - Disabled by default to keep production consoles clean - Example: `setDebugLoggingEnabled(import.meta.env.DEV)` - **`getDebugLoggingEnabled()` function** - Check current debug logging state - **Color-coded console output** - Easy visual distinction between log categories - `[Router]` logs in orange (#ff3e00) - Route matching, component loading, guard execution, metadata updates - `[Router:Utils]` logs in green (#10b981) - Navigation functions (push, pop, replace, goBack), scroll restoration - **Multiple log levels** - debug, info, warn, error for different severity - **Zero overhead when disabled** - If checks can be eliminated by bundlers in production - **Internal architecture** - Generic logging utility (`src/lib/internal/logging.js`) not exposed to users - `createLogger(category, prefix, color)` factory function - `enableLoggingCategory()` / `disableLoggingCategory()` for granular control - Logger instances: `routerLogger`, `utilsLogger` - **Configuration warnings unaffected** - Critical configuration errors always show regardless of debug setting - **Example output**: ``` [Router] Running pipeline for: /document/123 [Router] Route loaded successfully: /document/:id [Router:Utils] Called - navigationContext: { source: 'menu' } [Router] Scroll effect triggered - restoreScrollState: true ``` - **TypeScript support** - Full type definitions with comprehensive JSDoc - Package export: `@keenmate/svelte-spa-router/utils` (setDebugLoggingEnabled, getDebugLoggingEnabled) ## [5.0.0-rc09] - 2025-01-30 ### Added #### Tree/Nested Route Structure (Alternative API) - **`createHierarchy()` helper** - Define routes in a tree structure as an alternative to flat definitions - Child paths automatically concatenated to parent paths (relative paths) - Routes automatically inherit breadcrumbs, permissions, conditions, and authorization from parents - Optional route names - only add when needed for programmatic navigation - Coexists seamlessly with flat route definitions - Example: ```javascript import { createHierarchy } from '@keenmate/svelte-spa-router/helpers/hierarchy' const routes = createHierarchy({ '/admin': { name: 'admin', component: AdminLayout, breadcrumbs: [{ label: 'Admin' }], permissions: { any: ['admin'] }, children: { 'users': { name: 'adminUsers', component: AdminUsers, breadcrumbs: [{ label: 'Users' }], children: { ':id': { name: 'adminUserDetail', component: AdminUserDetail } } } } } }) // Results in: /admin, /admin/users, /admin/users/:id ``` - Requires hierarchical mode enabled: `setHierarchicalRoutesEnabled(true)` - Package export: `@keenmate/svelte-spa-router/helpers/hierarchy` #### Named Routes Enhancement - **`getRouteByName()` function** - Get route pattern by name - Returns the path pattern for a registered route name - Useful for testing and debugging - Example: `getRouteByName('documentDetail')` returns `'/documents/:documentId'` #### Automatic Referrer Tracking System - **Configurable referrer tracking** - Router automatically tracks previous route information and injects it into navigationContext - **`setIncludeReferrer()` configuration** - Control when referrer info is injected - `'never'` - Disable referrer tracking (default) - `'notfound'` - Only inject referrer for 404/NotFound routes - `'always'` - Inject referrer for all route navigations - **Referrer object structure** - Complete information about the previous route: - `location` - Previous route path (e.g., '/documents/123') - `querystring` - Previous route query string (e.g., 'tab=info&view=grid') - `params` - Previous route parameters object (e.g., { id: '123' }) - `routeName` - Previous route name if it was registered (e.g., 'documentDetail'), or URL path as fallback - **NotFound integration** - When catch-all route (`'*'`) matches, router automatically injects: - `attemptedRoute` - The URL that was not found - `attemptedQuerystring` - Query string from the attempted route - `referrer` - Complete previous route information (see structure above) - **Zero programmer effort** - Works automatically once configured - **Safe "Go Back" implementation** - Using `push()` with explicit URL instead of `history.back()` - Safer than `history.back()` which breaks with `replace()` navigation - Allows conditional logic (e.g., don't go back to unauthorized routes) - Supports custom fallback destinations - **Works correctly with both `push()` and `replace()` navigation** - **Timing behavior** - On first navigation after referrer is updated, component $effects may run twice: - First run: location changes but referrer not yet injected (sees undefined) - Second run: navigationContext updates with referrer (sees correct value) - This is expected Svelte 5 behavior and doesn't affect functionality - Subsequent navigations are smooth with single $effect run - UI always renders correctly on the second run - **Example usage in NotFound component**: ```javascript const navContext = $derived(navigationContext()) const referrer = $derived(navContext?.referrer) const canGoBack = $derived(referrer?.location && referrer.location !== '/') function goBack() { const returnPath = referrer?.location || '/' const returnQuery = referrer?.querystring const returnUrl = returnQuery ? `${returnPath}?${returnQuery}` : returnPath push(returnUrl) } ``` - **Example usage in protected routes** (avoid going back to authorization failures): ```javascript const navContext = $derived(navigationContext()) const referrer = $derived(navContext?.referrer) const returnPath = $derived(referrer?.location || navContext?.returnTo || '/') const returnQuery = $derived(referrer?.querystring || navContext?.returnQuery) // Referrer tracks last SUCCESSFULLY loaded route, not attempted routes that failed auth ``` - Example implementations: - `example/src/routes/NotFound.svelte` - 404 page with "Go Back" - `example/src/routes/Unauthorized.svelte` - Authorization failure with safe "Go Back" - `example/src/routes/User.svelte` - Regular page with referrer-based navigation - `example/src/routes/ReferrerDemo.svelte` - Comprehensive demo with console logging - **`setNavigationContext()` function** - Exported for advanced use cases - Allows manual setting of navigation context - Used internally by router for referrer auto-injection - Example: `setNavigationContext({ customData: 123 })` ### Fixed #### Named Routes Navigation - **Fixed `push()` and `replace()` with single-argument named routes** - Resolved "Invalid parameter location" error - Issue: Calling `push('routeName')` with a single named route argument threw error - Root cause: Legacy signature detection treated non-slash strings as href paths instead of named routes - Solution: Added check in legacy signature branch - strings without leading `/` or `#/` are treated as named routes - Now works: `push('about')`, `push('userProfile')`, `replace('home')` - Multi-param signature still required for routes with params: `push('userProfile', {id: 123})` #### Hierarchical Routes & Breadcrumb System - **Fixed infinite loop in route metadata updates** - Resolved state comparison issues causing Router to re-render continuously - Issue: Router's `$effect()` was triggering on every update due to `routeContext` object reference changes - Root cause: `updateRouteMetadata()` created new objects every call, even when values didn't change - Solution 1: Added `lastAssignedContext` reference tracking to only update when context reference actually changes - Solution 2: Implemented route key tracking (`${location}|${querystring}|${JSON.stringify(params)}`) to detect real route changes vs metadata updates - Fixed `$state` proxy comparison warnings by using `$state.snapshot()` for value comparisons - **Breadcrumb cache system for preserving manual updates** - Child route navigation no longer resets dynamic breadcrumbs to "Loading..." - Issue: Navigating from `/documents/1` to `/documents/1/logs` reset "Document 1" breadcrumb back to "Loading..." - Root cause: Router composed fresh breadcrumbs on every route change, losing manual `updateBreadcrumb()` calls - Solution: Implemented breadcrumb cache (`updatedBreadcrumbsCache` Map) to preserve manual updates across child route navigation - Cache intelligently clears only when navigating to different base routes (not child routes) - `updateBreadcrumb(id, updates)` stores updates in cache, Router applies them during breadcrumb composition - **Child component breadcrumb initialization pattern** - Direct child route reloads now show correct parent breadcrumbs - Issue: Reloading `/documents/1/logs` directly showed "Loading..." for parent breadcrumb because parent component never mounted - Pattern: Child components call `updateBreadcrumb()` in `onMount()` to update their parent's dynamic breadcrumbs - Example: DocumentLogs component updates 'documentDetail' breadcrumb with actual document name on mount - Works with breadcrumb cache to ensure updates persist across subsequent child navigation - Applied to all child components in test apps: DocumentLogs, DocumentPermissions, DocumentHistory, AdminUserPermissions, AdminUserActivity ### Documentation - **Added comprehensive documentation showcase** - New SvelteKit-based documentation site - Built with @keenmate/svelte-docs for consistent styling and components - Complete API reference with all functions, parameters, and return types - Feature guides: Routing Modes, Route Configuration, Parameters, Guards, Named Routes, Programmatic Navigation, Querystring, Filters, Permissions, Multi-Zone Routing, Hierarchical Routes, Nested Routes - Interactive code examples with syntax highlighting - Live demo apps for both hash and history modes - Deployed at https://svelte-spa-router.keenmate.dev - Demo apps: https://history.svelte-spa-router.keenmate.dev and https://hash.svelte-spa-router.keenmate.dev - **Added Multi-Zone Routing documentation** - Comprehensive guide for multi-zone layouts - New showcase page at `/features/multi-zone` - Visual diagram showing zone layout structure - Complete examples: route configuration, layout setup, zone components - Covers async loading, route parameters, guards, permissions, and metadata in zones - Use cases: admin dashboards, email clients, music players, document editors - Best practices and responsive layout patterns - **Updated CLAUDE.md** - Added comprehensive section on tree/nested route structure - **Added examples** - Tree structure examples in main example app ## [5.0.0-rc08] - 2025-10-26 ### Breaking Changes - **BREAKING: Renamed `params` to `routeParams`** for clarity - Component prop: `let { params } = $props()``let { routeParams } = $props()` - Function call: `params()``routeParams()` - This makes it immediately clear these are route parameters from URL patterns - Update all route components to use `routeParams` instead of `params` - TypeScript: `params<T>()``routeParams<T>()` ## [5.0.0-rc07] - 2025-10-26 ### Fixed - **Critical: Tree-shaking issue in production builds** - Added `sideEffects` field to package.json to prevent Vite from incorrectly tree-shaking `.svelte` and `.svelte.js` files during production builds - Fixes "link is not defined" and similar errors in production builds with npm package - `.svelte.js` files contain Svelte 5 runes (`$state`, `$derived`, `$effect`) at module level which have side effects and must not be tree-shaken - Issue only affected production builds, not development mode ## [5.0.0-rc06] - 2025-10-26 ### Added #### Multi-Parameter Navigation & Strict Parameter Replacement - **Multi-parameter signature for `push()` and `replace()`** - Natural function call style - `push(route, routeParams, queryString, navigationContext)` - `replace(route, routeParams, queryString, navigationContext)` - Route resolution: Starts with `/` = exact path, otherwise = named route lookup - Examples: `push('userProfile', { userId: 123 }, { tab: 'settings' })` - Backward compatible with all existing formats (string, array, object) - **4-element array support** - Navigation context in arrays - `push(['route', params, query, navigationContext])` - `link={['route', params, query, navigationContext]}` - Example: `<a use:link={['bookDetail', {bookId: 123}, {tab: 'reviews'}, {source: 'list'}]}>` - **Strict parameter replacement with placeholder** - `setParamReplacementPlaceholder(value)` - Configure placeholder for missing params (default: 'N-A') - Missing params replaced with placeholder instead of being removed - Predictable URLs: `/users/:userId/:section` with missing section → `/users/123/N-A` - Triggers `onNotFound` callback for error tracking #### Resource-Based Authorization - **`authorizationCallback` parameter for `createProtectedRoute()`** - Combine role + resource checks - Supports both role-based (permissions) and resource-based (authorizationCallback) authorization - Conditions execute in order: permissions first (fast), then authorizationCallback (API call) - Example: Check if user has 'editor' role, then check if they can access specific document - Perfect for document access, resource ownership, dynamic permissions - Callback receives full route detail: `{ route, location, params, query, routeContext, navigationContext }` #### Global Error Handler System - **Production-ready global error handler** for catching and recovering from unhandled errors - `configureGlobalErrorHandler()` - Configure error handling behavior in `main.js` - `GlobalErrorHandler` component - Wraps your app and catches all errors - `ErrorDisplay` component - Beautiful default error UI with recovery options - **Recovery Strategies**: `navigateSafe`, `restart`, `showError`, `custom` - **Loop Prevention**: SessionStorage-based restart tracking prevents infinite reload loops - **Custom Callbacks**: `onError` for logging/monitoring, `onRecover` for custom recovery logic - **Helper Functions**: `restart()`, `navigate()`, `showError()`, `canRestart()`, `getRestartCount()` - **Error Filtering**: Ignore known non-critical errors (ResizeObserver, etc.) - **UI Options**: Toast notifications, full-page error component, or custom error component - **TypeScript Support**: Full type definitions for all APIs - **Example Integration**: Working demo with Sentry integration example #### 404 Not Found Tracking - **`onNotFound` callback on Router component** - Track 404s for analytics/monitoring - Fires when catch-all route (`'*'`) matches (user sees 404 page) - Fires when no route matches at all (no 404 page defined) - Perfect for logging to Sentry, Google Analytics, or other monitoring services - Event detail includes `{ location, querystring }` - Example: `<Router {routes} onNotFound={(e) => Sentry.captureMessage('404', { extra: e.detail })} />` #### Convenient Route Creation API - **New `createRoute()` and `createRouteDefinition()` functions** for easier route configuration - `createRoute()` - Returns already wrapped component (most convenient, no `wrap()` needed) - `createRouteDefinition()` - Returns route definition for use with `wrap()` (advanced use) - Consistent API pattern matching `createProtectedRoute()` / `createProtectedRouteDefinition()` - Support for `title` and `breadcrumbs` metadata directly in route options - Automatic detection of sync vs async components - **Enhanced Permission System** - `createProtectedRoute()` - Now returns already wrapped component (breaking change from definition-only) - `createProtectedRouteDefinition()` - New function that returns definition (replaces old `createProtectedRoute()` behavior) - Maintains backward compatibility for existing `wrap(createProtectedRoute(...))` usage - **Route Metadata Support** - `title` - Set page title for routes - `breadcrumbs` - Define breadcrumb trail with `{ label, path? }` structure - Metadata stored in `routeContext` object, accessible in route events and components #### Dynamic Metadata & Loading Control - **Reactive Metadata Helpers**: `helpers/route-metadata.svelte.js` - `routeTitle()` - Get current route title reactively - `routeBreadcrumbs()` - Get current breadcrumb trail reactively - `routeContext()` - Get full route context reactively - `updateRouteMetadata(routeContext)` - Update metadata after data loads (e.g., change title from "Document" to "Invoice.pdf") - **`updateTitle(title)` - Update just the title** (simpler than updateRouteMetadata) - **`updateBreadcrumb(id, updates)` - Partial breadcrumb updates** (update specific segments by ID) - Automatically updated by Router on route changes - **Partial Breadcrumb Updates** (NEW!) - Add `id` property to breadcrumb items in route config: `{ id: 'docDetail', label: 'Loading...', path: '/doc/:id' }` - Update specific breadcrumbs after data loads: `updateBreadcrumb('docDetail', { label: 'Invoice.pdf', path: '/doc/123' })` - **No need to replace the entire breadcrumbs array** - only update what changes! - Perfect for nested paths: `/documents/:id/logs/:logId` where each segment needs dynamic data - Static segments (Home, Documents, etc.) stay unchanged - **Flexible Loading Control** with `shouldDisplayLoadingOnRouteLoad` flag - **Pattern 1 (Router-managed with loadingComponent)**: Set `shouldDisplayLoadingOnRouteLoad: true` - Router keeps loading component visible until component calls `hideLoading()` - **Pattern 2 (Component-managed)**: Default behavior - component handles its own loading state - **Pattern 3 (Global overlay)**: User-defined global loading UI in App.svelte that reacts to `routeIsLoading()` - `showLoading()` - Manually show loading state (triggers global overlay if defined) - `hideLoading()` - Component signals data is loaded (hides loading component/overlay) - `routeIsLoading()` - Check if route is currently loading (reactive state) - Perfect for routes that fetch data and need dynamic titles/breadcrumbs - Supports multi-zone layouts (toolpanel + content) with different loading UIs per zone **Use cases**: - Document detail page: Show "Document" while loading, then "Invoice template.pdf" after data loads - User profile: Show "User Profile" while loading, then "John Doe" after data loads - Product page: Show "Product" while loading, then "iPhone 15 Pro" after data loads - Nested paths: `/documents/:id/logs` where both document name and "Logs" need to be in breadcrumbs #### Named Routes Enhancement - **Array/Object Syntax for Navigation**: `push()` and `replace()` functions now support the same convenient array/object syntax as the `link` action - Array format: `push(['userProfile', { userId: 123 }, { tab: 'settings' }])` - Object format: `push({ route: 'userProfile', params: { userId: 123 }, query: { tab: 'settings' } })` - String format (legacy): `push('/about')` - still fully supported for backward compatibility - Eliminates the need to manually call `buildUrl()` for programmatic navigation - See `example-history/src/routes/LinksDemo.svelte` for interactive demos ### Added - Querystring & Filter System #### Querystring Helpers - **Shared Reactive State**: `helpers/querystring.svelte.js` - `configureQuerystring(options)` - Configure querystring parsing for entire app - `query<T>()` - Get reactive parsed querystring with TypeScript generics - Auto-detection of array formats (repeat vs comma-separated) - Configure once, use everywhere pattern - **Querystring Utilities**: `helpers/querystring-helpers.svelte.js` - `parseQuerystring(qs, options)` - Parse querystring with array format support - `stringifyQuerystring(obj, options)` - Convert object to querystring - `getParsedQuerystring(options)` - Get parsed querystring (non-reactive) - `updateQuerystring(updates, options)` - Update URL querystring (partial or full) - `createQuerystringHelpers(parser, stringifier)` - Custom parser support - **Array Format Support**: - `'auto'` (default) - Auto-detects both repeat and comma formats - `'repeat'` - `?tags=foo&tags=bar` (multiple parameters with same key) - `'comma'` - `?tags=foo,bar,baz` (comma-separated values) #### Filter System - **Flexible Filters**: `helpers/filters.svelte.js` - `configureFilters(options)` - Configure filter mode and parsing - `filters<T>()` - Get reactive parsed filters with TypeScript generics - `updateFilters<T>(updates, options)` - Update filters with type safety - `getFiltersConfig()` - Get current filter configuration - **Dual Mode Support**: - **Flat Mode** (default): Each filter as separate query parameter - Example: `?search=java&category=books&status=active` - **Structured Mode**: Single parameter with custom syntax - Example: `?$filter=search eq 'java' AND category eq 'books'` - Supports custom parse/stringify functions for OData, Microsoft Graph API, etc. #### TypeScript Support - Full generic support for type-safe parameter access: - `params<T>()` - Route parameters with intellisense - `query<T>()` - Query parameters with intellisense - `filters<T>()` - Filter parameters with intellisense - `updateFilters<T>(updates, options)` - Type-safe filter updates #### Value Handling - **Filters**: - `undefined` - Always removes the parameter - `null` - Keeps parameter with empty value - **Querystring**: - `undefined` - Always removes the parameter - `null` - Controlled by `dropNull` option (default: true removes it) - Empty string - Controlled by `dropEmpty` option (default: false keeps it) #### Example Applications - `example-history/src/routes/QuerystringDemo.svelte` - Interactive querystring demo - `example-history/src/routes/FiltersDemo.svelte` - Filter system with products demo - `example-history/src/routes/RouteDataDemo.svelte` - Route data extraction examples ### Fixed #### Router Core Fixes - **Router Initialization**: Fixed location state initialization to be reactive to config changes - Issue: Direct URL access (e.g., `http://localhost:5050/querystring-demo`) showed homepage - Solution: Changed from `$state(getLocation())` to `$derived.by()` to react to config changes - Now correctly reads `setHashRoutingEnabled()` before initializing location state #### Loading State Fixes - **Component Double Mounting**: Fixed race condition causing components to mount twice - Issue: Router template had component in two separate `{:else if}` blocks - one hidden, one visible - When `isWaitingForData` changed, Svelte unmounted from first block and remounted in second - Resulted in `onMount()` running twice, causing duplicate data fetches and delays - Solution: Refactored template to keep component in single block with `style:display` toggle - Component now mounts once and visibility is controlled via CSS - **Component Params Not Available on Mount**: Fixed params being empty when component loads - Issue: Router set `isWaitingForData = true` and waited for `hideLoading()` BEFORE setting `componentParams` - Component mounted without params, causing "params not ready" errors - Solution: Moved `componentParams`, `componentProps`, `componentrouteContext` assignment BEFORE waiting logic - Component now has access to params immediately on mount - **Global vs Route Loading Conflict**: Fixed overlapping loading indicators - Issue: Both global loader and route-specific loader showed simultaneously - Solution: Added `shouldShowGlobalLoading()` helper that only returns true when route doesn't have custom loading - Added `hasCustomLoadingComponent` flag set by `startRouteLoading(hasCustomComponent)` - Global loader now only shows for routes without custom loading components - **Manual Loading Not Showing**: Fixed `showLoading()` not displaying global loader - Issue: `showLoading()` set `isRouteLoading = true` but didn't reset `hasCustomLoadingComponent` flag - If current route had custom loading, `shouldShowGlobalLoading()` returned false - Solution: Made `showLoading()` also set `hasCustomLoadingComponent = false` - Manual loading triggers now correctly show global loader #### Authorization & Navigation Context Fixes - **"Go Back" After Unauthorized**: Fixed return navigation after authorization failures - Issue: Unauthorized page's "Return to Home" button always went to home instead of previous location - Root cause: Using `push('/unauthorized', { data })` was ambiguous - interpreted as routeParams instead of navigationContext - Solution: Changed all unauthorized redirects to use explicit 4-parameter signature: `push(route, {}, {}, navContext)` - Added `returnTo` and `returnQuery` to navigation context for proper "Go Back" functionality - Unauthorized page now displays "Go Back" button when `returnTo` is available - **Document Authorization Navigation**: Fixed document access redirects - Updated `authorizationCallback` to pass `returnTo` and `returnQuery` via navigationContext - Fixed "View Document" buttons to pass navigation context for proper back navigation - All authorization demo flows now preserve return path for better UX #### Protected Route Fixes - **Async Component Params Empty**: Fixed params not being passed to protected routes - Issue: `createProtectedRouteDefinition` was using `asyncComponent: component` which caused wrap() to double-wrap async imports - Solution: Kept `asyncComponent: component` which properly passes async imports to wrap() - Protected routes now receive params correctly when using `() => import('./Component.svelte')` #### Example Application Fixes - **Missing Routes**: Added missing example routes - Added `/about` route (About component) - Added `/user/:first/:last?` route (User component with optional last name) - Links in LinksDemo now work correctly - **Navigation Context Demo**: Fixed incorrect route paths and push signatures - Changed `/context-demo` references to correct `/navigation-context-demo` route - Fixed `backToList()` function to navigate to correct route - Updated code examples to show proper 4-parameter signature - Fixed "View" and "Edit" buttons to use correct navigation context syntax - **HTML Entity Escaping**: Fixed Svelte parse errors - Changed unescaped `{}` in code examples to HTML entities `&#123;&#125;` - Prevents Svelte from treating them as reactive expressions ### Documentation - Updated `README.md` with comprehensive querystring and filter system documentation - Added TypeScript generic examples throughout - Added Quick Reference section with all available imports - Updated main features list to highlight TypeScript and URL helpers ## [1.0.0] - 2024 ### Package Information - **Package Name:** @keenmate/svelte-spa-router - **Publisher:** KeenMate (https://keenmate.com) - **Repository:** https://github.com/keenmate/svelte-spa-router ### Added - Svelte 5 Conversion #### Core Router - Complete rewrite using Svelte 5 runes (`$state`, `$props`, `$effect`) - Reactive location tracking with rune-based state management - Event handlers converted from `on:` directives to callback props - Maintained full backward compatibility in route definition syntax #### Dual-Mode Routing System - **Hash Mode (Default)**: Traditional hash-based routing (`#/path`) - No configuration required - Works everywhere, including `file://` protocol - Perfect for static hosting - **History Mode (New)**: Clean URL routing (`/path`) - Uses History API for clean URLs without hash - Supports modifier keys (Ctrl+Click to open in new tab) - Respects `target` attribute on links - Requires server configuration to serve index.html for all routes #### Configuration Functions - `setHashRoutingEnabled(boolean)` - Toggle between hash and history mode - `setBasePath(string)` - Set base path for history mode - `getHashRoutingEnabled()` - Get current routing mode - `getBasePath()` - Get current base path #### Permission System (New) - `helpers/permissions.svelte.js` - Comprehensive RBAC system - `configurePermissions(config)` - Setup permission checking logic - `createPermissionCondition(requirements)` - Create route guards - `createProtectedRoute(options)` - Helper for protected routes - `hasPermission(requirements)` - UI-level permission checking - Support for `any` (OR) and `all` (AND) permission logic - Flexible integration with existing route guards #### URL Helpers - `helpers/url-helpers.svelte.js` - Path manipulation utilities - `joinPaths(...paths)` - Intelligent path joining with slash handling #### Enhanced Active Link Detection - Updated to work with both hash and history modes - Automatic CSS class application on matching routes - Pattern matching support ### Changed #### Naming Changes (Non-Breaking in Usage, Breaking for Type Imports) - **`userData` → `routeContext`**: Renamed for clarity - Refers to static route-level configuration data - `wrap({ routeContext: { ... } })` - `routeContext()` helper function - All TypeScript interfaces updated - **`context` → `navigationContext`**: Renamed for clarity - Refers to dynamic data passed during navigation (doesn't appear in URL) - `push('/path', { orderId: 123 })` - second parameter is navigationContext - `navigationContext()` accessor function - `wrap({ navigationContext: { ... } })` These changes clarify the distinction between: - **routeContext**: Static metadata defined in route configuration - **navigationContext**: Dynamic data passed at navigation time (WinForms-like experience) #### API Changes (Breaking) - **Stores → Functions**: - `$location``location()` - `$querystring``querystring()` - `$params``params()` - `$loc``loc()` - **Events → Props**: - `on:routeLoading``onrouteLoading` - `on:routeLoaded``onrouteLoaded` - `on:conditionsFailed``onconditionsFailed` - `on:routeEvent``onrouteEvent` - **Component Props**: - `export let params``let { params } = $props()` - **Reactive Subscriptions**: - `.subscribe()``$effect(() => { ... })` ### Unchanged (Backward Compatible) - ✅ Route definition syntax (same object/Map structure) -`push()`, `pop()`, `replace()` navigation functions -`link` action for anchor tags -`active` action for link highlighting -`wrap()` utility for async components and conditions - ✅ Route guards/pre-conditions - ✅ Dynamic imports and code-splitting - ✅ Nested routers with prefix - ✅ Scroll restoration - ✅ Loading components ### Documentation #### New Files - `CONTEXT.md` - Comprehensive project overview - `CHANGELOG.md` - This file - `DEVELOPMENT.md` - Development workflow guide - `MIGRATION.md` - Detailed v4 → v5 migration guide - `HASHLESS_MERGE_PLAN.md` - Technical implementation notes #### Updated Files - `README.md` - Added routing modes and permission system documentation - `package.json` - Updated exports for new modules ### Examples - `example/` - Hash mode example (updated for Svelte 5) - `example-history/` - History mode example (new) ### Infrastructure - ESLint configuration for Svelte 5 - Makefile for cross-platform development commands - Package exports for all modules ## Migration Guide See [MIGRATION.md](./MIGRATION.md) for detailed instructions on upgrading from v4 to v5. ### Quick Migration Checklist 1. ✅ Update imports: `svelte-spa-router``@keenmate/svelte-spa-router` 2. ✅ Change stores to functions: `$location``location()` 3. ✅ Update event handlers: `on:routeLoaded``onrouteLoaded` 4. ✅ Convert component props: `export let params``let { params } = $props()` 5. ✅ Replace subscriptions with `$effect` 6. ✅ Test all routes and navigation ### Optional: Enable History Mode ```javascript // main.js import { setHashRoutingEnabled, setBasePath } from '@keenmate/svelte-spa-router/utils' setHashRoutingEnabled(false) setBasePath('/') mount(App, { target: document.body }) ``` ### Optional: Add Permission System ```javascript // main.js import { configurePermissions } from '@keenmate/svelte-spa-router/helpers/permissions' configurePermissions({ checkPermissions: (user, requirements) => { // Your permission logic }, getCurrentUser: () => getCurrentUser(), onUnauthorized: (detail) => push('/unauthorized') }) ``` ## Browser Compatibility - **Svelte 5**: All modern browsers (Chrome, Firefox, Safari, Edge) - **Hash Mode**: All browsers including IE10+ - **History Mode**: All browsers with History API support (IE10+) - **Permissions**: All modern browsers ## Dependencies ### Runtime - `regexparam@2.0.2` - Route pattern matching ### Peer Dependencies - `svelte@^5.0.0` - Required ### Dev Dependencies - `eslint@^9.0.0` - Code linting ## Credits - **Publisher:** KeenMate (https://keenmate.com) - **Original svelte-spa-router:** Alessandro Segala (@ItalyPaleAle) - **Svelte 5 Conversion:** KeenMate team - **History Mode:** Adapted from svelte-spa-router-hashless - **Permission System:** KeenMate team ## License MIT License - See [LICENSE.md](./LICENSE.md)