@real-router/lifecycle-plugin 0.0.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/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # @real-router/lifecycle-plugin
2
+
3
+ [![npm](https://img.shields.io/npm/v/@real-router/lifecycle-plugin.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/lifecycle-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@real-router/lifecycle-plugin.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/lifecycle-plugin)
5
+ [![bundle size](https://deno.bundlejs.com/?q=@real-router/lifecycle-plugin&treeshake=[*]&badge=detailed)](https://bundlejs.com/?q=@real-router/lifecycle-plugin&treeshake=[*])
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
7
+
8
+ > Route-level lifecycle hooks for [Real-Router](https://github.com/greydragon888/real-router). Add `onEnter`, `onStay`, `onLeave` callbacks directly to route definitions.
9
+
10
+ ```typescript
11
+ // Without plugin — scattered subscribe() calls with route checks:
12
+ router.subscribe(({ route, previousRoute }) => {
13
+ if (route.name === "dashboard") trackPageView("dashboard");
14
+ if (previousRoute?.name === "editor") saveEditorState();
15
+ });
16
+
17
+ // With plugin — declarative, per-route:
18
+ { name: "dashboard", path: "/dashboard", onEnter: () => trackPageView("dashboard") }
19
+ { name: "editor", path: "/editor", onLeave: () => saveEditorState() }
20
+ ```
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install @real-router/lifecycle-plugin
26
+ ```
27
+
28
+ **Peer dependency:** `@real-router/core`
29
+
30
+ ## Quick Start
31
+
32
+ ```typescript
33
+ import { createRouter } from "@real-router/core";
34
+ import { lifecyclePluginFactory } from "@real-router/lifecycle-plugin";
35
+
36
+ const routes = [
37
+ {
38
+ name: "home",
39
+ path: "/",
40
+ onLeave: (toState, fromState) => {
41
+ console.log("Leaving home for", toState.name);
42
+ },
43
+ },
44
+ {
45
+ name: "users.view",
46
+ path: "/users/:id",
47
+ onEnter: (toState) => {
48
+ analytics.track("user_profile_viewed", { userId: toState.params.id });
49
+ },
50
+ onStay: (toState, fromState) => {
51
+ console.log("User changed:", fromState.params.id, "→", toState.params.id);
52
+ },
53
+ },
54
+ ];
55
+
56
+ const router = createRouter(routes);
57
+ router.usePlugin(lifecyclePluginFactory());
58
+
59
+ await router.start("/");
60
+ ```
61
+
62
+ ## Hook Reference
63
+
64
+ | Hook | Fires when | Typical use case |
65
+ | --------- | -------------------------- | -------------------------- |
66
+ | `onEnter` | Route is entered | Analytics, data prefetch |
67
+ | `onStay` | Same route, params changed | Refresh data, update UI |
68
+ | `onLeave` | Route is left | Cleanup timers, save state |
69
+
70
+ All hooks receive `(toState: State, fromState: State | undefined) => void`.
71
+
72
+ ### Execution order
73
+
74
+ `onLeave` fires first (at leave-approve phase), then `onEnter` or `onStay` (at transition success).
75
+
76
+ ## Use Cases
77
+
78
+ ### Analytics tracking
79
+
80
+ ```typescript
81
+ {
82
+ name: "product",
83
+ path: "/products/:id",
84
+ onEnter: (toState) => {
85
+ analytics.track("product_viewed", { productId: toState.params.id });
86
+ },
87
+ }
88
+ ```
89
+
90
+ ### Cleanup on leave
91
+
92
+ ```typescript
93
+ {
94
+ name: "editor",
95
+ path: "/editor/:docId",
96
+ onLeave: () => {
97
+ autosaveTimer.clear();
98
+ webSocket.disconnect();
99
+ },
100
+ }
101
+ ```
102
+
103
+ ### React to param changes
104
+
105
+ ```typescript
106
+ {
107
+ name: "search",
108
+ path: "/search?q",
109
+ onStay: (toState) => {
110
+ searchStore.setQuery(toState.params.q);
111
+ },
112
+ }
113
+ ```
114
+
115
+ ## Documentation
116
+
117
+ - [ARCHITECTURE.md](ARCHITECTURE.md) — Design decisions and data flow
118
+ - [Plugin Architecture](https://github.com/greydragon888/real-router/wiki/plugin-architecture) — How plugins integrate with the router
119
+
120
+ ## Related Packages
121
+
122
+ | Package | Description |
123
+ | ---------------------------------------------------------------------------------------- | -------------------------------------- |
124
+ | [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required peer dependency) |
125
+ | [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | Browser History API integration |
126
+ | [@real-router/logger-plugin](https://www.npmjs.com/package/@real-router/logger-plugin) | Development logging |
127
+
128
+ ## License
129
+
130
+ [MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
@@ -0,0 +1,30 @@
1
+ import { PluginFactory, State } from "@real-router/core";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Lifecycle hook callback for route transitions.
6
+ * Fire-and-forget: return values are ignored, errors are caught and warned.
7
+ */
8
+ type LifecycleHook = (toState: State, fromState: State | undefined) => void;
9
+ //#endregion
10
+ //#region src/factory.d.ts
11
+ declare function lifecyclePluginFactory(): PluginFactory;
12
+ //#endregion
13
+ //#region src/index.d.ts
14
+ /**
15
+ * Module augmentation for real-router.
16
+ * Extends Route interface with lifecycle hooks.
17
+ */
18
+ declare module "@real-router/core" {
19
+ interface Route {
20
+ /** Called when this route segment is newly activated (entered). */
21
+ onEnter?: LifecycleHook;
22
+ /** Called when this route segment stays active but params changed. */
23
+ onStay?: LifecycleHook;
24
+ /** Called when this route segment is deactivated (left). */
25
+ onLeave?: LifecycleHook;
26
+ }
27
+ }
28
+ //#endregion
29
+ export { type LifecycleHook, lifecyclePluginFactory };
30
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,2 @@
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);function t(e){return(t,n,r,i)=>{let a=e.getRouteConfig(n)?.[t];typeof a==`function`&&a(r,i)}}function n(n){let r=t((0,e.getPluginApi)(n));return{onTransitionLeaveApprove:(e,t)=>{t&&e.name!==t.name&&r(`onLeave`,t.name,e,t)},onTransitionSuccess:(e,t)=>{e.name===t?.name?r(`onStay`,e.name,e,t):r(`onEnter`,e.name,e,t)}}}function r(){return n}exports.lifecyclePluginFactory=r;
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/factory.ts"],"sourcesContent":["import { getPluginApi } from \"@real-router/core/api\";\n\nimport type { LifecycleHook } from \"./types\";\nimport type { PluginFactory, State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nfunction createInvokeHook(api: PluginApi) {\n return (\n hookName: \"onEnter\" | \"onStay\" | \"onLeave\",\n routeName: string,\n toState: State,\n fromState: State | undefined,\n ): void => {\n const hook = api.getRouteConfig(routeName)?.[hookName];\n\n if (typeof hook === \"function\") {\n (hook as LifecycleHook)(toState, fromState);\n }\n };\n}\n\nfunction createPlugin(router: Parameters<PluginFactory>[0]) {\n const invokeHook = createInvokeHook(getPluginApi(router));\n\n return {\n onTransitionLeaveApprove: (\n toState: State,\n fromState: State | undefined,\n ) => {\n if (fromState && toState.name !== fromState.name) {\n invokeHook(\"onLeave\", fromState.name, toState, fromState);\n }\n },\n\n onTransitionSuccess: (toState: State, fromState: State | undefined) => {\n if (toState.name === fromState?.name) {\n invokeHook(\"onStay\", toState.name, toState, fromState);\n } else {\n invokeHook(\"onEnter\", toState.name, toState, fromState);\n }\n },\n };\n}\n\nexport function lifecyclePluginFactory(): PluginFactory {\n return createPlugin;\n}\n"],"mappings":"0GAMA,SAAS,EAAiB,EAAgB,CACxC,OACE,EACA,EACA,EACA,IACS,CACT,IAAM,EAAO,EAAI,eAAe,EAAU,GAAG,GAEzC,OAAO,GAAS,YACjB,EAAuB,EAAS,EAAU,EAKjD,SAAS,EAAa,EAAsC,CAC1D,IAAM,EAAa,GAAA,EAAA,EAAA,cAA8B,EAAO,CAAC,CAEzD,MAAO,CACL,0BACE,EACA,IACG,CACC,GAAa,EAAQ,OAAS,EAAU,MAC1C,EAAW,UAAW,EAAU,KAAM,EAAS,EAAU,EAI7D,qBAAsB,EAAgB,IAAiC,CACjE,EAAQ,OAAS,GAAW,KAC9B,EAAW,SAAU,EAAQ,KAAM,EAAS,EAAU,CAEtD,EAAW,UAAW,EAAQ,KAAM,EAAS,EAAU,EAG5D,CAGH,SAAgB,GAAwC,CACtD,OAAO"}
@@ -0,0 +1,30 @@
1
+ import { PluginFactory, State } from "@real-router/core";
2
+
3
+ //#region src/types.d.ts
4
+ /**
5
+ * Lifecycle hook callback for route transitions.
6
+ * Fire-and-forget: return values are ignored, errors are caught and warned.
7
+ */
8
+ type LifecycleHook = (toState: State, fromState: State | undefined) => void;
9
+ //#endregion
10
+ //#region src/factory.d.ts
11
+ declare function lifecyclePluginFactory(): PluginFactory;
12
+ //#endregion
13
+ //#region src/index.d.ts
14
+ /**
15
+ * Module augmentation for real-router.
16
+ * Extends Route interface with lifecycle hooks.
17
+ */
18
+ declare module "@real-router/core" {
19
+ interface Route {
20
+ /** Called when this route segment is newly activated (entered). */
21
+ onEnter?: LifecycleHook;
22
+ /** Called when this route segment stays active but params changed. */
23
+ onStay?: LifecycleHook;
24
+ /** Called when this route segment is deactivated (left). */
25
+ onLeave?: LifecycleHook;
26
+ }
27
+ }
28
+ //#endregion
29
+ export { type LifecycleHook, lifecyclePluginFactory };
30
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,2 @@
1
+ import{getPluginApi as e}from"@real-router/core/api";function t(e){return(t,n,r,i)=>{let a=e.getRouteConfig(n)?.[t];typeof a==`function`&&a(r,i)}}function n(n){let r=t(e(n));return{onTransitionLeaveApprove:(e,t)=>{t&&e.name!==t.name&&r(`onLeave`,t.name,e,t)},onTransitionSuccess:(e,t)=>{e.name===t?.name?r(`onStay`,e.name,e,t):r(`onEnter`,e.name,e,t)}}}function r(){return n}export{r as lifecyclePluginFactory};
2
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/factory.ts"],"sourcesContent":["import { getPluginApi } from \"@real-router/core/api\";\n\nimport type { LifecycleHook } from \"./types\";\nimport type { PluginFactory, State } from \"@real-router/core\";\nimport type { PluginApi } from \"@real-router/core/api\";\n\nfunction createInvokeHook(api: PluginApi) {\n return (\n hookName: \"onEnter\" | \"onStay\" | \"onLeave\",\n routeName: string,\n toState: State,\n fromState: State | undefined,\n ): void => {\n const hook = api.getRouteConfig(routeName)?.[hookName];\n\n if (typeof hook === \"function\") {\n (hook as LifecycleHook)(toState, fromState);\n }\n };\n}\n\nfunction createPlugin(router: Parameters<PluginFactory>[0]) {\n const invokeHook = createInvokeHook(getPluginApi(router));\n\n return {\n onTransitionLeaveApprove: (\n toState: State,\n fromState: State | undefined,\n ) => {\n if (fromState && toState.name !== fromState.name) {\n invokeHook(\"onLeave\", fromState.name, toState, fromState);\n }\n },\n\n onTransitionSuccess: (toState: State, fromState: State | undefined) => {\n if (toState.name === fromState?.name) {\n invokeHook(\"onStay\", toState.name, toState, fromState);\n } else {\n invokeHook(\"onEnter\", toState.name, toState, fromState);\n }\n },\n };\n}\n\nexport function lifecyclePluginFactory(): PluginFactory {\n return createPlugin;\n}\n"],"mappings":"qDAMA,SAAS,EAAiB,EAAgB,CACxC,OACE,EACA,EACA,EACA,IACS,CACT,IAAM,EAAO,EAAI,eAAe,EAAU,GAAG,GAEzC,OAAO,GAAS,YACjB,EAAuB,EAAS,EAAU,EAKjD,SAAS,EAAa,EAAsC,CAC1D,IAAM,EAAa,EAAiB,EAAa,EAAO,CAAC,CAEzD,MAAO,CACL,0BACE,EACA,IACG,CACC,GAAa,EAAQ,OAAS,EAAU,MAC1C,EAAW,UAAW,EAAU,KAAM,EAAS,EAAU,EAI7D,qBAAsB,EAAgB,IAAiC,CACjE,EAAQ,OAAS,GAAW,KAC9B,EAAW,SAAU,EAAQ,KAAM,EAAS,EAAU,CAEtD,EAAW,UAAW,EAAQ,KAAM,EAAS,EAAU,EAG5D,CAGH,SAAgB,GAAwC,CACtD,OAAO"}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@real-router/lifecycle-plugin",
3
+ "version": "0.0.1",
4
+ "type": "commonjs",
5
+ "description": "Route-level lifecycle hooks: onEnter, onStay, onLeave",
6
+ "main": "./dist/cjs/index.js",
7
+ "module": "./dist/esm/index.mjs",
8
+ "types": "./dist/esm/index.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "development": "./src/index.ts",
12
+ "types": {
13
+ "import": "./dist/esm/index.d.mts",
14
+ "require": "./dist/cjs/index.d.ts"
15
+ },
16
+ "import": "./dist/esm/index.mjs",
17
+ "require": "./dist/cjs/index.js"
18
+ }
19
+ },
20
+ "files": [
21
+ "dist",
22
+ "src"
23
+ ],
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/greydragon888/real-router.git"
27
+ },
28
+ "keywords": [
29
+ "real-router",
30
+ "lifecycle",
31
+ "hooks",
32
+ "onEnter",
33
+ "onStay",
34
+ "onLeave"
35
+ ],
36
+ "author": {
37
+ "name": "Oleg Ivanov",
38
+ "email": "greydragon888@gmail.com",
39
+ "url": "https://github.com/greydragon888"
40
+ },
41
+ "license": "MIT",
42
+ "bugs": {
43
+ "url": "https://github.com/greydragon888/real-router/issues"
44
+ },
45
+ "homepage": "https://github.com/greydragon888/real-router",
46
+ "scripts": {
47
+ "build": "tsdown --config-loader unrun",
48
+ "test": "vitest run",
49
+ "type-check": "tsc --noEmit",
50
+ "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
51
+ "lint:package": "publint",
52
+ "lint:types": "attw --pack .",
53
+ "build:dist-only": "tsdown --config-loader unrun"
54
+ },
55
+ "sideEffects": false,
56
+ "dependencies": {
57
+ "@real-router/core": "workspace:^"
58
+ }
59
+ }
package/src/factory.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { getPluginApi } from "@real-router/core/api";
2
+
3
+ import type { LifecycleHook } from "./types";
4
+ import type { PluginFactory, State } from "@real-router/core";
5
+ import type { PluginApi } from "@real-router/core/api";
6
+
7
+ function createInvokeHook(api: PluginApi) {
8
+ return (
9
+ hookName: "onEnter" | "onStay" | "onLeave",
10
+ routeName: string,
11
+ toState: State,
12
+ fromState: State | undefined,
13
+ ): void => {
14
+ const hook = api.getRouteConfig(routeName)?.[hookName];
15
+
16
+ if (typeof hook === "function") {
17
+ (hook as LifecycleHook)(toState, fromState);
18
+ }
19
+ };
20
+ }
21
+
22
+ function createPlugin(router: Parameters<PluginFactory>[0]) {
23
+ const invokeHook = createInvokeHook(getPluginApi(router));
24
+
25
+ return {
26
+ onTransitionLeaveApprove: (
27
+ toState: State,
28
+ fromState: State | undefined,
29
+ ) => {
30
+ if (fromState && toState.name !== fromState.name) {
31
+ invokeHook("onLeave", fromState.name, toState, fromState);
32
+ }
33
+ },
34
+
35
+ onTransitionSuccess: (toState: State, fromState: State | undefined) => {
36
+ if (toState.name === fromState?.name) {
37
+ invokeHook("onStay", toState.name, toState, fromState);
38
+ } else {
39
+ invokeHook("onEnter", toState.name, toState, fromState);
40
+ }
41
+ },
42
+ };
43
+ }
44
+
45
+ export function lifecyclePluginFactory(): PluginFactory {
46
+ return createPlugin;
47
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { LifecycleHook } from "./types";
2
+
3
+ export { lifecyclePluginFactory } from "./factory";
4
+
5
+ /**
6
+ * Module augmentation for real-router.
7
+ * Extends Route interface with lifecycle hooks.
8
+ */
9
+ declare module "@real-router/core" {
10
+ interface Route {
11
+ /** Called when this route segment is newly activated (entered). */
12
+ onEnter?: LifecycleHook;
13
+ /** Called when this route segment stays active but params changed. */
14
+ onStay?: LifecycleHook;
15
+ /** Called when this route segment is deactivated (left). */
16
+ onLeave?: LifecycleHook;
17
+ }
18
+ }
19
+
20
+ export type { LifecycleHook } from "./types";
package/src/types.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { State } from "@real-router/core";
2
+
3
+ /**
4
+ * Lifecycle hook callback for route transitions.
5
+ * Fire-and-forget: return values are ignored, errors are caught and warned.
6
+ */
7
+ export type LifecycleHook = (
8
+ toState: State,
9
+ fromState: State | undefined,
10
+ ) => void;