@real-router/lifecycle-plugin 0.2.0 → 0.3.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/README.md CHANGED
@@ -15,8 +15,8 @@ router.subscribe(({ route, previousRoute }) => {
15
15
  });
16
16
 
17
17
  // With plugin — declarative, per-route:
18
- { name: "dashboard", path: "/dashboard", onEnter: () => trackPageView("dashboard") }
19
- { name: "editor", path: "/editor", onLeave: () => saveEditorState() }
18
+ { name: "dashboard", path: "/dashboard", onEnter: () => () => trackPageView("dashboard") }
19
+ { name: "editor", path: "/editor", onLeave: () => () => saveEditorState() }
20
20
  ```
21
21
 
22
22
  ## Installation
@@ -37,17 +37,17 @@ const routes = [
37
37
  {
38
38
  name: "home",
39
39
  path: "/",
40
- onLeave: (toState, fromState) => {
40
+ onLeave: () => (toState, fromState) => {
41
41
  console.log("Leaving home for", toState.name);
42
42
  },
43
43
  },
44
44
  {
45
45
  name: "users.view",
46
46
  path: "/users/:id",
47
- onEnter: (toState) => {
47
+ onEnter: () => (toState) => {
48
48
  analytics.track("user_profile_viewed", { userId: toState.params.id });
49
49
  },
50
- onStay: (toState, fromState) => {
50
+ onStay: () => (toState, fromState) => {
51
51
  console.log("User changed:", fromState.params.id, "→", toState.params.id);
52
52
  },
53
53
  },
@@ -67,7 +67,18 @@ await router.start("/");
67
67
  | `onStay` | Same route, params changed | Refresh data, update UI |
68
68
  | `onLeave` | Route is left | Cleanup timers, save state |
69
69
 
70
- All hooks receive `(toState: State, fromState: State | undefined) => void`.
70
+ Each hook field is a **factory function** `(router, getDependency) => (toState, fromState?) => void`. The factory runs once per route; the returned callback is cached and invoked on each matching transition. When you don't need DI, omit the factory params:
71
+
72
+ ```typescript
73
+ // Without DI — ignore factory params:
74
+ onEnter: () => (toState) => { console.log("entered", toState.name); }
75
+
76
+ // With DI — access router and dependencies:
77
+ onEnter: (router, getDependency) => (toState) => {
78
+ const analytics = getDependency("analytics");
79
+ analytics.track("page_viewed", { route: toState.name });
80
+ }
81
+ ```
71
82
 
72
83
  ### Execution order
73
84
 
@@ -81,7 +92,7 @@ All hooks receive `(toState: State, fromState: State | undefined) => void`.
81
92
  {
82
93
  name: "product",
83
94
  path: "/products/:id",
84
- onEnter: (toState) => {
95
+ onEnter: () => (toState) => {
85
96
  analytics.track("product_viewed", { productId: toState.params.id });
86
97
  },
87
98
  }
@@ -93,7 +104,7 @@ All hooks receive `(toState: State, fromState: State | undefined) => void`.
93
104
  {
94
105
  name: "editor",
95
106
  path: "/editor/:docId",
96
- onLeave: () => {
107
+ onLeave: () => () => {
97
108
  autosaveTimer.clear();
98
109
  webSocket.disconnect();
99
110
  },
@@ -106,7 +117,7 @@ All hooks receive `(toState: State, fromState: State | undefined) => void`.
106
117
  {
107
118
  name: "search",
108
119
  path: "/search?q",
109
- onStay: (toState) => {
120
+ onStay: () => (toState) => {
110
121
  searchStore.setQuery(toState.params.q);
111
122
  },
112
123
  }
@@ -1,11 +1,18 @@
1
- import { PluginFactory, State } from "@real-router/core";
1
+ import { DefaultDependencies, PluginFactory, State } from "@real-router/core";
2
+ import { DefaultDependencies as DefaultDependencies$1, Router } from "@real-router/types";
2
3
 
3
4
  //#region src/types.d.ts
4
5
  /**
5
6
  * Lifecycle hook callback for route transitions.
6
- * Fire-and-forget: return values are ignored, errors are caught and warned.
7
+ * Fire-and-forget: return values are ignored, errors propagate to EventEmitter (logged to stderr).
7
8
  */
8
9
  type LifecycleHook = (toState: State, fromState: State | undefined) => void;
10
+ /**
11
+ * Factory function for creating lifecycle hooks.
12
+ * Receives the router instance and a dependency getter (same pattern as GuardFnFactory).
13
+ * Factory runs once at first invocation; the returned hook is cached per route.
14
+ */
15
+ type LifecycleHookFactory<Dependencies extends DefaultDependencies$1 = DefaultDependencies$1> = (router: Router<Dependencies>, getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K]) => LifecycleHook;
9
16
  //#endregion
10
17
  //#region src/factory.d.ts
11
18
  declare function lifecyclePluginFactory(): PluginFactory;
@@ -13,18 +20,18 @@ declare function lifecyclePluginFactory(): PluginFactory;
13
20
  //#region src/index.d.ts
14
21
  /**
15
22
  * Module augmentation for real-router.
16
- * Extends Route interface with lifecycle hooks.
23
+ * Extends Route interface with lifecycle hook factories.
17
24
  */
18
25
  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
+ interface Route<Dependencies extends DefaultDependencies> {
27
+ /** Factory that returns a hook called when this route segment is newly activated (entered). */
28
+ onEnter?: LifecycleHookFactory<Dependencies>;
29
+ /** Factory that returns a hook called when this route segment stays active but params changed. */
30
+ onStay?: LifecycleHookFactory<Dependencies>;
31
+ /** Factory that returns a hook called when this route segment is deactivated (left). */
32
+ onLeave?: LifecycleHookFactory<Dependencies>;
26
33
  }
27
34
  }
28
35
  //#endregion
29
- export { type LifecycleHook, lifecyclePluginFactory };
36
+ export { type LifecycleHook, type LifecycleHookFactory, lifecyclePluginFactory };
30
37
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;AAMA;;KAAY,aAAA,IACV,OAAA,EAAS,KAAA,EACT,SAAA,EAAW,KAAA;;;iBCoCG,sBAAA,CAAA,GAA0B,aAAA;;;;ADtC1C;;;;YEGY,KAAA;IFFV;IEIE,OAAA,GAAU,aAAA;IFHZ;IEKE,MAAA,GAAS,aAAA;IFLiB;IEO1B,OAAA,GAAU,aAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAOA;;KAAY,aAAA,IACV,OAAA,EAAS,KAAA,EACT,SAAA,EAAW,KAAA;;;;;;KAQD,oBAAA,sBACW,qBAAA,GAAsB,qBAAA,KAE3C,MAAA,EAAQ,MAAA,CAAO,YAAA,GACf,aAAA,mBAAgC,YAAA,EAAc,GAAA,EAAK,CAAA,KAAM,YAAA,CAAa,CAAA,MACnE,aAAA;;;iBC2CW,sBAAA,CAAA,GAA0B,aAAA;;;;AD1D1C;;;;YEGY,KAAA,sBAA2B,mBAAA;IFFrC;IEIE,OAAA,GAAU,oBAAA,CAAqB,YAAA;IFHjC;IEKE,MAAA,GAAS,oBAAA,CAAqB,YAAA;IFLJ;IEO1B,OAAA,GAAU,oBAAA,CAAqB,YAAA;EAAA;AAAA"}
package/dist/cjs/index.js CHANGED
@@ -1,2 +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;
1
+ Object.defineProperty(exports,Symbol.toStringTag,{value:`Module`});let e=require(`@real-router/core/api`);function t(...t){let[n,r]=t,i=(0,e.getPluginApi)(n),a=new Map;function o(e,t){let o=`${e}:${t}`,s=i.getRouteConfig(t),c=typeof s?.[e]==`function`?s[e]:void 0;if(!c){a.delete(o);return}let l=a.get(o);if(l?.factory===c)return l.hook;let u=c(n,r);return a.set(o,{hook:u,factory:c}),u}return{onTransitionLeaveApprove:(e,t)=>{t&&e.name!==t.name&&o(`onLeave`,t.name)?.(e,t)},onTransitionSuccess:(e,t)=>{e.name===t?.name?o(`onStay`,e.name)?.(e,t):o(`onEnter`,e.name)?.(e,t)}}}function n(){return t}exports.lifecyclePluginFactory=n;
2
2
  //# sourceMappingURL=index.js.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/factory.ts"],"sourcesContent":["import { getPluginApi } from \"@real-router/core/api\";\n\nimport type { LifecycleHook, LifecycleHookFactory } from \"./types\";\nimport type { PluginFactory, State } from \"@real-router/core\";\n\nfunction createPlugin(\n ...args: Parameters<PluginFactory>\n): ReturnType<PluginFactory> {\n const [router, getDependency] = args;\n const api = getPluginApi(router);\n const compiledHooks = new Map<\n string,\n { hook: LifecycleHook; factory: LifecycleHookFactory }\n >();\n\n function compileHook(\n hookName: \"onEnter\" | \"onStay\" | \"onLeave\",\n routeName: string,\n ): LifecycleHook | undefined {\n const key = `${hookName}:${routeName}`;\n const config = api.getRouteConfig(routeName);\n const factory =\n typeof config?.[hookName] === \"function\"\n ? (config[hookName] as LifecycleHookFactory)\n : undefined;\n\n if (!factory) {\n compiledHooks.delete(key);\n\n return undefined;\n }\n\n const cached = compiledHooks.get(key);\n\n if (cached?.factory === factory) {\n return cached.hook;\n }\n\n const hook = factory(router, getDependency);\n\n compiledHooks.set(key, { hook, factory });\n\n return hook;\n }\n\n return {\n onTransitionLeaveApprove: (\n toState: State,\n fromState: State | undefined,\n ) => {\n if (fromState && toState.name !== fromState.name) {\n compileHook(\"onLeave\", fromState.name)?.(toState, fromState);\n }\n },\n\n onTransitionSuccess: (toState: State, fromState: State | undefined) => {\n if (toState.name === fromState?.name) {\n compileHook(\"onStay\", toState.name)?.(toState, fromState);\n } else {\n compileHook(\"onEnter\", toState.name)?.(toState, fromState);\n }\n },\n };\n}\n\nexport function lifecyclePluginFactory(): PluginFactory {\n return createPlugin;\n}\n"],"mappings":"0GAKA,SAAS,EACP,GAAG,EACwB,CAC3B,GAAM,CAAC,EAAQ,GAAiB,EAC1B,GAAA,EAAA,EAAA,cAAmB,EAAO,CAC1B,EAAgB,IAAI,IAK1B,SAAS,EACP,EACA,EAC2B,CAC3B,IAAM,EAAM,GAAG,EAAS,GAAG,IACrB,EAAS,EAAI,eAAe,EAAU,CACtC,EACJ,OAAO,IAAS,IAAc,WACzB,EAAO,GACR,IAAA,GAEN,GAAI,CAAC,EAAS,CACZ,EAAc,OAAO,EAAI,CAEzB,OAGF,IAAM,EAAS,EAAc,IAAI,EAAI,CAErC,GAAI,GAAQ,UAAY,EACtB,OAAO,EAAO,KAGhB,IAAM,EAAO,EAAQ,EAAQ,EAAc,CAI3C,OAFA,EAAc,IAAI,EAAK,CAAE,OAAM,UAAS,CAAC,CAElC,EAGT,MAAO,CACL,0BACE,EACA,IACG,CACC,GAAa,EAAQ,OAAS,EAAU,MAC1C,EAAY,UAAW,EAAU,KAAK,GAAG,EAAS,EAAU,EAIhE,qBAAsB,EAAgB,IAAiC,CACjE,EAAQ,OAAS,GAAW,KAC9B,EAAY,SAAU,EAAQ,KAAK,GAAG,EAAS,EAAU,CAEzD,EAAY,UAAW,EAAQ,KAAK,GAAG,EAAS,EAAU,EAG/D,CAGH,SAAgB,GAAwC,CACtD,OAAO"}
@@ -1,11 +1,18 @@
1
- import { PluginFactory, State } from "@real-router/core";
1
+ import { DefaultDependencies, PluginFactory, State } from "@real-router/core";
2
+ import { DefaultDependencies as DefaultDependencies$1, Router } from "@real-router/types";
2
3
 
3
4
  //#region src/types.d.ts
4
5
  /**
5
6
  * Lifecycle hook callback for route transitions.
6
- * Fire-and-forget: return values are ignored, errors are caught and warned.
7
+ * Fire-and-forget: return values are ignored, errors propagate to EventEmitter (logged to stderr).
7
8
  */
8
9
  type LifecycleHook = (toState: State, fromState: State | undefined) => void;
10
+ /**
11
+ * Factory function for creating lifecycle hooks.
12
+ * Receives the router instance and a dependency getter (same pattern as GuardFnFactory).
13
+ * Factory runs once at first invocation; the returned hook is cached per route.
14
+ */
15
+ type LifecycleHookFactory<Dependencies extends DefaultDependencies$1 = DefaultDependencies$1> = (router: Router<Dependencies>, getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K]) => LifecycleHook;
9
16
  //#endregion
10
17
  //#region src/factory.d.ts
11
18
  declare function lifecyclePluginFactory(): PluginFactory;
@@ -13,18 +20,18 @@ declare function lifecyclePluginFactory(): PluginFactory;
13
20
  //#region src/index.d.ts
14
21
  /**
15
22
  * Module augmentation for real-router.
16
- * Extends Route interface with lifecycle hooks.
23
+ * Extends Route interface with lifecycle hook factories.
17
24
  */
18
25
  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
+ interface Route<Dependencies extends DefaultDependencies> {
27
+ /** Factory that returns a hook called when this route segment is newly activated (entered). */
28
+ onEnter?: LifecycleHookFactory<Dependencies>;
29
+ /** Factory that returns a hook called when this route segment stays active but params changed. */
30
+ onStay?: LifecycleHookFactory<Dependencies>;
31
+ /** Factory that returns a hook called when this route segment is deactivated (left). */
32
+ onLeave?: LifecycleHookFactory<Dependencies>;
26
33
  }
27
34
  }
28
35
  //#endregion
29
- export { type LifecycleHook, lifecyclePluginFactory };
36
+ export { type LifecycleHook, type LifecycleHookFactory, lifecyclePluginFactory };
30
37
  //# sourceMappingURL=index.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;AAMA;;KAAY,aAAA,IACV,OAAA,EAAS,KAAA,EACT,SAAA,EAAW,KAAA;;;iBCoCG,sBAAA,CAAA,GAA0B,aAAA;;;;ADtC1C;;;;YEGY,KAAA;IFFV;IEIE,OAAA,GAAU,aAAA;IFHZ;IEKE,MAAA,GAAS,aAAA;IFLiB;IEO1B,OAAA,GAAU,aAAA;EAAA;AAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../../src/types.ts","../../src/factory.ts","../../src/index.ts"],"mappings":";;;;;;AAOA;;KAAY,aAAA,IACV,OAAA,EAAS,KAAA,EACT,SAAA,EAAW,KAAA;;;;;;KAQD,oBAAA,sBACW,qBAAA,GAAsB,qBAAA,KAE3C,MAAA,EAAQ,MAAA,CAAO,YAAA,GACf,aAAA,mBAAgC,YAAA,EAAc,GAAA,EAAK,CAAA,KAAM,YAAA,CAAa,CAAA,MACnE,aAAA;;;iBC2CW,sBAAA,CAAA,GAA0B,aAAA;;;;AD1D1C;;;;YEGY,KAAA,sBAA2B,mBAAA;IFFrC;IEIE,OAAA,GAAU,oBAAA,CAAqB,YAAA;IFHjC;IEKE,MAAA,GAAS,oBAAA,CAAqB,YAAA;IFLJ;IEO1B,OAAA,GAAU,oBAAA,CAAqB,YAAA;EAAA;AAAA"}
@@ -1,2 +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};
1
+ import{getPluginApi as e}from"@real-router/core/api";function t(...t){let[n,r]=t,i=e(n),a=new Map;function o(e,t){let o=`${e}:${t}`,s=i.getRouteConfig(t),c=typeof s?.[e]==`function`?s[e]:void 0;if(!c){a.delete(o);return}let l=a.get(o);if(l?.factory===c)return l.hook;let u=c(n,r);return a.set(o,{hook:u,factory:c}),u}return{onTransitionLeaveApprove:(e,t)=>{t&&e.name!==t.name&&o(`onLeave`,t.name)?.(e,t)},onTransitionSuccess:(e,t)=>{e.name===t?.name?o(`onStay`,e.name)?.(e,t):o(`onEnter`,e.name)?.(e,t)}}}function n(){return t}export{n as lifecyclePluginFactory};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/factory.ts"],"sourcesContent":["import { getPluginApi } from \"@real-router/core/api\";\n\nimport type { LifecycleHook, LifecycleHookFactory } from \"./types\";\nimport type { PluginFactory, State } from \"@real-router/core\";\n\nfunction createPlugin(\n ...args: Parameters<PluginFactory>\n): ReturnType<PluginFactory> {\n const [router, getDependency] = args;\n const api = getPluginApi(router);\n const compiledHooks = new Map<\n string,\n { hook: LifecycleHook; factory: LifecycleHookFactory }\n >();\n\n function compileHook(\n hookName: \"onEnter\" | \"onStay\" | \"onLeave\",\n routeName: string,\n ): LifecycleHook | undefined {\n const key = `${hookName}:${routeName}`;\n const config = api.getRouteConfig(routeName);\n const factory =\n typeof config?.[hookName] === \"function\"\n ? (config[hookName] as LifecycleHookFactory)\n : undefined;\n\n if (!factory) {\n compiledHooks.delete(key);\n\n return undefined;\n }\n\n const cached = compiledHooks.get(key);\n\n if (cached?.factory === factory) {\n return cached.hook;\n }\n\n const hook = factory(router, getDependency);\n\n compiledHooks.set(key, { hook, factory });\n\n return hook;\n }\n\n return {\n onTransitionLeaveApprove: (\n toState: State,\n fromState: State | undefined,\n ) => {\n if (fromState && toState.name !== fromState.name) {\n compileHook(\"onLeave\", fromState.name)?.(toState, fromState);\n }\n },\n\n onTransitionSuccess: (toState: State, fromState: State | undefined) => {\n if (toState.name === fromState?.name) {\n compileHook(\"onStay\", toState.name)?.(toState, fromState);\n } else {\n compileHook(\"onEnter\", toState.name)?.(toState, fromState);\n }\n },\n };\n}\n\nexport function lifecyclePluginFactory(): PluginFactory {\n return createPlugin;\n}\n"],"mappings":"qDAKA,SAAS,EACP,GAAG,EACwB,CAC3B,GAAM,CAAC,EAAQ,GAAiB,EAC1B,EAAM,EAAa,EAAO,CAC1B,EAAgB,IAAI,IAK1B,SAAS,EACP,EACA,EAC2B,CAC3B,IAAM,EAAM,GAAG,EAAS,GAAG,IACrB,EAAS,EAAI,eAAe,EAAU,CACtC,EACJ,OAAO,IAAS,IAAc,WACzB,EAAO,GACR,IAAA,GAEN,GAAI,CAAC,EAAS,CACZ,EAAc,OAAO,EAAI,CAEzB,OAGF,IAAM,EAAS,EAAc,IAAI,EAAI,CAErC,GAAI,GAAQ,UAAY,EACtB,OAAO,EAAO,KAGhB,IAAM,EAAO,EAAQ,EAAQ,EAAc,CAI3C,OAFA,EAAc,IAAI,EAAK,CAAE,OAAM,UAAS,CAAC,CAElC,EAGT,MAAO,CACL,0BACE,EACA,IACG,CACC,GAAa,EAAQ,OAAS,EAAU,MAC1C,EAAY,UAAW,EAAU,KAAK,GAAG,EAAS,EAAU,EAIhE,qBAAsB,EAAgB,IAAiC,CACjE,EAAQ,OAAS,GAAW,KAC9B,EAAY,SAAU,EAAQ,KAAK,GAAG,EAAS,EAAU,CAEzD,EAAY,UAAW,EAAQ,KAAK,GAAG,EAAS,EAAU,EAG/D,CAGH,SAAgB,GAAwC,CACtD,OAAO"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@real-router/lifecycle-plugin",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "type": "commonjs",
5
5
  "description": "Route-level lifecycle hooks: onEnter, onStay, onLeave",
6
6
  "main": "./dist/cjs/index.js",
@@ -45,16 +45,17 @@
45
45
  "homepage": "https://github.com/greydragon888/real-router",
46
46
  "sideEffects": false,
47
47
  "dependencies": {
48
- "@real-router/core": "^0.47.0"
48
+ "@real-router/core": "^0.48.0",
49
+ "@real-router/types": "^0.34.0"
49
50
  },
50
51
  "scripts": {
51
- "build": "tsdown --config-loader unrun",
52
52
  "test": "vitest run",
53
53
  "test:properties": "vitest run --config vitest.config.properties.mts",
54
54
  "test:stress": "vitest --config vitest.config.stress.mts --run",
55
55
  "type-check": "tsc --noEmit",
56
56
  "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
57
57
  "lint:package": "publint",
58
- "lint:types": "attw --pack ."
58
+ "lint:types": "attw --pack .",
59
+ "bundle": "tsdown --config-loader unrun"
59
60
  }
60
61
  }
package/src/factory.ts CHANGED
@@ -1,26 +1,47 @@
1
1
  import { getPluginApi } from "@real-router/core/api";
2
2
 
3
- import type { LifecycleHook } from "./types";
3
+ import type { LifecycleHook, LifecycleHookFactory } from "./types";
4
4
  import type { PluginFactory, State } from "@real-router/core";
5
- import type { PluginApi } from "@real-router/core/api";
6
5
 
7
- function createInvokeHook(api: PluginApi) {
8
- return (
6
+ function createPlugin(
7
+ ...args: Parameters<PluginFactory>
8
+ ): ReturnType<PluginFactory> {
9
+ const [router, getDependency] = args;
10
+ const api = getPluginApi(router);
11
+ const compiledHooks = new Map<
12
+ string,
13
+ { hook: LifecycleHook; factory: LifecycleHookFactory }
14
+ >();
15
+
16
+ function compileHook(
9
17
  hookName: "onEnter" | "onStay" | "onLeave",
10
18
  routeName: string,
11
- toState: State,
12
- fromState: State | undefined,
13
- ): void => {
14
- const hook = api.getRouteConfig(routeName)?.[hookName];
19
+ ): LifecycleHook | undefined {
20
+ const key = `${hookName}:${routeName}`;
21
+ const config = api.getRouteConfig(routeName);
22
+ const factory =
23
+ typeof config?.[hookName] === "function"
24
+ ? (config[hookName] as LifecycleHookFactory)
25
+ : undefined;
26
+
27
+ if (!factory) {
28
+ compiledHooks.delete(key);
15
29
 
16
- if (typeof hook === "function") {
17
- (hook as LifecycleHook)(toState, fromState);
30
+ return undefined;
18
31
  }
19
- };
20
- }
21
32
 
22
- function createPlugin(router: Parameters<PluginFactory>[0]) {
23
- const invokeHook = createInvokeHook(getPluginApi(router));
33
+ const cached = compiledHooks.get(key);
34
+
35
+ if (cached?.factory === factory) {
36
+ return cached.hook;
37
+ }
38
+
39
+ const hook = factory(router, getDependency);
40
+
41
+ compiledHooks.set(key, { hook, factory });
42
+
43
+ return hook;
44
+ }
24
45
 
25
46
  return {
26
47
  onTransitionLeaveApprove: (
@@ -28,15 +49,15 @@ function createPlugin(router: Parameters<PluginFactory>[0]) {
28
49
  fromState: State | undefined,
29
50
  ) => {
30
51
  if (fromState && toState.name !== fromState.name) {
31
- invokeHook("onLeave", fromState.name, toState, fromState);
52
+ compileHook("onLeave", fromState.name)?.(toState, fromState);
32
53
  }
33
54
  },
34
55
 
35
56
  onTransitionSuccess: (toState: State, fromState: State | undefined) => {
36
57
  if (toState.name === fromState?.name) {
37
- invokeHook("onStay", toState.name, toState, fromState);
58
+ compileHook("onStay", toState.name)?.(toState, fromState);
38
59
  } else {
39
- invokeHook("onEnter", toState.name, toState, fromState);
60
+ compileHook("onEnter", toState.name)?.(toState, fromState);
40
61
  }
41
62
  },
42
63
  };
package/src/index.ts CHANGED
@@ -1,20 +1,21 @@
1
- import type { LifecycleHook } from "./types";
1
+ import type { LifecycleHookFactory } from "./types";
2
+ import type { DefaultDependencies } from "@real-router/core";
2
3
 
3
4
  export { lifecyclePluginFactory } from "./factory";
4
5
 
5
6
  /**
6
7
  * Module augmentation for real-router.
7
- * Extends Route interface with lifecycle hooks.
8
+ * Extends Route interface with lifecycle hook factories.
8
9
  */
9
10
  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;
11
+ interface Route<Dependencies extends DefaultDependencies> {
12
+ /** Factory that returns a hook called when this route segment is newly activated (entered). */
13
+ onEnter?: LifecycleHookFactory<Dependencies>;
14
+ /** Factory that returns a hook called when this route segment stays active but params changed. */
15
+ onStay?: LifecycleHookFactory<Dependencies>;
16
+ /** Factory that returns a hook called when this route segment is deactivated (left). */
17
+ onLeave?: LifecycleHookFactory<Dependencies>;
17
18
  }
18
19
  }
19
20
 
20
- export type { LifecycleHook } from "./types";
21
+ export type { LifecycleHook, LifecycleHookFactory } from "./types";
package/src/types.ts CHANGED
@@ -1,10 +1,23 @@
1
1
  import type { State } from "@real-router/core";
2
+ import type { DefaultDependencies, Router } from "@real-router/types";
2
3
 
3
4
  /**
4
5
  * Lifecycle hook callback for route transitions.
5
- * Fire-and-forget: return values are ignored, errors are caught and warned.
6
+ * Fire-and-forget: return values are ignored, errors propagate to EventEmitter (logged to stderr).
6
7
  */
7
8
  export type LifecycleHook = (
8
9
  toState: State,
9
10
  fromState: State | undefined,
10
11
  ) => void;
12
+
13
+ /**
14
+ * Factory function for creating lifecycle hooks.
15
+ * Receives the router instance and a dependency getter (same pattern as GuardFnFactory).
16
+ * Factory runs once at first invocation; the returned hook is cached per route.
17
+ */
18
+ export type LifecycleHookFactory<
19
+ Dependencies extends DefaultDependencies = DefaultDependencies,
20
+ > = (
21
+ router: Router<Dependencies>,
22
+ getDependency: <K extends keyof Dependencies>(key: K) => Dependencies[K],
23
+ ) => LifecycleHook;