@real-router/sources 0.2.3 → 0.2.4

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/README.md CHANGED
@@ -1,22 +1,22 @@
1
1
  # @real-router/sources
2
2
 
3
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue.svg)](https://www.typescriptlang.org/)
3
+ [![npm](https://img.shields.io/npm/v/@real-router/sources.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/sources)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@real-router/sources.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/sources)
5
+ [![bundle size](https://deno.bundlejs.com/?q=@real-router/sources&treeshake=[*]&badge=detailed)](https://bundlejs.com/?q=@real-router/sources&treeshake=[*])
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
5
7
 
6
- Framework-agnostic subscription layer for Real-Router. Subscribe to router state slices with automatic filtering and deduplication. Provides minimal reactive primitives for building UI bindings.
8
+ > Framework-agnostic subscription layer for [Real-Router](https://github.com/greydragon888/real-router). Reactive primitives compatible with `useSyncExternalStore` and vanilla JS.
9
+
10
+ Used internally by [`@real-router/react`](https://www.npmjs.com/package/@real-router/react). Use this package directly when building integrations for other frameworks or vanilla JS applications.
7
11
 
8
12
  ## Installation
9
13
 
10
14
  ```bash
11
15
  npm install @real-router/sources
12
- # or
13
- pnpm add @real-router/sources
14
- # or
15
- yarn add @real-router/sources
16
- # or
17
- bun add @real-router/sources
18
16
  ```
19
17
 
18
+ **Peer dependency:** `@real-router/core`
19
+
20
20
  ## Quick Start
21
21
 
22
22
  ```typescript
@@ -25,200 +25,115 @@ import { createRouteSource } from "@real-router/sources";
25
25
 
26
26
  const router = createRouter([
27
27
  { name: "home", path: "/" },
28
- { name: "users", path: "/users" },
29
- { name: "users.profile", path: "/:id" },
28
+ { name: "users", path: "/users/:id" },
30
29
  ]);
31
30
 
32
- router.start();
31
+ await router.start("/");
33
32
 
34
33
  const source = createRouteSource(router);
35
-
36
- // Subscribe to route changes
37
34
  const unsubscribe = source.subscribe(() => {
38
35
  console.log("Route:", source.getSnapshot().route?.name);
39
36
  });
40
-
41
- // Clean up when done
42
- unsubscribe();
43
- ```
44
-
45
- ---
46
-
47
- ## API
48
-
49
- ### `createRouteSource(router)`
50
-
51
- Creates a source for the full router state. Subscribes to the router on the first listener and unsubscribes when all listeners are removed (lazy-connection pattern).\
52
- `router: Router` — router instance\
53
- Returns: `RouterSource<RouteSnapshot>`
54
-
55
- ```typescript
56
- const source = createRouteSource(router);
57
- ```
58
-
59
- ---
60
-
61
- ### `createRouteNodeSource(router, nodeName)`
62
-
63
- Creates a source scoped to a specific route node. Only updates when the node is in the transition path, avoiding unnecessary re-renders for unrelated navigations. Uses a lazy-connection pattern: subscribes to the router on the first listener and unsubscribes when all listeners are removed.\
64
- `router: Router` — router instance\
65
- `nodeName: string` — route node name to scope updates to\
66
- Returns: `RouterSource<RouteNodeSnapshot>`
67
-
68
- ```typescript
69
- const source = createRouteNodeSource(router, "users");
70
- ```
71
-
72
- ---
73
-
74
- ### `createActiveRouteSource(router, routeName, params?, options?)`
75
-
76
- Creates a source that tracks whether a specific route is active. Returns a boolean snapshot.\
77
- `router: Router` — router instance\
78
- `routeName: string` — route name to check\
79
- `params?: Record<string, unknown>` — optional params to match\
80
- `options?: ActiveRouteSourceOptions` — matching options\
81
- Returns: `RouterSource<boolean>`
82
-
83
- ```typescript
84
- const source = createActiveRouteSource(router, "users.profile", { id: "123" });
85
- const source = createActiveRouteSource(router, "users", undefined, {
86
- strict: false,
87
- ignoreQueryParams: true,
88
- });
89
- ```
90
-
91
- Call `source.destroy()` when the source is no longer needed.
92
-
93
- **Options:**
94
-
95
- | Option | Type | Default | Description |
96
- | ------------------- | --------- | ------- | ---------------------------------------------------------- |
97
- | `strict` | `boolean` | `false` | When `true`, only matches the exact route, not descendants |
98
- | `ignoreQueryParams` | `boolean` | `true` | When `true`, ignores query parameters when matching |
99
-
100
- ---
101
-
102
- ### `createTransitionSource(router)`
103
-
104
- Creates a source that tracks the router's transition lifecycle. Updates on `TRANSITION_START`, `TRANSITION_SUCCESS`, `TRANSITION_ERROR`, and `TRANSITION_CANCEL` events. Unlike other sources, this uses eager subscription (subscribes to events immediately, not lazily on first listener).\
105
- `router: Router` — router instance\
106
- Returns: `RouterSource<RouterTransitionSnapshot>`
107
-
108
- ```typescript
109
- const source = createTransitionSource(router);
110
-
111
- source.subscribe(() => {
112
- const { isTransitioning, toRoute, fromRoute } = source.getSnapshot();
113
- if (isTransitioning) {
114
- console.log(`Navigating: ${fromRoute?.name} → ${toRoute?.name}`);
115
- }
116
- });
117
37
  ```
118
38
 
119
- Call `source.destroy()` when the source is no longer needed.
39
+ ## Source Factories
120
40
 
121
- ---
41
+ | Factory | Snapshot | Updates when |
42
+ |---------|----------|--------------|
43
+ | `createRouteSource(router)` | `{ route, previousRoute }` | Every navigation |
44
+ | `createRouteNodeSource(router, node)` | `{ route, previousRoute }` | Only when node activates/deactivates |
45
+ | `createActiveRouteSource(router, name, params?, opts?)` | `boolean` | Route active status changes |
46
+ | `createTransitionSource(router)` | `{ isTransitioning, toRoute, fromRoute }` | Transition start/end/cancel/error |
122
47
 
123
- ### `RouterSource<T>` Interface
124
-
125
- All four factories return a `RouterSource<T>`:
48
+ All factories return a `RouterSource<T>`:
126
49
 
127
50
  ```typescript
128
51
  interface RouterSource<T> {
129
- subscribe(listener: () => void): () => void;
130
- getSnapshot(): T;
131
- destroy(): void;
52
+ subscribe(listener: () => void): () => void; // useSyncExternalStore-compatible
53
+ getSnapshot(): T; // current value, synchronous
54
+ destroy(): void; // teardown, remove router subscription
132
55
  }
133
56
  ```
134
57
 
135
- `subscribe` registers a listener and returns an unsubscribe function. Compatible with `useSyncExternalStore`.\
136
- `getSnapshot` — returns the current snapshot synchronously.\
137
- `destroy` — tears down the source and removes the router subscription.
58
+ ### Lazy vs Eager Subscription
138
59
 
139
- ---
60
+ - `createRouteSource`, `createRouteNodeSource`, `createActiveRouteSource` — **lazy**: subscribe to the router on first listener, unsubscribe when all removed
61
+ - `createTransitionSource` — **eager**: subscribes immediately (needs to track `TRANSITION_START`)
140
62
 
141
- ## Types
63
+ ### `createActiveRouteSource` Options
142
64
 
143
65
  ```typescript
144
- import type {
145
- RouterSource,
146
- RouteSnapshot,
147
- RouteNodeSnapshot,
148
- RouterTransitionSnapshot,
149
- ActiveRouteSourceOptions,
150
- } from "@real-router/sources";
66
+ const source = createActiveRouteSource(router, "users", undefined, {
67
+ strict: false, // default: false — match descendants too
68
+ ignoreQueryParams: true, // default: true
69
+ });
151
70
  ```
152
71
 
153
- `RouteSnapshot` — full router state: `{ route: State | undefined, previousRoute: State | undefined }`\
154
- `RouteNodeSnapshot` — node-scoped state: `{ route: State | undefined, previousRoute: State | undefined }`\
155
- `RouterTransitionSnapshot` — transition state: `{ isTransitioning: boolean, toRoute: State | null, fromRoute: State | null }`\
156
- `ActiveRouteSourceOptions` — options for `createActiveRouteSource`: `{ strict?: boolean, ignoreQueryParams?: boolean }`\
157
- `RouterSource<T>` — the source interface returned by all four factories
158
-
159
- ---
160
-
161
72
  ## Usage Examples
162
73
 
163
74
  ### With React (`useSyncExternalStore`)
164
75
 
165
- ```typescript
76
+ ```tsx
166
77
  import { useSyncExternalStore } from "react";
167
78
  import { createRouteSource } from "@real-router/sources";
168
79
 
169
80
  const source = createRouteSource(router);
170
81
 
171
82
  function CurrentRoute() {
172
- const { route } = useSyncExternalStore(
173
- source.subscribe,
174
- source.getSnapshot,
175
- );
176
-
177
- return <p>Current route: {route.name}</p>;
83
+ const { route } = useSyncExternalStore(source.subscribe, source.getSnapshot);
84
+ return <p>Current route: {route?.name}</p>;
178
85
  }
179
86
  ```
180
87
 
181
88
  ### With Vanilla JS
182
89
 
183
90
  ```typescript
184
- import { createRouteSource } from "@real-router/sources";
91
+ import { createRouteNodeSource } from "@real-router/sources";
185
92
 
186
- const source = createRouteSource(router);
93
+ // Only fires when navigating within the "users" subtree
94
+ const source = createRouteNodeSource(router, "users");
187
95
 
188
96
  const unsubscribe = source.subscribe(() => {
189
- const { route, previousRoute } = source.getSnapshot();
190
- console.log("Navigation:", previousRoute?.name, "->", route.name);
97
+ const { route } = source.getSnapshot();
98
+ console.log("Users section:", route?.name);
191
99
  });
192
100
 
193
- // Later, clean up
194
- unsubscribe();
101
+ unsubscribe(); // automatically unsubscribes from router
195
102
  ```
196
103
 
197
- ### Node-Scoped Updates
104
+ ### Transition Tracking
198
105
 
199
106
  ```typescript
200
- import { createRouteNodeSource } from "@real-router/sources";
107
+ import { createTransitionSource } from "@real-router/sources";
201
108
 
202
- // Only re-renders when navigating within the "users" subtree
203
- const source = createRouteNodeSource(router, "users");
109
+ const source = createTransitionSource(router);
204
110
 
205
- const unsubscribe = source.subscribe(() => {
206
- const { route } = source.getSnapshot();
207
- console.log("Users section route:", route?.name);
111
+ source.subscribe(() => {
112
+ const { isTransitioning, toRoute, fromRoute } = source.getSnapshot();
113
+ if (isTransitioning) {
114
+ showSpinner();
115
+ } else {
116
+ hideSpinner();
117
+ }
208
118
  });
209
-
210
- // Later, clean up (automatically unsubscribes from router)
211
- unsubscribe();
212
119
  ```
213
120
 
214
- ---
121
+ ## Documentation
122
+
123
+ Full documentation: [Wiki — sources](https://github.com/greydragon888/real-router/wiki/sources-package)
215
124
 
216
125
  ## Related Packages
217
126
 
218
- - [@real-router/core](https://www.npmjs.com/package/@real-router/core) Core router
219
- - [@real-router/react](https://www.npmjs.com/package/@real-router/react) — React integration (uses sources internally)
220
- - [@real-router/rx](https://www.npmjs.com/package/@real-router/rx) Reactive Observable API
127
+ | Package | Description |
128
+ |---------|-------------|
129
+ | [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required dependency) |
130
+ | [@real-router/react](https://www.npmjs.com/package/@real-router/react) | React integration (uses sources internally) |
131
+ | [@real-router/rx](https://www.npmjs.com/package/@real-router/rx) | Observable API (`state$`, `events$`) |
132
+
133
+ ## Contributing
134
+
135
+ See [contributing guidelines](../../CONTRIBUTING.md) for development setup and PR process.
221
136
 
222
137
  ## License
223
138
 
224
- MIT © [Oleg Ivanov](https://github.com/greydragon888)
139
+ [MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
package/dist/cjs/index.js CHANGED
@@ -1 +1 @@
1
- var e=require("@real-router/route-utils"),t=require("@real-router/core"),s=require("@real-router/core/api"),r=class{#e;#t=!1;#s=new Set;#r;#o;#n;constructor(e,t){this.#e=e,this.#r=t?.onFirstSubscribe,this.#o=t?.onLastUnsubscribe,this.#n=t?.onDestroy,this.subscribe=this.subscribe.bind(this),this.getSnapshot=this.getSnapshot.bind(this),this.destroy=this.destroy.bind(this)}subscribe(e){return this.#t?()=>{}:(0===this.#s.size&&this.#r&&this.#r(),this.#s.add(e),()=>{this.#s.delete(e),!this.#t&&0===this.#s.size&&this.#o&&this.#o()})}getSnapshot(){return this.#e}updateSnapshot(e){this.#t||(this.#e=e,this.#s.forEach(e=>{e()}))}destroy(){this.#t||(this.#t=!0,this.#n?.(),this.#s.clear())}};function o(e,t,s,r){const o=r?.route??t.getState(),n=r?.previousRoute,i=""===s||void 0!==o&&(o.name===s||o.name.startsWith(`${s}.`))?o:void 0;return i===e.route&&n===e.previousRoute?e:{route:i,previousRoute:n}}var n=new WeakMap,i={isTransitioning:!1,toRoute:null,fromRoute:null};exports.createActiveRouteSource=function(t,s,o,n){const i=n?.strict??!1,u=n?.ignoreQueryParams??!0,a=t.isActiveRoute(s,o,i,u),c=new r(a,{onDestroy:()=>{h()}}),h=t.subscribe(r=>{const n=e.areRoutesRelated(s,r.route.name),a=r.previousRoute&&e.areRoutesRelated(s,r.previousRoute.name);if(!n&&!a)return;const h=!!n&&t.isActiveRoute(s,o,i,u);Object.is(c.getSnapshot(),h)||c.updateSnapshot(h)});return c},exports.createRouteNodeSource=function(e,t){let s=null;const i=function(e,t){let s=n.get(e);s||(s=new Map,n.set(e,s));let r=s.get(t);return r||(r=e.shouldUpdateNode(t),s.set(t,r)),r}(e,t),u=()=>{const e=s;s=null,e?.()},a=new r(o({route:void 0,previousRoute:void 0},e,t),{onFirstSubscribe:()=>{a.updateSnapshot(o(a.getSnapshot(),e,t)),s=e.subscribe(s=>{if(!i(s.route,s.previousRoute))return;const r=o(a.getSnapshot(),e,t,s);Object.is(a.getSnapshot(),r)||a.updateSnapshot(r)})},onLastUnsubscribe:u,onDestroy:u});return a},exports.createRouteSource=function(e){let t=null;const s=()=>{const e=t;t=null,e?.()},o=new r({route:e.getState(),previousRoute:void 0},{onFirstSubscribe:()=>{t=e.subscribe(e=>{o.updateSnapshot({route:e.route,previousRoute:e.previousRoute})})},onLastUnsubscribe:s,onDestroy:s});return o},exports.createTransitionSource=function(e){const o=new r(i,{onDestroy:()=>{a.forEach(e=>{e()})}}),n=s.getPluginApi(e),u=()=>{o.updateSnapshot(i)},a=[n.addEventListener(t.events.TRANSITION_START,(e,t)=>{o.updateSnapshot({isTransitioning:!0,toRoute:e,fromRoute:t??null})}),n.addEventListener(t.events.TRANSITION_SUCCESS,u),n.addEventListener(t.events.TRANSITION_ERROR,u),n.addEventListener(t.events.TRANSITION_CANCEL,u)];return o};//# sourceMappingURL=index.js.map
1
+ "use strict";var e=require("@real-router/route-utils"),t=require("@real-router/core"),s=require("@real-router/core/api"),r=class{#e;#t=!1;#s=new Set;#r;#o;#n;constructor(e,t){this.#e=e,this.#r=t?.onFirstSubscribe,this.#o=t?.onLastUnsubscribe,this.#n=t?.onDestroy,this.subscribe=this.subscribe.bind(this),this.getSnapshot=this.getSnapshot.bind(this),this.destroy=this.destroy.bind(this)}subscribe(e){return this.#t?()=>{}:(0===this.#s.size&&this.#r&&this.#r(),this.#s.add(e),()=>{this.#s.delete(e),!this.#t&&0===this.#s.size&&this.#o&&this.#o()})}getSnapshot(){return this.#e}updateSnapshot(e){this.#t||(this.#e=e,this.#s.forEach(e=>{e()}))}destroy(){this.#t||(this.#t=!0,this.#n?.(),this.#s.clear())}};function o(e,t,s,r){const o=r?.route??t.getState(),n=r?.previousRoute,i=""===s||void 0!==o&&(o.name===s||o.name.startsWith(`${s}.`))?o:void 0;return i===e.route&&n===e.previousRoute?e:{route:i,previousRoute:n}}var n=new WeakMap,i={isTransitioning:!1,toRoute:null,fromRoute:null};exports.createActiveRouteSource=function(t,s,o,n){const i=n?.strict??!1,u=n?.ignoreQueryParams??!0,a=t.isActiveRoute(s,o,i,u),c=new r(a,{onDestroy:()=>{h()}}),h=t.subscribe(r=>{const n=e.areRoutesRelated(s,r.route.name),a=r.previousRoute&&e.areRoutesRelated(s,r.previousRoute.name);if(!n&&!a)return;const h=!!n&&t.isActiveRoute(s,o,i,u);Object.is(c.getSnapshot(),h)||c.updateSnapshot(h)});return c},exports.createRouteNodeSource=function(e,t){let s=null;const i=function(e,t){let s=n.get(e);s||(s=new Map,n.set(e,s));let r=s.get(t);return r||(r=e.shouldUpdateNode(t),s.set(t,r)),r}(e,t),u=()=>{const e=s;s=null,e?.()},a=new r(o({route:void 0,previousRoute:void 0},e,t),{onFirstSubscribe:()=>{a.updateSnapshot(o(a.getSnapshot(),e,t)),s=e.subscribe(s=>{if(!i(s.route,s.previousRoute))return;const r=o(a.getSnapshot(),e,t,s);Object.is(a.getSnapshot(),r)||a.updateSnapshot(r)})},onLastUnsubscribe:u,onDestroy:u});return a},exports.createRouteSource=function(e){let t=null;const s=()=>{const e=t;t=null,e?.()},o=new r({route:e.getState(),previousRoute:void 0},{onFirstSubscribe:()=>{t=e.subscribe(e=>{o.updateSnapshot({route:e.route,previousRoute:e.previousRoute})})},onLastUnsubscribe:s,onDestroy:s});return o},exports.createTransitionSource=function(e){const o=new r(i,{onDestroy:()=>{a.forEach(e=>{e()})}}),n=s.getPluginApi(e),u=()=>{o.updateSnapshot(i)},a=[n.addEventListener(t.events.TRANSITION_START,(e,t)=>{o.updateSnapshot({isTransitioning:!0,toRoute:e,fromRoute:t??null})}),n.addEventListener(t.events.TRANSITION_SUCCESS,u),n.addEventListener(t.events.TRANSITION_ERROR,u),n.addEventListener(t.events.TRANSITION_CANCEL,u)];return o};//# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/sources",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "type": "commonjs",
5
5
  "description": "Framework-agnostic subscription layer for Real-Router state",
6
6
  "main": "./dist/cjs/index.js",
@@ -43,8 +43,8 @@
43
43
  "homepage": "https://github.com/greydragon888/real-router",
44
44
  "sideEffects": false,
45
45
  "dependencies": {
46
- "@real-router/core": "^0.36.0",
47
- "@real-router/route-utils": "^0.1.4"
46
+ "@real-router/core": "^0.37.0",
47
+ "@real-router/route-utils": "^0.1.5"
48
48
  },
49
49
  "devDependencies": {
50
50
  "mitata": "1.0.34"