@mxweb/router 1.0.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2024-12-27
9
+
10
+ ### Added
11
+
12
+ - Initial release
13
+ - `Router` component with basename support for adhoc features
14
+ - `Route` component with index and path-based routing
15
+ - `Link` component with `asChild` pattern support
16
+ - `useRouter` hook for accessing router state and navigation
17
+ - Built-in browser History API navigation
18
+ - Next.js App Router adapter support (`next/navigation`)
19
+ - React Router v5 adapter support (`useHistory`)
20
+ - React Router v6 adapter support (`useNavigate`)
21
+ - TypeScript support with full type definitions
22
+ - JSDoc documentation for all public APIs
23
+
24
+ [1.0.0]: https://github.com/mxwebio/mxweb-router/releases/tag/v1.0.0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 MXWeb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @mxweb/router
2
+
3
+ A lightweight router for Next.js App Router adhoc features. Supports both built-in browser navigation and Next.js navigation for seamless routing within embedded applications.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @mxweb/router
9
+ # or
10
+ yarn add @mxweb/router
11
+ # or
12
+ pnpm add @mxweb/router
13
+ ```
14
+
15
+ ## Quick Start
16
+
17
+ ```tsx
18
+ import { Router, Route, Link, useRouter } from '@mxweb/router';
19
+ import { useRouter as useNextRouter } from 'next/navigation';
20
+
21
+ function MyAdhocFeature() {
22
+ const nextRouter = useNextRouter();
23
+
24
+ return (
25
+ <Router basename="/my-feature" adapter={nextRouter}>
26
+ <nav>
27
+ <Link href="/">Home</Link>
28
+ <Link href="/settings">Settings</Link>
29
+ </nav>
30
+
31
+ <Route index element={HomePage} />
32
+ <Route path="settings" element={SettingsPage} />
33
+ </Router>
34
+ );
35
+ }
36
+
37
+ function HomePage({ navigation }) {
38
+ return <button onClick={() => navigation.push('/settings')}>Go to Settings</button>;
39
+ }
40
+
41
+ function SettingsPage({ navigation }) {
42
+ return <button onClick={() => navigation.pop()}>Go Back</button>;
43
+ }
44
+ ```
45
+
46
+ ## Documentation
47
+
48
+ For full documentation, visit **[docs.mxweb.io/router](https://docs.mxweb.io/router)**
49
+
50
+ ## License
51
+
52
+ MIT © MxWeb
@@ -0,0 +1,371 @@
1
+ /**
2
+ * @fileoverview MxWeb Router - A lightweight router for Next.js App Router adhoc features
3
+ *
4
+ * This package provides routing support for embedding features (adhoc apps) into Next.js App Router
5
+ * with a specific basename. It supports both built-in browser navigation and Next.js navigation
6
+ * for seamless routing within the adhoc application.
7
+ *
8
+ * @example
9
+ * // Using with Next.js App Router
10
+ * import { Router, Route, Link } from '@mxweb/router';
11
+ * import { useRouter } from 'next/navigation';
12
+ *
13
+ * function MyAdhocApp() {
14
+ * const nextRouter = useRouter();
15
+ *
16
+ * return (
17
+ * <Router basename="/my-feature" adapter={nextRouter}>
18
+ * <Route index element={HomePage} />
19
+ * <Route path="settings" element={SettingsPage} />
20
+ * <Link href="/settings">Go to Settings</Link>
21
+ * </Router>
22
+ * );
23
+ * }
24
+ *
25
+ * @example
26
+ * // Using with built-in navigation (no adapter)
27
+ * function StandaloneApp() {
28
+ * return (
29
+ * <Router basename="/app">
30
+ * <Route index element={HomePage} />
31
+ * <Route path="about" element={AboutPage} />
32
+ * </Router>
33
+ * );
34
+ * }
35
+ *
36
+ * @packageDocumentation
37
+ * @module @mxweb/router
38
+ */
39
+ import React, { AnchorHTMLAttributes, ComponentType, MouseEventHandler, PropsWithChildren, ReactElement, ReactNode, Ref } from "react";
40
+ import { LiteralObject } from "@mxweb/utils";
41
+ /**
42
+ * Navigation interface for routing operations within the adhoc app.
43
+ * Provides methods to navigate between routes programmatically.
44
+ */
45
+ export interface RouteNavigation {
46
+ /**
47
+ * Navigate to a new path by pushing it onto the history stack.
48
+ * @param path - The target path relative to the basename
49
+ * @param state - Optional state object to pass to the new route
50
+ */
51
+ push(path: string, state?: unknown): void;
52
+ /**
53
+ * Replace the current path in the history stack without creating a new entry.
54
+ * @param path - The target path relative to the basename
55
+ * @param state - Optional state object to pass to the new route
56
+ */
57
+ replace(path: string, state?: unknown): void;
58
+ /**
59
+ * Navigate backward or forward in the history stack.
60
+ * @param delta - Number of steps to go back (negative) or forward (positive). Defaults to -1.
61
+ */
62
+ pop(delta?: number): void;
63
+ }
64
+ /**
65
+ * Utility type that adds navigation prop to component props.
66
+ * Use this type for route element components that need access to navigation.
67
+ *
68
+ * @typeParam Props - The base props type for the component
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * function MyPage({ navigation }: PropsWithNavigation) {
73
+ * return <button onClick={() => navigation.push('/home')}>Go Home</button>;
74
+ * }
75
+ * ```
76
+ */
77
+ export type PropsWithNavigation<Props = {}> = Props & {
78
+ navigation: RouteNavigation;
79
+ };
80
+ interface RouteIndex {
81
+ index: true;
82
+ path?: never;
83
+ element: ComponentType<PropsWithNavigation>;
84
+ }
85
+ interface RoutePath<Pathname extends string = string> {
86
+ index?: never;
87
+ path: Pathname;
88
+ element: ComponentType<PropsWithNavigation>;
89
+ }
90
+ /**
91
+ * Props for the Route component. Can be either an index route or a path-based route.
92
+ *
93
+ * @typeParam Pathname - The type of the path string for type-safe routing
94
+ *
95
+ * @example
96
+ * ```tsx
97
+ * // Index route (matches the basename exactly)
98
+ * <Route index element={HomePage} />
99
+ *
100
+ * // Path-based route
101
+ * <Route path="settings" element={SettingsPage} />
102
+ * ```
103
+ */
104
+ export type RouteProps<Pathname extends string = string> = RouteIndex | RoutePath<Pathname>;
105
+ /**
106
+ * Options for Next.js router navigation methods.
107
+ * @internal
108
+ */
109
+ type NextRouteOptions = {
110
+ scroll?: boolean;
111
+ };
112
+ /**
113
+ * Adapter interface for Next.js App Router navigation.
114
+ * Compatible with the `useRouter()` hook from `next/navigation`.
115
+ *
116
+ * @example
117
+ * ```tsx
118
+ * import { useRouter } from 'next/navigation';
119
+ *
120
+ * function App() {
121
+ * const nextRouter = useRouter();
122
+ * return <Router adapter={nextRouter}>...</Router>;
123
+ * }
124
+ * ```
125
+ */
126
+ export interface NextRouteAdapter {
127
+ push(href: string, options?: NextRouteOptions): void;
128
+ replace(href: string, options?: NextRouteOptions): void;
129
+ back(): void;
130
+ forward?(): void;
131
+ }
132
+ /**
133
+ * Adapter interface for React Router v5 history object.
134
+ * Compatible with the `useHistory()` hook from `react-router-dom` v5.
135
+ *
136
+ * @example
137
+ * ```tsx
138
+ * import { useHistory } from 'react-router-dom';
139
+ *
140
+ * function App() {
141
+ * const history = useHistory();
142
+ * return <Router adapter={history}>...</Router>;
143
+ * }
144
+ * ```
145
+ */
146
+ export interface ReactRouterV5Adapter {
147
+ push(path: string, state?: unknown): void;
148
+ replace(path: string, state?: unknown): void;
149
+ go(n: number): void;
150
+ goBack?(): void;
151
+ goForward?(): void;
152
+ }
153
+ /**
154
+ * Adapter interface for React Router v6 navigate function.
155
+ * Compatible with the `useNavigate()` hook from `react-router-dom` v6.
156
+ *
157
+ * @example
158
+ * ```tsx
159
+ * import { useNavigate } from 'react-router-dom';
160
+ *
161
+ * function App() {
162
+ * const navigate = useNavigate();
163
+ * return <Router adapter={navigate}>...</Router>;
164
+ * }
165
+ * ```
166
+ */
167
+ export interface ReactRouterAdapter {
168
+ (to: string | number, options?: {
169
+ replace?: boolean;
170
+ state?: unknown;
171
+ }): void;
172
+ }
173
+ /**
174
+ * Union type for all supported router adapters.
175
+ * The router automatically detects which adapter type is provided and uses the appropriate navigation methods.
176
+ *
177
+ * Supported adapters:
178
+ * - **NextRouteAdapter**: Next.js App Router (`useRouter` from `next/navigation`)
179
+ * - **ReactRouterV5Adapter**: React Router v5 (`useHistory` from `react-router-dom`)
180
+ * - **ReactRouterAdapter**: React Router v6 (`useNavigate` from `react-router-dom`)
181
+ *
182
+ * If no adapter is provided, the router uses the built-in browser History API.
183
+ */
184
+ export type RouteAdapter = NextRouteAdapter | ReactRouterV5Adapter | ReactRouterAdapter;
185
+ /**
186
+ * State object representing the current router state.
187
+ *
188
+ * @typeParam Basename - The type of the basename string for type-safe routing
189
+ */
190
+ export interface RouterState<Basename extends string = string> {
191
+ /** The base path prefix for all routes in this router instance */
192
+ basename: Basename;
193
+ /** The current path relative to the basename */
194
+ pathname: string;
195
+ /** The complete URL path including the basename */
196
+ fullpath: string;
197
+ }
198
+ /**
199
+ * Hook to access the router state and navigation methods.
200
+ * Must be used within a `<Router>` component.
201
+ *
202
+ * @returns An object containing:
203
+ * - `state` - The current router state (basename, pathname, fullpath)
204
+ * - `navigation` - The navigation object with push, replace, pop methods
205
+ * - `push` - Shorthand for navigation.push
206
+ * - `replace` - Shorthand for navigation.replace
207
+ * - `pop` - Shorthand for navigation.pop
208
+ * - `isIndexPath` - Function to check if current path is the index route
209
+ *
210
+ * @example
211
+ * ```tsx
212
+ * function MyComponent() {
213
+ * const { state, push, isIndexPath } = useRouter();
214
+ *
215
+ * return (
216
+ * <div>
217
+ * <p>Current path: {state.pathname}</p>
218
+ * <button onClick={() => push('/settings')}>Go to Settings</button>
219
+ * {isIndexPath() && <p>You are on the home page</p>}
220
+ * </div>
221
+ * );
222
+ * }
223
+ * ```
224
+ */
225
+ export declare function useRouter(): {
226
+ state: RouterState<string>;
227
+ navigation: RouteNavigation;
228
+ push: <S extends LiteralObject = {}>(path: string, pushState?: S) => void;
229
+ replace: <S extends LiteralObject = {}>(path: string, replaceState?: S) => void;
230
+ pop: (delta?: number) => void;
231
+ isIndexPath: () => boolean;
232
+ };
233
+ /**
234
+ * Props for the Router component.
235
+ *
236
+ * @typeParam Basename - The type of the basename string for type-safe routing
237
+ */
238
+ export interface RouterProps<Basename extends string = string> {
239
+ /**
240
+ * The base path for all routes in this router.
241
+ * All route paths will be relative to this basename.
242
+ * @default "/"
243
+ */
244
+ basename?: Basename;
245
+ /**
246
+ * An optional navigation adapter for integrating with external routers.
247
+ * Supports Next.js App Router, React Router v5, and React Router v6.
248
+ * If not provided, uses the built-in browser History API.
249
+ */
250
+ adapter?: RouteAdapter;
251
+ }
252
+ /**
253
+ * The main Router provider component.
254
+ * Wraps your adhoc application and provides routing context to child components.
255
+ *
256
+ * @typeParam Basename - The type of the basename string for type-safe routing
257
+ *
258
+ * @example
259
+ * ```tsx
260
+ * // With Next.js App Router
261
+ * import { useRouter } from 'next/navigation';
262
+ *
263
+ * function MyAdhocFeature() {
264
+ * const nextRouter = useRouter();
265
+ *
266
+ * return (
267
+ * <Router basename="/my-feature" adapter={nextRouter}>
268
+ * <Route index element={HomePage} />
269
+ * <Route path="settings" element={SettingsPage} />
270
+ * </Router>
271
+ * );
272
+ * }
273
+ * ```
274
+ *
275
+ * @example
276
+ * ```tsx
277
+ * // With built-in navigation (standalone mode)
278
+ * function StandaloneApp() {
279
+ * return (
280
+ * <Router basename="/app">
281
+ * <Route index element={HomePage} />
282
+ * <Route path="about" element={AboutPage} />
283
+ * </Router>
284
+ * );
285
+ * }
286
+ * ```
287
+ */
288
+ export declare function Router<Basename extends string = string>(props: PropsWithChildren<RouterProps<Basename>>): React.JSX.Element;
289
+ /**
290
+ * A route component that renders its element when the current path matches.
291
+ *
292
+ * @typeParam Pathname - The type of the path string for type-safe routing
293
+ *
294
+ * @example
295
+ * ```tsx
296
+ * // Index route - matches when pathname is empty or "/"
297
+ * <Route index element={HomePage} />
298
+ *
299
+ * // Path route - matches when pathname starts with "settings"
300
+ * <Route path="settings" element={SettingsPage} />
301
+ *
302
+ * // Nested route example
303
+ * <Route path="users" element={UsersPage} />
304
+ * <Route path="users/profile" element={UserProfilePage} />
305
+ * ```
306
+ */
307
+ export declare function Route<Pathname extends string = string>(props: RouteProps<Pathname>): React.JSX.Element | null;
308
+ /**
309
+ * Base props shared by all Link variants.
310
+ * @internal
311
+ */
312
+ interface LinkPropsBase {
313
+ /** The target path to navigate to */
314
+ href: string;
315
+ /** If true, replaces the current history entry instead of pushing a new one */
316
+ replace?: boolean;
317
+ /** Optional state object to pass to the target route */
318
+ state?: LiteralObject;
319
+ }
320
+ /**
321
+ * Props for Link when used with asChild pattern.
322
+ * @internal
323
+ */
324
+ interface LinkPropsAsChild extends LinkPropsBase {
325
+ asChild: true;
326
+ children: ReactElement<{
327
+ href?: string;
328
+ onClick?: MouseEventHandler;
329
+ ref?: Ref<HTMLAnchorElement>;
330
+ }>;
331
+ }
332
+ /**
333
+ * Props for Link when rendering as a standard anchor element.
334
+ * @internal
335
+ */
336
+ interface LinkPropsDefault extends LinkPropsBase, Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
337
+ asChild?: false;
338
+ children?: ReactNode;
339
+ }
340
+ /**
341
+ * Props for the Link component.
342
+ * Supports two modes:
343
+ * - Default mode: Renders as an `<a>` element
344
+ * - asChild mode: Passes props to a child element (similar to Radix UI pattern)
345
+ */
346
+ export type LinkProps = LinkPropsAsChild | LinkPropsDefault;
347
+ /**
348
+ * A navigation link component that handles client-side routing.
349
+ * Intercepts clicks and uses the router's navigation methods instead of full page reloads.
350
+ *
351
+ * Supports modifier keys (Ctrl, Cmd, Shift, Alt) for opening in new tabs/windows.
352
+ *
353
+ * @example
354
+ * ```tsx
355
+ * // Basic usage
356
+ * <Link href="/settings">Go to Settings</Link>
357
+ *
358
+ * // With replace (no new history entry)
359
+ * <Link href="/settings" replace>Go to Settings</Link>
360
+ *
361
+ * // With state
362
+ * <Link href="/details" state={{ id: 123 }}>View Details</Link>
363
+ *
364
+ * // asChild pattern - pass navigation to a custom component
365
+ * <Link href="/home" asChild>
366
+ * <MyCustomButton>Go Home</MyCustomButton>
367
+ * </Link>
368
+ * ```
369
+ */
370
+ export declare const Link: React.ForwardRefExoticComponent<LinkProps & React.RefAttributes<HTMLAnchorElement>>;
371
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ "use strict";var t=require("react"),e=require("@mxweb/store"),a=require("@mxweb/react-hooks"),s=require("@mxweb/utils");function r(t){return t&&t.__esModule?t:{default:t}}var i=r(t);class n{constructor(t,e=null){this.routerState=t,this.adapter=e}getFullPath(t){return[this.routerState.basename,t].join("/").replace(/\/\/+/g,"/")}isNextAdapter(t){return s.hasOwnProperty(t,"back")&&"function"==typeof t.back}isV5Adapter(t){return s.hasOwnProperty(t,"go")&&"function"==typeof t.go}isV6Adapter(t){return"function"==typeof t}push(t,e={}){if(this.adapter)if(this.isV6Adapter(this.adapter))this.adapter(this.getFullPath(t),{state:e});else{if(this.isNextAdapter(this.adapter)){const a=s.hasOwnProperty(e,"scroll")?{scroll:e.scroll}:void 0;return void this.adapter.push(this.getFullPath(t),a)}this.isV5Adapter(this.adapter)&&this.adapter.push(this.getFullPath(t),e)}else s.isBrowser()&&(window.history.pushState(e,"",this.getFullPath(t)),window.dispatchEvent(new PopStateEvent("popstate")))}replace(t,e={}){if(this.adapter)if(this.isV6Adapter(this.adapter))this.adapter(this.getFullPath(t),{replace:!0,state:e});else{if(this.isNextAdapter(this.adapter)){const a=s.hasOwnProperty(e,"scroll")?{scroll:e.scroll}:void 0;return void this.adapter.replace(this.getFullPath(t),a)}this.isV5Adapter(this.adapter)&&this.adapter.replace(this.getFullPath(t),e)}else s.isBrowser()&&(window.history.replaceState(e,"",this.getFullPath(t)),window.dispatchEvent(new PopStateEvent("popstate")))}pop(t=-1){this.adapter?this.isV6Adapter(this.adapter)?this.adapter(t):this.isNextAdapter(this.adapter)?-1===t?this.adapter.back():1===t&&this.adapter.forward?this.adapter.forward():s.isBrowser()&&window.history.go(t):this.isV5Adapter(this.adapter)&&this.adapter.go(t):s.isBrowser()&&window.history.go(t)}setAdapter(t){return this.adapter=t,this}setState(t){return this.routerState=t,this}}class o extends e.StoreBase{constructor(){super({basename:"",pathname:"",fullpath:""}),this.navigation=new n(this.getState())}setState(t){return super.setState(t),this.navigation.setState(this.getState()),this}setAdapter(t){return this.navigation.setAdapter(t),this}getNavigation(){return{push:this.navigation.push.bind(this.navigation),replace:this.navigation.replace.bind(this.navigation),pop:this.navigation.pop.bind(this.navigation)}}isIndex(t){return s.isNullish(t)||""===t||"/"===t}isIndexPath(){const{basename:t,pathname:e}=this.getState();return!!this.isIndex(e)||(e===t||e===`${t}/`)}isIndexRoute(t){return s.hasOwnProperty(t,"index")&&!0===t.index}isPathRoute(t){return s.hasOwnProperty(t,"path")}isMatch(t,e){return`${e}/`.startsWith(`${t.replace(/^\//,"")}/`)}snapshot(t=""){if(!s.isBrowser())return{basename:t,fullpath:"",pathname:""};const e=window.location.pathname;let a=e;const r=t.startsWith("/")?t:`/${t}`;return r&&"/"!==r&&e.startsWith(r)?a=e.slice(r.length).replace(/^\//,""):e.startsWith("/")&&(a=e.slice(1)),{basename:r,fullpath:e,pathname:a}}}const h=new o,p=t.createContext(null);function l(){const e=t.useContext(p),a=e??h.getState(),s=h.getNavigation();return{state:a,navigation:s,push:(t,e)=>s.push(t,e),replace:(t,e)=>s.replace(t,e),pop:t=>s.pop(t),isIndexPath:()=>h.isIndexPath()}}const u=t.forwardRef((e,s)=>{const{href:r,replace:n=!1,state:o,asChild:h=!1}=e,{push:p,replace:u}=l(),d=t=>{t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||0!==t.button||(t.preventDefault(),n?u(r,o):p(r,o))};if(h){const i=t.Children.only(e.children);if(!t.isValidElement(i))return null;const n=i.props;return t.cloneElement(i,{href:r,ref:a.combineRefs(s,n.ref),onClick:a.combineEvents(n.onClick,d)})}const{onClick:c,children:f,...g}=e;return i.default.createElement("a",{...g,ref:s,href:r,onClick:t=>{c?.(t),d(t)}},f)});u.displayName="Link",exports.Link=u,exports.Route=function(t){const{element:e}=t,{state:{pathname:a},navigation:s,isIndexPath:r}=l();return h.isIndexRoute(t)?r()?i.default.createElement(e,{navigation:s}):null:h.isPathRoute(t)&&h.isMatch(t.path,a)?i.default.createElement(e,{navigation:s}):null},exports.Router=function(e){const{basename:a="/",adapter:r=null,children:n}=e,[o,l]=t.useState(()=>(r&&h.setAdapter(r),h.setState(h.snapshot(a)).getState()));return t.useEffect(()=>{const t=h.onStateChange(()=>{l(t=>{const e=h.getState();return s.isEqualPrimitive(t,e)?t:e})});return h.setState(h.snapshot(a)),t},[a]),t.useEffect(()=>{r&&h.setAdapter(r)},[r]),i.default.createElement(p.Provider,{value:o},n)},exports.useRouter=l;
package/dist/index.mjs ADDED
@@ -0,0 +1 @@
1
+ import t,{createContext as e,forwardRef as a,Children as s,isValidElement as i,cloneElement as r,useContext as n,useState as h,useEffect as o}from"react";import{StoreBase as p}from"@mxweb/store";import{combineEvents as l,combineRefs as d}from"@mxweb/react-hooks";import{isNullish as u,hasOwnProperty as c,isBrowser as g,isEqualPrimitive as f}from"@mxweb/utils";class m{constructor(t,e=null){this.routerState=t,this.adapter=e}getFullPath(t){return[this.routerState.basename,t].join("/").replace(/\/\/+/g,"/")}isNextAdapter(t){return c(t,"back")&&"function"==typeof t.back}isV5Adapter(t){return c(t,"go")&&"function"==typeof t.go}isV6Adapter(t){return"function"==typeof t}push(t,e={}){if(this.adapter)if(this.isV6Adapter(this.adapter))this.adapter(this.getFullPath(t),{state:e});else{if(this.isNextAdapter(this.adapter)){const a=c(e,"scroll")?{scroll:e.scroll}:void 0;return void this.adapter.push(this.getFullPath(t),a)}this.isV5Adapter(this.adapter)&&this.adapter.push(this.getFullPath(t),e)}else g()&&(window.history.pushState(e,"",this.getFullPath(t)),window.dispatchEvent(new PopStateEvent("popstate")))}replace(t,e={}){if(this.adapter)if(this.isV6Adapter(this.adapter))this.adapter(this.getFullPath(t),{replace:!0,state:e});else{if(this.isNextAdapter(this.adapter)){const a=c(e,"scroll")?{scroll:e.scroll}:void 0;return void this.adapter.replace(this.getFullPath(t),a)}this.isV5Adapter(this.adapter)&&this.adapter.replace(this.getFullPath(t),e)}else g()&&(window.history.replaceState(e,"",this.getFullPath(t)),window.dispatchEvent(new PopStateEvent("popstate")))}pop(t=-1){this.adapter?this.isV6Adapter(this.adapter)?this.adapter(t):this.isNextAdapter(this.adapter)?-1===t?this.adapter.back():1===t&&this.adapter.forward?this.adapter.forward():g()&&window.history.go(t):this.isV5Adapter(this.adapter)&&this.adapter.go(t):g()&&window.history.go(t)}setAdapter(t){return this.adapter=t,this}setState(t){return this.routerState=t,this}}const v=new class extends p{constructor(){super({basename:"",pathname:"",fullpath:""}),this.navigation=new m(this.getState())}setState(t){return super.setState(t),this.navigation.setState(this.getState()),this}setAdapter(t){return this.navigation.setAdapter(t),this}getNavigation(){return{push:this.navigation.push.bind(this.navigation),replace:this.navigation.replace.bind(this.navigation),pop:this.navigation.pop.bind(this.navigation)}}isIndex(t){return u(t)||""===t||"/"===t}isIndexPath(){const{basename:t,pathname:e}=this.getState();return!!this.isIndex(e)||(e===t||e===`${t}/`)}isIndexRoute(t){return c(t,"index")&&!0===t.index}isPathRoute(t){return c(t,"path")}isMatch(t,e){return`${e}/`.startsWith(`${t.replace(/^\//,"")}/`)}snapshot(t=""){if(!g())return{basename:t,fullpath:"",pathname:""};const e=window.location.pathname;let a=e;const s=t.startsWith("/")?t:`/${t}`;return s&&"/"!==s&&e.startsWith(s)?a=e.slice(s.length).replace(/^\//,""):e.startsWith("/")&&(a=e.slice(1)),{basename:s,fullpath:e,pathname:a}}},w=e(null);function S(){const t=n(w),e=t??v.getState(),a=v.getNavigation();return{state:e,navigation:a,push:(t,e)=>a.push(t,e),replace:(t,e)=>a.replace(t,e),pop:t=>a.pop(t),isIndexPath:()=>v.isIndexPath()}}function x(e){const{basename:a="/",adapter:s=null,children:i}=e,[r,n]=h(()=>(s&&v.setAdapter(s),v.setState(v.snapshot(a)).getState()));return o(()=>{const t=v.onStateChange(()=>{n(t=>{const e=v.getState();return f(t,e)?t:e})});return v.setState(v.snapshot(a)),t},[a]),o(()=>{s&&v.setAdapter(s)},[s]),t.createElement(w.Provider,{value:r},i)}function P(e){const{element:a}=e,{state:{pathname:s},navigation:i,isIndexPath:r}=S();return v.isIndexRoute(e)?r()?t.createElement(a,{navigation:i}):null:v.isPathRoute(e)&&v.isMatch(e.path,s)?t.createElement(a,{navigation:i}):null}const A=a((e,a)=>{const{href:n,replace:h=!1,state:o,asChild:p=!1}=e,{push:u,replace:c}=S(),g=t=>{t.metaKey||t.ctrlKey||t.shiftKey||t.altKey||0!==t.button||(t.preventDefault(),h?c(n,o):u(n,o))};if(p){const t=s.only(e.children);if(!i(t))return null;const h=t.props;return r(t,{href:n,ref:d(a,h.ref),onClick:l(h.onClick,g)})}const{onClick:f,children:m,...v}=e;return t.createElement("a",{...v,ref:a,href:n,onClick:t=>{f?.(t),g(t)}},m)});A.displayName="Link";export{A as Link,P as Route,x as Router,S as useRouter};
package/package.json ADDED
@@ -0,0 +1,76 @@
1
+ {
2
+ "name": "@mxweb/router",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight router for Next.js App Router adhoc features with built-in and adapter-based navigation support",
5
+ "keywords": [
6
+ "react",
7
+ "router",
8
+ "nextjs",
9
+ "app-router",
10
+ "navigation",
11
+ "adhoc",
12
+ "embedded",
13
+ "mxweb",
14
+ "client-side-routing",
15
+ "spa"
16
+ ],
17
+ "main": "dist/index.js",
18
+ "module": "dist/index.mjs",
19
+ "types": "dist/index.d.ts",
20
+ "license": "MIT",
21
+ "author": "MxWeb Team <mxwebio@gmail.com>",
22
+ "homepage": "https://docs.mxweb.io/router",
23
+ "sideEffects": false,
24
+ "files": [
25
+ "dist",
26
+ "README.md",
27
+ "CHANGELOG.md",
28
+ "LICENSE"
29
+ ],
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "engines": {
34
+ "node": ">=14.0.0"
35
+ },
36
+ "scripts": {
37
+ "clean": "rimraf dist",
38
+ "build": "yarn clean && yarn lint && rollup -c",
39
+ "build:watch": "rollup -c -w",
40
+ "lint": "eslint \"src/**/*.{ts,tsx}\"",
41
+ "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
42
+ "format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
43
+ "format:check": "prettier --check \"src/**/*.{ts,tsx,json,md}\"",
44
+ "prepublishOnly": "yarn build"
45
+ },
46
+ "devDependencies": {
47
+ "@mxweb/react-hooks": "^0.0.3",
48
+ "@mxweb/store": "^1.1.0",
49
+ "@mxweb/utils": "^0.0.5",
50
+ "@rollup/plugin-babel": "^6.1.0",
51
+ "@rollup/plugin-commonjs": "^29.0.0",
52
+ "@rollup/plugin-node-resolve": "^16.0.3",
53
+ "@rollup/plugin-terser": "^0.4.4",
54
+ "@rollup/plugin-typescript": "^12.3.0",
55
+ "@types/node": "^20",
56
+ "@types/react": "^19",
57
+ "@typescript-eslint/eslint-plugin": "^8.50.1",
58
+ "@typescript-eslint/parser": "^8.50.1",
59
+ "eslint": "^9.39.2",
60
+ "eslint-config-prettier": "^10.1.8",
61
+ "eslint-plugin-prettier": "^5.5.4",
62
+ "glob": "^13.0.0",
63
+ "prettier": "^3.7.4",
64
+ "react": "19.2.3",
65
+ "rimraf": "^6.1.2",
66
+ "rollup": "^4.54.0",
67
+ "tslib": "^2.8.1",
68
+ "typescript": "^5"
69
+ },
70
+ "peerDependencies": {
71
+ "@mxweb/react-hooks": "^0.0.3",
72
+ "@mxweb/store": "^1.1.0",
73
+ "@mxweb/utils": "^0.0.5",
74
+ "react": "^19.2.3"
75
+ }
76
+ }