@real-router/ssr-data-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,120 @@
1
+ # @real-router/ssr-data-plugin
2
+
3
+ [![npm](https://img.shields.io/npm/v/@real-router/ssr-data-plugin.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/ssr-data-plugin)
4
+ [![npm downloads](https://img.shields.io/npm/dm/@real-router/ssr-data-plugin.svg?style=flat-square)](https://www.npmjs.com/package/@real-router/ssr-data-plugin)
5
+ [![bundle size](https://deno.bundlejs.com/?q=@real-router/ssr-data-plugin&treeshake=[*]&badge=detailed)](https://bundlejs.com/?q=@real-router/ssr-data-plugin&treeshake=[*])
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg?style=flat-square)](../../LICENSE)
7
+
8
+ > Per-route data loading for SSR with [Real-Router](https://github.com/greydragon888/real-router). Intercepts `start()` to load data before server rendering.
9
+
10
+ ```typescript
11
+ // Without plugin:
12
+ const state = await router.start(url);
13
+ const data = await loadRouteData(state.name, state.params); // manual
14
+
15
+ // With plugin:
16
+ router.usePlugin(ssrDataPluginFactory(loaders));
17
+ const state = await router.start(url);
18
+ const data = router.getRouteData(); // loaded automatically
19
+ ```
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @real-router/ssr-data-plugin
25
+ ```
26
+
27
+ **Peer dependency:** `@real-router/core`
28
+
29
+ ## Quick Start
30
+
31
+ ```typescript
32
+ import { createRouter } from "@real-router/core";
33
+ import { cloneRouter } from "@real-router/core/api";
34
+ import { ssrDataPluginFactory } from "@real-router/ssr-data-plugin";
35
+
36
+ const loaders = {
37
+ "users.profile": async (params) => fetchUser(params.id),
38
+ "users.list": async () => fetchUsers(),
39
+ };
40
+
41
+ // Base router — created once
42
+ const baseRouter = createRouter(routes, { defaultRoute: "home", allowNotFound: true });
43
+
44
+ // Per-request SSR
45
+ const router = cloneRouter(baseRouter, { isAuthenticated: true });
46
+ router.usePlugin(ssrDataPluginFactory(loaders));
47
+
48
+ const state = await router.start(url);
49
+ const data = router.getRouteData(); // data loaded by matching loader
50
+
51
+ const html = renderToString(<App />);
52
+ router.dispose();
53
+ ```
54
+
55
+ ## Configuration
56
+
57
+ Loaders are keyed by **route name** (not path). Each loader receives route `params` and returns a `Promise`:
58
+
59
+ ```typescript
60
+ import type { DataLoaderMap } from "@real-router/ssr-data-plugin";
61
+
62
+ const loaders: DataLoaderMap = {
63
+ home: async () => ({ featured: await fetchFeatured() }),
64
+ "users.profile": async (params) => ({ user: await fetchUser(params.id) }),
65
+ "users.list": async () => ({ users: await fetchUsers() }),
66
+ };
67
+ ```
68
+
69
+ Routes without a matching loader produce no data — `getRouteData()` returns `null`.
70
+
71
+ ## Router Extension
72
+
73
+ The plugin extends the router instance with one method via [`extendRouter()`](https://github.com/greydragon888/real-router/wiki/plugin-architecture):
74
+
75
+ | Method | Returns | Description |
76
+ | ---------------------- | --------- | ------------------------------------------ |
77
+ | `getRouteData(state?)` | `unknown` | Get loaded data for current or given state |
78
+
79
+ ```typescript
80
+ router.getRouteData(); // data for current state
81
+ router.getRouteData(state); // data for a specific state
82
+ ```
83
+
84
+ ## SSR-Only by Design
85
+
86
+ This plugin intercepts `start()` only — not `navigate()`. In SSR, the flow is:
87
+
88
+ ```
89
+ cloneRouter → usePlugin → start(url) → data loaded → renderToString → getRouteData()
90
+ ```
91
+
92
+ Client-side navigation and data fetching is the application's responsibility (React Query, Suspense, `useEffect`, etc.).
93
+
94
+ ## Cleanup
95
+
96
+ ```typescript
97
+ const unsubscribe = router.usePlugin(ssrDataPluginFactory(loaders));
98
+
99
+ // Later — removes getRouteData and stops data loading
100
+ unsubscribe();
101
+ ```
102
+
103
+ In SSR, `router.dispose()` handles cleanup automatically.
104
+
105
+ ## Documentation
106
+
107
+ - [ARCHITECTURE.md](ARCHITECTURE.md) — Design decisions and data flow
108
+ - [SSR Example](../../examples/ssr-react) — Full working example with React + Vite
109
+
110
+ ## Related Packages
111
+
112
+ | Package | Description |
113
+ | ---------------------------------------------------------------------------------------- | -------------------------------------- |
114
+ | [@real-router/core](https://www.npmjs.com/package/@real-router/core) | Core router (required peer dependency) |
115
+ | [@real-router/browser-plugin](https://www.npmjs.com/package/@real-router/browser-plugin) | Browser History API integration |
116
+ | [@real-router/react](https://www.npmjs.com/package/@real-router/react) | React bindings |
117
+
118
+ ## License
119
+
120
+ [MIT](../../LICENSE) © [Oleg Ivanov](https://github.com/greydragon888)
@@ -0,0 +1,14 @@
1
+ import { Params, PluginFactory, State } from '@real-router/core';
2
+
3
+ type DataLoaderFn = (params: Params) => Promise<unknown>;
4
+ type DataLoaderMap = Record<string, DataLoaderFn>;
5
+
6
+ declare function ssrDataPluginFactory(loaders: DataLoaderMap): PluginFactory;
7
+
8
+ declare module "@real-router/core" {
9
+ interface Router {
10
+ getRouteData: (state?: State) => unknown;
11
+ }
12
+ }
13
+
14
+ export { type DataLoaderFn, type DataLoaderMap, ssrDataPluginFactory };
@@ -0,0 +1 @@
1
+ var t=require("@real-router/core/api"),e="[@real-router/ssr-data-plugin]";exports.ssrDataPluginFactory=function(r){return function(t){if(null===t||"object"!=typeof t)throw new TypeError(`${e} loaders must be a non-null object`);for(const[r,n]of Object.entries(t))if("function"!=typeof n)throw new TypeError(`${e} loader for route "${r}" must be a function`)}(r),e=>{const n=t.getPluginApi(e),o=new WeakMap,a=n.addInterceptor("start",async(t,e)=>{const n=await t(e);return Object.hasOwn(r,n.name)&&o.set(n,await r[n.name](n.params)),n}),u=n.extendRouter({getRouteData(t){const r=t??e.getState();return r?o.get(r)??null:null}});return{teardown(){a(),u()}}}};//# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants.ts","../../src/validation.ts","../../src/factory.ts"],"names":["api","getPluginApi"],"mappings":";;;AAAO,IAAM,cAAA,GAAiB,iBAAA;AAEvB,IAAM,YAAA,GAAe,iBAAiB,cAAc,CAAA,CAAA,CAAA;;;ACEpD,SAAS,gBACd,OAAA,EACkC;AAClC,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAO,OAAA,KAAY,QAAA,EAAU;AACnD,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,EAAG,YAAY,CAAA,kCAAA,CAAoC,CAAA;AAAA,EACzE;AAEA,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,CAAA,IAAK,MAAA,CAAO,OAAA;AAAA,IAChC;AAAA,GACF,EAAG;AACD,IAAA,IAAI,OAAO,UAAU,UAAA,EAAY;AAC/B,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,CAAA,EAAG,YAAY,CAAA,mBAAA,EAAsB,GAAG,CAAA,oBAAA;AAAA,OAC1C;AAAA,IACF;AAAA,EACF;AACF;;;ACbO,SAAS,qBAAqB,OAAA,EAAuC;AAC1E,EAAA,eAAA,CAAgB,OAAO,CAAA;AAEvB,EAAA,OAAO,CAAC,MAAA,KAAmB;AACzB,IAAA,MAAMA,KAAA,GAAMC,iBAAa,MAAM,CAAA;AAC/B,IAAA,MAAM,SAAA,uBAAgB,OAAA,EAAwB;AAE9C,IAAA,MAAM,yBAAyBD,KAAA,CAAI,cAAA;AAAA,MACjC,OAAA;AAAA,MACA,OAAO,MAAM,IAAA,KAAS;AACpB,QAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,IAAI,CAAA;AAE7B,QAAA,IAAI,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,KAAA,CAAM,IAAI,CAAA,EAAG;AACtC,UAAA,SAAA,CAAU,GAAA,CAAI,OAAO,MAAM,OAAA,CAAQ,MAAM,IAAI,CAAA,CAAE,KAAA,CAAM,MAAM,CAAC,CAAA;AAAA,QAC9D;AAEA,QAAA,OAAO,KAAA;AAAA,MACT;AAAA,KACF;AAEA,IAAA,MAAM,gBAAA,GAAmBA,MAAI,YAAA,CAAa;AAAA,MACxC,aAAa,KAAA,EAAwB;AACnC,QAAA,MAAM,CAAA,GAAI,KAAA,IAAS,MAAA,CAAO,QAAA,EAAS;AAEnC,QAAA,OAAO,CAAA,GAAK,SAAA,CAAU,GAAA,CAAI,CAAC,KAAK,IAAA,GAAQ,IAAA;AAAA,MAC1C;AAAA,KACD,CAAA;AAED,IAAA,OAAO;AAAA,MACL,QAAA,GAAW;AACT,QAAA,sBAAA,EAAuB;AACvB,QAAA,gBAAA,EAAiB;AAAA,MACnB;AAAA,KACF;AAAA,EACF,CAAA;AACF","file":"index.js","sourcesContent":["export const LOGGER_CONTEXT = \"ssr-data-plugin\";\n\nexport const ERROR_PREFIX = `[@real-router/${LOGGER_CONTEXT}]`;\n","import { ERROR_PREFIX } from \"./constants\";\n\nimport type { DataLoaderMap } from \"./types\";\n\nexport function validateLoaders(\n loaders: unknown,\n): asserts loaders is DataLoaderMap {\n if (loaders === null || typeof loaders !== \"object\") {\n throw new TypeError(`${ERROR_PREFIX} loaders must be a non-null object`);\n }\n\n for (const [key, value] of Object.entries(\n loaders as Record<string, unknown>,\n )) {\n if (typeof value !== \"function\") {\n throw new TypeError(\n `${ERROR_PREFIX} loader for route \"${key}\" must be a function`,\n );\n }\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { validateLoaders } from \"./validation\";\n\nimport type { DataLoaderMap } from \"./types\";\nimport type { State, PluginFactory, Plugin } from \"@real-router/core\";\n\nexport function ssrDataPluginFactory(loaders: DataLoaderMap): PluginFactory {\n validateLoaders(loaders);\n\n return (router): Plugin => {\n const api = getPluginApi(router);\n const dataStore = new WeakMap<State, unknown>();\n\n const removeStartInterceptor = api.addInterceptor(\n \"start\",\n async (next, path) => {\n const state = await next(path);\n\n if (Object.hasOwn(loaders, state.name)) {\n dataStore.set(state, await loaders[state.name](state.params));\n }\n\n return state;\n },\n );\n\n const removeExtensions = api.extendRouter({\n getRouteData(state?: State): unknown {\n const s = state ?? router.getState();\n\n return s ? (dataStore.get(s) ?? null) : null;\n },\n });\n\n return {\n teardown() {\n removeStartInterceptor();\n removeExtensions();\n },\n };\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ {"inputs":{"../../node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.19.4_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js":{"bytes":569,"imports":[],"format":"esm"},"src/constants.ts":{"bytes":114,"imports":[{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.19.4_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/validation.ts":{"bytes":574,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.19.4_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/factory.ts":{"bytes":1095,"imports":[{"path":"@real-router/core/api","kind":"import-statement","external":true},{"path":"src/validation.ts","kind":"import-statement","original":"./validation"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.19.4_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"},"src/index.ts":{"bytes":271,"imports":[{"path":"src/factory.ts","kind":"import-statement","original":"./factory"},{"path":"/Users/olegivanov/WebstormProjects/real-router/node_modules/.pnpm/tsup@8.5.1_jiti@2.6.1_postcss@8.5.8_tsx@4.19.4_typescript@5.9.3_yaml@2.8.2/node_modules/tsup/assets/cjs_shims.js","kind":"import-statement","external":true}],"format":"esm"}},"outputs":{"dist/cjs/index.js.map":{"imports":[],"exports":[],"inputs":{},"bytes":2982},"dist/cjs/index.js":{"imports":[{"path":"@real-router/core/api","kind":"import-statement","external":true}],"exports":["ssrDataPluginFactory"],"entryPoint":"src/index.ts","inputs":{"src/factory.ts":{"bytesInOutput":853},"src/constants.ts":{"bytesInOutput":95},"src/validation.ts":{"bytesInOutput":397},"src/index.ts":{"bytesInOutput":0}},"bytes":1460}}}
@@ -0,0 +1,14 @@
1
+ import { Params, PluginFactory, State } from '@real-router/core';
2
+
3
+ type DataLoaderFn = (params: Params) => Promise<unknown>;
4
+ type DataLoaderMap = Record<string, DataLoaderFn>;
5
+
6
+ declare function ssrDataPluginFactory(loaders: DataLoaderMap): PluginFactory;
7
+
8
+ declare module "@real-router/core" {
9
+ interface Router {
10
+ getRouteData: (state?: State) => unknown;
11
+ }
12
+ }
13
+
14
+ export { type DataLoaderFn, type DataLoaderMap, ssrDataPluginFactory };
@@ -0,0 +1 @@
1
+ import{getPluginApi as t}from"@real-router/core/api";var e="[@real-router/ssr-data-plugin]";function r(r){return function(t){if(null===t||"object"!=typeof t)throw new TypeError(`${e} loaders must be a non-null object`);for(const[r,n]of Object.entries(t))if("function"!=typeof n)throw new TypeError(`${e} loader for route "${r}" must be a function`)}(r),e=>{const n=t(e),o=new WeakMap,a=n.addInterceptor("start",async(t,e)=>{const n=await t(e);return Object.hasOwn(r,n.name)&&o.set(n,await r[n.name](n.params)),n}),u=n.extendRouter({getRouteData(t){const r=t??e.getState();return r?o.get(r)??null:null}});return{teardown(){a(),u()}}}}export{r as ssrDataPluginFactory};//# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/constants.ts","../../src/validation.ts","../../src/factory.ts"],"names":[],"mappings":";;;AAAO,IAAM,cAAA,GAAiB,iBAAA;AAEvB,IAAM,YAAA,GAAe,iBAAiB,cAAc,CAAA,CAAA,CAAA;;;ACEpD,SAAS,gBACd,OAAA,EACkC;AAClC,EAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAO,OAAA,KAAY,QAAA,EAAU;AACnD,IAAA,MAAM,IAAI,SAAA,CAAU,CAAA,EAAG,YAAY,CAAA,kCAAA,CAAoC,CAAA;AAAA,EACzE;AAEA,EAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,CAAA,IAAK,MAAA,CAAO,OAAA;AAAA,IAChC;AAAA,GACF,EAAG;AACD,IAAA,IAAI,OAAO,UAAU,UAAA,EAAY;AAC/B,MAAA,MAAM,IAAI,SAAA;AAAA,QACR,CAAA,EAAG,YAAY,CAAA,mBAAA,EAAsB,GAAG,CAAA,oBAAA;AAAA,OAC1C;AAAA,IACF;AAAA,EACF;AACF;;;ACbO,SAAS,qBAAqB,OAAA,EAAuC;AAC1E,EAAA,eAAA,CAAgB,OAAO,CAAA;AAEvB,EAAA,OAAO,CAAC,MAAA,KAAmB;AACzB,IAAA,MAAM,GAAA,GAAM,aAAa,MAAM,CAAA;AAC/B,IAAA,MAAM,SAAA,uBAAgB,OAAA,EAAwB;AAE9C,IAAA,MAAM,yBAAyB,GAAA,CAAI,cAAA;AAAA,MACjC,OAAA;AAAA,MACA,OAAO,MAAM,IAAA,KAAS;AACpB,QAAA,MAAM,KAAA,GAAQ,MAAM,IAAA,CAAK,IAAI,CAAA;AAE7B,QAAA,IAAI,MAAA,CAAO,MAAA,CAAO,OAAA,EAAS,KAAA,CAAM,IAAI,CAAA,EAAG;AACtC,UAAA,SAAA,CAAU,GAAA,CAAI,OAAO,MAAM,OAAA,CAAQ,MAAM,IAAI,CAAA,CAAE,KAAA,CAAM,MAAM,CAAC,CAAA;AAAA,QAC9D;AAEA,QAAA,OAAO,KAAA;AAAA,MACT;AAAA,KACF;AAEA,IAAA,MAAM,gBAAA,GAAmB,IAAI,YAAA,CAAa;AAAA,MACxC,aAAa,KAAA,EAAwB;AACnC,QAAA,MAAM,CAAA,GAAI,KAAA,IAAS,MAAA,CAAO,QAAA,EAAS;AAEnC,QAAA,OAAO,CAAA,GAAK,SAAA,CAAU,GAAA,CAAI,CAAC,KAAK,IAAA,GAAQ,IAAA;AAAA,MAC1C;AAAA,KACD,CAAA;AAED,IAAA,OAAO;AAAA,MACL,QAAA,GAAW;AACT,QAAA,sBAAA,EAAuB;AACvB,QAAA,gBAAA,EAAiB;AAAA,MACnB;AAAA,KACF;AAAA,EACF,CAAA;AACF","file":"index.mjs","sourcesContent":["export const LOGGER_CONTEXT = \"ssr-data-plugin\";\n\nexport const ERROR_PREFIX = `[@real-router/${LOGGER_CONTEXT}]`;\n","import { ERROR_PREFIX } from \"./constants\";\n\nimport type { DataLoaderMap } from \"./types\";\n\nexport function validateLoaders(\n loaders: unknown,\n): asserts loaders is DataLoaderMap {\n if (loaders === null || typeof loaders !== \"object\") {\n throw new TypeError(`${ERROR_PREFIX} loaders must be a non-null object`);\n }\n\n for (const [key, value] of Object.entries(\n loaders as Record<string, unknown>,\n )) {\n if (typeof value !== \"function\") {\n throw new TypeError(\n `${ERROR_PREFIX} loader for route \"${key}\" must be a function`,\n );\n }\n }\n}\n","import { getPluginApi } from \"@real-router/core/api\";\n\nimport { validateLoaders } from \"./validation\";\n\nimport type { DataLoaderMap } from \"./types\";\nimport type { State, PluginFactory, Plugin } from \"@real-router/core\";\n\nexport function ssrDataPluginFactory(loaders: DataLoaderMap): PluginFactory {\n validateLoaders(loaders);\n\n return (router): Plugin => {\n const api = getPluginApi(router);\n const dataStore = new WeakMap<State, unknown>();\n\n const removeStartInterceptor = api.addInterceptor(\n \"start\",\n async (next, path) => {\n const state = await next(path);\n\n if (Object.hasOwn(loaders, state.name)) {\n dataStore.set(state, await loaders[state.name](state.params));\n }\n\n return state;\n },\n );\n\n const removeExtensions = api.extendRouter({\n getRouteData(state?: State): unknown {\n const s = state ?? router.getState();\n\n return s ? (dataStore.get(s) ?? null) : null;\n },\n });\n\n return {\n teardown() {\n removeStartInterceptor();\n removeExtensions();\n },\n };\n };\n}\n"]}
@@ -0,0 +1 @@
1
+ {"inputs":{"src/constants.ts":{"bytes":114,"imports":[],"format":"esm"},"src/validation.ts":{"bytes":574,"imports":[{"path":"src/constants.ts","kind":"import-statement","original":"./constants"}],"format":"esm"},"src/factory.ts":{"bytes":1095,"imports":[{"path":"@real-router/core/api","kind":"import-statement","external":true},{"path":"src/validation.ts","kind":"import-statement","original":"./validation"}],"format":"esm"},"src/index.ts":{"bytes":271,"imports":[{"path":"src/factory.ts","kind":"import-statement","original":"./factory"}],"format":"esm"}},"outputs":{"dist/esm/index.mjs.map":{"imports":[],"exports":[],"inputs":{},"bytes":2982},"dist/esm/index.mjs":{"imports":[{"path":"@real-router/core/api","kind":"import-statement","external":true}],"exports":["ssrDataPluginFactory"],"entryPoint":"src/index.ts","inputs":{"src/factory.ts":{"bytesInOutput":853},"src/constants.ts":{"bytesInOutput":95},"src/validation.ts":{"bytesInOutput":397},"src/index.ts":{"bytesInOutput":0}},"bytes":1460}}}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@real-router/ssr-data-plugin",
3
+ "version": "0.0.1",
4
+ "type": "commonjs",
5
+ "description": "SSR per-route data loading plugin for Real-Router",
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
+ "author": {
29
+ "name": "Oleg Ivanov",
30
+ "email": "greydragon888@gmail.com",
31
+ "url": "https://github.com/greydragon888"
32
+ },
33
+ "license": "MIT",
34
+ "bugs": {
35
+ "url": "https://github.com/greydragon888/real-router/issues"
36
+ },
37
+ "homepage": "https://github.com/greydragon888/real-router",
38
+ "scripts": {
39
+ "test": "vitest",
40
+ "build": "tsup",
41
+ "type-check": "tsc --noEmit",
42
+ "lint": "eslint --cache --ext .ts src/ tests/ --fix --max-warnings 0",
43
+ "lint:package": "publint",
44
+ "lint:types": "attw --pack ."
45
+ },
46
+ "sideEffects": false,
47
+ "peerDependencies": {
48
+ "@real-router/core": "workspace:^"
49
+ }
50
+ }
@@ -0,0 +1,3 @@
1
+ export const LOGGER_CONTEXT = "ssr-data-plugin";
2
+
3
+ export const ERROR_PREFIX = `[@real-router/${LOGGER_CONTEXT}]`;
package/src/factory.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { getPluginApi } from "@real-router/core/api";
2
+
3
+ import { validateLoaders } from "./validation";
4
+
5
+ import type { DataLoaderMap } from "./types";
6
+ import type { State, PluginFactory, Plugin } from "@real-router/core";
7
+
8
+ export function ssrDataPluginFactory(loaders: DataLoaderMap): PluginFactory {
9
+ validateLoaders(loaders);
10
+
11
+ return (router): Plugin => {
12
+ const api = getPluginApi(router);
13
+ const dataStore = new WeakMap<State, unknown>();
14
+
15
+ const removeStartInterceptor = api.addInterceptor(
16
+ "start",
17
+ async (next, path) => {
18
+ const state = await next(path);
19
+
20
+ if (Object.hasOwn(loaders, state.name)) {
21
+ dataStore.set(state, await loaders[state.name](state.params));
22
+ }
23
+
24
+ return state;
25
+ },
26
+ );
27
+
28
+ const removeExtensions = api.extendRouter({
29
+ getRouteData(state?: State): unknown {
30
+ const s = state ?? router.getState();
31
+
32
+ return s ? (dataStore.get(s) ?? null) : null;
33
+ },
34
+ });
35
+
36
+ return {
37
+ teardown() {
38
+ removeStartInterceptor();
39
+ removeExtensions();
40
+ },
41
+ };
42
+ };
43
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ import type { State } from "@real-router/core";
2
+
3
+ export type { DataLoaderMap, DataLoaderFn } from "./types";
4
+
5
+ export { ssrDataPluginFactory } from "./factory";
6
+
7
+ declare module "@real-router/core" {
8
+ interface Router {
9
+ getRouteData: (state?: State) => unknown;
10
+ }
11
+ }
package/src/types.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { Params } from "@real-router/core";
2
+
3
+ export type DataLoaderFn = (params: Params) => Promise<unknown>;
4
+
5
+ export type DataLoaderMap = Record<string, DataLoaderFn>;
@@ -0,0 +1,21 @@
1
+ import { ERROR_PREFIX } from "./constants";
2
+
3
+ import type { DataLoaderMap } from "./types";
4
+
5
+ export function validateLoaders(
6
+ loaders: unknown,
7
+ ): asserts loaders is DataLoaderMap {
8
+ if (loaders === null || typeof loaders !== "object") {
9
+ throw new TypeError(`${ERROR_PREFIX} loaders must be a non-null object`);
10
+ }
11
+
12
+ for (const [key, value] of Object.entries(
13
+ loaders as Record<string, unknown>,
14
+ )) {
15
+ if (typeof value !== "function") {
16
+ throw new TypeError(
17
+ `${ERROR_PREFIX} loader for route "${key}" must be a function`,
18
+ );
19
+ }
20
+ }
21
+ }