@revealui/router 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -92,20 +92,55 @@ function Navigate({ to, replace = false }) {
92
92
  }
93
93
 
94
94
  // src/router.ts
95
- import { match as pathMatch } from "path-to-regexp";
96
-
97
- // ../core/src/observability/logger.ts
98
- import { logger as utilsLogger } from "@revealui/utils/logger";
99
- import {
100
- createLogger,
101
- Logger,
102
- logAudit,
103
- logError,
104
- logger,
105
- logQuery
106
- } from "@revealui/utils/logger";
107
-
108
- // src/router.ts
95
+ import { logger } from "@revealui/core/observability/logger";
96
+ function compilePathPattern(pattern) {
97
+ const keys = [];
98
+ let src = "^";
99
+ let i = 0;
100
+ while (i < pattern.length) {
101
+ const ch = pattern[i];
102
+ if (ch === "{") {
103
+ src += "(?:";
104
+ i++;
105
+ } else if (ch === "}") {
106
+ src += ")?";
107
+ i++;
108
+ } else if (ch === ":") {
109
+ i++;
110
+ let name = "";
111
+ while (i < pattern.length && /\w/.test(pattern[i])) name += pattern[i++];
112
+ keys.push({ name, wildcard: false });
113
+ src += "([^/]+)";
114
+ } else if (ch === "*") {
115
+ i++;
116
+ let name = "";
117
+ while (i < pattern.length && /\w/.test(pattern[i])) name += pattern[i++];
118
+ keys.push({ name: name || "0", wildcard: true });
119
+ src += "(.+)";
120
+ } else {
121
+ src += ch.replace(/[.+?^$|()[\]\\]/g, "\\$&");
122
+ i++;
123
+ }
124
+ }
125
+ src += "$";
126
+ return { regex: new RegExp(src), keys };
127
+ }
128
+ function pathMatch(pattern, options = {}) {
129
+ const { regex, keys } = compilePathPattern(pattern);
130
+ const decode = options.decode ?? ((s) => s);
131
+ return (path) => {
132
+ const m = regex.exec(path);
133
+ if (!m) return false;
134
+ const params = {};
135
+ for (let j = 0; j < keys.length; j++) {
136
+ const key = keys[j];
137
+ const val = m[j + 1];
138
+ if (val === void 0) continue;
139
+ params[key.name] = key.wildcard ? val.split("/").map(decode) : decode(val);
140
+ }
141
+ return { params };
142
+ };
143
+ }
109
144
  var Router = class {
110
145
  routes = [];
111
146
  options;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/components.tsx","../src/router.ts","../../core/src/observability/logger.ts"],"sourcesContent":["import type React from 'react'\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react'\nimport type { Router } from './router'\nimport type { NavigateOptions, RouteMatch } from './types'\n\n/**\n * Router context\n */\nconst RouterContext = createContext<Router | null>(null)\n\n/**\n * Current match context\n */\nconst MatchContext = createContext<RouteMatch | null>(null)\n\n/**\n * RouterProvider - Provides router instance to the app\n */\nexport function RouterProvider({\n router,\n children,\n}: {\n router: Router\n children: React.ReactNode\n}) {\n return <RouterContext.Provider value={router}>{children}</RouterContext.Provider>\n}\n\n/**\n * Routes - Renders the matched route component\n */\nexport function Routes() {\n const router = useRouter()\n\n // Subscribe to router changes\n const match = useSyncExternalStore(\n (callback) => router.subscribe(callback),\n () => router.getCurrentMatch(),\n () => router.getCurrentMatch(), // Server-side snapshot (same as client)\n )\n\n if (!match) {\n return <NotFound />\n }\n\n const { route, params, data } = match\n const Component = route.component\n const Layout = route.layout\n\n const element = <Component params={params} data={data} />\n\n return (\n <MatchContext.Provider value={match}>\n {Layout ? <Layout>{element}</Layout> : element}\n </MatchContext.Provider>\n )\n}\n\n/**\n * Link - Client-side navigation link\n */\nexport function Link({\n to,\n replace = false,\n children,\n className,\n style,\n onClick,\n ...props\n}: {\n to: string\n replace?: boolean\n children: React.ReactNode\n className?: string\n style?: React.CSSProperties\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n [key: string]: unknown\n}) {\n const router = useRouter()\n\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Call custom onClick if provided\n onClick?.(e)\n\n // Don't navigate if default was prevented\n if (e.defaultPrevented) {\n return\n }\n\n // Only handle left clicks\n if (e.button !== 0) {\n return\n }\n\n // Ignore if modifier keys are pressed\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return\n }\n\n e.preventDefault()\n router.navigate(to, { replace })\n }\n\n return (\n <a href={to} onClick={handleClick} className={className} style={style} {...props}>\n {children}\n </a>\n )\n}\n\n/**\n * useRouter - Hook to access router instance\n */\nexport function useRouter(): Router {\n const router = useContext(RouterContext)\n\n if (!router) {\n throw new Error('useRouter must be used within a RouterProvider')\n }\n\n return router\n}\n\n/**\n * useMatch - Hook to access current route match\n */\nexport function useMatch(): RouteMatch | null {\n return useContext(MatchContext)\n}\n\n/**\n * useParams - Hook to access route parameters\n */\nexport function useParams<T = Record<string, string>>(): T {\n const match = useMatch()\n return (match?.params as T) || ({} as T)\n}\n\n/**\n * useData - Hook to access route data\n */\nexport function useData<T = unknown>(): T | undefined {\n const match = useMatch()\n return match?.data as T | undefined\n}\n\n/**\n * useNavigate - Hook to get navigation function\n */\nexport function useNavigate() {\n const router = useRouter()\n\n return (to: string, options?: NavigateOptions) => {\n router.navigate(to, options)\n }\n}\n\n/**\n * NotFound - Default 404 component\n */\nfunction NotFound() {\n return (\n <div style={{ padding: '2rem', textAlign: 'center' }}>\n <h1>404 - Page Not Found</h1>\n <p>The page you're looking for doesn't exist.</p>\n <Link to=\"/\">Go Home</Link>\n </div>\n )\n}\n\n/**\n * Navigate - Component for declarative navigation\n */\nexport function Navigate({ to, replace = false }: { to: string; replace?: boolean }) {\n const router = useRouter()\n\n useEffect(() => {\n router.navigate(to, { replace })\n }, [to, replace, router])\n\n return null\n}\n","import { match as pathMatch } from 'path-to-regexp'\nimport { logger } from '../../core/src/observability/logger.js'\nimport type { NavigateOptions, Route, RouteMatch, RouteParams, RouterOptions } from './types'\n\n/**\n * RevealUI Router - Lightweight file-based routing with SSR support\n */\nexport class Router {\n private routes: Route[] = []\n private options: RouterOptions\n private listeners: Set<() => void> = new Set()\n private currentMatch: RouteMatch | null = null\n\n constructor(options: RouterOptions = {}) {\n this.options = {\n basePath: '',\n ...options,\n }\n }\n\n /**\n * Register a route\n */\n register(route: Route): void {\n this.routes.push(route)\n }\n\n /**\n * Register multiple routes\n */\n registerRoutes(routes: Route[]): void {\n routes.forEach((route) => {\n this.register(route)\n })\n }\n\n /**\n * Match a URL to a route\n */\n match(url: string): RouteMatch | null {\n // Remove base path if present\n const path = this.normalizePath(url)\n\n // Try to match each route\n for (const route of this.routes) {\n const matcher = pathMatch(route.path, { decode: decodeURIComponent })\n const result = matcher(path)\n\n if (result) {\n return {\n route,\n params: (result.params as RouteParams) || {},\n }\n }\n }\n\n return null\n }\n\n /**\n * Resolve a route with data loading\n */\n async resolve(url: string): Promise<RouteMatch | null> {\n const matched = this.match(url)\n\n if (!matched) {\n return null\n }\n\n // Load data if loader exists\n if (matched.route.loader) {\n try {\n matched.data = await matched.route.loader(matched.params)\n } catch (error) {\n logger.error(\n 'Route loader error',\n error instanceof Error ? error : new Error(String(error)),\n )\n throw error\n }\n }\n\n // Store match for SSR (if on server)\n if (typeof window === 'undefined') {\n this.currentMatch = matched\n }\n\n return matched\n }\n\n /**\n * Navigate to a URL (client-side only)\n */\n navigate(url: string, options: NavigateOptions = {}): void {\n if (typeof window === 'undefined') {\n return\n }\n\n const fullUrl = this.options.basePath + url\n\n if (options.replace) {\n window.history.replaceState(options.state || null, '', fullUrl)\n } else {\n window.history.pushState(options.state || null, '', fullUrl)\n }\n\n this.notifyListeners()\n }\n\n /**\n * Go back in history\n */\n back(): void {\n if (typeof window !== 'undefined') {\n window.history.back()\n }\n }\n\n /**\n * Go forward in history\n */\n forward(): void {\n if (typeof window !== 'undefined') {\n window.history.forward()\n }\n }\n\n /**\n * Subscribe to route changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /**\n * Get current route match\n */\n getCurrentMatch(): RouteMatch | null {\n // On server, return stored match (set by resolve during SSR)\n if (typeof window === 'undefined') {\n return this.currentMatch\n }\n\n // On client, match against current URL\n return this.match(window.location.pathname)\n }\n\n /**\n * Get all registered routes\n */\n getRoutes(): Route[] {\n return [...this.routes]\n }\n\n /**\n * Clear all routes\n */\n clear(): void {\n this.routes = []\n }\n\n private normalizePath(url: string): string {\n // Remove base path\n let path = url\n if (this.options.basePath && path.startsWith(this.options.basePath)) {\n path = path.slice(this.options.basePath.length)\n }\n\n // Remove query string and hash\n path = path.split('?')[0].split('#')[0]\n\n // Ensure leading slash\n if (!path.startsWith('/')) {\n path = `/${path}`\n }\n\n return path\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => {\n listener()\n })\n }\n\n /**\n * Initialize client-side routing\n */\n initClient(): void {\n if (typeof window === 'undefined') {\n return\n }\n\n // Handle browser back/forward buttons\n window.addEventListener('popstate', () => {\n this.notifyListeners()\n })\n\n // Intercept link clicks\n document.addEventListener('click', (e) => {\n const target = (e.target as HTMLElement).closest('a')\n\n if (!target) return\n\n const href = target.getAttribute('href')\n\n // Only handle internal links\n if (\n href?.startsWith('/') &&\n !target.hasAttribute('target') &&\n !target.hasAttribute('download') &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey\n ) {\n e.preventDefault()\n this.navigate(href)\n }\n })\n }\n}\n","/**\n * Structured Logging Infrastructure\n *\n * Re-exports from @revealui/utils to maintain backward compatibility.\n * The actual implementation has been moved to @revealui/utils to break circular dependencies.\n */\n\n// Import logger for internal use\nimport { logger as utilsLogger } from '@revealui/utils/logger'\n\n// Re-export all types and functions from utils\nexport type {\n LogContext,\n LogEntry,\n LoggerConfig,\n LogLevel,\n} from '@revealui/utils/logger'\n\nexport {\n createLogger,\n Logger,\n logAudit,\n logError,\n logger,\n logQuery,\n} from '@revealui/utils/logger'\n\n// Additional helper functions that were in core but not in utils\n// These can stay here as they're core-specific\n\n/**\n * Request logger middleware\n */\nexport function createRequestLogger<TRequest = unknown, TResponse = unknown>(\n options: { includeBody?: boolean; includeHeaders?: boolean } = {},\n) {\n return async (\n request: TRequest & {\n method: string\n url: string\n headers?: { get?: (key: string) => string | null; entries?: () => Iterable<[string, string]> }\n },\n next: () => Promise<TResponse>,\n ): Promise<TResponse> => {\n // Import logger at runtime to avoid circular deps\n const { logger } = await import('@revealui/utils/logger')\n const requestId = crypto.randomUUID()\n const startTime = Date.now()\n\n const requestLogger = logger.child({\n requestId,\n method: request.method,\n url: request.url,\n userAgent: request.headers?.get?.('user-agent'),\n })\n\n requestLogger.info('Request started')\n\n if (options.includeHeaders) {\n requestLogger.debug('Request headers', {\n headers: Object.fromEntries(request.headers?.entries?.() || []),\n })\n }\n\n try {\n const response = await next()\n\n const duration = Date.now() - startTime\n const responseWithStatus = response as typeof response & { status?: number }\n\n requestLogger.info('Request completed', {\n status: responseWithStatus.status ?? 200,\n duration,\n })\n\n return response\n } catch (error) {\n const duration = Date.now() - startTime\n\n requestLogger.error(\n 'Request failed',\n error instanceof Error ? error : new Error(String(error)),\n { duration },\n )\n\n throw error\n }\n }\n}\n\n/**\n * Performance logger\n */\nexport function logPerformance(\n operation: string,\n duration: number,\n context?: Record<string, unknown>,\n): void {\n const level = duration > 1000 ? 'warn' : 'info'\n\n utilsLogger[level](`Performance: ${operation}`, {\n ...context,\n operation,\n duration,\n slow: duration > 1000,\n })\n}\n\n/**\n * API call logger\n */\nexport function logAPICall(\n method: string,\n url: string,\n status: number,\n duration: number,\n context?: Record<string, unknown>,\n): void {\n const apiContext = {\n ...context,\n method,\n url,\n status,\n duration,\n }\n\n if (status >= 400) {\n utilsLogger.error('API call', undefined, apiContext)\n } else if (status >= 300) {\n utilsLogger.warn('API call', apiContext)\n } else {\n utilsLogger.info('API call', apiContext)\n }\n}\n\n/**\n * Cache operation logger\n */\nexport function logCache(\n operation: 'hit' | 'miss' | 'set' | 'delete',\n key: string,\n context?: Record<string, unknown>,\n): void {\n utilsLogger.debug(`Cache ${operation}`, {\n ...context,\n operation,\n key,\n })\n}\n\n/**\n * User action logger\n */\nexport function logUserAction(\n action: string,\n userId?: string,\n context?: Record<string, unknown>,\n): void {\n utilsLogger.info('User action', {\n ...context,\n action,\n userId,\n })\n}\n\n/**\n * System event logger\n */\nexport function logSystemEvent(event: string, context?: Record<string, unknown>): void {\n utilsLogger.info('System event', {\n ...context,\n event,\n })\n}\n\n/**\n * Sanitize sensitive data from logs\n */\nexport function sanitizeLogData(data: Record<string, unknown>): Record<string, unknown> {\n const sensitiveKeys = [\n 'password',\n 'token',\n 'secret',\n 'apiKey',\n 'accessToken',\n 'refreshToken',\n 'creditCard',\n 'ssn',\n ]\n\n const sanitized: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase()\n\n if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) {\n sanitized[key] = '[REDACTED]'\n } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n sanitized[key] = sanitizeLogData(value as Record<string, unknown>)\n } else {\n sanitized[key] = value\n }\n }\n\n return sanitized\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/components.tsx","../src/router.ts"],"sourcesContent":["import type React from 'react'\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react'\nimport type { Router } from './router'\nimport type { NavigateOptions, RouteMatch } from './types'\n\n/**\n * Router context\n */\nconst RouterContext = createContext<Router | null>(null)\n\n/**\n * Current match context\n */\nconst MatchContext = createContext<RouteMatch | null>(null)\n\n/**\n * RouterProvider - Provides router instance to the app\n */\nexport function RouterProvider({\n router,\n children,\n}: {\n router: Router\n children: React.ReactNode\n}) {\n return <RouterContext.Provider value={router}>{children}</RouterContext.Provider>\n}\n\n/**\n * Routes - Renders the matched route component\n */\nexport function Routes() {\n const router = useRouter()\n\n // Subscribe to router changes\n const match = useSyncExternalStore(\n (callback) => router.subscribe(callback),\n () => router.getCurrentMatch(),\n () => router.getCurrentMatch(), // Server-side snapshot (same as client)\n )\n\n if (!match) {\n return <NotFound />\n }\n\n const { route, params, data } = match\n const Component = route.component\n const Layout = route.layout\n\n const element = <Component params={params} data={data} />\n\n return (\n <MatchContext.Provider value={match}>\n {Layout ? <Layout>{element}</Layout> : element}\n </MatchContext.Provider>\n )\n}\n\n/**\n * Link - Client-side navigation link\n */\nexport function Link({\n to,\n replace = false,\n children,\n className,\n style,\n onClick,\n ...props\n}: {\n to: string\n replace?: boolean\n children: React.ReactNode\n className?: string\n style?: React.CSSProperties\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n [key: string]: unknown\n}) {\n const router = useRouter()\n\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Call custom onClick if provided\n onClick?.(e)\n\n // Don't navigate if default was prevented\n if (e.defaultPrevented) {\n return\n }\n\n // Only handle left clicks\n if (e.button !== 0) {\n return\n }\n\n // Ignore if modifier keys are pressed\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return\n }\n\n e.preventDefault()\n router.navigate(to, { replace })\n }\n\n return (\n <a href={to} onClick={handleClick} className={className} style={style} {...props}>\n {children}\n </a>\n )\n}\n\n/**\n * useRouter - Hook to access router instance\n */\nexport function useRouter(): Router {\n const router = useContext(RouterContext)\n\n if (!router) {\n throw new Error('useRouter must be used within a RouterProvider')\n }\n\n return router\n}\n\n/**\n * useMatch - Hook to access current route match\n */\nexport function useMatch(): RouteMatch | null {\n return useContext(MatchContext)\n}\n\n/**\n * useParams - Hook to access route parameters\n */\nexport function useParams<T = Record<string, string>>(): T {\n const match = useMatch()\n return (match?.params as T) || ({} as T)\n}\n\n/**\n * useData - Hook to access route data\n */\nexport function useData<T = unknown>(): T | undefined {\n const match = useMatch()\n return match?.data as T | undefined\n}\n\n/**\n * useNavigate - Hook to get navigation function\n */\nexport function useNavigate() {\n const router = useRouter()\n\n return (to: string, options?: NavigateOptions) => {\n router.navigate(to, options)\n }\n}\n\n/**\n * NotFound - Default 404 component\n */\nfunction NotFound() {\n return (\n <div style={{ padding: '2rem', textAlign: 'center' }}>\n <h1>404 - Page Not Found</h1>\n <p>The page you're looking for doesn't exist.</p>\n <Link to=\"/\">Go Home</Link>\n </div>\n )\n}\n\n/**\n * Navigate - Component for declarative navigation\n */\nexport function Navigate({ to, replace = false }: { to: string; replace?: boolean }) {\n const router = useRouter()\n\n useEffect(() => {\n router.navigate(to, { replace })\n }, [to, replace, router])\n\n return null\n}\n","import { logger } from '@revealui/core/observability/logger'\nimport type { NavigateOptions, Route, RouteMatch, RouteParams, RouterOptions } from './types'\n\n// ---------------------------------------------------------------------------\n// Hand-rolled path matcher — replaces `path-to-regexp`.\n// Supports: exact paths, named params (:id), wildcards (*path),\n// and optional segments ({/...}).\n// ---------------------------------------------------------------------------\n\ninterface PathKey {\n name: string\n wildcard: boolean\n}\n\nfunction compilePathPattern(pattern: string): { regex: RegExp; keys: PathKey[] } {\n const keys: PathKey[] = []\n let src = '^'\n let i = 0\n while (i < pattern.length) {\n const ch = pattern[i]!\n if (ch === '{') {\n src += '(?:'\n i++\n } else if (ch === '}') {\n src += ')?'\n i++\n } else if (ch === ':') {\n i++\n let name = ''\n while (i < pattern.length && /\\w/.test(pattern[i]!)) name += pattern[i++]\n keys.push({ name, wildcard: false })\n src += '([^/]+)'\n } else if (ch === '*') {\n i++\n let name = ''\n while (i < pattern.length && /\\w/.test(pattern[i]!)) name += pattern[i++]\n keys.push({ name: name || '0', wildcard: true })\n src += '(.+)'\n } else {\n src += ch.replace(/[.+?^$|()[\\]\\\\]/g, '\\\\$&')\n i++\n }\n }\n src += '$'\n return { regex: new RegExp(src), keys }\n}\n\nfunction pathMatch(\n pattern: string,\n options: { decode?: (s: string) => string } = {},\n): (path: string) => { params: Record<string, string | string[]> } | false {\n const { regex, keys } = compilePathPattern(pattern)\n const decode = options.decode ?? ((s) => s)\n return (path: string) => {\n const m = regex.exec(path)\n if (!m) return false\n const params: Record<string, string | string[]> = {}\n for (let j = 0; j < keys.length; j++) {\n const key = keys[j]!\n const val = m[j + 1]\n if (val === undefined) continue\n params[key.name] = key.wildcard ? val.split('/').map(decode) : decode(val)\n }\n return { params }\n }\n}\n\n/**\n * RevealUI Router - Lightweight file-based routing with SSR support\n */\nexport class Router {\n private routes: Route[] = []\n private options: RouterOptions\n private listeners: Set<() => void> = new Set()\n private currentMatch: RouteMatch | null = null\n\n constructor(options: RouterOptions = {}) {\n this.options = {\n basePath: '',\n ...options,\n }\n }\n\n /**\n * Register a route\n */\n register(route: Route): void {\n this.routes.push(route)\n }\n\n /**\n * Register multiple routes\n */\n registerRoutes(routes: Route[]): void {\n routes.forEach((route) => {\n this.register(route)\n })\n }\n\n /**\n * Match a URL to a route\n */\n match(url: string): RouteMatch | null {\n // Remove base path if present\n const path = this.normalizePath(url)\n\n // Try to match each route\n for (const route of this.routes) {\n const matcher = pathMatch(route.path, { decode: decodeURIComponent })\n const result = matcher(path)\n\n if (result) {\n return {\n route,\n params: (result.params as RouteParams) || {},\n }\n }\n }\n\n return null\n }\n\n /**\n * Resolve a route with data loading\n */\n async resolve(url: string): Promise<RouteMatch | null> {\n const matched = this.match(url)\n\n if (!matched) {\n return null\n }\n\n // Load data if loader exists\n if (matched.route.loader) {\n try {\n matched.data = await matched.route.loader(matched.params)\n } catch (error) {\n logger.error(\n 'Route loader error',\n error instanceof Error ? error : new Error(String(error)),\n )\n throw error\n }\n }\n\n // Store match for SSR (if on server)\n if (typeof window === 'undefined') {\n this.currentMatch = matched\n }\n\n return matched\n }\n\n /**\n * Navigate to a URL (client-side only)\n */\n navigate(url: string, options: NavigateOptions = {}): void {\n if (typeof window === 'undefined') {\n return\n }\n\n const fullUrl = this.options.basePath + url\n\n if (options.replace) {\n window.history.replaceState(options.state || null, '', fullUrl)\n } else {\n window.history.pushState(options.state || null, '', fullUrl)\n }\n\n this.notifyListeners()\n }\n\n /**\n * Go back in history\n */\n back(): void {\n if (typeof window !== 'undefined') {\n window.history.back()\n }\n }\n\n /**\n * Go forward in history\n */\n forward(): void {\n if (typeof window !== 'undefined') {\n window.history.forward()\n }\n }\n\n /**\n * Subscribe to route changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /**\n * Get current route match\n */\n getCurrentMatch(): RouteMatch | null {\n // On server, return stored match (set by resolve during SSR)\n if (typeof window === 'undefined') {\n return this.currentMatch\n }\n\n // On client, match against current URL\n return this.match(window.location.pathname)\n }\n\n /**\n * Get all registered routes\n */\n getRoutes(): Route[] {\n return [...this.routes]\n }\n\n /**\n * Clear all routes\n */\n clear(): void {\n this.routes = []\n }\n\n private normalizePath(url: string): string {\n // Remove base path\n let path = url\n if (this.options.basePath && path.startsWith(this.options.basePath)) {\n path = path.slice(this.options.basePath.length)\n }\n\n // Remove query string and hash\n path = path.split('?')[0].split('#')[0]\n\n // Ensure leading slash\n if (!path.startsWith('/')) {\n path = `/${path}`\n }\n\n return path\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => {\n listener()\n })\n }\n\n /**\n * Initialize client-side routing\n */\n initClient(): void {\n if (typeof window === 'undefined') {\n return\n }\n\n // Handle browser back/forward buttons\n window.addEventListener('popstate', () => {\n this.notifyListeners()\n })\n\n // Intercept link clicks\n document.addEventListener('click', (e) => {\n const target = (e.target as HTMLElement).closest('a')\n\n if (!target) return\n\n const href = target.getAttribute('href')\n\n // Only handle internal links\n if (\n href?.startsWith('/') &&\n !target.hasAttribute('target') &&\n !target.hasAttribute('download') &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey\n ) {\n e.preventDefault()\n this.navigate(href)\n }\n })\n }\n}\n"],"mappings":"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","names":[]}
package/dist/server.js CHANGED
@@ -79,20 +79,55 @@ function NotFound() {
79
79
  }
80
80
 
81
81
  // src/router.ts
82
- import { match as pathMatch } from "path-to-regexp";
83
-
84
- // ../core/src/observability/logger.ts
85
- import { logger as utilsLogger } from "@revealui/utils/logger";
86
- import {
87
- createLogger,
88
- Logger,
89
- logAudit,
90
- logError,
91
- logger,
92
- logQuery
93
- } from "@revealui/utils/logger";
94
-
95
- // src/router.ts
82
+ import { logger } from "@revealui/core/observability/logger";
83
+ function compilePathPattern(pattern) {
84
+ const keys = [];
85
+ let src = "^";
86
+ let i = 0;
87
+ while (i < pattern.length) {
88
+ const ch = pattern[i];
89
+ if (ch === "{") {
90
+ src += "(?:";
91
+ i++;
92
+ } else if (ch === "}") {
93
+ src += ")?";
94
+ i++;
95
+ } else if (ch === ":") {
96
+ i++;
97
+ let name = "";
98
+ while (i < pattern.length && /\w/.test(pattern[i])) name += pattern[i++];
99
+ keys.push({ name, wildcard: false });
100
+ src += "([^/]+)";
101
+ } else if (ch === "*") {
102
+ i++;
103
+ let name = "";
104
+ while (i < pattern.length && /\w/.test(pattern[i])) name += pattern[i++];
105
+ keys.push({ name: name || "0", wildcard: true });
106
+ src += "(.+)";
107
+ } else {
108
+ src += ch.replace(/[.+?^$|()[\]\\]/g, "\\$&");
109
+ i++;
110
+ }
111
+ }
112
+ src += "$";
113
+ return { regex: new RegExp(src), keys };
114
+ }
115
+ function pathMatch(pattern, options = {}) {
116
+ const { regex, keys } = compilePathPattern(pattern);
117
+ const decode = options.decode ?? ((s) => s);
118
+ return (path) => {
119
+ const m = regex.exec(path);
120
+ if (!m) return false;
121
+ const params = {};
122
+ for (let j = 0; j < keys.length; j++) {
123
+ const key = keys[j];
124
+ const val = m[j + 1];
125
+ if (val === void 0) continue;
126
+ params[key.name] = key.wildcard ? val.split("/").map(decode) : decode(val);
127
+ }
128
+ return { params };
129
+ };
130
+ }
96
131
  var Router = class {
97
132
  routes = [];
98
133
  options;
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server.tsx","../src/components.tsx","../src/router.ts","../../core/src/observability/logger.ts"],"sourcesContent":["import { logger } from '@revealui/core/observability/logger'\nimport type { Context } from 'hono'\nimport { renderToPipeableStream, renderToString } from 'react-dom/server'\nimport { RouterProvider, Routes } from './components'\nimport { Router } from './router'\nimport type { Route } from './types'\n\n/**\n * SSR Options\n */\nexport interface SSROptions {\n /** HTML template function */\n template?: (html: string, data?: Record<string, unknown>) => string\n /** Enable streaming SSR */\n streaming?: boolean\n /** Error handler */\n onError?: (error: Error, context: Context) => void\n}\n\n/**\n * Create a Hono handler for SSR\n */\nexport function createSSRHandler(routes: Route[], options: SSROptions = {}) {\n const router = new Router()\n router.registerRoutes(routes)\n\n const defaultTemplate = (html: string, data?: Record<string, unknown>) =>\n `\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${data?.title || 'RevealUI'}</title>\n ${data?.meta || ''}\n </head>\n <body>\n <div id=\"root\">${html}</div>\n <script id=\"__REVEALUI_DATA__\" type=\"application/json\">${JSON.stringify(data || {})}</script>\n <script type=\"module\" src=\"/src/client.tsx\"></script>\n </body>\n</html>\n `.trim()\n\n const template = options.template || defaultTemplate\n\n return async (c: Context) => {\n const url = c.req.url\n const pathname = new URL(url).pathname\n\n try {\n // Match and resolve route\n logger.debug('Attempting to match pathname', { pathname })\n const match = await router.resolve(pathname)\n logger.debug('Match result', {\n match: match ? { path: match.route.path, hasComponent: !!match.route.component } : null,\n })\n\n if (!match) {\n c.status(404)\n return c.html(template('<div>404 - Page Not Found</div>'))\n }\n\n // Render with streaming if enabled\n if (options.streaming) {\n return new Promise<Response>((resolve, reject) => {\n const { pipe } = renderToPipeableStream(\n <RouterProvider router={router}>\n <Routes />\n </RouterProvider>,\n {\n onShellReady() {\n c.header('Content-Type', 'text/html')\n const html = pipe\n // biome-ignore lint/suspicious/noExplicitAny: pipe stream typing incompatibility with Hono\n resolve(c.body(html as any) as Response)\n },\n onError(error) {\n logger.error('SSR error', error instanceof Error ? error : new Error(String(error)))\n if (options.onError) {\n options.onError(error as Error, c)\n }\n reject(error)\n },\n },\n )\n })\n }\n\n // Regular SSR\n const html = renderToString(\n <RouterProvider router={router}>\n <Routes />\n </RouterProvider>,\n )\n\n const data = {\n route: match.route.path,\n params: match.params,\n data: match.data,\n title: match.route.meta?.title,\n }\n\n return c.html(template(html, data))\n } catch (error) {\n logger.error('SSR error', error instanceof Error ? error : new Error(String(error)))\n\n if (options.onError) {\n options.onError(error as Error, c)\n }\n\n c.status(500)\n return c.html(template('<div>500 - Server Error</div>'))\n }\n }\n}\n\n/**\n * Create a simple dev server with Hono\n */\nexport async function createDevServer(\n routes: Route[],\n options: SSROptions & { port?: number } = {},\n) {\n const { Hono } = await import('hono')\n const { serve } = await import('@hono/node-server')\n\n const app = new Hono()\n const port = options.port || 3000\n\n // Static files (you'll need to add your own static middleware)\n // app.use('/assets/*', serveStatic({ root: './public' }))\n\n // SSR handler for all routes\n app.get('*', createSSRHandler(routes, options))\n\n const server = serve({ fetch: app.fetch, port })\n\n logger.info('RevealUI dev server running', { url: `http://localhost:${port}` })\n\n return server\n}\n\n/**\n * Hydrate the client-side app\n */\nexport async function hydrate(router: Router, rootElement: HTMLElement | null = null) {\n if (typeof window === 'undefined') {\n return\n }\n\n const root = rootElement || document.getElementById('root')\n\n if (!root) {\n logger.error('Root element not found', new Error('Root element not found'))\n return\n }\n\n // Get SSR data\n const dataScript = document.getElementById('__REVEALUI_DATA__')\n const _ssrData = dataScript ? JSON.parse(dataScript.textContent || '{}') : {}\n\n // Initialize client-side routing\n router.initClient()\n\n // Use hydrateRoot for React 18+\n const { hydrateRoot } = require('react-dom/client')\n\n hydrateRoot(\n root,\n <RouterProvider router={router}>\n <Routes />\n </RouterProvider>,\n )\n\n logger.info('RevealUI hydrated')\n}\n","import type React from 'react'\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react'\nimport type { Router } from './router'\nimport type { NavigateOptions, RouteMatch } from './types'\n\n/**\n * Router context\n */\nconst RouterContext = createContext<Router | null>(null)\n\n/**\n * Current match context\n */\nconst MatchContext = createContext<RouteMatch | null>(null)\n\n/**\n * RouterProvider - Provides router instance to the app\n */\nexport function RouterProvider({\n router,\n children,\n}: {\n router: Router\n children: React.ReactNode\n}) {\n return <RouterContext.Provider value={router}>{children}</RouterContext.Provider>\n}\n\n/**\n * Routes - Renders the matched route component\n */\nexport function Routes() {\n const router = useRouter()\n\n // Subscribe to router changes\n const match = useSyncExternalStore(\n (callback) => router.subscribe(callback),\n () => router.getCurrentMatch(),\n () => router.getCurrentMatch(), // Server-side snapshot (same as client)\n )\n\n if (!match) {\n return <NotFound />\n }\n\n const { route, params, data } = match\n const Component = route.component\n const Layout = route.layout\n\n const element = <Component params={params} data={data} />\n\n return (\n <MatchContext.Provider value={match}>\n {Layout ? <Layout>{element}</Layout> : element}\n </MatchContext.Provider>\n )\n}\n\n/**\n * Link - Client-side navigation link\n */\nexport function Link({\n to,\n replace = false,\n children,\n className,\n style,\n onClick,\n ...props\n}: {\n to: string\n replace?: boolean\n children: React.ReactNode\n className?: string\n style?: React.CSSProperties\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n [key: string]: unknown\n}) {\n const router = useRouter()\n\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Call custom onClick if provided\n onClick?.(e)\n\n // Don't navigate if default was prevented\n if (e.defaultPrevented) {\n return\n }\n\n // Only handle left clicks\n if (e.button !== 0) {\n return\n }\n\n // Ignore if modifier keys are pressed\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return\n }\n\n e.preventDefault()\n router.navigate(to, { replace })\n }\n\n return (\n <a href={to} onClick={handleClick} className={className} style={style} {...props}>\n {children}\n </a>\n )\n}\n\n/**\n * useRouter - Hook to access router instance\n */\nexport function useRouter(): Router {\n const router = useContext(RouterContext)\n\n if (!router) {\n throw new Error('useRouter must be used within a RouterProvider')\n }\n\n return router\n}\n\n/**\n * useMatch - Hook to access current route match\n */\nexport function useMatch(): RouteMatch | null {\n return useContext(MatchContext)\n}\n\n/**\n * useParams - Hook to access route parameters\n */\nexport function useParams<T = Record<string, string>>(): T {\n const match = useMatch()\n return (match?.params as T) || ({} as T)\n}\n\n/**\n * useData - Hook to access route data\n */\nexport function useData<T = unknown>(): T | undefined {\n const match = useMatch()\n return match?.data as T | undefined\n}\n\n/**\n * useNavigate - Hook to get navigation function\n */\nexport function useNavigate() {\n const router = useRouter()\n\n return (to: string, options?: NavigateOptions) => {\n router.navigate(to, options)\n }\n}\n\n/**\n * NotFound - Default 404 component\n */\nfunction NotFound() {\n return (\n <div style={{ padding: '2rem', textAlign: 'center' }}>\n <h1>404 - Page Not Found</h1>\n <p>The page you're looking for doesn't exist.</p>\n <Link to=\"/\">Go Home</Link>\n </div>\n )\n}\n\n/**\n * Navigate - Component for declarative navigation\n */\nexport function Navigate({ to, replace = false }: { to: string; replace?: boolean }) {\n const router = useRouter()\n\n useEffect(() => {\n router.navigate(to, { replace })\n }, [to, replace, router])\n\n return null\n}\n","import { match as pathMatch } from 'path-to-regexp'\nimport { logger } from '../../core/src/observability/logger.js'\nimport type { NavigateOptions, Route, RouteMatch, RouteParams, RouterOptions } from './types'\n\n/**\n * RevealUI Router - Lightweight file-based routing with SSR support\n */\nexport class Router {\n private routes: Route[] = []\n private options: RouterOptions\n private listeners: Set<() => void> = new Set()\n private currentMatch: RouteMatch | null = null\n\n constructor(options: RouterOptions = {}) {\n this.options = {\n basePath: '',\n ...options,\n }\n }\n\n /**\n * Register a route\n */\n register(route: Route): void {\n this.routes.push(route)\n }\n\n /**\n * Register multiple routes\n */\n registerRoutes(routes: Route[]): void {\n routes.forEach((route) => {\n this.register(route)\n })\n }\n\n /**\n * Match a URL to a route\n */\n match(url: string): RouteMatch | null {\n // Remove base path if present\n const path = this.normalizePath(url)\n\n // Try to match each route\n for (const route of this.routes) {\n const matcher = pathMatch(route.path, { decode: decodeURIComponent })\n const result = matcher(path)\n\n if (result) {\n return {\n route,\n params: (result.params as RouteParams) || {},\n }\n }\n }\n\n return null\n }\n\n /**\n * Resolve a route with data loading\n */\n async resolve(url: string): Promise<RouteMatch | null> {\n const matched = this.match(url)\n\n if (!matched) {\n return null\n }\n\n // Load data if loader exists\n if (matched.route.loader) {\n try {\n matched.data = await matched.route.loader(matched.params)\n } catch (error) {\n logger.error(\n 'Route loader error',\n error instanceof Error ? error : new Error(String(error)),\n )\n throw error\n }\n }\n\n // Store match for SSR (if on server)\n if (typeof window === 'undefined') {\n this.currentMatch = matched\n }\n\n return matched\n }\n\n /**\n * Navigate to a URL (client-side only)\n */\n navigate(url: string, options: NavigateOptions = {}): void {\n if (typeof window === 'undefined') {\n return\n }\n\n const fullUrl = this.options.basePath + url\n\n if (options.replace) {\n window.history.replaceState(options.state || null, '', fullUrl)\n } else {\n window.history.pushState(options.state || null, '', fullUrl)\n }\n\n this.notifyListeners()\n }\n\n /**\n * Go back in history\n */\n back(): void {\n if (typeof window !== 'undefined') {\n window.history.back()\n }\n }\n\n /**\n * Go forward in history\n */\n forward(): void {\n if (typeof window !== 'undefined') {\n window.history.forward()\n }\n }\n\n /**\n * Subscribe to route changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /**\n * Get current route match\n */\n getCurrentMatch(): RouteMatch | null {\n // On server, return stored match (set by resolve during SSR)\n if (typeof window === 'undefined') {\n return this.currentMatch\n }\n\n // On client, match against current URL\n return this.match(window.location.pathname)\n }\n\n /**\n * Get all registered routes\n */\n getRoutes(): Route[] {\n return [...this.routes]\n }\n\n /**\n * Clear all routes\n */\n clear(): void {\n this.routes = []\n }\n\n private normalizePath(url: string): string {\n // Remove base path\n let path = url\n if (this.options.basePath && path.startsWith(this.options.basePath)) {\n path = path.slice(this.options.basePath.length)\n }\n\n // Remove query string and hash\n path = path.split('?')[0].split('#')[0]\n\n // Ensure leading slash\n if (!path.startsWith('/')) {\n path = `/${path}`\n }\n\n return path\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => {\n listener()\n })\n }\n\n /**\n * Initialize client-side routing\n */\n initClient(): void {\n if (typeof window === 'undefined') {\n return\n }\n\n // Handle browser back/forward buttons\n window.addEventListener('popstate', () => {\n this.notifyListeners()\n })\n\n // Intercept link clicks\n document.addEventListener('click', (e) => {\n const target = (e.target as HTMLElement).closest('a')\n\n if (!target) return\n\n const href = target.getAttribute('href')\n\n // Only handle internal links\n if (\n href?.startsWith('/') &&\n !target.hasAttribute('target') &&\n !target.hasAttribute('download') &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey\n ) {\n e.preventDefault()\n this.navigate(href)\n }\n })\n }\n}\n","/**\n * Structured Logging Infrastructure\n *\n * Re-exports from @revealui/utils to maintain backward compatibility.\n * The actual implementation has been moved to @revealui/utils to break circular dependencies.\n */\n\n// Import logger for internal use\nimport { logger as utilsLogger } from '@revealui/utils/logger'\n\n// Re-export all types and functions from utils\nexport type {\n LogContext,\n LogEntry,\n LoggerConfig,\n LogLevel,\n} from '@revealui/utils/logger'\n\nexport {\n createLogger,\n Logger,\n logAudit,\n logError,\n logger,\n logQuery,\n} from '@revealui/utils/logger'\n\n// Additional helper functions that were in core but not in utils\n// These can stay here as they're core-specific\n\n/**\n * Request logger middleware\n */\nexport function createRequestLogger<TRequest = unknown, TResponse = unknown>(\n options: { includeBody?: boolean; includeHeaders?: boolean } = {},\n) {\n return async (\n request: TRequest & {\n method: string\n url: string\n headers?: { get?: (key: string) => string | null; entries?: () => Iterable<[string, string]> }\n },\n next: () => Promise<TResponse>,\n ): Promise<TResponse> => {\n // Import logger at runtime to avoid circular deps\n const { logger } = await import('@revealui/utils/logger')\n const requestId = crypto.randomUUID()\n const startTime = Date.now()\n\n const requestLogger = logger.child({\n requestId,\n method: request.method,\n url: request.url,\n userAgent: request.headers?.get?.('user-agent'),\n })\n\n requestLogger.info('Request started')\n\n if (options.includeHeaders) {\n requestLogger.debug('Request headers', {\n headers: Object.fromEntries(request.headers?.entries?.() || []),\n })\n }\n\n try {\n const response = await next()\n\n const duration = Date.now() - startTime\n const responseWithStatus = response as typeof response & { status?: number }\n\n requestLogger.info('Request completed', {\n status: responseWithStatus.status ?? 200,\n duration,\n })\n\n return response\n } catch (error) {\n const duration = Date.now() - startTime\n\n requestLogger.error(\n 'Request failed',\n error instanceof Error ? error : new Error(String(error)),\n { duration },\n )\n\n throw error\n }\n }\n}\n\n/**\n * Performance logger\n */\nexport function logPerformance(\n operation: string,\n duration: number,\n context?: Record<string, unknown>,\n): void {\n const level = duration > 1000 ? 'warn' : 'info'\n\n utilsLogger[level](`Performance: ${operation}`, {\n ...context,\n operation,\n duration,\n slow: duration > 1000,\n })\n}\n\n/**\n * API call logger\n */\nexport function logAPICall(\n method: string,\n url: string,\n status: number,\n duration: number,\n context?: Record<string, unknown>,\n): void {\n const apiContext = {\n ...context,\n method,\n url,\n status,\n duration,\n }\n\n if (status >= 400) {\n utilsLogger.error('API call', undefined, apiContext)\n } else if (status >= 300) {\n utilsLogger.warn('API call', apiContext)\n } else {\n utilsLogger.info('API call', apiContext)\n }\n}\n\n/**\n * Cache operation logger\n */\nexport function logCache(\n operation: 'hit' | 'miss' | 'set' | 'delete',\n key: string,\n context?: Record<string, unknown>,\n): void {\n utilsLogger.debug(`Cache ${operation}`, {\n ...context,\n operation,\n key,\n })\n}\n\n/**\n * User action logger\n */\nexport function logUserAction(\n action: string,\n userId?: string,\n context?: Record<string, unknown>,\n): void {\n utilsLogger.info('User action', {\n ...context,\n action,\n userId,\n })\n}\n\n/**\n * System event logger\n */\nexport function logSystemEvent(event: string, context?: Record<string, unknown>): void {\n utilsLogger.info('System event', {\n ...context,\n event,\n })\n}\n\n/**\n * Sanitize sensitive data from logs\n */\nexport function sanitizeLogData(data: Record<string, unknown>): Record<string, unknown> {\n const sensitiveKeys = [\n 'password',\n 'token',\n 'secret',\n 'apiKey',\n 'accessToken',\n 'refreshToken',\n 'creditCard',\n 'ssn',\n ]\n\n const sanitized: Record<string, unknown> = {}\n\n for (const [key, value] of Object.entries(data)) {\n const lowerKey = key.toLowerCase()\n\n if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) {\n sanitized[key] = '[REDACTED]'\n } else if (typeof value === 'object' && value !== null && !Array.isArray(value)) {\n sanitized[key] = sanitizeLogData(value as Record<string, unknown>)\n } else {\n sanitized[key] = value\n }\n }\n\n return sanitized\n}\n"],"mappings":"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","names":["logger","jsx","logger","html"]}
1
+ {"version":3,"sources":["../src/server.tsx","../src/components.tsx","../src/router.ts"],"sourcesContent":["import { logger } from '@revealui/core/observability/logger'\nimport type { Context } from 'hono'\nimport { renderToPipeableStream, renderToString } from 'react-dom/server'\nimport { RouterProvider, Routes } from './components'\nimport { Router } from './router'\nimport type { Route } from './types'\n\n/**\n * SSR Options\n */\nexport interface SSROptions {\n /** HTML template function */\n template?: (html: string, data?: Record<string, unknown>) => string\n /** Enable streaming SSR */\n streaming?: boolean\n /** Error handler */\n onError?: (error: Error, context: Context) => void\n}\n\n/**\n * Create a Hono handler for SSR\n */\nexport function createSSRHandler(\n routes: Route[],\n options: SSROptions = {},\n): (c: Context) => Promise<Response> {\n const router = new Router()\n router.registerRoutes(routes)\n\n const defaultTemplate = (html: string, data?: Record<string, unknown>) =>\n `\n<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"UTF-8\">\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n <title>${data?.title || 'RevealUI'}</title>\n ${data?.meta || ''}\n </head>\n <body>\n <div id=\"root\">${html}</div>\n <script id=\"__REVEALUI_DATA__\" type=\"application/json\">${JSON.stringify(data || {})}</script>\n <script type=\"module\" src=\"/src/client.tsx\"></script>\n </body>\n</html>\n `.trim()\n\n const template = options.template || defaultTemplate\n\n return async (c: Context) => {\n const url = c.req.url\n const pathname = new URL(url).pathname\n\n try {\n // Match and resolve route\n logger.debug('Attempting to match pathname', { pathname })\n const match = await router.resolve(pathname)\n logger.debug('Match result', {\n match: match ? { path: match.route.path, hasComponent: !!match.route.component } : null,\n })\n\n if (!match) {\n c.status(404)\n return c.html(template('<div>404 - Page Not Found</div>'))\n }\n\n // Render with streaming if enabled\n if (options.streaming) {\n return new Promise<Response>((resolve, reject) => {\n const { pipe } = renderToPipeableStream(\n <RouterProvider router={router}>\n <Routes />\n </RouterProvider>,\n {\n onShellReady() {\n c.header('Content-Type', 'text/html')\n const html = pipe\n // biome-ignore lint/suspicious/noExplicitAny: pipe stream typing incompatibility with Hono\n resolve(c.body(html as any) as Response)\n },\n onError(error) {\n logger.error('SSR error', error instanceof Error ? error : new Error(String(error)))\n if (options.onError) {\n options.onError(error as Error, c)\n }\n reject(error)\n },\n },\n )\n })\n }\n\n // Regular SSR\n const html = renderToString(\n <RouterProvider router={router}>\n <Routes />\n </RouterProvider>,\n )\n\n const data = {\n route: match.route.path,\n params: match.params,\n data: match.data,\n title: match.route.meta?.title,\n }\n\n return c.html(template(html, data))\n } catch (error) {\n logger.error('SSR error', error instanceof Error ? error : new Error(String(error)))\n\n if (options.onError) {\n options.onError(error as Error, c)\n }\n\n c.status(500)\n return c.html(template('<div>500 - Server Error</div>'))\n }\n }\n}\n\n/**\n * Create a simple dev server with Hono\n */\nexport async function createDevServer(\n routes: Route[],\n options: SSROptions & { port?: number } = {},\n) {\n const { Hono } = await import('hono')\n const { serve } = await import('@hono/node-server')\n\n const app = new Hono()\n const port = options.port || 3000\n\n // Static files (you'll need to add your own static middleware)\n // app.use('/assets/*', serveStatic({ root: './public' }))\n\n // SSR handler for all routes\n app.get('*', createSSRHandler(routes, options))\n\n const server = serve({ fetch: app.fetch, port })\n\n logger.info('RevealUI dev server running', { url: `http://localhost:${port}` })\n\n return server\n}\n\n/**\n * Hydrate the client-side app\n */\nexport async function hydrate(router: Router, rootElement: HTMLElement | null = null) {\n if (typeof window === 'undefined') {\n return\n }\n\n const root = rootElement || document.getElementById('root')\n\n if (!root) {\n logger.error('Root element not found', new Error('Root element not found'))\n return\n }\n\n // Get SSR data\n const dataScript = document.getElementById('__REVEALUI_DATA__')\n const _ssrData = dataScript ? JSON.parse(dataScript.textContent || '{}') : {}\n\n // Initialize client-side routing\n router.initClient()\n\n // Use hydrateRoot for React 18+\n const { hydrateRoot } = require('react-dom/client')\n\n hydrateRoot(\n root,\n <RouterProvider router={router}>\n <Routes />\n </RouterProvider>,\n )\n\n logger.info('RevealUI hydrated')\n}\n","import type React from 'react'\nimport { createContext, useContext, useEffect, useSyncExternalStore } from 'react'\nimport type { Router } from './router'\nimport type { NavigateOptions, RouteMatch } from './types'\n\n/**\n * Router context\n */\nconst RouterContext = createContext<Router | null>(null)\n\n/**\n * Current match context\n */\nconst MatchContext = createContext<RouteMatch | null>(null)\n\n/**\n * RouterProvider - Provides router instance to the app\n */\nexport function RouterProvider({\n router,\n children,\n}: {\n router: Router\n children: React.ReactNode\n}) {\n return <RouterContext.Provider value={router}>{children}</RouterContext.Provider>\n}\n\n/**\n * Routes - Renders the matched route component\n */\nexport function Routes() {\n const router = useRouter()\n\n // Subscribe to router changes\n const match = useSyncExternalStore(\n (callback) => router.subscribe(callback),\n () => router.getCurrentMatch(),\n () => router.getCurrentMatch(), // Server-side snapshot (same as client)\n )\n\n if (!match) {\n return <NotFound />\n }\n\n const { route, params, data } = match\n const Component = route.component\n const Layout = route.layout\n\n const element = <Component params={params} data={data} />\n\n return (\n <MatchContext.Provider value={match}>\n {Layout ? <Layout>{element}</Layout> : element}\n </MatchContext.Provider>\n )\n}\n\n/**\n * Link - Client-side navigation link\n */\nexport function Link({\n to,\n replace = false,\n children,\n className,\n style,\n onClick,\n ...props\n}: {\n to: string\n replace?: boolean\n children: React.ReactNode\n className?: string\n style?: React.CSSProperties\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n [key: string]: unknown\n}) {\n const router = useRouter()\n\n const handleClick = (e: React.MouseEvent<HTMLAnchorElement>) => {\n // Call custom onClick if provided\n onClick?.(e)\n\n // Don't navigate if default was prevented\n if (e.defaultPrevented) {\n return\n }\n\n // Only handle left clicks\n if (e.button !== 0) {\n return\n }\n\n // Ignore if modifier keys are pressed\n if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {\n return\n }\n\n e.preventDefault()\n router.navigate(to, { replace })\n }\n\n return (\n <a href={to} onClick={handleClick} className={className} style={style} {...props}>\n {children}\n </a>\n )\n}\n\n/**\n * useRouter - Hook to access router instance\n */\nexport function useRouter(): Router {\n const router = useContext(RouterContext)\n\n if (!router) {\n throw new Error('useRouter must be used within a RouterProvider')\n }\n\n return router\n}\n\n/**\n * useMatch - Hook to access current route match\n */\nexport function useMatch(): RouteMatch | null {\n return useContext(MatchContext)\n}\n\n/**\n * useParams - Hook to access route parameters\n */\nexport function useParams<T = Record<string, string>>(): T {\n const match = useMatch()\n return (match?.params as T) || ({} as T)\n}\n\n/**\n * useData - Hook to access route data\n */\nexport function useData<T = unknown>(): T | undefined {\n const match = useMatch()\n return match?.data as T | undefined\n}\n\n/**\n * useNavigate - Hook to get navigation function\n */\nexport function useNavigate() {\n const router = useRouter()\n\n return (to: string, options?: NavigateOptions) => {\n router.navigate(to, options)\n }\n}\n\n/**\n * NotFound - Default 404 component\n */\nfunction NotFound() {\n return (\n <div style={{ padding: '2rem', textAlign: 'center' }}>\n <h1>404 - Page Not Found</h1>\n <p>The page you're looking for doesn't exist.</p>\n <Link to=\"/\">Go Home</Link>\n </div>\n )\n}\n\n/**\n * Navigate - Component for declarative navigation\n */\nexport function Navigate({ to, replace = false }: { to: string; replace?: boolean }) {\n const router = useRouter()\n\n useEffect(() => {\n router.navigate(to, { replace })\n }, [to, replace, router])\n\n return null\n}\n","import { logger } from '@revealui/core/observability/logger'\nimport type { NavigateOptions, Route, RouteMatch, RouteParams, RouterOptions } from './types'\n\n// ---------------------------------------------------------------------------\n// Hand-rolled path matcher — replaces `path-to-regexp`.\n// Supports: exact paths, named params (:id), wildcards (*path),\n// and optional segments ({/...}).\n// ---------------------------------------------------------------------------\n\ninterface PathKey {\n name: string\n wildcard: boolean\n}\n\nfunction compilePathPattern(pattern: string): { regex: RegExp; keys: PathKey[] } {\n const keys: PathKey[] = []\n let src = '^'\n let i = 0\n while (i < pattern.length) {\n const ch = pattern[i]!\n if (ch === '{') {\n src += '(?:'\n i++\n } else if (ch === '}') {\n src += ')?'\n i++\n } else if (ch === ':') {\n i++\n let name = ''\n while (i < pattern.length && /\\w/.test(pattern[i]!)) name += pattern[i++]\n keys.push({ name, wildcard: false })\n src += '([^/]+)'\n } else if (ch === '*') {\n i++\n let name = ''\n while (i < pattern.length && /\\w/.test(pattern[i]!)) name += pattern[i++]\n keys.push({ name: name || '0', wildcard: true })\n src += '(.+)'\n } else {\n src += ch.replace(/[.+?^$|()[\\]\\\\]/g, '\\\\$&')\n i++\n }\n }\n src += '$'\n return { regex: new RegExp(src), keys }\n}\n\nfunction pathMatch(\n pattern: string,\n options: { decode?: (s: string) => string } = {},\n): (path: string) => { params: Record<string, string | string[]> } | false {\n const { regex, keys } = compilePathPattern(pattern)\n const decode = options.decode ?? ((s) => s)\n return (path: string) => {\n const m = regex.exec(path)\n if (!m) return false\n const params: Record<string, string | string[]> = {}\n for (let j = 0; j < keys.length; j++) {\n const key = keys[j]!\n const val = m[j + 1]\n if (val === undefined) continue\n params[key.name] = key.wildcard ? val.split('/').map(decode) : decode(val)\n }\n return { params }\n }\n}\n\n/**\n * RevealUI Router - Lightweight file-based routing with SSR support\n */\nexport class Router {\n private routes: Route[] = []\n private options: RouterOptions\n private listeners: Set<() => void> = new Set()\n private currentMatch: RouteMatch | null = null\n\n constructor(options: RouterOptions = {}) {\n this.options = {\n basePath: '',\n ...options,\n }\n }\n\n /**\n * Register a route\n */\n register(route: Route): void {\n this.routes.push(route)\n }\n\n /**\n * Register multiple routes\n */\n registerRoutes(routes: Route[]): void {\n routes.forEach((route) => {\n this.register(route)\n })\n }\n\n /**\n * Match a URL to a route\n */\n match(url: string): RouteMatch | null {\n // Remove base path if present\n const path = this.normalizePath(url)\n\n // Try to match each route\n for (const route of this.routes) {\n const matcher = pathMatch(route.path, { decode: decodeURIComponent })\n const result = matcher(path)\n\n if (result) {\n return {\n route,\n params: (result.params as RouteParams) || {},\n }\n }\n }\n\n return null\n }\n\n /**\n * Resolve a route with data loading\n */\n async resolve(url: string): Promise<RouteMatch | null> {\n const matched = this.match(url)\n\n if (!matched) {\n return null\n }\n\n // Load data if loader exists\n if (matched.route.loader) {\n try {\n matched.data = await matched.route.loader(matched.params)\n } catch (error) {\n logger.error(\n 'Route loader error',\n error instanceof Error ? error : new Error(String(error)),\n )\n throw error\n }\n }\n\n // Store match for SSR (if on server)\n if (typeof window === 'undefined') {\n this.currentMatch = matched\n }\n\n return matched\n }\n\n /**\n * Navigate to a URL (client-side only)\n */\n navigate(url: string, options: NavigateOptions = {}): void {\n if (typeof window === 'undefined') {\n return\n }\n\n const fullUrl = this.options.basePath + url\n\n if (options.replace) {\n window.history.replaceState(options.state || null, '', fullUrl)\n } else {\n window.history.pushState(options.state || null, '', fullUrl)\n }\n\n this.notifyListeners()\n }\n\n /**\n * Go back in history\n */\n back(): void {\n if (typeof window !== 'undefined') {\n window.history.back()\n }\n }\n\n /**\n * Go forward in history\n */\n forward(): void {\n if (typeof window !== 'undefined') {\n window.history.forward()\n }\n }\n\n /**\n * Subscribe to route changes\n */\n subscribe(listener: () => void): () => void {\n this.listeners.add(listener)\n\n // Return unsubscribe function\n return () => {\n this.listeners.delete(listener)\n }\n }\n\n /**\n * Get current route match\n */\n getCurrentMatch(): RouteMatch | null {\n // On server, return stored match (set by resolve during SSR)\n if (typeof window === 'undefined') {\n return this.currentMatch\n }\n\n // On client, match against current URL\n return this.match(window.location.pathname)\n }\n\n /**\n * Get all registered routes\n */\n getRoutes(): Route[] {\n return [...this.routes]\n }\n\n /**\n * Clear all routes\n */\n clear(): void {\n this.routes = []\n }\n\n private normalizePath(url: string): string {\n // Remove base path\n let path = url\n if (this.options.basePath && path.startsWith(this.options.basePath)) {\n path = path.slice(this.options.basePath.length)\n }\n\n // Remove query string and hash\n path = path.split('?')[0].split('#')[0]\n\n // Ensure leading slash\n if (!path.startsWith('/')) {\n path = `/${path}`\n }\n\n return path\n }\n\n private notifyListeners(): void {\n this.listeners.forEach((listener) => {\n listener()\n })\n }\n\n /**\n * Initialize client-side routing\n */\n initClient(): void {\n if (typeof window === 'undefined') {\n return\n }\n\n // Handle browser back/forward buttons\n window.addEventListener('popstate', () => {\n this.notifyListeners()\n })\n\n // Intercept link clicks\n document.addEventListener('click', (e) => {\n const target = (e.target as HTMLElement).closest('a')\n\n if (!target) return\n\n const href = target.getAttribute('href')\n\n // Only handle internal links\n if (\n href?.startsWith('/') &&\n !target.hasAttribute('target') &&\n !target.hasAttribute('download') &&\n !e.metaKey &&\n !e.ctrlKey &&\n !e.shiftKey &&\n !e.altKey\n ) {\n e.preventDefault()\n this.navigate(href)\n }\n })\n }\n}\n"],"mappings":"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","names":["logger","jsx","logger","html"]}
package/package.json CHANGED
@@ -1,16 +1,15 @@
1
1
  {
2
2
  "name": "@revealui/router",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "license": "MIT",
5
5
  "files": [
6
6
  "dist"
7
7
  ],
8
8
  "description": "Lightweight file-based router for RevealUI with SSR support",
9
9
  "dependencies": {
10
- "@hono/node-server": "^1.19.9",
11
- "hono": "^4.11.9",
12
- "path-to-regexp": "^8.2.0",
13
- "@revealui/core": "0.2.0"
10
+ "@hono/node-server": "^1.19.10",
11
+ "hono": "^4.12.4",
12
+ "@revealui/core": "0.2.1"
14
13
  },
15
14
  "devDependencies": {
16
15
  "@types/node": "^25.3.0",