@kasperrt/smolter 0.4.2 → 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.
- package/.generated/client/hash-history.ts +27 -0
- package/.generated/client/hydration.ts +98 -0
- package/.generated/client/root-data.ts +52 -0
- package/.generated/core/environment.ts +1 -0
- package/.generated/core/navigation.ts +1 -0
- package/.generated/core/prepare-routes.ts +50 -0
- package/.generated/core/presentation.ts +1 -1
- package/.generated/core/responses.ts +12 -0
- package/.generated/core/utils.ts +5 -2
- package/.generated/preact/await.tsx +1 -0
- package/.generated/preact/client.tsx +8 -27
- package/.generated/preact/deferred-hydration.ts +1 -98
- package/.generated/preact/document.tsx +10 -0
- package/.generated/preact/index.ts +11 -2
- package/.generated/preact/lazy-route.ts +7 -0
- package/.generated/preact/navigation.tsx +19 -5
- package/.generated/preact/root-data.ts +1 -45
- package/.generated/preact/route-hooks.tsx +12 -2
- package/.generated/preact/route-types.ts +14 -0
- package/.generated/preact/router.tsx +5 -48
- package/.generated/react/client.tsx +130 -0
- package/.generated/react/document.tsx +235 -0
- package/.generated/react/fetcher.ts +29 -7
- package/.generated/react/hooks.ts +20 -18
- package/.generated/react/index.ts +28 -2
- package/.generated/react/lazy-route.ts +116 -0
- package/.generated/react/link.ts +1 -38
- package/.generated/react/navigation-state.ts +13 -0
- package/.generated/react/navigation.ts +145 -0
- package/.generated/react/outlet.ts +48 -0
- package/.generated/react/root-context.ts +22 -0
- package/.generated/react/route-hooks.ts +159 -0
- package/.generated/react/route-types.ts +91 -0
- package/.generated/react/router.ts +50 -0
- package/.generated/react/runtime.ts +1 -0
- package/.generated/server/application.tsx +18 -250
- package/.generated/server/assets.ts +7 -0
- package/.generated/server/dispatch.ts +87 -0
- package/.generated/server/handler.ts +3 -0
- package/.generated/server/http.ts +2 -0
- package/.generated/server/index.ts +2 -0
- package/.generated/server/node.ts +5 -2
- package/.generated/server/preact.ts +5 -2
- package/.generated/server/react.ts +34 -0
- package/.generated/server/render-application.ts +203 -0
- package/.generated/vite/application/lazy-files.ts +2 -1
- package/.generated/vite/application/vite.ts +12 -4
- package/.generated/vite/react.ts +11 -0
- package/CHANGELOG.md +20 -0
- package/README.md +48 -48
- package/dist/client/hash-history.d.ts +2 -0
- package/dist/client/hash-history.js +26 -0
- package/dist/client/hydration.d.ts +10 -0
- package/dist/{preact/deferred-hydration.js → client/hydration.js} +1 -1
- package/dist/client/root-data.d.ts +14 -0
- package/dist/{preact → client}/root-data.js +1 -1
- package/dist/core/environment.d.ts +1 -0
- package/dist/core/navigation.d.ts +1 -0
- package/dist/core/prepare-routes.d.ts +7 -0
- package/dist/core/prepare-routes.js +28 -0
- package/dist/core/presentation.js +1 -1
- package/dist/core/responses.d.ts +12 -0
- package/dist/core/responses.js +5 -0
- package/dist/core/utils.d.ts +5 -2
- package/dist/core/utils.js +1 -2
- package/dist/preact/await.d.ts +1 -0
- package/dist/preact/await.js +1 -0
- package/dist/preact/client.d.ts +7 -1
- package/dist/preact/client.js +11 -27
- package/dist/preact/deferred-hydration.d.ts +1 -10
- package/dist/preact/document.d.ts +10 -0
- package/dist/preact/document.js +11 -2
- package/dist/preact/index.d.ts +6 -2
- package/dist/preact/index.js +1 -1
- package/dist/preact/lazy-route.d.ts +2 -0
- package/dist/preact/lazy-route.js +6 -0
- package/dist/preact/navigation-state.js +1 -1
- package/dist/preact/navigation.d.ts +6 -0
- package/dist/preact/navigation.js +9 -5
- package/dist/preact/root-data.d.ts +1 -8
- package/dist/preact/route-hooks.d.ts +10 -0
- package/dist/preact/route-hooks.js +13 -3
- package/dist/preact/route-types.d.ts +14 -0
- package/dist/preact/router.d.ts +5 -2
- package/dist/preact/router.js +4 -26
- package/dist/react/client.d.ts +20 -0
- package/dist/react/client.js +86 -0
- package/dist/react/document.d.ts +25 -0
- package/dist/react/document.js +169 -0
- package/dist/react/fetcher.d.ts +8 -4
- package/dist/react/fetcher.js +12 -1
- package/dist/react/form.js +1 -1
- package/dist/react/hooks.d.ts +7 -6
- package/dist/react/index.d.ts +13 -2
- package/dist/react/index.js +7 -2
- package/dist/react/lazy-route.d.ts +22 -0
- package/dist/react/lazy-route.js +57 -0
- package/dist/react/link.d.ts +1 -12
- package/dist/react/navigation-state.d.ts +2 -0
- package/dist/react/navigation-state.js +9 -0
- package/dist/react/navigation.d.ts +33 -0
- package/dist/react/navigation.js +64 -0
- package/dist/react/outlet.d.ts +16 -0
- package/dist/react/outlet.js +26 -0
- package/dist/react/root-context.d.ts +14 -0
- package/dist/react/root-context.js +10 -0
- package/dist/react/route-hooks.d.ts +34 -0
- package/dist/react/route-hooks.js +89 -0
- package/dist/react/route-types.d.ts +69 -0
- package/dist/react/router.d.ts +10 -0
- package/dist/react/router.js +22 -0
- package/dist/react/runtime.d.ts +1 -1
- package/dist/react/runtime.js +1 -1
- package/dist/server/application.d.ts +4 -5
- package/dist/server/application.js +9 -154
- package/dist/server/assets.d.ts +2 -0
- package/dist/server/assets.js +6 -0
- package/dist/server/dispatch.d.ts +5 -0
- package/dist/server/dispatch.js +50 -0
- package/dist/server/handler.d.ts +3 -0
- package/dist/server/http.d.ts +2 -0
- package/dist/server/http.js +2 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +2 -1
- package/dist/server/node.d.ts +5 -1
- package/dist/server/preact.d.ts +5 -1
- package/dist/server/preact.js +2 -1
- package/dist/server/react.d.ts +10 -0
- package/dist/server/react.js +22 -0
- package/dist/server/render-application.d.ts +31 -0
- package/dist/server/render-application.js +118 -0
- package/dist/vite/application/lazy-files.d.ts +1 -1
- package/dist/vite/application/lazy-files.js +2 -2
- package/dist/vite/application/vite.d.ts +2 -1
- package/dist/vite/application/vite.js +12 -5
- package/dist/vite/react.d.ts +4 -0
- package/dist/vite/react.js +9 -1
- package/jsr.json +10 -6
- package/package.json +21 -12
- package/.generated/core/resource-types.ts +0 -44
- package/.generated/core/resources.ts +0 -444
- package/.generated/react/data.ts +0 -25
- package/.generated/react/resources/components.tsx +0 -404
- package/dist/core/resource-types.d.ts +0 -37
- package/dist/core/resources.d.ts +0 -19
- package/dist/core/resources.js +0 -345
- package/dist/react/data.d.ts +0 -5
- package/dist/react/data.js +0 -3
- package/dist/react/link.js +0 -27
- package/dist/react/resources/components.d.ts +0 -64
- package/dist/react/resources/components.js +0 -239
|
@@ -8,6 +8,7 @@ export interface ServerBuild {
|
|
|
8
8
|
handleDataRequest: (request: Request) => Response | Promise<Response>;
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/** SSR build, browser entry URL and application-scoped data endpoint for Node or Express. */
|
|
11
12
|
export interface RequestHandlerOptions {
|
|
12
13
|
build: ServerBuild;
|
|
13
14
|
/** Browser entry URL resolved from Vite's production manifest. */
|
|
@@ -16,12 +17,14 @@ export interface RequestHandlerOptions {
|
|
|
16
17
|
dataEndpoint?: string;
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
/** Node request with optional Express protocol and original URL information. */
|
|
19
21
|
export interface ServerIncomingMessage extends IncomingMessage {
|
|
20
22
|
/** Express supplies this using the application's trust proxy policy. */
|
|
21
23
|
protocol?: string;
|
|
22
24
|
originalUrl?: string;
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/** Express-compatible callback for forwarding middleware errors. */
|
|
25
28
|
export type NextHandler = (error?: unknown) => void;
|
|
26
29
|
|
|
27
30
|
/** Node/Express middleware; mount after metrics, tracing and static assets. */
|
|
@@ -3,6 +3,7 @@ import { Readable, Transform } from "node:stream";
|
|
|
3
3
|
import { pipeline } from "node:stream/promises";
|
|
4
4
|
import type { ServerIncomingMessage } from "./handler.ts";
|
|
5
5
|
|
|
6
|
+
/** Convert a Node request to a Web Request with cancellation and an optional normalized route target. */
|
|
6
7
|
export async function webRequest(
|
|
7
8
|
incoming: ServerIncomingMessage,
|
|
8
9
|
outgoing: ServerResponse,
|
|
@@ -40,6 +41,7 @@ interface StreamingResponse extends ServerResponse {
|
|
|
40
41
|
flush?: () => void;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
/** Write a Web Response to Node with streaming backpressure and disconnect cancellation. */
|
|
43
45
|
export async function send(response: Response, outgoing: StreamingResponse) {
|
|
44
46
|
outgoing.statusCode = response.status;
|
|
45
47
|
response.headers.forEach((value, name) => {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React SSR rendering and route data dispatch.
|
|
3
|
+
* @module
|
|
4
|
+
*/
|
|
5
|
+
import type { AppRoute, RootModule } from "../react/route-types.ts";
|
|
6
|
+
import { RouterApplication } from "../react/router.ts";
|
|
7
|
+
import { createElement } from "react";
|
|
8
|
+
import * as ReactDOMServer from "react-dom/server";
|
|
9
|
+
import {
|
|
10
|
+
type RenderApplicationOptions,
|
|
11
|
+
renderDocument,
|
|
12
|
+
} from "./render-application.ts";
|
|
13
|
+
|
|
14
|
+
export * from "./index.ts";
|
|
15
|
+
export type { RenderApplicationOptions } from "./render-application.ts";
|
|
16
|
+
|
|
17
|
+
/** Render a React document with root and route data using React's streaming renderer. */
|
|
18
|
+
export async function renderApplication(
|
|
19
|
+
request: Request,
|
|
20
|
+
routes: AppRoute[],
|
|
21
|
+
rootModule: RootModule,
|
|
22
|
+
entry: string,
|
|
23
|
+
options: RenderApplicationOptions = {},
|
|
24
|
+
): Promise<Response> {
|
|
25
|
+
const { renderToReadableStream } = ReactDOMServer;
|
|
26
|
+
if (!renderToReadableStream)
|
|
27
|
+
throw new Error("renderApplication requires React DOM 19 or newer.");
|
|
28
|
+
return renderDocument(request, routes, rootModule, entry, options, (props) =>
|
|
29
|
+
renderToReadableStream(
|
|
30
|
+
createElement(RouterApplication, { ...props, rootModule }),
|
|
31
|
+
{ nonce: options.nonce, signal: request.signal },
|
|
32
|
+
),
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import type { DataArgs, Route, Router } from "../core/index.ts";
|
|
2
|
+
import { createDataRouter, matchRoutes, safeWrapAsync } from "../core/index.ts";
|
|
3
|
+
import { prepareRoutes } from "../core/prepare-routes.ts";
|
|
4
|
+
import { normalizeResult } from "../core/responses.ts";
|
|
5
|
+
import { loadRouter } from "./index.ts";
|
|
6
|
+
import { deferredHydration } from "./deferred-hydration.ts";
|
|
7
|
+
import { dispatchRoute } from "./dispatch.ts";
|
|
8
|
+
|
|
9
|
+
interface ServerRoot {
|
|
10
|
+
loader?: (args: DataArgs) => unknown;
|
|
11
|
+
ErrorBoundary?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RenderProps {
|
|
15
|
+
router: Router;
|
|
16
|
+
routes: (Route & { routePath?: string })[];
|
|
17
|
+
root: unknown;
|
|
18
|
+
entry: string;
|
|
19
|
+
rootError: boolean;
|
|
20
|
+
streaming: boolean;
|
|
21
|
+
initialData?: string;
|
|
22
|
+
document: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
import { progressiveDocument } from "./progressive-document.ts";
|
|
26
|
+
import { resolveHydration, streamDocument } from "./stream-document.ts";
|
|
27
|
+
|
|
28
|
+
/** SSR settings for CSP nonce and early deferred-data hydration. */
|
|
29
|
+
export interface RenderApplicationOptions {
|
|
30
|
+
nonce?: string;
|
|
31
|
+
earlyHydration?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Render a Preact application response with root and route data, streaming deferred content when enabled. */
|
|
35
|
+
export async function renderDocument(
|
|
36
|
+
request: Request,
|
|
37
|
+
routes: (Route & { routePath?: string })[],
|
|
38
|
+
rootModule: ServerRoot,
|
|
39
|
+
entry: string,
|
|
40
|
+
options: RenderApplicationOptions,
|
|
41
|
+
render: (
|
|
42
|
+
props: RenderProps,
|
|
43
|
+
) =>
|
|
44
|
+
| (ReadableStream<Uint8Array> & { allReady: Promise<unknown> })
|
|
45
|
+
| Promise<ReadableStream<Uint8Array> & { allReady: Promise<unknown> }>,
|
|
46
|
+
): Promise<Response> {
|
|
47
|
+
routes = routes.map((route) => ({
|
|
48
|
+
...route,
|
|
49
|
+
path: route.routePath ?? route.path,
|
|
50
|
+
}));
|
|
51
|
+
const match = matchRoutes(routes, new URL(request.url).pathname);
|
|
52
|
+
|
|
53
|
+
if (match && !match.route.default)
|
|
54
|
+
return dispatchRoute(request, routes, true);
|
|
55
|
+
|
|
56
|
+
let actionResponse: Response | undefined;
|
|
57
|
+
let actionData: unknown;
|
|
58
|
+
|
|
59
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
60
|
+
actionResponse = await dispatchRoute(request, routes, true);
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
actionResponse.headers.has("Location") ||
|
|
64
|
+
!actionResponse.headers.get("Content-Type")?.includes("application/json")
|
|
65
|
+
)
|
|
66
|
+
return actionResponse;
|
|
67
|
+
|
|
68
|
+
actionData = await normalizeResult(await actionResponse.json());
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const rootResult = safeWrapAsync(() =>
|
|
72
|
+
Promise.resolve()
|
|
73
|
+
.then(() =>
|
|
74
|
+
rootModule.loader?.({ request, params: {}, signal: request.signal }),
|
|
75
|
+
)
|
|
76
|
+
.then(
|
|
77
|
+
(value) => normalizeResult(value, false),
|
|
78
|
+
(error: unknown) =>
|
|
79
|
+
error instanceof Response
|
|
80
|
+
? normalizeResult(error, false)
|
|
81
|
+
: error instanceof Error
|
|
82
|
+
? error
|
|
83
|
+
: new Error(String(error), { cause: error }),
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
let redirectResponse: Response | undefined;
|
|
87
|
+
const prepared = prepareRoutes(
|
|
88
|
+
routes.map((route) => ({
|
|
89
|
+
...route,
|
|
90
|
+
ErrorBoundary: route.ErrorBoundary ?? rootModule.ErrorBoundary,
|
|
91
|
+
})),
|
|
92
|
+
true,
|
|
93
|
+
).map((route) => {
|
|
94
|
+
if (!route.loader) return route;
|
|
95
|
+
|
|
96
|
+
const loader = route.loader;
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
...route,
|
|
100
|
+
loader: async (args: Parameters<typeof loader>[0]) => {
|
|
101
|
+
const value = await loader(args);
|
|
102
|
+
|
|
103
|
+
if (value instanceof Response && value.headers.has("Location"))
|
|
104
|
+
redirectResponse = value;
|
|
105
|
+
|
|
106
|
+
return value;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
const [rootState, loadedRouter] = await Promise.all([
|
|
111
|
+
rootResult,
|
|
112
|
+
loadRouter(prepared, request),
|
|
113
|
+
]);
|
|
114
|
+
let router = loadedRouter;
|
|
115
|
+
const rootError =
|
|
116
|
+
rootState.error ??
|
|
117
|
+
(rootState.data instanceof Error ? rootState.data : undefined);
|
|
118
|
+
const root = rootError ? undefined : rootState.data;
|
|
119
|
+
|
|
120
|
+
if (root instanceof Response && root.headers.has("Location")) {
|
|
121
|
+
router.dispose();
|
|
122
|
+
return root;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (redirectResponse) {
|
|
126
|
+
router.dispose();
|
|
127
|
+
|
|
128
|
+
return redirectResponse;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (actionResponse || rootError) {
|
|
132
|
+
const hydrationData = {
|
|
133
|
+
...router.getSnapshot(),
|
|
134
|
+
actionData: actionData instanceof Error ? undefined : actionData,
|
|
135
|
+
...(actionData instanceof Error && { error: actionData }),
|
|
136
|
+
...(rootError && { error: rootError }),
|
|
137
|
+
};
|
|
138
|
+
router.dispose();
|
|
139
|
+
router = createDataRouter(router.routes, { hydrationData });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const snapshot = router.getSnapshot();
|
|
143
|
+
const hydrationData = {
|
|
144
|
+
root,
|
|
145
|
+
snapshot: {
|
|
146
|
+
...snapshot,
|
|
147
|
+
error: snapshot.error && {
|
|
148
|
+
...snapshot.error,
|
|
149
|
+
name: snapshot.error.name,
|
|
150
|
+
message: snapshot.error.message,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
...(rootError && { rootError: true }),
|
|
154
|
+
};
|
|
155
|
+
const deferred = options.earlyHydration
|
|
156
|
+
? deferredHydration(hydrationData, options.nonce)
|
|
157
|
+
: undefined;
|
|
158
|
+
const stream = await Promise.resolve()
|
|
159
|
+
.then(() =>
|
|
160
|
+
render({
|
|
161
|
+
router,
|
|
162
|
+
routes,
|
|
163
|
+
root,
|
|
164
|
+
entry,
|
|
165
|
+
rootError: Boolean(rootError),
|
|
166
|
+
streaming: true,
|
|
167
|
+
initialData: deferred?.initial,
|
|
168
|
+
document: true,
|
|
169
|
+
}),
|
|
170
|
+
)
|
|
171
|
+
.catch(async (error: unknown) => {
|
|
172
|
+
router.dispose();
|
|
173
|
+
await deferred?.stream.cancel();
|
|
174
|
+
throw error;
|
|
175
|
+
});
|
|
176
|
+
// The renderer reports the same rejection through its stream reader.
|
|
177
|
+
void stream.allReady.catch(() => {});
|
|
178
|
+
const headers = new Headers(actionResponse?.headers);
|
|
179
|
+
headers.delete("Content-Length");
|
|
180
|
+
headers.delete("Content-Encoding");
|
|
181
|
+
headers.set("Content-Type", "text/html; charset=utf-8");
|
|
182
|
+
headers.set("Cache-Control", "no-store");
|
|
183
|
+
|
|
184
|
+
return new Response(
|
|
185
|
+
deferred
|
|
186
|
+
? progressiveDocument(stream, deferred.stream, {
|
|
187
|
+
signal: request.signal,
|
|
188
|
+
dispose: () => router.dispose(),
|
|
189
|
+
})
|
|
190
|
+
: streamDocument(stream, {
|
|
191
|
+
hydration: resolveHydration(hydrationData),
|
|
192
|
+
signal: request.signal,
|
|
193
|
+
nonce: options.nonce,
|
|
194
|
+
dispose: () => router.dispose(),
|
|
195
|
+
}),
|
|
196
|
+
{
|
|
197
|
+
status: snapshot.error
|
|
198
|
+
? Number("status" in snapshot.error ? snapshot.error.status : 500)
|
|
199
|
+
: (actionResponse?.status ?? 200),
|
|
200
|
+
headers,
|
|
201
|
+
},
|
|
202
|
+
);
|
|
203
|
+
}
|
|
@@ -12,6 +12,7 @@ interface ChunkMetadata {
|
|
|
12
12
|
export function lazyFiles(
|
|
13
13
|
directory: string,
|
|
14
14
|
dataEndpoint = "/_smolter",
|
|
15
|
+
framework: "react" | "preact" = "preact",
|
|
15
16
|
): Plugin {
|
|
16
17
|
let root: string;
|
|
17
18
|
const modules = new Map<string, string>();
|
|
@@ -139,7 +140,7 @@ export function lazyFiles(
|
|
|
139
140
|
}
|
|
140
141
|
|
|
141
142
|
return [
|
|
142
|
-
|
|
143
|
+
`import { lazyRoute } from "@kasperrt/smolter/${framework}/lazy-route";`,
|
|
143
144
|
`export default [${definitions.join(",\n")}];`,
|
|
144
145
|
].join("\n");
|
|
145
146
|
},
|
|
@@ -6,6 +6,7 @@ import { lazyFiles } from "./lazy-files.ts";
|
|
|
6
6
|
|
|
7
7
|
export interface ApplicationOptions {
|
|
8
8
|
directory?: string;
|
|
9
|
+
framework?: "react" | "preact";
|
|
9
10
|
dataEndpoint?: string;
|
|
10
11
|
ssr?: boolean;
|
|
11
12
|
splitRoutes?: boolean;
|
|
@@ -43,6 +44,7 @@ export const preactConfig: UserConfig = {
|
|
|
43
44
|
|
|
44
45
|
export function applicationPlugins({
|
|
45
46
|
directory = "src/routes",
|
|
47
|
+
framework = "preact",
|
|
46
48
|
dataEndpoint = "/_smolter",
|
|
47
49
|
ssr = true,
|
|
48
50
|
splitRoutes = true,
|
|
@@ -51,7 +53,13 @@ export function applicationPlugins({
|
|
|
51
53
|
const application: Plugin = {
|
|
52
54
|
name: "smolter-application",
|
|
53
55
|
enforce: "pre",
|
|
54
|
-
config: () =>
|
|
56
|
+
config: () =>
|
|
57
|
+
framework === "preact"
|
|
58
|
+
? preactConfig
|
|
59
|
+
: {
|
|
60
|
+
resolve: { dedupe: ["react", "react-dom", "@kasperrt/smolter"] },
|
|
61
|
+
ssr: { noExternal: ["@kasperrt/smolter"] },
|
|
62
|
+
},
|
|
55
63
|
transformIndexHtml: {
|
|
56
64
|
order: "post",
|
|
57
65
|
handler(html, context) {
|
|
@@ -93,7 +101,7 @@ export function applicationPlugins({
|
|
|
93
101
|
const request = await webRequest(incoming, outgoing, target);
|
|
94
102
|
const [{ default: routes }, { dispatchRoute }] = await Promise.all([
|
|
95
103
|
server.ssrLoadModule("virtual:smolter/routes"),
|
|
96
|
-
server.ssrLoadModule(
|
|
104
|
+
server.ssrLoadModule(`@kasperrt/smolter/${framework}/server`),
|
|
97
105
|
]);
|
|
98
106
|
await send(await dispatchRoute(request, routes), outgoing);
|
|
99
107
|
};
|
|
@@ -144,7 +152,7 @@ export function applicationPlugins({
|
|
|
144
152
|
return [
|
|
145
153
|
application,
|
|
146
154
|
browserBoundary(),
|
|
147
|
-
...(splitRoutes ? [lazyFiles(directory, dataEndpoint)] : []),
|
|
148
|
-
routerPlugin({ directory, framework
|
|
155
|
+
...(splitRoutes ? [lazyFiles(directory, dataEndpoint, framework)] : []),
|
|
156
|
+
routerPlugin({ directory, framework, dataEndpoint }),
|
|
149
157
|
];
|
|
150
158
|
}
|
package/.generated/vite/react.ts
CHANGED
|
@@ -17,7 +17,18 @@ export function routerPlugin(options: FileRouterOptions = {}): Plugin {
|
|
|
17
17
|
/** File routing is the default; this alias keeps configuration explicit. */
|
|
18
18
|
export const fileRouter = routerPlugin;
|
|
19
19
|
|
|
20
|
+
export type { ApplicationOptions } from "./application/vite.ts";
|
|
20
21
|
export {
|
|
21
22
|
type DeploymentPluginOptions,
|
|
22
23
|
deploymentPlugin,
|
|
23
24
|
} from "./deployment.ts";
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
type ApplicationOptions,
|
|
28
|
+
applicationPlugins as plugins,
|
|
29
|
+
} from "./application/vite.ts";
|
|
30
|
+
|
|
31
|
+
/** Configure React application SSR, scoped data transport and lazy file routing. */
|
|
32
|
+
export function applicationPlugins(options: ApplicationOptions = {}): Plugin[] {
|
|
33
|
+
return plugins({ ...options, framework: "react" });
|
|
34
|
+
}
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.4
|
|
4
|
+
|
|
5
|
+
- Remove the separate `/react/data` resource router. Both main adapters provide route data, forms, submissions and independent fetchers through `createDataRouter`; migrate ResourceProvider/Resource hooks to RouterProvider and the main data hooks. The old pathless resource store and tuple fetcher API are removed, not aliased to incompatible signatures.
|
|
6
|
+
|
|
7
|
+
- Complete React routing-hook parity: navigation/submission state, URL/search/history helpers, route errors, root data and route-ID data. Add React RouterApplication and match Preact loader/fetcher type inference.
|
|
8
|
+
- Support React application SSR and hydration, document helpers, lazy routes and application Vite middleware. React DOM 19+ powers integrated documents; routing hooks and manual loadRouter SSR retain React 18 support. Share transport, root revalidation, hash history, hydration payloads and server orchestration across renderers.
|
|
9
|
+
- Export renderer-independent dispatchRoute from both server entries. Preserve scoped endpoints, private caching, redirects, cancellation and mutation ownership.
|
|
10
|
+
- Add React link intent prefetching, structured/GET fetcher submissions and real NavLink pending state in both adapters. Report pending destinations under plain providers as well as retained application views.
|
|
11
|
+
- Update compatibility badges, guides, package-isolation checks and bundle tables. Verify React production early hydration, persistent input, root refresh, CSP and abandoned deferred boundaries.
|
|
12
|
+
|
|
13
|
+
## 0.4.3
|
|
14
|
+
|
|
15
|
+
- Prefetch individual guide articles on pointer, keyboard and touch intent. Keep the shared header, sidebar and current article visible during cold guide navigation; show a quiet pending status without replacing the layout. Link the header directly to the canonical Getting started guide.
|
|
16
|
+
- Publish compact React/Preact and SSR compatibility indicators in guide navigation and detailed support information on each page.
|
|
17
|
+
- Document every public JSR entrypoint and exported symbol. Validate the generated documentation before publishing, preserving JSR-before-npm release ordering.
|
|
18
|
+
|
|
19
|
+
- Add React `Outlet`, `useMatches` and `useOutletContext`; allow Preact matches under a plain RouterProvider. Show renderer and SSR compatibility in documentation navigation and on each page. Integrated document/early-hydration APIs remain Preact-only.
|
|
20
|
+
|
|
21
|
+
- Remove the public `safeWrapAsync` utility; retain it internally for router and server error handling. Existing consumers of this export must provide their own error handling. `useMatches` and `useOutletContext` remain public Preact APIs.
|
|
22
|
+
|
|
3
23
|
## 0.4.2
|
|
4
24
|
|
|
5
25
|
- Add integrated `Await fallback` for early hydration: pending and resolved content belongs to the mounted component, so navigation, root revalidation and late settlement cannot leave abandoned content behind. Migrate early-hydrated deferred sections from outer Preact `Suspense` to this prop, including root sections. Native Preact streamed Suspense remains unsupported for this ownership contract; it is not patched. Ordinary non-early SSR still streams resolved HTML.
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# smolter _
|
|
2
2
|
|
|
3
3
|
<!-- bundlejs:start -->
|
|
4
|
-
[](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.4%2Freact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22react%22%2C%22react%2F*%22%5D%7D%7D&bundle)
|
|
5
|
+
[](https://bundlejs.com/?q=%40kasperrt%2Fsmolter%400.4.4%2Fpreact&treeshake=%5B*%5D&config=%7B%22compression%22%3A%22gzip%22%2C%22esbuild%22%3A%7B%22external%22%3A%5B%22preact%22%2C%22preact%2F*%22%5D%7D%7D&bundle)
|
|
6
6
|
<!-- bundlejs:end -->
|
|
7
7
|
|
|
8
8
|
A typed data router for React and Preact. **Zero runtime dependencies.**
|
|
@@ -14,9 +14,15 @@ loader; `shouldRevalidate` decides when that work is needed.
|
|
|
14
14
|
Inspired by [wouter](https://github.com/molefrog/wouter)
|
|
15
15
|
and [React Router](https://reactrouter.com/).
|
|
16
16
|
|
|
17
|
-
[Read the documentation](https://kasperrt.github.io/smolter/
|
|
17
|
+
[Read the documentation](https://kasperrt.github.io/smolter/docs/) for guides and the complete API reference. The prerendered site uses Preact and Smolter routes. Articles load individually; React examples remain available in the repository.
|
|
18
18
|
|
|
19
|
-
Start with [loaders](https://kasperrt.github.io/smolter/
|
|
19
|
+
Start with [loaders](https://kasperrt.github.io/smolter/docs/loaders/), [actions](https://kasperrt.github.io/smolter/docs/actions/), [forms and validation](https://kasperrt.github.io/smolter/docs/forms/), or [fetcher reads and mutations](https://kasperrt.github.io/smolter/docs/fetchers/). Each guide connects the handler to an actual route URL and its consuming view.
|
|
20
|
+
|
|
21
|
+
## React and Preact application APIs
|
|
22
|
+
|
|
23
|
+
Both adapters expose navigation, submission, error, root-data and route-data hooks. Both server entries expose `dispatchRoute` and `renderApplication`; both client entries provide `mountApplication`, scoped transport and deployment recovery. Import `applicationPlugins` from the matching renderer's `/vite` entry for lazy file routes and development SSR.
|
|
24
|
+
|
|
25
|
+
Integrated React documents use React DOM 19+ (`react/document`, `react/client`, `react/server`). React 18 remains supported for routing hooks and manual `loadRouter` SSR. Use React's `use(promise)` inside `Suspense` for deferred rendering. Preact uses `Await` with an explicit `fallback` for early hydration. These renderer choices share transport and data ownership, without loading the other framework.
|
|
20
26
|
|
|
21
27
|
## Contents
|
|
22
28
|
|
|
@@ -445,18 +451,18 @@ Choose one renderer for your application. Both expose the same routing API:
|
|
|
445
451
|
| SSR and HTTP data dispatch | `@kasperrt/smolter/react/server` | `@kasperrt/smolter/preact/server` |
|
|
446
452
|
| Vite plugin and flat-file discovery | `@kasperrt/smolter/react/vite` | `@kasperrt/smolter/preact/vite` |
|
|
447
453
|
|
|
448
|
-
`@kasperrt/smolter/utils` contains browser-safe matching and
|
|
454
|
+
`@kasperrt/smolter/utils` contains browser-safe matching and URL helpers.
|
|
449
455
|
The main renderer entries never import server or build modules. Named imports
|
|
450
456
|
are tree-shaken; optional transport is included only when you import `/client`.
|
|
451
457
|
Server entries do not import either UI renderer. Vite entries are build-time only.
|
|
452
458
|
|
|
453
459
|
**Migrating renderer subpaths:** replace `/core` imports for types, router factories, and
|
|
454
460
|
`defineRoute` with your renderer entry. Use `/utils` for `matchRoutes`,
|
|
455
|
-
|
|
461
|
+
and `internalURL`. Move `/client`, `/server`,
|
|
456
462
|
`/files`, and `/vite` imports to the corresponding renderer-prefixed path above.
|
|
457
463
|
The bare package root and old top-level subpaths are no longer exported.
|
|
458
464
|
|
|
459
|
-
See the [
|
|
465
|
+
See the [changelog](./CHANGELOG.md) for release history.
|
|
460
466
|
|
|
461
467
|
**Router factory migration:** code that previously used `createRouter` with
|
|
462
468
|
loaders, actions, fetchers or revalidation must now use `createDataRouter`.
|
|
@@ -525,7 +531,7 @@ re-exports and installs server proxies. `entry.client.tsx` and `entry.server.tsx
|
|
|
525
531
|
are application-owned files wiring those manifests to hydration and SSR.
|
|
526
532
|
Manual tables can be imported directly instead.
|
|
527
533
|
|
|
528
|
-
See the [virtual routes guide](https://kasperrt.github.io/smolter/
|
|
534
|
+
See the [virtual routes guide](https://kasperrt.github.io/smolter/docs/virtual-routes)
|
|
529
535
|
for the exact contract, TypeScript declaration, and both entrypoint examples.
|
|
530
536
|
|
|
531
537
|
### Express hosting
|
|
@@ -537,39 +543,33 @@ server entry and `entry` is the browser entry URL from Vite's manifest. Do not
|
|
|
537
543
|
consume request bodies with Express body parsers before this handler.
|
|
538
544
|
The adapter has no Express dependency and forwards failures to `next(error)`.
|
|
539
545
|
|
|
540
|
-
See the [Express guide](https://kasperrt.github.io/smolter/
|
|
546
|
+
See the [Express guide](https://kasperrt.github.io/smolter/docs/express)
|
|
541
547
|
for a complete server example and streaming and hydration behavior.
|
|
542
548
|
|
|
543
|
-
##
|
|
549
|
+
## Independent data requests
|
|
544
550
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
551
|
+
Both adapters include `useFetcher`, `useSubmit`, `Form` and the loader/action hooks
|
|
552
|
+
in their main entrypoint. Use `createDataRouter` for data routes and `useFetcher`
|
|
553
|
+
for reads or mutations that should not navigate the page. There is no separate
|
|
554
|
+
React resource router or `/react/data` entrypoint.
|
|
548
555
|
|
|
549
|
-
```
|
|
550
|
-
import {
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
`ResourceProvider` owns the store lifetime. `Resource` selects a resource by
|
|
567
|
-
`routeId` and params. `useResourceData` reads it; `useRouteLoaderData` reads a
|
|
568
|
-
named resource. `useResourceFetcher` returns `[state, operations]`, with
|
|
569
|
-
independent `load` and `submit` operations. Successful mutations consult the
|
|
570
|
-
resource's `shouldRevalidate` before reloading its data. Last-subscriber cleanup
|
|
571
|
-
aborts pending work; `persistent` keeps a resource until explicitly disposed.
|
|
572
|
-
This optional entry does not contribute to the base React bundle measurement.
|
|
556
|
+
```tsx
|
|
557
|
+
import { useFetcher } from "@kasperrt/smolter/react";
|
|
558
|
+
|
|
559
|
+
function Settings() {
|
|
560
|
+
const settings = useFetcher();
|
|
561
|
+
return (
|
|
562
|
+
<settings.Form method="post" action="/settings">
|
|
563
|
+
<input name="name" aria-label="Profile name" />
|
|
564
|
+
<button disabled={settings.state !== "idle"}>Save</button>
|
|
565
|
+
</settings.Form>
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
Use the same imports from `/preact` in a Preact application. Independent
|
|
571
|
+
fetchers share the application router's cancellation, revalidation and deployment
|
|
572
|
+
recovery ownership; they do not require another provider or store.
|
|
573
573
|
|
|
574
574
|
## Size and checks
|
|
575
575
|
|
|
@@ -581,7 +581,6 @@ This optional entry does not contribute to the base React bundle measurement.
|
|
|
581
581
|
| Preact application hydration | `@kasperrt/smolter/preact/client` |
|
|
582
582
|
| Preact document components | `@kasperrt/smolter/preact/document` |
|
|
583
583
|
| Lazy Preact route modules | `@kasperrt/smolter/preact/lazy-route` |
|
|
584
|
-
| Optional React resources | `@kasperrt/smolter/react/data` |
|
|
585
584
|
| Server rendering / Vite integration | Renderer `/server` / `/vite` subpaths; keep out of browser imports |
|
|
586
585
|
|
|
587
586
|
The measurements below describe retained exports, not the cost of an import
|
|
@@ -605,16 +604,17 @@ a claim of identical features.
|
|
|
605
604
|
| --- | ---: |
|
|
606
605
|
| `React Router: declarative routing subset` | 12882 |
|
|
607
606
|
| `React Router: data routing subset` | 29780 |
|
|
608
|
-
| `React: routing-only named exports` |
|
|
607
|
+
| `React: routing-only named exports` | 1419 |
|
|
609
608
|
| `Preact: routing-only named exports` | 1405 |
|
|
610
|
-
| `@kasperrt/smolter/react` |
|
|
611
|
-
| `@kasperrt/smolter/preact` |
|
|
612
|
-
| `@kasperrt/smolter/react +
|
|
613
|
-
| `@kasperrt/smolter/preact + server transport` |
|
|
614
|
-
| `@kasperrt/smolter/
|
|
615
|
-
| `@kasperrt/smolter/
|
|
616
|
-
| `@kasperrt/smolter/
|
|
617
|
-
|
|
|
609
|
+
| `@kasperrt/smolter/react` | 4801 |
|
|
610
|
+
| `@kasperrt/smolter/preact` | 5076 |
|
|
611
|
+
| `@kasperrt/smolter/react + server transport (named client exports)` | 6060 |
|
|
612
|
+
| `@kasperrt/smolter/preact + server transport` | 6345 |
|
|
613
|
+
| `@kasperrt/smolter/react + client + document (application)` | 9124 |
|
|
614
|
+
| `@kasperrt/smolter/react + client + document + lazy-route` | 9401 |
|
|
615
|
+
| `@kasperrt/smolter/preact + client + document (application without lazy routes)` | 9435 |
|
|
616
|
+
| `@kasperrt/smolter/preact + client + document + lazy-route (file-routed application)` | 9721 |
|
|
617
|
+
| `React: common routing/data exports` | 3071 |
|
|
618
618
|
| `Preact: common routing/data exports` | 3046 |
|
|
619
619
|
| `wouter: routing subset (Router, Route, Switch, Link, useParams, useLocation)` | 2854 |
|
|
620
620
|
| `wouter-preact: same routing subset` | 2211 |
|
|
@@ -735,7 +735,7 @@ normally, and a session marker prevents reload loops. Development/SSR builds
|
|
|
735
735
|
disable the helper. This optional client module adds nothing to the routing-only
|
|
736
736
|
entrypoints and never replays form submissions.
|
|
737
737
|
|
|
738
|
-
See [deployment updates](https://kasperrt.github.io/smolter/
|
|
738
|
+
See [deployment updates](https://kasperrt.github.io/smolter/docs/deployments/)
|
|
739
739
|
for Express, CDN paths, TypeScript declarations and unsaved-state handling.
|
|
740
740
|
|
|
741
741
|
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//#region .generated/client/hash-history.ts
|
|
2
|
+
function connectHashHistory(router) {
|
|
3
|
+
const read = router.getSnapshot;
|
|
4
|
+
const navigate = router.navigate;
|
|
5
|
+
router.navigate = async (url, options) => {
|
|
6
|
+
const target = new URL(url, router.getSnapshot().url);
|
|
7
|
+
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
8
|
+
history[options?.replace ? "replaceState" : "pushState"](null, "", hash);
|
|
9
|
+
await navigate(url, options);
|
|
10
|
+
};
|
|
11
|
+
const unsubscribe = router.subscribe(() => {
|
|
12
|
+
const target = new URL(read().url);
|
|
13
|
+
const hash = `#${target.pathname}${target.search}${target.hash}`;
|
|
14
|
+
if (location.hash !== hash) history.replaceState(history.state, "", hash);
|
|
15
|
+
});
|
|
16
|
+
const onHashChange = () => void navigate(location.hash.slice(1) || "/");
|
|
17
|
+
window.addEventListener("hashchange", onHashChange);
|
|
18
|
+
const dispose = router.dispose;
|
|
19
|
+
router.dispose = () => {
|
|
20
|
+
unsubscribe();
|
|
21
|
+
window.removeEventListener("hashchange", onHashChange);
|
|
22
|
+
dispose();
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
//#endregion
|
|
26
|
+
export { connectHashHistory };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Settlement = [number, unknown, string | null];
|
|
2
|
+
export interface DeferredWindow extends Window {
|
|
3
|
+
__smolterDeferred?: Settlement[];
|
|
4
|
+
}
|
|
5
|
+
/** Restore promise identity before hydrating Suspense boundaries. */
|
|
6
|
+
export declare function readHydration(json: string, timeout?: number): {
|
|
7
|
+
hydration: any;
|
|
8
|
+
dispose: () => void;
|
|
9
|
+
};
|
|
10
|
+
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//#region .generated/
|
|
1
|
+
//#region .generated/client/hydration.ts
|
|
2
2
|
/** Restore promise identity before hydrating Suspense boundaries. */
|
|
3
3
|
function readHydration(json, timeout = 3e4) {
|
|
4
4
|
if (!Number.isInteger(timeout) || timeout < 1 || timeout > 2147483647) throw new RangeError("deferredTimeout must be an integer between 1 and 2147483647 ms");
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { RevalidationArgs, Router } from "../core/index.ts";
|
|
2
|
+
interface RootModule {
|
|
3
|
+
default?: unknown;
|
|
4
|
+
Layout?: unknown;
|
|
5
|
+
id?: string;
|
|
6
|
+
loaderPath?: string;
|
|
7
|
+
shouldRevalidate?: (args: RevalidationArgs) => boolean;
|
|
8
|
+
}
|
|
9
|
+
export { createStore as createRootStore } from "../core/store.ts";
|
|
10
|
+
export declare function attachRootData(router: Router, initial: unknown, rootModule?: RootModule): {
|
|
11
|
+
getSnapshot: () => unknown;
|
|
12
|
+
subscribe(listener: () => void): () => void;
|
|
13
|
+
update(next: unknown): void;
|
|
14
|
+
};
|