@esmx/router-react 3.0.0-rc.105

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.
@@ -0,0 +1,169 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as RouterReactModule from "./index.mjs";
3
+ describe("index.ts - Package Entry Point", () => {
4
+ describe("Hook Exports", () => {
5
+ it("should export useRouter hook", () => {
6
+ expect(RouterReactModule.useRouter).toBeDefined();
7
+ expect(typeof RouterReactModule.useRouter).toBe("function");
8
+ });
9
+ it("should export useRoute hook", () => {
10
+ expect(RouterReactModule.useRoute).toBeDefined();
11
+ expect(typeof RouterReactModule.useRoute).toBe("function");
12
+ });
13
+ it("should export useLink hook", () => {
14
+ expect(RouterReactModule.useLink).toBeDefined();
15
+ expect(typeof RouterReactModule.useLink).toBe("function");
16
+ });
17
+ it("should export useRouterViewDepth hook", () => {
18
+ expect(RouterReactModule.useRouterViewDepth).toBeDefined();
19
+ expect(typeof RouterReactModule.useRouterViewDepth).toBe(
20
+ "function"
21
+ );
22
+ });
23
+ });
24
+ describe("Component Exports", () => {
25
+ it("should export RouterProvider component", () => {
26
+ expect(RouterReactModule.RouterProvider).toBeDefined();
27
+ expect(typeof RouterReactModule.RouterProvider).toBe("function");
28
+ });
29
+ it("should export RouterLink component", () => {
30
+ expect(RouterReactModule.RouterLink).toBeDefined();
31
+ expect(typeof RouterReactModule.RouterLink).toBe("object");
32
+ });
33
+ it("should export RouterView component", () => {
34
+ expect(RouterReactModule.RouterView).toBeDefined();
35
+ expect(typeof RouterReactModule.RouterView).toBe("function");
36
+ });
37
+ });
38
+ describe("Context Exports", () => {
39
+ it("should export RouterContext", () => {
40
+ expect(RouterReactModule.RouterContext).toBeDefined();
41
+ expect(RouterReactModule.RouterContext.displayName).toBe(
42
+ "RouterContext"
43
+ );
44
+ });
45
+ it("should export RouterViewDepthContext", () => {
46
+ expect(RouterReactModule.RouterViewDepthContext).toBeDefined();
47
+ expect(RouterReactModule.RouterViewDepthContext.displayName).toBe(
48
+ "RouterViewDepthContext"
49
+ );
50
+ });
51
+ });
52
+ describe("Export Completeness", () => {
53
+ it("should export all expected functions and components", () => {
54
+ const expectedExports = [
55
+ // Hooks
56
+ "useRouter",
57
+ "useRoute",
58
+ "useLink",
59
+ "useRouterViewDepth",
60
+ // Components
61
+ "RouterProvider",
62
+ "RouterLink",
63
+ "RouterView",
64
+ // Context
65
+ "RouterContext",
66
+ "RouterViewDepthContext"
67
+ ];
68
+ expectedExports.forEach((exportName) => {
69
+ expect(RouterReactModule).toHaveProperty(exportName);
70
+ expect(Object.hasOwn(RouterReactModule, exportName)).toBe(true);
71
+ });
72
+ });
73
+ it("should not export unexpected items", () => {
74
+ const actualExports = Object.keys(RouterReactModule);
75
+ const expectedExports = [
76
+ "useRouter",
77
+ "useRoute",
78
+ "useLink",
79
+ "useRouterViewDepth",
80
+ "RouterProvider",
81
+ "RouterLink",
82
+ "RouterView",
83
+ "RouterContext",
84
+ "RouterViewDepthContext"
85
+ ];
86
+ const unexpectedExports = actualExports.filter(
87
+ (exportName) => !expectedExports.includes(exportName)
88
+ );
89
+ expect(unexpectedExports).toEqual([]);
90
+ });
91
+ });
92
+ describe("Hook Error Handling", () => {
93
+ it("hooks should throw when used incorrectly", () => {
94
+ expect(() => {
95
+ RouterReactModule.useRouter();
96
+ }).toThrow();
97
+ expect(() => {
98
+ RouterReactModule.useRoute();
99
+ }).toThrow();
100
+ });
101
+ });
102
+ describe("Component Properties", () => {
103
+ it("should have RouterProvider with displayName", () => {
104
+ expect(RouterReactModule.RouterProvider.displayName).toBe(
105
+ "RouterProvider"
106
+ );
107
+ });
108
+ it("should have RouterView with displayName", () => {
109
+ expect(RouterReactModule.RouterView.displayName).toBe("RouterView");
110
+ });
111
+ it("should have RouterLink with displayName", () => {
112
+ expect(RouterReactModule.RouterLink.displayName).toBe("RouterLink");
113
+ });
114
+ });
115
+ describe("Module Structure", () => {
116
+ it("should be a proper ES module", () => {
117
+ expect(typeof RouterReactModule).toBe("object");
118
+ expect(RouterReactModule).not.toBeNull();
119
+ expect("default" in RouterReactModule).toBe(false);
120
+ });
121
+ it("should have consistent export naming", () => {
122
+ const hookExports = [
123
+ "useRouter",
124
+ "useRoute",
125
+ "useLink",
126
+ "useRouterViewDepth"
127
+ ];
128
+ hookExports.forEach((exportName) => {
129
+ expect(exportName).toMatch(/^use[A-Z][a-zA-Z]*$/);
130
+ });
131
+ const componentExports = [
132
+ "RouterProvider",
133
+ "RouterLink",
134
+ "RouterView",
135
+ "RouterContext",
136
+ "RouterViewDepthContext"
137
+ ];
138
+ componentExports.forEach((exportName) => {
139
+ expect(exportName).toMatch(/^[A-Z][a-zA-Z]*$/);
140
+ });
141
+ });
142
+ });
143
+ describe("TypeScript Types", () => {
144
+ it("should export RouterContextValue type (via typeof)", () => {
145
+ const context = RouterReactModule.RouterContext;
146
+ expect(context).toBeDefined();
147
+ expect(context.Provider).toBeDefined();
148
+ expect(context.Consumer).toBeDefined();
149
+ });
150
+ it("should have RouterViewDepthContext with correct default value", () => {
151
+ const context = RouterReactModule.RouterViewDepthContext;
152
+ expect(context).toBeDefined();
153
+ expect(context.Provider).toBeDefined();
154
+ });
155
+ });
156
+ describe("React Best Practices", () => {
157
+ it("RouterLink should be a forwardRef component", () => {
158
+ const link = RouterReactModule.RouterLink;
159
+ expect(link.$$typeof).toBeDefined();
160
+ expect(link.render || link.type).toBeDefined();
161
+ });
162
+ it("RouterProvider should accept router and children props", () => {
163
+ expect(RouterReactModule.RouterProvider).toBeDefined();
164
+ });
165
+ it("RouterView should accept fallback prop", () => {
166
+ expect(RouterReactModule.RouterView).toBeDefined();
167
+ });
168
+ });
169
+ });
@@ -0,0 +1,35 @@
1
+ import type { RouteLayerOptions, RouteLocationInput, RouteMatchType, RouterLinkType } from '@esmx/router';
2
+ import { type CSSProperties, type ReactNode } from 'react';
3
+ /**
4
+ * Props for RouterLink component.
5
+ * Similar to RouterLinkProps from @esmx/router with React-specific additions.
6
+ */
7
+ export interface RouterLinkComponentProps {
8
+ /** Target route location (required) */
9
+ to: RouteLocationInput;
10
+ /** Navigation type */
11
+ type?: RouterLinkType;
12
+ /** @deprecated Use type='replace' instead */
13
+ replace?: boolean;
14
+ /** Active matching mode */
15
+ exact?: RouteMatchType;
16
+ /** CSS class for active state */
17
+ activeClass?: string;
18
+ /** Event(s) that trigger navigation */
19
+ event?: string | string[];
20
+ /** HTML tag to render */
21
+ tag?: string;
22
+ /** Layer navigation options */
23
+ layerOptions?: RouteLayerOptions;
24
+ /** Hook function called before navigation */
25
+ beforeNavigate?: (event: Event, eventName: string) => void;
26
+ /** Link content */
27
+ children?: ReactNode;
28
+ /** Additional CSS class name */
29
+ className?: string;
30
+ /** Inline styles */
31
+ style?: CSSProperties;
32
+ }
33
+ export declare const RouterLink: React.ForwardRefExoticComponent<RouterLinkComponentProps & {
34
+ [key: string]: any;
35
+ } & React.RefAttributes<HTMLAnchorElement>>;
@@ -0,0 +1,85 @@
1
+ import {
2
+ createElement,
3
+ forwardRef,
4
+ useCallback,
5
+ useMemo
6
+ } from "react";
7
+ import { useRouter } from "./context.mjs";
8
+ function RouterLinkInner(props, ref) {
9
+ const {
10
+ to,
11
+ type = "push",
12
+ replace,
13
+ exact = "include",
14
+ activeClass,
15
+ event = "click",
16
+ tag = "a",
17
+ layerOptions,
18
+ beforeNavigate,
19
+ children,
20
+ className,
21
+ style,
22
+ ...rest
23
+ } = props;
24
+ const router = useRouter();
25
+ const linkResolved = useMemo(() => {
26
+ return router.resolveLink({
27
+ to,
28
+ type,
29
+ replace,
30
+ exact,
31
+ activeClass,
32
+ event,
33
+ tag,
34
+ layerOptions,
35
+ beforeNavigate
36
+ });
37
+ }, [
38
+ router,
39
+ to,
40
+ type,
41
+ replace,
42
+ exact,
43
+ activeClass,
44
+ event,
45
+ tag,
46
+ layerOptions,
47
+ beforeNavigate
48
+ ]);
49
+ const handleClick = useCallback(
50
+ async (e) => {
51
+ beforeNavigate == null ? void 0 : beforeNavigate(e, "click");
52
+ if (e.defaultPrevented) return;
53
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
54
+ if (e.button !== 0) return;
55
+ e.preventDefault();
56
+ await linkResolved.navigate(e);
57
+ },
58
+ [linkResolved, beforeNavigate]
59
+ );
60
+ const computedClassName = useMemo(() => {
61
+ const classes = [];
62
+ if (linkResolved.attributes.class) {
63
+ classes.push(linkResolved.attributes.class);
64
+ }
65
+ if (className) {
66
+ classes.push(className);
67
+ }
68
+ return classes.join(" ") || void 0;
69
+ }, [linkResolved.attributes.class, className]);
70
+ const elementProps = {
71
+ ref,
72
+ href: linkResolved.attributes.href,
73
+ target: linkResolved.attributes.target,
74
+ rel: linkResolved.attributes.rel,
75
+ className: computedClassName,
76
+ style,
77
+ onClick: handleClick,
78
+ ...rest
79
+ };
80
+ return createElement(tag, elementProps, children);
81
+ }
82
+ export const RouterLink = forwardRef(
83
+ RouterLinkInner
84
+ );
85
+ RouterLink.displayName = "RouterLink";
@@ -0,0 +1,61 @@
1
+ import type { RouterProviderProps } from './types';
2
+ /**
3
+ * RouterProvider component that provides router context to the React tree.
4
+ * This must wrap your application to enable routing functionality.
5
+ * Uses useSyncExternalStore for optimal React 18+ integration with concurrent features.
6
+ *
7
+ * @param props - Component props
8
+ * @param props.router - Router instance to provide
9
+ * @param props.children - Child components
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * import { Router, RouterMode } from '@esmx/router';
14
+ * import { RouterProvider } from '@esmx/router-react';
15
+ *
16
+ * const routes = [
17
+ * { path: '/', component: Home },
18
+ * { path: '/about', component: About }
19
+ * ];
20
+ *
21
+ * const router = new Router({
22
+ * routes,
23
+ * mode: RouterMode.history
24
+ * });
25
+ *
26
+ * function App() {
27
+ * return (
28
+ * <RouterProvider router={router}>
29
+ * <Layout>
30
+ * <RouterView />
31
+ * </Layout>
32
+ * </RouterProvider>
33
+ * );
34
+ * }
35
+ * ```
36
+ *
37
+ * @example
38
+ * ```tsx
39
+ * // SSR usage - initialize router with server URL
40
+ * import { Router, RouterMode } from '@esmx/router';
41
+ * import { RouterProvider } from '@esmx/router-react';
42
+ *
43
+ * const router = new Router({
44
+ * routes,
45
+ * mode: RouterMode.history,
46
+ * base: new URL(serverUrl)
47
+ * });
48
+ *
49
+ * function ServerApp() {
50
+ * return (
51
+ * <RouterProvider router={router}>
52
+ * <App />
53
+ * </RouterProvider>
54
+ * );
55
+ * }
56
+ * ```
57
+ */
58
+ export declare function RouterProvider({ router, children }: RouterProviderProps): React.ReactElement;
59
+ export declare namespace RouterProvider {
60
+ var displayName: string;
61
+ }
@@ -0,0 +1,34 @@
1
+ import { createElement, useCallback, useSyncExternalStore } from "react";
2
+ import { RouterContext } from "./context.mjs";
3
+ export function RouterProvider({
4
+ router,
5
+ children
6
+ }) {
7
+ const subscribe = useCallback(
8
+ (callback) => {
9
+ return router.afterEach(callback);
10
+ },
11
+ [router]
12
+ );
13
+ const getSnapshot = useCallback(() => {
14
+ return router.route;
15
+ }, [router]);
16
+ const getServerSnapshot = useCallback(() => {
17
+ return router.route;
18
+ }, [router]);
19
+ const route = useSyncExternalStore(
20
+ subscribe,
21
+ getSnapshot,
22
+ getServerSnapshot
23
+ );
24
+ const contextValue = {
25
+ router,
26
+ route
27
+ };
28
+ return createElement(
29
+ RouterContext.Provider,
30
+ { value: contextValue },
31
+ children
32
+ );
33
+ }
34
+ RouterProvider.displayName = "RouterProvider";
@@ -0,0 +1,63 @@
1
+ import { type ReactElement } from 'react';
2
+ import type { RouterViewProps } from './types';
3
+ /**
4
+ * RouterView component that renders the matched route component.
5
+ * Acts as a placeholder where route components are rendered based on the current route.
6
+ * Supports nested routing with automatic depth tracking.
7
+ *
8
+ * @param props - Component props
9
+ * @param props.fallback - Optional fallback component when no route matches
10
+ *
11
+ * @example
12
+ * ```tsx
13
+ * // Basic usage
14
+ * import { RouterView } from '@esmx/router-react';
15
+ *
16
+ * function App() {
17
+ * return (
18
+ * <div>
19
+ * <nav>
20
+ * <RouterLink to="/">Home</RouterLink>
21
+ * <RouterLink to="/about">About</RouterLink>
22
+ * </nav>
23
+ * <RouterView />
24
+ * </div>
25
+ * );
26
+ * }
27
+ * ```
28
+ *
29
+ * @example
30
+ * ```tsx
31
+ * // Nested routing
32
+ * // Routes: [
33
+ * // { path: '/users', component: UsersLayout, children: [
34
+ * // { path: ':id', component: UserProfile }
35
+ * // ]}
36
+ * // ]
37
+ *
38
+ * function UsersLayout() {
39
+ * return (
40
+ * <div>
41
+ * <h1>Users</h1>
42
+ * <RouterView /> // Renders UserProfile for /users/:id
43
+ * </div>
44
+ * );
45
+ * }
46
+ * ```
47
+ *
48
+ * @example
49
+ * ```tsx
50
+ * // With fallback
51
+ * import { RouterView } from '@esmx/router-react';
52
+ *
53
+ * function App() {
54
+ * return (
55
+ * <RouterView fallback={<div>Page not found</div>} />
56
+ * );
57
+ * }
58
+ * ```
59
+ */
60
+ export declare function RouterView({ fallback }: RouterViewProps): ReactElement | null;
61
+ export declare namespace RouterView {
62
+ var displayName: string;
63
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ createElement,
3
+ isValidElement,
4
+ useMemo
5
+ } from "react";
6
+ import {
7
+ RouterViewDepthContext,
8
+ useRoute,
9
+ useRouterViewDepth
10
+ } from "./context.mjs";
11
+ import { resolveComponent } from "./util.mjs";
12
+ export function RouterView({ fallback }) {
13
+ const route = useRoute();
14
+ const depth = useRouterViewDepth();
15
+ const matchedRoute = route.matched[depth];
16
+ const Component = useMemo(() => {
17
+ if (!(matchedRoute == null ? void 0 : matchedRoute.component)) {
18
+ return null;
19
+ }
20
+ return resolveComponent(matchedRoute.component);
21
+ }, [matchedRoute == null ? void 0 : matchedRoute.component]);
22
+ if (!Component) {
23
+ if (fallback) {
24
+ if (isValidElement(fallback)) {
25
+ return fallback;
26
+ }
27
+ if (typeof fallback === "function") {
28
+ return createElement(fallback);
29
+ }
30
+ return null;
31
+ }
32
+ return null;
33
+ }
34
+ return createElement(
35
+ RouterViewDepthContext.Provider,
36
+ { value: depth + 1 },
37
+ createElement(Component, { key: matchedRoute.compilePath })
38
+ );
39
+ }
40
+ RouterView.displayName = "RouterView";
@@ -0,0 +1,27 @@
1
+ import type { Route, Router } from '@esmx/router';
2
+ /**
3
+ * Interface for the router context value.
4
+ * Contains the router instance and current route.
5
+ */
6
+ export interface RouterContextValue {
7
+ /** Router instance for navigation */
8
+ router: Router;
9
+ /** Current route object */
10
+ route: Route;
11
+ }
12
+ /**
13
+ * Props for the RouterProvider component.
14
+ */
15
+ export interface RouterProviderProps {
16
+ /** Router instance to provide to child components */
17
+ router: Router;
18
+ /** Child components */
19
+ children: React.ReactNode;
20
+ }
21
+ /**
22
+ * Props for the RouterView component.
23
+ */
24
+ export interface RouterViewProps {
25
+ /** Optional fallback component to render when no route matches */
26
+ fallback?: React.ComponentType | React.ReactNode;
27
+ }
package/dist/types.mjs ADDED
File without changes
@@ -0,0 +1,97 @@
1
+ import type { RouterLinkProps, RouterLinkResolved } from '@esmx/router';
2
+ /**
3
+ * Hook to create reactive link helpers for custom navigation components.
4
+ * Returns a resolved link object with attributes, state, and event handlers.
5
+ *
6
+ * This hook is useful when you need to build custom link components
7
+ * with full control over rendering while retaining router functionality.
8
+ *
9
+ * @param props - RouterLink properties
10
+ * @returns Resolved link object with attributes, state, and navigation methods
11
+ *
12
+ * @example
13
+ * ```tsx
14
+ * import { useLink } from '@esmx/router-react';
15
+ *
16
+ * function CustomNavButton({ to, children }) {
17
+ * const link = useLink({ to, type: 'push', exact: 'include' });
18
+ *
19
+ * return (
20
+ * <button
21
+ * onClick={(e) => link.navigate(e)}
22
+ * className={link.isActive ? 'active' : ''}
23
+ * disabled={link.isExactActive}
24
+ * >
25
+ * {children}
26
+ * {link.isActive && <span>✓</span>}
27
+ * </button>
28
+ * );
29
+ * }
30
+ * ```
31
+ *
32
+ * @example
33
+ * ```tsx
34
+ * // Building a custom navigation card
35
+ * import { useLink } from '@esmx/router-react';
36
+ *
37
+ * interface NavCardProps {
38
+ * to: string;
39
+ * title: string;
40
+ * description: string;
41
+ * icon: React.ReactNode;
42
+ * }
43
+ *
44
+ * function NavCard({ to, title, description, icon }: NavCardProps) {
45
+ * const link = useLink({ to });
46
+ *
47
+ * return (
48
+ * <div
49
+ * className={`nav-card ${link.isActive ? 'active' : ''}`}
50
+ * onClick={(e) => link.navigate(e)}
51
+ * role="link"
52
+ * tabIndex={0}
53
+ * >
54
+ * <div className="icon">{icon}</div>
55
+ * <h3>{title}</h3>
56
+ * <p>{description}</p>
57
+ * {link.isExternal && <span className="external-badge">↗</span>}
58
+ * </div>
59
+ * );
60
+ * }
61
+ * ```
62
+ *
63
+ * @example
64
+ * ```tsx
65
+ * // Using all resolved properties
66
+ * import { useLink } from '@esmx/router-react';
67
+ *
68
+ * function DebugLink({ to }) {
69
+ * const link = useLink({ to, exact: 'exact' });
70
+ *
71
+ * return (
72
+ * <div>
73
+ * <a
74
+ * href={link.attributes.href}
75
+ * target={link.attributes.target}
76
+ * rel={link.attributes.rel}
77
+ * className={link.attributes.class}
78
+ * onClick={(e) => {
79
+ * e.preventDefault();
80
+ * link.navigate(e);
81
+ * }}
82
+ * >
83
+ * Link Text
84
+ * </a>
85
+ * <pre>
86
+ * isActive: {String(link.isActive)}
87
+ * isExactActive: {String(link.isExactActive)}
88
+ * isExternal: {String(link.isExternal)}
89
+ * type: {link.type}
90
+ * tag: {link.tag}
91
+ * </pre>
92
+ * </div>
93
+ * );
94
+ * }
95
+ * ```
96
+ */
97
+ export declare function useLink(props: RouterLinkProps): RouterLinkResolved;
@@ -0,0 +1,40 @@
1
+ import { useMemo } from "react";
2
+ import { useRouter } from "./context.mjs";
3
+ export function useLink(props) {
4
+ const router = useRouter();
5
+ const {
6
+ to,
7
+ type,
8
+ replace,
9
+ exact,
10
+ activeClass,
11
+ event,
12
+ tag,
13
+ layerOptions,
14
+ beforeNavigate
15
+ } = props;
16
+ return useMemo(() => {
17
+ return router.resolveLink({
18
+ to,
19
+ type,
20
+ replace,
21
+ exact,
22
+ activeClass,
23
+ event,
24
+ tag,
25
+ layerOptions,
26
+ beforeNavigate
27
+ });
28
+ }, [
29
+ router,
30
+ to,
31
+ type,
32
+ replace,
33
+ exact,
34
+ activeClass,
35
+ event,
36
+ tag,
37
+ layerOptions,
38
+ beforeNavigate
39
+ ]);
40
+ }
package/dist/util.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Check if a value is an ES module.
3
+ * @param obj - The object to check
4
+ * @returns True if the object is an ES module
5
+ */
6
+ export declare function isESModule(obj: unknown): obj is Record<string | symbol, any>;
7
+ /**
8
+ * Resolve a component from potentially wrapped module format.
9
+ * Handles ES modules with default exports.
10
+ * @param component - The component to resolve
11
+ * @returns The resolved component
12
+ */
13
+ export declare function resolveComponent(component: unknown): unknown;
package/dist/util.mjs ADDED
@@ -0,0 +1,15 @@
1
+ export function isESModule(obj) {
2
+ if (!obj || typeof obj !== "object") return false;
3
+ const module = obj;
4
+ return Boolean(module.__esModule) || module[Symbol.toStringTag] === "Module";
5
+ }
6
+ export function resolveComponent(component) {
7
+ if (!component) return null;
8
+ if (isESModule(component)) {
9
+ return component.default || component;
10
+ }
11
+ if (component && typeof component === "object" && !Array.isArray(component) && "default" in component && Object.keys(component).length === 1) {
12
+ return component.default;
13
+ }
14
+ return component;
15
+ }