@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.
package/package.json ADDED
@@ -0,0 +1,85 @@
1
+ {
2
+ "name": "@esmx/router-react",
3
+ "description": "React integration for @esmx/router - A powerful router with React 18+ support using modern hooks and context patterns",
4
+ "keywords": [
5
+ "react",
6
+ "router",
7
+ "routing",
8
+ "react-router",
9
+ "hooks",
10
+ "typescript",
11
+ "navigation",
12
+ "esmx",
13
+ "esm",
14
+ "single-page-application",
15
+ "spa",
16
+ "framework",
17
+ "frontend"
18
+ ],
19
+ "template": "library",
20
+ "scripts": {
21
+ "lint:js": "biome check --write --no-errors-on-unmatched",
22
+ "lint:css": "pnpm run lint:js",
23
+ "lint:type": "tsc --noEmit",
24
+ "test": "vitest run --pass-with-no-tests",
25
+ "coverage": "vitest run --coverage --pass-with-no-tests",
26
+ "build": "unbuild"
27
+ },
28
+ "contributors": [
29
+ {
30
+ "name": "lzxb",
31
+ "url": "https://github.com/lzxb"
32
+ },
33
+ {
34
+ "name": "RockShi1994",
35
+ "url": "https://github.com/RockShi1994"
36
+ },
37
+ {
38
+ "name": "jerrychan7",
39
+ "url": "https://github.com/jerrychan7"
40
+ },
41
+ {
42
+ "name": "wesloong",
43
+ "url": "https://github.com/wesloong"
44
+ }
45
+ ],
46
+ "peerDependencies": {
47
+ "react": "^18.0.0 || ^19.0.0"
48
+ },
49
+ "dependencies": {
50
+ "@esmx/router": "3.0.0-rc.105"
51
+ },
52
+ "devDependencies": {
53
+ "@biomejs/biome": "2.3.7",
54
+ "@types/node": "^24.0.0",
55
+ "@types/react": "^19.1.8",
56
+ "@types/react-dom": "^19.1.6",
57
+ "@vitest/coverage-v8": "3.2.4",
58
+ "happy-dom": "^20.0.10",
59
+ "react": "^19.1.0",
60
+ "react-dom": "^19.1.0",
61
+ "typescript": "5.9.3",
62
+ "unbuild": "3.6.1",
63
+ "vitest": "3.2.4"
64
+ },
65
+ "version": "3.0.0-rc.105",
66
+ "type": "module",
67
+ "private": false,
68
+ "exports": {
69
+ ".": {
70
+ "import": "./dist/index.mjs",
71
+ "types": "./dist/index.d.ts"
72
+ }
73
+ },
74
+ "module": "dist/index.mjs",
75
+ "types": "./dist/index.d.ts",
76
+ "files": [
77
+ "lib",
78
+ "src",
79
+ "dist",
80
+ "*.mjs",
81
+ "template",
82
+ "public"
83
+ ],
84
+ "gitHead": "db0f4a2a8a54a932f900d0b4f09ef0bf1ca9243f"
85
+ }
package/src/context.ts ADDED
@@ -0,0 +1,109 @@
1
+ import type { Route, Router } from '@esmx/router';
2
+ import { createContext, useContext } from 'react';
3
+ import type { RouterContextValue } from './types';
4
+
5
+ /**
6
+ * React Context for router state.
7
+ * Contains the router instance and current route.
8
+ * Using null as default to detect missing provider.
9
+ */
10
+ export const RouterContext = createContext<RouterContextValue | null>(null);
11
+ RouterContext.displayName = 'RouterContext';
12
+
13
+ /**
14
+ * React Context for RouterView depth tracking.
15
+ * Used for nested routing to determine which matched route to render.
16
+ */
17
+ export const RouterViewDepthContext = createContext<number>(0);
18
+ RouterViewDepthContext.displayName = 'RouterViewDepthContext';
19
+
20
+ /**
21
+ * Get the router context value.
22
+ * @throws {Error} If used outside of RouterProvider
23
+ * @internal
24
+ */
25
+ export function useRouterContext(): RouterContextValue {
26
+ const context = useContext(RouterContext);
27
+ if (!context) {
28
+ throw new Error(
29
+ '[@esmx/router-react] Router context not found. ' +
30
+ 'Please ensure your component is wrapped in a RouterProvider.'
31
+ );
32
+ }
33
+ return context;
34
+ }
35
+
36
+ /**
37
+ * Get the router instance for navigation.
38
+ * Must be used within a RouterProvider.
39
+ *
40
+ * @returns Router instance with navigation methods
41
+ * @throws {Error} If used outside of RouterProvider
42
+ *
43
+ * @example
44
+ * ```tsx
45
+ * import { useRouter } from '@esmx/router-react';
46
+ *
47
+ * function NavigationButton() {
48
+ * const router = useRouter();
49
+ *
50
+ * const handleClick = () => {
51
+ * router.push('/dashboard');
52
+ * };
53
+ *
54
+ * return <button onClick={handleClick}>Go to Dashboard</button>;
55
+ * }
56
+ * ```
57
+ */
58
+ export function useRouter(): Router {
59
+ return useRouterContext().router;
60
+ }
61
+
62
+ /**
63
+ * Get the current route information.
64
+ * Returns a reactive route object that updates when navigation occurs.
65
+ * Must be used within a RouterProvider.
66
+ *
67
+ * @returns Current route object with path, params, query, etc.
68
+ * @throws {Error} If used outside of RouterProvider
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * import { useRoute } from '@esmx/router-react';
73
+ *
74
+ * function CurrentPath() {
75
+ * const route = useRoute();
76
+ *
77
+ * return (
78
+ * <div>
79
+ * <p>Path: {route.path}</p>
80
+ * <p>Params: {JSON.stringify(route.params)}</p>
81
+ * <p>Query: {JSON.stringify(route.query)}</p>
82
+ * </div>
83
+ * );
84
+ * }
85
+ * ```
86
+ */
87
+ export function useRoute(): Route {
88
+ return useRouterContext().route;
89
+ }
90
+
91
+ /**
92
+ * Get the current RouterView depth.
93
+ * Used internally by RouterView for nested routing.
94
+ *
95
+ * @returns Current depth level (0 for root, 1 for first nested, etc.)
96
+ *
97
+ * @example
98
+ * ```tsx
99
+ * import { useRouterViewDepth } from '@esmx/router-react';
100
+ *
101
+ * function DebugView() {
102
+ * const depth = useRouterViewDepth();
103
+ * return <div>Current depth: {depth}</div>;
104
+ * }
105
+ * ```
106
+ */
107
+ export function useRouterViewDepth(): number {
108
+ return useContext(RouterViewDepthContext);
109
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { describe, expect, it } from 'vitest';
5
+ import * as RouterReactModule from './index';
6
+
7
+ describe('index.ts - Package Entry Point', () => {
8
+ describe('Hook Exports', () => {
9
+ it('should export useRouter hook', () => {
10
+ expect(RouterReactModule.useRouter).toBeDefined();
11
+ expect(typeof RouterReactModule.useRouter).toBe('function');
12
+ });
13
+
14
+ it('should export useRoute hook', () => {
15
+ expect(RouterReactModule.useRoute).toBeDefined();
16
+ expect(typeof RouterReactModule.useRoute).toBe('function');
17
+ });
18
+
19
+ it('should export useLink hook', () => {
20
+ expect(RouterReactModule.useLink).toBeDefined();
21
+ expect(typeof RouterReactModule.useLink).toBe('function');
22
+ });
23
+
24
+ it('should export useRouterViewDepth hook', () => {
25
+ expect(RouterReactModule.useRouterViewDepth).toBeDefined();
26
+ expect(typeof RouterReactModule.useRouterViewDepth).toBe(
27
+ 'function'
28
+ );
29
+ });
30
+ });
31
+
32
+ describe('Component Exports', () => {
33
+ it('should export RouterProvider component', () => {
34
+ expect(RouterReactModule.RouterProvider).toBeDefined();
35
+ expect(typeof RouterReactModule.RouterProvider).toBe('function');
36
+ });
37
+
38
+ it('should export RouterLink component', () => {
39
+ expect(RouterReactModule.RouterLink).toBeDefined();
40
+ expect(typeof RouterReactModule.RouterLink).toBe('object'); // forwardRef returns object
41
+ });
42
+
43
+ it('should export RouterView component', () => {
44
+ expect(RouterReactModule.RouterView).toBeDefined();
45
+ expect(typeof RouterReactModule.RouterView).toBe('function');
46
+ });
47
+ });
48
+
49
+ describe('Context Exports', () => {
50
+ it('should export RouterContext', () => {
51
+ expect(RouterReactModule.RouterContext).toBeDefined();
52
+ expect(RouterReactModule.RouterContext.displayName).toBe(
53
+ 'RouterContext'
54
+ );
55
+ });
56
+
57
+ it('should export RouterViewDepthContext', () => {
58
+ expect(RouterReactModule.RouterViewDepthContext).toBeDefined();
59
+ expect(RouterReactModule.RouterViewDepthContext.displayName).toBe(
60
+ 'RouterViewDepthContext'
61
+ );
62
+ });
63
+ });
64
+
65
+ describe('Export Completeness', () => {
66
+ it('should export all expected functions and components', () => {
67
+ const expectedExports = [
68
+ // Hooks
69
+ 'useRouter',
70
+ 'useRoute',
71
+ 'useLink',
72
+ 'useRouterViewDepth',
73
+ // Components
74
+ 'RouterProvider',
75
+ 'RouterLink',
76
+ 'RouterView',
77
+ // Context
78
+ 'RouterContext',
79
+ 'RouterViewDepthContext'
80
+ ];
81
+
82
+ expectedExports.forEach((exportName) => {
83
+ expect(RouterReactModule).toHaveProperty(exportName);
84
+ expect(Object.hasOwn(RouterReactModule, exportName)).toBe(true);
85
+ });
86
+ });
87
+
88
+ it('should not export unexpected items', () => {
89
+ const actualExports = Object.keys(RouterReactModule);
90
+ const expectedExports = [
91
+ 'useRouter',
92
+ 'useRoute',
93
+ 'useLink',
94
+ 'useRouterViewDepth',
95
+ 'RouterProvider',
96
+ 'RouterLink',
97
+ 'RouterView',
98
+ 'RouterContext',
99
+ 'RouterViewDepthContext'
100
+ ];
101
+
102
+ // Check that we don't have unexpected exports
103
+ const unexpectedExports = actualExports.filter(
104
+ (exportName) => !expectedExports.includes(exportName)
105
+ );
106
+
107
+ expect(unexpectedExports).toEqual([]);
108
+ });
109
+ });
110
+
111
+ describe('Hook Error Handling', () => {
112
+ it('hooks should throw when used incorrectly', () => {
113
+ // React hooks cannot be called outside of components
114
+ // This is expected React behavior - hooks depend on internal React state
115
+ // The error message varies depending on the React version and environment
116
+ expect(() => {
117
+ RouterReactModule.useRouter();
118
+ }).toThrow(); // Will throw "Invalid hook call" or similar
119
+
120
+ expect(() => {
121
+ RouterReactModule.useRoute();
122
+ }).toThrow(); // Will throw "Invalid hook call" or similar
123
+ });
124
+ });
125
+
126
+ describe('Component Properties', () => {
127
+ it('should have RouterProvider with displayName', () => {
128
+ expect(RouterReactModule.RouterProvider.displayName).toBe(
129
+ 'RouterProvider'
130
+ );
131
+ });
132
+
133
+ it('should have RouterView with displayName', () => {
134
+ expect(RouterReactModule.RouterView.displayName).toBe('RouterView');
135
+ });
136
+
137
+ it('should have RouterLink with displayName', () => {
138
+ expect(RouterReactModule.RouterLink.displayName).toBe('RouterLink');
139
+ });
140
+ });
141
+
142
+ describe('Module Structure', () => {
143
+ it('should be a proper ES module', () => {
144
+ expect(typeof RouterReactModule).toBe('object');
145
+ expect(RouterReactModule).not.toBeNull();
146
+
147
+ // Verify it's not a default export
148
+ expect('default' in RouterReactModule).toBe(false);
149
+ });
150
+
151
+ it('should have consistent export naming', () => {
152
+ // All hooks should start with 'use'
153
+ const hookExports = [
154
+ 'useRouter',
155
+ 'useRoute',
156
+ 'useLink',
157
+ 'useRouterViewDepth'
158
+ ];
159
+
160
+ hookExports.forEach((exportName) => {
161
+ expect(exportName).toMatch(/^use[A-Z][a-zA-Z]*$/);
162
+ });
163
+
164
+ // Component exports should be PascalCase
165
+ const componentExports = [
166
+ 'RouterProvider',
167
+ 'RouterLink',
168
+ 'RouterView',
169
+ 'RouterContext',
170
+ 'RouterViewDepthContext'
171
+ ];
172
+
173
+ componentExports.forEach((exportName) => {
174
+ expect(exportName).toMatch(/^[A-Z][a-zA-Z]*$/);
175
+ });
176
+ });
177
+ });
178
+
179
+ describe('TypeScript Types', () => {
180
+ it('should export RouterContextValue type (via typeof)', () => {
181
+ // Types are compile-time only, but we can verify the runtime shape
182
+ const context = RouterReactModule.RouterContext;
183
+ expect(context).toBeDefined();
184
+ expect(context.Provider).toBeDefined();
185
+ expect(context.Consumer).toBeDefined();
186
+ });
187
+
188
+ it('should have RouterViewDepthContext with correct default value', () => {
189
+ // RouterViewDepthContext should default to 0
190
+ const context = RouterReactModule.RouterViewDepthContext;
191
+ expect(context).toBeDefined();
192
+ expect(context.Provider).toBeDefined();
193
+ });
194
+ });
195
+
196
+ describe('React Best Practices', () => {
197
+ it('RouterLink should be a forwardRef component', () => {
198
+ // forwardRef components have $$typeof and render properties
199
+ const link = RouterReactModule.RouterLink as any;
200
+ expect(link.$$typeof).toBeDefined();
201
+ // The render property exists on forwardRef components
202
+ expect(link.render || link.type).toBeDefined();
203
+ });
204
+
205
+ it('RouterProvider should accept router and children props', () => {
206
+ // Function components show their parameter count
207
+ // RouterProvider should be a regular function component
208
+ expect(RouterReactModule.RouterProvider).toBeDefined();
209
+ });
210
+
211
+ it('RouterView should accept fallback prop', () => {
212
+ expect(RouterReactModule.RouterView).toBeDefined();
213
+ });
214
+ });
215
+ });
package/src/index.ts ADDED
@@ -0,0 +1,21 @@
1
+ // Components
2
+
3
+ // Hooks
4
+ export {
5
+ RouterContext,
6
+ RouterViewDepthContext,
7
+ useRoute,
8
+ useRouter,
9
+ useRouterViewDepth
10
+ } from './context';
11
+ export type { RouterLinkComponentProps } from './router-link';
12
+ export { RouterLink } from './router-link';
13
+ export { RouterProvider } from './router-provider';
14
+ export { RouterView } from './router-view';
15
+ // Types
16
+ export type {
17
+ RouterContextValue,
18
+ RouterProviderProps,
19
+ RouterViewProps
20
+ } from './types';
21
+ export { useLink } from './use-link';
@@ -0,0 +1,222 @@
1
+ import type {
2
+ RouteLayerOptions,
3
+ RouteLocationInput,
4
+ RouteMatchType,
5
+ RouterLinkResolved,
6
+ RouterLinkType
7
+ } from '@esmx/router';
8
+ import {
9
+ type CSSProperties,
10
+ createElement,
11
+ type ElementType,
12
+ forwardRef,
13
+ type MouseEvent,
14
+ type ReactElement,
15
+ type ReactNode,
16
+ type Ref,
17
+ useCallback,
18
+ useMemo
19
+ } from 'react';
20
+ import { useRouter } from './context';
21
+
22
+ /**
23
+ * Props for RouterLink component.
24
+ * Similar to RouterLinkProps from @esmx/router with React-specific additions.
25
+ */
26
+ export interface RouterLinkComponentProps {
27
+ /** Target route location (required) */
28
+ to: RouteLocationInput;
29
+ /** Navigation type */
30
+ type?: RouterLinkType;
31
+ /** @deprecated Use type='replace' instead */
32
+ replace?: boolean;
33
+ /** Active matching mode */
34
+ exact?: RouteMatchType;
35
+ /** CSS class for active state */
36
+ activeClass?: string;
37
+ /** Event(s) that trigger navigation */
38
+ event?: string | string[];
39
+ /** HTML tag to render */
40
+ tag?: string;
41
+ /** Layer navigation options */
42
+ layerOptions?: RouteLayerOptions;
43
+ /** Hook function called before navigation */
44
+ beforeNavigate?: (event: Event, eventName: string) => void;
45
+ /** Link content */
46
+ children?: ReactNode;
47
+ /** Additional CSS class name */
48
+ className?: string;
49
+ /** Inline styles */
50
+ style?: CSSProperties;
51
+ }
52
+
53
+ /**
54
+ * RouterLink component for declarative navigation.
55
+ * Renders an anchor tag with proper navigation behavior and active state management.
56
+ * Supports all navigation types: push, replace, pushWindow, replaceWindow, pushLayer.
57
+ *
58
+ * @param props - Component props
59
+ * @param props.to - Target route location
60
+ * @param props.type - Navigation type ('push' | 'replace' | 'pushWindow' | 'replaceWindow' | 'pushLayer')
61
+ * @param props.exact - Active matching mode ('include' | 'exact' | 'route')
62
+ * @param props.activeClass - CSS class for active state
63
+ * @param props.event - Event(s) that trigger navigation
64
+ * @param props.tag - HTML tag to render (default: 'a')
65
+ * @param props.layerOptions - Options for layer navigation
66
+ * @param props.beforeNavigate - Callback before navigation
67
+ * @param props.children - Link content
68
+ *
69
+ * @example
70
+ * ```tsx
71
+ * // Basic navigation
72
+ * <RouterLink to="/home">Home</RouterLink>
73
+ * <RouterLink to="/about">About</RouterLink>
74
+ *
75
+ * // With object location
76
+ * <RouterLink to={{ path: '/users', query: { page: '1' } }}>
77
+ * Users
78
+ * </RouterLink>
79
+ *
80
+ * // Replace navigation (no history entry)
81
+ * <RouterLink to="/login" type="replace">Login</RouterLink>
82
+ *
83
+ * // Open in new window
84
+ * <RouterLink to="/external" type="pushWindow">External</RouterLink>
85
+ *
86
+ * // Custom active class
87
+ * <RouterLink
88
+ * to="/dashboard"
89
+ * activeClass="nav-active"
90
+ * exact="exact"
91
+ * >
92
+ * Dashboard
93
+ * </RouterLink>
94
+ *
95
+ * // Custom tag (button)
96
+ * <RouterLink to="/submit" tag="button" className="btn">
97
+ * Submit
98
+ * </RouterLink>
99
+ *
100
+ * // With beforeNavigate callback
101
+ * <RouterLink
102
+ * to="/protected"
103
+ * beforeNavigate={(e, eventName) => {
104
+ * if (!isAuthenticated) {
105
+ * e.preventDefault();
106
+ * showLoginModal();
107
+ * }
108
+ * }}
109
+ * >
110
+ * Protected Page
111
+ * </RouterLink>
112
+ * ```
113
+ */
114
+ function RouterLinkInner(
115
+ props: RouterLinkComponentProps & { [key: string]: any },
116
+ ref: Ref<HTMLAnchorElement>
117
+ ): ReactElement {
118
+ const {
119
+ to,
120
+ type = 'push',
121
+ replace,
122
+ exact = 'include',
123
+ activeClass,
124
+ event = 'click',
125
+ tag = 'a',
126
+ layerOptions,
127
+ beforeNavigate,
128
+ children,
129
+ className,
130
+ style,
131
+ ...rest
132
+ } = props;
133
+
134
+ const router = useRouter();
135
+
136
+ // Resolve the link using router's built-in resolver
137
+ const linkResolved: RouterLinkResolved = useMemo(() => {
138
+ return router.resolveLink({
139
+ to,
140
+ type,
141
+ replace,
142
+ exact,
143
+ activeClass,
144
+ event,
145
+ tag,
146
+ layerOptions,
147
+ beforeNavigate
148
+ });
149
+ }, [
150
+ router,
151
+ to,
152
+ type,
153
+ replace,
154
+ exact,
155
+ activeClass,
156
+ event,
157
+ tag,
158
+ layerOptions,
159
+ beforeNavigate
160
+ ]);
161
+
162
+ // Handle click event
163
+ const handleClick = useCallback(
164
+ async (e: MouseEvent<HTMLElement>) => {
165
+ // Call beforeNavigate callback if provided
166
+ beforeNavigate?.(e as unknown as Event, 'click');
167
+
168
+ // If default was prevented by beforeNavigate or by modifier keys, skip navigation
169
+ if (e.defaultPrevented) return;
170
+
171
+ // Check for modifier keys - let browser handle these
172
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
173
+ if (e.button !== 0) return;
174
+
175
+ // Prevent default browser navigation
176
+ e.preventDefault();
177
+
178
+ // Navigate using the resolved link's navigate function
179
+ await linkResolved.navigate(e as unknown as Event);
180
+ },
181
+ [linkResolved, beforeNavigate]
182
+ );
183
+
184
+ // Build class names
185
+ const computedClassName = useMemo(() => {
186
+ const classes: string[] = [];
187
+ if (linkResolved.attributes.class) {
188
+ classes.push(linkResolved.attributes.class);
189
+ }
190
+ if (className) {
191
+ classes.push(className);
192
+ }
193
+ return classes.join(' ') || undefined;
194
+ }, [linkResolved.attributes.class, className]);
195
+
196
+ // Build props for the element
197
+ const elementProps = {
198
+ ref,
199
+ href: linkResolved.attributes.href,
200
+ target: linkResolved.attributes.target,
201
+ rel: linkResolved.attributes.rel,
202
+ className: computedClassName,
203
+ style,
204
+ onClick: handleClick,
205
+ ...rest
206
+ };
207
+
208
+ // Render the element with the appropriate tag using createElement
209
+ return createElement(tag as ElementType, elementProps, children);
210
+ }
211
+
212
+ // Cast needed to fix TypeScript forwardRef type inference issue
213
+ // This is a common pattern when index signatures conflict with forwardRef's prop handling
214
+ export const RouterLink = forwardRef(
215
+ RouterLinkInner as any
216
+ ) as React.ForwardRefExoticComponent<
217
+ RouterLinkComponentProps & {
218
+ [key: string]: any;
219
+ } & React.RefAttributes<HTMLAnchorElement>
220
+ >;
221
+
222
+ RouterLink.displayName = 'RouterLink';