@kasperrt/smolter 0.4.3 → 0.4.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.
Files changed (112) hide show
  1. package/.generated/client/hash-history.ts +27 -0
  2. package/.generated/client/hydration.ts +98 -0
  3. package/.generated/client/root-data.ts +52 -0
  4. package/.generated/core/prepare-routes.ts +50 -0
  5. package/.generated/core/presentation.ts +1 -1
  6. package/.generated/preact/client.tsx +1 -26
  7. package/.generated/preact/deferred-hydration.ts +1 -98
  8. package/.generated/preact/index.ts +6 -1
  9. package/.generated/preact/navigation.tsx +13 -5
  10. package/.generated/preact/root-data.ts +1 -45
  11. package/.generated/preact/router.tsx +2 -48
  12. package/.generated/react/client.tsx +130 -0
  13. package/.generated/react/document.tsx +235 -0
  14. package/.generated/react/fetcher.ts +29 -7
  15. package/.generated/react/hooks.ts +20 -18
  16. package/.generated/react/index.ts +17 -1
  17. package/.generated/react/lazy-route.ts +116 -0
  18. package/.generated/react/link.ts +1 -38
  19. package/.generated/react/navigation-state.ts +13 -0
  20. package/.generated/react/navigation.ts +145 -0
  21. package/.generated/react/root-context.ts +22 -0
  22. package/.generated/react/route-hooks.ts +159 -0
  23. package/.generated/react/route-types.ts +91 -0
  24. package/.generated/react/router.ts +50 -0
  25. package/.generated/react/runtime.ts +1 -0
  26. package/.generated/server/application.tsx +18 -253
  27. package/.generated/server/dispatch.ts +87 -0
  28. package/.generated/server/index.ts +2 -0
  29. package/.generated/server/react.ts +34 -0
  30. package/.generated/server/render-application.ts +203 -0
  31. package/.generated/vite/application/lazy-files.ts +2 -1
  32. package/.generated/vite/application/vite.ts +12 -4
  33. package/.generated/vite/react.ts +11 -0
  34. package/CHANGELOG.md +10 -0
  35. package/README.md +41 -41
  36. package/dist/client/hash-history.d.ts +2 -0
  37. package/dist/client/hash-history.js +26 -0
  38. package/dist/client/hydration.d.ts +10 -0
  39. package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
  40. package/dist/client/root-data.d.ts +14 -0
  41. package/dist/{preact → client}/root-data.js +1 -1
  42. package/dist/core/prepare-routes.d.ts +7 -0
  43. package/dist/core/prepare-routes.js +28 -0
  44. package/dist/core/presentation.js +1 -1
  45. package/dist/preact/client.js +5 -26
  46. package/dist/preact/deferred-hydration.d.ts +1 -10
  47. package/dist/preact/document.js +2 -2
  48. package/dist/preact/index.d.ts +1 -1
  49. package/dist/preact/index.js +1 -1
  50. package/dist/preact/navigation-state.js +1 -1
  51. package/dist/preact/navigation.js +7 -5
  52. package/dist/preact/root-data.d.ts +1 -8
  53. package/dist/preact/route-hooks.js +1 -1
  54. package/dist/preact/router.d.ts +2 -2
  55. package/dist/preact/router.js +2 -26
  56. package/dist/react/client.d.ts +20 -0
  57. package/dist/react/client.js +86 -0
  58. package/dist/react/document.d.ts +25 -0
  59. package/dist/react/document.js +169 -0
  60. package/dist/react/fetcher.d.ts +8 -4
  61. package/dist/react/fetcher.js +12 -1
  62. package/dist/react/form.js +1 -1
  63. package/dist/react/hooks.d.ts +7 -6
  64. package/dist/react/index.d.ts +7 -1
  65. package/dist/react/index.js +6 -2
  66. package/dist/react/lazy-route.d.ts +22 -0
  67. package/dist/react/lazy-route.js +57 -0
  68. package/dist/react/link.d.ts +1 -12
  69. package/dist/react/navigation-state.d.ts +2 -0
  70. package/dist/react/navigation-state.js +9 -0
  71. package/dist/react/navigation.d.ts +33 -0
  72. package/dist/react/navigation.js +64 -0
  73. package/dist/react/root-context.d.ts +14 -0
  74. package/dist/react/root-context.js +10 -0
  75. package/dist/react/route-hooks.d.ts +34 -0
  76. package/dist/react/route-hooks.js +89 -0
  77. package/dist/react/route-types.d.ts +69 -0
  78. package/dist/react/router.d.ts +10 -0
  79. package/dist/react/router.js +22 -0
  80. package/dist/react/runtime.d.ts +1 -1
  81. package/dist/react/runtime.js +1 -1
  82. package/dist/server/application.d.ts +4 -8
  83. package/dist/server/application.js +9 -156
  84. package/dist/server/dispatch.d.ts +5 -0
  85. package/dist/server/dispatch.js +50 -0
  86. package/dist/server/index.d.ts +1 -0
  87. package/dist/server/index.js +2 -1
  88. package/dist/server/preact.js +2 -1
  89. package/dist/server/react.d.ts +10 -0
  90. package/dist/server/react.js +22 -0
  91. package/dist/server/render-application.d.ts +31 -0
  92. package/dist/server/render-application.js +118 -0
  93. package/dist/vite/application/lazy-files.d.ts +1 -1
  94. package/dist/vite/application/lazy-files.js +2 -2
  95. package/dist/vite/application/vite.d.ts +2 -1
  96. package/dist/vite/application/vite.js +12 -5
  97. package/dist/vite/react.d.ts +4 -0
  98. package/dist/vite/react.js +9 -1
  99. package/jsr.json +10 -6
  100. package/package.json +18 -10
  101. package/.generated/core/resource-types.ts +0 -48
  102. package/.generated/core/resources.ts +0 -446
  103. package/.generated/react/data.ts +0 -29
  104. package/.generated/react/resources/components.tsx +0 -416
  105. package/dist/core/resource-types.d.ts +0 -41
  106. package/dist/core/resources.d.ts +0 -21
  107. package/dist/core/resources.js +0 -347
  108. package/dist/react/data.d.ts +0 -9
  109. package/dist/react/data.js +0 -3
  110. package/dist/react/link.js +0 -27
  111. package/dist/react/resources/components.d.ts +0 -76
  112. package/dist/react/resources/components.js +0 -250
@@ -43,12 +43,19 @@ var preactConfig = {
43
43
  "@kasperrt/smolter/preact/lazy-route"
44
44
  ] }
45
45
  };
46
- function applicationPlugins({ directory = "src/routes", dataEndpoint = "/_smolter", ssr = true, splitRoutes = true, resourceRequest } = {}) {
46
+ function applicationPlugins({ directory = "src/routes", framework = "preact", dataEndpoint = "/_smolter", ssr = true, splitRoutes = true, resourceRequest } = {}) {
47
47
  return [
48
48
  {
49
49
  name: "smolter-application",
50
50
  enforce: "pre",
51
- config: () => preactConfig,
51
+ config: () => framework === "preact" ? preactConfig : {
52
+ resolve: { dedupe: [
53
+ "react",
54
+ "react-dom",
55
+ "@kasperrt/smolter"
56
+ ] },
57
+ ssr: { noExternal: ["@kasperrt/smolter"] }
58
+ },
52
59
  transformIndexHtml: {
53
60
  order: "post",
54
61
  handler(html, context) {
@@ -66,7 +73,7 @@ function applicationPlugins({ directory = "src/routes", dataEndpoint = "/_smolte
66
73
  const target = new URL(incoming.url, "http://local").searchParams.get("url");
67
74
  if (!target?.startsWith("/") || target.startsWith("//")) return send(new Response("Invalid route URL", { status: 400 }), outgoing);
68
75
  const request = await webRequest(incoming, outgoing, target);
69
- const [{ default: routes }, { dispatchRoute }] = await Promise.all([server.ssrLoadModule("virtual:smolter/routes"), server.ssrLoadModule("@kasperrt/smolter/preact/server")]);
76
+ const [{ default: routes }, { dispatchRoute }] = await Promise.all([server.ssrLoadModule("virtual:smolter/routes"), server.ssrLoadModule(`@kasperrt/smolter/${framework}/server`)]);
70
77
  await send(await dispatchRoute(request, routes), outgoing);
71
78
  };
72
79
  run().catch(next);
@@ -92,10 +99,10 @@ function applicationPlugins({ directory = "src/routes", dataEndpoint = "/_smolte
92
99
  }
93
100
  },
94
101
  browserBoundary(),
95
- ...splitRoutes ? [lazyFiles(directory, dataEndpoint)] : [],
102
+ ...splitRoutes ? [lazyFiles(directory, dataEndpoint, framework)] : [],
96
103
  routerPlugin({
97
104
  directory,
98
- framework: "preact",
105
+ framework,
99
106
  dataEndpoint
100
107
  })
101
108
  ];
@@ -7,4 +7,8 @@ export type { FileRouterOptions } from "./index.ts";
7
7
  export declare function routerPlugin(options?: FileRouterOptions): Plugin;
8
8
  /** File routing is the default; this alias keeps configuration explicit. */
9
9
  export declare const fileRouter: typeof routerPlugin;
10
+ export type { ApplicationOptions } from "./application/vite.ts";
10
11
  export { type DeploymentPluginOptions, deploymentPlugin, } from "./deployment.ts";
12
+ import { type ApplicationOptions } from "./application/vite.ts";
13
+ /** Configure React application SSR, scoped data transport and lazy file routing. */
14
+ export declare function applicationPlugins(options?: ApplicationOptions): Plugin[];
@@ -1,6 +1,7 @@
1
1
  import { flatRoutes } from "../files/index.js";
2
2
  import { routerPlugin as routerPlugin$1 } from "./index.js";
3
3
  import { deploymentPlugin } from "./deployment.js";
4
+ import { applicationPlugins as applicationPlugins$1 } from "./application/vite.js";
4
5
  //#region .generated/vite/react.ts
5
6
  /** Configure flat routes and server transport for React. */
6
7
  function routerPlugin(options = {}) {
@@ -11,5 +12,12 @@ function routerPlugin(options = {}) {
11
12
  }
12
13
  /** File routing is the default; this alias keeps configuration explicit. */
13
14
  var fileRouter = routerPlugin;
15
+ /** Configure React application SSR, scoped data transport and lazy file routing. */
16
+ function applicationPlugins(options = {}) {
17
+ return applicationPlugins$1({
18
+ ...options,
19
+ framework: "react"
20
+ });
21
+ }
14
22
  //#endregion
15
- export { deploymentPlugin, fileRouter, flatRoutes, routerPlugin };
23
+ export { applicationPlugins, deploymentPlugin, fileRouter, flatRoutes, routerPlugin };
package/jsr.json CHANGED
@@ -1,20 +1,21 @@
1
1
  {
2
2
  "name": "@kasperrt/smolter",
3
- "version": "0.4.3",
3
+ "version": "0.4.4",
4
4
  "exports": {
5
5
  "./react": "./.generated/react/index.ts",
6
6
  "./preact": "./.generated/preact/index.ts",
7
- "./react/server": "./.generated/server/index.ts",
7
+ "./react/server": "./.generated/server/react.ts",
8
8
  "./preact/server": "./.generated/server/preact.ts",
9
- "./react/client": "./.generated/client/index.ts",
9
+ "./react/client": "./.generated/react/client.tsx",
10
10
  "./preact/client": "./.generated/preact/client.tsx",
11
11
  "./utils": "./.generated/core/utils.ts",
12
12
  "./utils/http": "./.generated/server/node.ts",
13
13
  "./utils/assets": "./.generated/server/assets.ts",
14
14
  "./utils/responses": "./.generated/core/responses.ts",
15
- "./react/data": "./.generated/react/data.ts",
16
15
  "./preact/lazy-route": "./.generated/preact/lazy-route.ts",
17
- "./preact/document": "./.generated/preact/document.tsx"
16
+ "./preact/document": "./.generated/preact/document.tsx",
17
+ "./react/document": "./.generated/react/document.tsx",
18
+ "./react/lazy-route": "./.generated/react/lazy-route.ts"
18
19
  },
19
20
  "imports": {
20
21
  "react": "npm:react@19.3.1",
@@ -23,7 +24,10 @@
23
24
  "preact/hooks": "npm:preact@11.0.1/hooks",
24
25
  "preact/jsx-runtime": "npm:preact@11.0.1/jsx-runtime",
25
26
  "preact-render-to-string": "npm:preact-render-to-string@6.8.1",
26
- "react/jsx-runtime": "npm:react@19.3.1/jsx-runtime"
27
+ "react/jsx-runtime": "npm:react@19.3.1/jsx-runtime",
28
+ "react-dom": "npm:react-dom@19.3.1",
29
+ "react-dom/client": "npm:react-dom@19.3.1/client",
30
+ "react-dom/server": "npm:react-dom@19.3.1/server"
27
31
  },
28
32
  "publish": {
29
33
  "include": [
package/package.json CHANGED
@@ -57,7 +57,7 @@
57
57
  "wouter": "3.13.0",
58
58
  "wouter-preact": "3.13.0"
59
59
  },
60
- "version": "0.4.3",
60
+ "version": "0.4.4",
61
61
  "description": "A tiny typed data router for React and Preact.",
62
62
  "sideEffects": false,
63
63
  "license": "MIT",
@@ -75,16 +75,16 @@
75
75
  "import": "./dist/preact/index.js"
76
76
  },
77
77
  "./react/server": {
78
- "types": "./dist/server/index.d.ts",
79
- "import": "./dist/server/index.js"
78
+ "types": "./dist/server/react.d.ts",
79
+ "import": "./dist/server/react.js"
80
80
  },
81
81
  "./preact/server": {
82
82
  "types": "./dist/server/preact.d.ts",
83
83
  "import": "./dist/server/preact.js"
84
84
  },
85
85
  "./react/client": {
86
- "types": "./dist/client/index.d.ts",
87
- "import": "./dist/client/index.js"
86
+ "types": "./dist/react/client.d.ts",
87
+ "import": "./dist/react/client.js"
88
88
  },
89
89
  "./preact/client": {
90
90
  "types": "./dist/preact/client.d.ts",
@@ -114,10 +114,6 @@
114
114
  "types": "./dist/core/responses.d.ts",
115
115
  "import": "./dist/core/responses.js"
116
116
  },
117
- "./react/data": {
118
- "types": "./dist/react/data.d.ts",
119
- "import": "./dist/react/data.js"
120
- },
121
117
  "./preact/lazy-route": {
122
118
  "types": "./dist/preact/lazy-route.d.ts",
123
119
  "import": "./dist/preact/lazy-route.js"
@@ -125,6 +121,14 @@
125
121
  "./preact/document": {
126
122
  "types": "./dist/preact/document.d.ts",
127
123
  "import": "./dist/preact/document.js"
124
+ },
125
+ "./react/document": {
126
+ "types": "./dist/react/document.d.ts",
127
+ "import": "./dist/react/document.js"
128
+ },
129
+ "./react/lazy-route": {
130
+ "types": "./dist/react/lazy-route.d.ts",
131
+ "import": "./dist/react/lazy-route.js"
128
132
  }
129
133
  },
130
134
  "files": [
@@ -139,7 +143,8 @@
139
143
  "preact": ">=11.0.1",
140
144
  "preact-render-to-string": ">=6.8.1",
141
145
  "react": ">=18",
142
- "vite": ">=8"
146
+ "vite": ">=8",
147
+ "react-dom": ">=18"
143
148
  },
144
149
  "peerDependenciesMeta": {
145
150
  "react": {
@@ -153,6 +158,9 @@
153
158
  },
154
159
  "preact-render-to-string": {
155
160
  "optional": true
161
+ },
162
+ "react-dom": {
163
+ "optional": true
156
164
  }
157
165
  },
158
166
  "author": "Kasper Rynning-Tønnesen <kasperrt@users.noreply.github.com> (https://github.com/kasperrt)",
@@ -1,48 +0,0 @@
1
- export type Params = Readonly<Record<string, string>>;
2
-
3
- /** Resource parameters and cancellation signal supplied to a loader. */
4
- export interface LoaderFunctionArgs {
5
- params: Params;
6
- signal: AbortSignal;
7
- }
8
-
9
- /** Submitted input, resource parameters and cancellation signal supplied to an action. */
10
- export interface ActionFunctionArgs<Input = unknown> {
11
- input: Input;
12
- params: Params;
13
- signal: AbortSignal;
14
- }
15
-
16
- /** Successful resource data or a normalized resource failure. */
17
- export interface DataResult<Data> {
18
- data: Data | null;
19
- error: string;
20
- unexpected?: true;
21
- }
22
-
23
- export interface ResourceModule {
24
- action?: (args: ActionFunctionArgs) => Promise<DataResult<unknown>>;
25
- loader?: (args: LoaderFunctionArgs) => Promise<DataResult<unknown>>;
26
- shouldRevalidate?: (args: ShouldRevalidateFunctionArgs) => boolean;
27
- }
28
-
29
- /** Navigation or mutation context used to decide whether loader data should refresh. */
30
- export interface ShouldRevalidateFunctionArgs {
31
- actionResult: DataResult<unknown>;
32
- defaultShouldRevalidate: boolean;
33
- params: Params;
34
- }
35
-
36
- export type NavigationState = "idle" | "loading" | "submitting";
37
-
38
- export interface RouteState {
39
- actionData: unknown;
40
- actionError: string;
41
- actionState: "idle" | "submitting";
42
- loaderData: unknown;
43
- loaderError: string;
44
- loaderInitialized: boolean;
45
- loaderState: "idle" | "loading";
46
- navigationState: NavigationState;
47
- unexpectedError: boolean;
48
- }
@@ -1,446 +0,0 @@
1
- import { safeWrapAsync } from "./errors.ts";
2
- import type {
3
- DataResult,
4
- Params,
5
- ResourceModule,
6
- RouteState,
7
- } from "./resource-types.ts";
8
-
9
- const IDLE_ROUTE_STATE: RouteState = {
10
- actionData: null,
11
- actionError: "",
12
- actionState: "idle",
13
- loaderData: null,
14
- loaderError: "",
15
- loaderInitialized: false,
16
- loaderState: "idle",
17
- navigationState: "idle",
18
- unexpectedError: false,
19
- };
20
-
21
- /** Manage keyed resource reads, submissions, subscriptions and cancellation independently of navigation. */
22
- export class ResourceRouter {
23
- readonly #cleanupTimers = new Map<string, ReturnType<typeof setTimeout>>();
24
- readonly #controllers = new Map<string, AbortController>();
25
- readonly #inFlightLoads = new Map<string, Promise<DataResult<unknown>>>();
26
- readonly #listeners = new Map<string, Set<() => void>>();
27
- readonly #persistentRoutes = new Set<string>();
28
- readonly #routes: ReadonlyMap<string, ResourceModule>;
29
- readonly #states = new Map<string, RouteState>();
30
-
31
- constructor(routes: ReadonlyMap<string, ResourceModule>) {
32
- this.#routes = routes;
33
- }
34
-
35
- dispose() {
36
- for (const controller of this.#controllers.values()) controller.abort();
37
-
38
- this.#controllers.clear();
39
-
40
- for (const timer of this.#cleanupTimers.values()) clearTimeout(timer);
41
-
42
- this.#cleanupTimers.clear();
43
- this.#inFlightLoads.clear();
44
- this.#listeners.clear();
45
- this.#persistentRoutes.clear();
46
- }
47
-
48
- getRoute(routeId: string): ResourceModule | null {
49
- return this.#routes.get(routeId) ?? null;
50
- }
51
-
52
- getSnapshot(routeId: string, params: Params): RouteState {
53
- return (
54
- this.#states.get(createRouteKey(routeId, params)) ?? IDLE_ROUTE_STATE
55
- );
56
- }
57
-
58
- hydrateRoute<Data>(routeId: string, data: Data, params: Params = {}) {
59
- const routeKey = createRouteKey(routeId, params);
60
- const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
61
- this.#setState(routeKey, {
62
- ...currentState,
63
- loaderData: data,
64
- loaderError: "",
65
- loaderInitialized: true,
66
- loaderState: "idle",
67
- navigationState:
68
- currentState.actionState === "submitting" ? "submitting" : "idle",
69
- unexpectedError: false,
70
- });
71
- }
72
-
73
- persist(routeId: string, params: Params = {}) {
74
- const routeKey = createRouteKey(routeId, params);
75
- this.#persistentRoutes.add(routeKey);
76
- const cleanupTimer = this.#cleanupTimers.get(routeKey);
77
-
78
- if (!cleanupTimer) return;
79
-
80
- clearTimeout(cleanupTimer);
81
- this.#cleanupTimers.delete(routeKey);
82
- }
83
-
84
- disposeRoute(routeId: string, params: Params = {}) {
85
- this.#disposeRouteKey(createRouteKey(routeId, params));
86
- }
87
-
88
- #disposeRouteKey(routeKey: string) {
89
- this.#persistentRoutes.delete(routeKey);
90
- const cleanupTimer = this.#cleanupTimers.get(routeKey);
91
-
92
- if (cleanupTimer) clearTimeout(cleanupTimer);
93
-
94
- this.#cleanupTimers.delete(routeKey);
95
- this.#controllers.get(routeKey)?.abort();
96
- this.#controllers.get(`${routeKey}:action`)?.abort();
97
- this.#controllers.delete(routeKey);
98
- this.#controllers.delete(`${routeKey}:action`);
99
- this.#inFlightLoads.delete(routeKey);
100
- this.#states.delete(routeKey);
101
- this.#notify(routeKey);
102
- }
103
-
104
- load<Data>(
105
- routeId: string,
106
- params: Params = {},
107
- force = false,
108
- ): Promise<DataResult<Data>> {
109
- const routeKey = createRouteKey(routeId, params);
110
- const route = this.#routes.get(routeId);
111
-
112
- if (!route?.loader) {
113
- return Promise.resolve<DataResult<Data>>({
114
- data: null,
115
- error: "Route loader not found.",
116
- });
117
- }
118
-
119
- const currentState = this.#states.get(routeKey);
120
-
121
- if (!force && currentState?.loaderInitialized) {
122
- return Promise.resolve({
123
- data: currentState?.loaderData as Data,
124
- error: currentState?.loaderError ?? "",
125
- });
126
- }
127
-
128
- const currentLoad = this.#inFlightLoads.get(routeKey);
129
-
130
- if (force && currentLoad) {
131
- this.#controllers.get(routeKey)?.abort();
132
- this.#inFlightLoads.delete(routeKey);
133
- }
134
-
135
- if (!force && currentLoad) return currentLoad as Promise<DataResult<Data>>;
136
-
137
- const controller = this.#replaceController(routeKey);
138
- this.#setState(routeKey, {
139
- ...(currentState ?? IDLE_ROUTE_STATE),
140
- loaderError: "",
141
- loaderState: "loading",
142
- navigationState:
143
- currentState?.actionState === "submitting" ? "submitting" : "loading",
144
- unexpectedError: false,
145
- });
146
- const load = this.#runLoader(route, routeKey, params, controller);
147
- this.#inFlightLoads.set(routeKey, load);
148
-
149
- return load as Promise<DataResult<Data>>;
150
- }
151
-
152
- subscribe(routeId: string, params: Params, listener: () => void): () => void {
153
- const routeKey = createRouteKey(routeId, params);
154
- const cleanupTimer = this.#cleanupTimers.get(routeKey);
155
-
156
- if (cleanupTimer) {
157
- clearTimeout(cleanupTimer);
158
- this.#cleanupTimers.delete(routeKey);
159
- }
160
-
161
- const listeners = this.#listeners.get(routeKey) ?? new Set<() => void>();
162
- listeners.add(listener);
163
- this.#listeners.set(routeKey, listeners);
164
-
165
- return () => {
166
- if (!listeners.delete(listener) || listeners.size > 0) return;
167
-
168
- this.#listeners.delete(routeKey);
169
-
170
- if (this.#persistentRoutes.has(routeKey)) return;
171
-
172
- const timer = setTimeout(
173
- () => this.#disposeRouteKey(routeKey),
174
- routeCleanupGraceMs,
175
- );
176
- this.#cleanupTimers.set(routeKey, timer);
177
- };
178
- }
179
-
180
- async submit<Data>(
181
- routeId: string,
182
- input: unknown,
183
- params: Params = {},
184
- ): Promise<DataResult<Data>> {
185
- const routeKey = createRouteKey(routeId, params);
186
- const route = this.#routes.get(routeId);
187
-
188
- if (!route?.action) {
189
- return { data: null, error: "Route action not found." };
190
- }
191
-
192
- const controllerKey = `${routeKey}:action`;
193
- const controller = this.#replaceController(controllerKey);
194
- const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
195
- this.#setState(routeKey, {
196
- ...currentState,
197
- actionError: "",
198
- actionState: "submitting",
199
- navigationState: "submitting",
200
- unexpectedError: false,
201
- });
202
- const { error, data: result } = await safeWrapAsync(() =>
203
- route.action?.({ input, params, signal: controller.signal }),
204
- );
205
-
206
- if (this.#controllers.get(controllerKey) === controller) {
207
- this.#controllers.delete(controllerKey);
208
- }
209
-
210
- if (controller.signal.aborted) return { data: null, error: "" };
211
-
212
- const latestState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
213
-
214
- if (error || !result) {
215
- const publicError = "The request could not be completed.";
216
- this.#setState(routeKey, {
217
- ...latestState,
218
- actionData: null,
219
- actionError: publicError,
220
- actionState: "idle",
221
- navigationState:
222
- latestState.loaderState === "loading" ? "loading" : "idle",
223
- unexpectedError: true,
224
- });
225
-
226
- return { data: null, error: publicError, unexpected: true };
227
- }
228
-
229
- this.#setState(routeKey, {
230
- ...latestState,
231
- actionData: result.data,
232
- actionError: result.error,
233
- actionState: "idle",
234
- navigationState:
235
- latestState.loaderState === "loading" ? "loading" : "idle",
236
- unexpectedError: false,
237
- });
238
- const actionResult = {
239
- data: result.data as Data | null,
240
- error: result.error,
241
- };
242
- const { error: revalidationError, data: shouldRevalidate } =
243
- await safeWrapAsync(
244
- () =>
245
- route.shouldRevalidate?.({
246
- actionResult: result,
247
- defaultShouldRevalidate: Boolean(route.loader),
248
- params,
249
- }) ?? Boolean(route.loader),
250
- );
251
-
252
- if (revalidationError) {
253
- const publicError = "The request could not be completed.";
254
- this.#setState(routeKey, {
255
- ...(this.#states.get(routeKey) ?? IDLE_ROUTE_STATE),
256
- actionError: publicError,
257
- actionState: "idle",
258
- unexpectedError: true,
259
- });
260
-
261
- return { data: null, error: publicError, unexpected: true };
262
- }
263
-
264
- if (shouldRevalidate) await this.load(routeId, params, true);
265
-
266
- return actionResult;
267
- }
268
-
269
- async fetchLoader<Data>(
270
- routeId: string,
271
- params: Params,
272
- signal: AbortSignal,
273
- ): Promise<DataResult<Data>> {
274
- const route = this.#routes.get(routeId);
275
-
276
- if (!route?.loader) {
277
- return { data: null, error: "Route loader not found." };
278
- }
279
-
280
- const { error, data: result } = await safeWrapAsync(() =>
281
- route.loader?.({ params, signal }),
282
- );
283
-
284
- if (signal.aborted) return { data: null, error: "" };
285
-
286
- if (error || !result) {
287
- return {
288
- data: null,
289
- error: "The route could not be loaded.",
290
- unexpected: true,
291
- };
292
- }
293
-
294
- return result as DataResult<Data>;
295
- }
296
-
297
- async fetchAction<Data>(
298
- routeId: string,
299
- input: unknown,
300
- params: Params,
301
- signal: AbortSignal,
302
- ): Promise<DataResult<Data>> {
303
- const route = this.#routes.get(routeId);
304
-
305
- if (!route?.action) {
306
- return { data: null, error: "Route action not found." };
307
- }
308
-
309
- const { error, data: result } = await safeWrapAsync(() =>
310
- route.action?.({ input, params, signal }),
311
- );
312
-
313
- if (signal.aborted) return { data: null, error: "" };
314
-
315
- if (error || !result) {
316
- return {
317
- data: null,
318
- error: "The request could not be completed.",
319
- unexpected: true,
320
- };
321
- }
322
-
323
- const { error: revalidationError, data: shouldRevalidate } =
324
- await safeWrapAsync(
325
- () =>
326
- route.shouldRevalidate?.({
327
- actionResult: result,
328
- defaultShouldRevalidate: Boolean(route.loader),
329
- params,
330
- }) ?? Boolean(route.loader),
331
- );
332
-
333
- if (signal.aborted) return { data: null, error: "" };
334
-
335
- if (revalidationError) {
336
- return {
337
- data: null,
338
- error: "The request could not be completed.",
339
- unexpected: true,
340
- };
341
- }
342
-
343
- if (shouldRevalidate) await this.load(routeId, params, true);
344
-
345
- return result as DataResult<Data>;
346
- }
347
-
348
- retry(routeId: string, params: Params): Promise<DataResult<unknown>> {
349
- const routeKey = createRouteKey(routeId, params);
350
- this.#states.set(routeKey, IDLE_ROUTE_STATE);
351
- this.#notify(routeKey);
352
-
353
- return this.load(routeId, params, true);
354
- }
355
-
356
- reportUnexpected(routeId: string, params: Params) {
357
- const routeKey = createRouteKey(routeId, params);
358
- const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
359
- this.#setState(routeKey, { ...currentState, unexpectedError: true });
360
- }
361
-
362
- async #runLoader(
363
- route: ResourceModule,
364
- routeKey: string,
365
- params: Params,
366
- controller: AbortController,
367
- ): Promise<DataResult<unknown>> {
368
- const { error, data: result } = await safeWrapAsync(
369
- () =>
370
- route.loader?.({ params, signal: controller.signal }) ??
371
- Promise.resolve({ data: null, error: "" }),
372
- );
373
-
374
- if (this.#controllers.get(routeKey) === controller) {
375
- this.#controllers.delete(routeKey);
376
- this.#inFlightLoads.delete(routeKey);
377
- }
378
-
379
- if (controller.signal.aborted) return { data: null, error: "" };
380
-
381
- if (error || !result) {
382
- const publicError = "The route could not be loaded.";
383
- this.#setState(routeKey, {
384
- actionData: null,
385
- actionError: "",
386
- actionState: "idle",
387
- loaderData: null,
388
- loaderError: publicError,
389
- loaderInitialized: true,
390
- loaderState: "idle",
391
- navigationState: "idle",
392
- unexpectedError: true,
393
- });
394
-
395
- return { data: null, error: publicError, unexpected: true };
396
- }
397
-
398
- const currentState = this.#states.get(routeKey) ?? IDLE_ROUTE_STATE;
399
- this.#setState(routeKey, {
400
- ...currentState,
401
- loaderData: result.data,
402
- loaderError: result.error,
403
- loaderInitialized: true,
404
- loaderState: "idle",
405
- navigationState:
406
- currentState.actionState === "submitting" ? "submitting" : "idle",
407
- unexpectedError: false,
408
- });
409
-
410
- return result;
411
- }
412
-
413
- #replaceController(key: string) {
414
- this.#controllers.get(key)?.abort();
415
- const controller = new AbortController();
416
- this.#controllers.set(key, controller);
417
-
418
- return controller;
419
- }
420
-
421
- #setState(routeKey: string, state: RouteState) {
422
- this.#states.set(routeKey, state);
423
- this.#notify(routeKey);
424
- }
425
-
426
- #notify(routeKey: string) {
427
- for (const listener of this.#listeners.get(routeKey) ?? []) listener();
428
- }
429
- }
430
-
431
- /** Create an independent resource-data router from registered modules; this does not manage URL history. */
432
- export function createResourceRouter(
433
- routes: ReadonlyMap<string, ResourceModule>,
434
- ): ResourceRouter {
435
- return new ResourceRouter(routes);
436
- }
437
-
438
- function createRouteKey(routeId: string, params: Params) {
439
- const normalizedParams = Object.entries(params).sort(([left], [right]) =>
440
- left.localeCompare(right),
441
- );
442
-
443
- return `${routeId}:${JSON.stringify(normalizedParams)}`;
444
- }
445
-
446
- const routeCleanupGraceMs = 100;