@valentinkolb/ssr 0.8.0 → 0.9.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
@@ -145,7 +145,7 @@ import Counter from "../components/Counter.island";
145
145
 
146
146
  export default ssr(async (c) => {
147
147
  c.get("page").title = "Home";
148
- return <Counter initial={5} />;
148
+ return () => <Counter initial={5} />;
149
149
  });
150
150
  ```
151
151
 
@@ -174,6 +174,45 @@ NODE_ENV=development bun --watch --preload=./scripts/preload.ts src/server.ts
174
174
  - Hono: `@valentinkolb/ssr/hono`
175
175
  - Elysia: `@valentinkolb/ssr/elysia`
176
176
 
177
+ ## Rendering API
178
+
179
+ `html()` and Hono `ssr()` handlers expect a synchronous render function:
180
+
181
+ ```tsx
182
+ export default ssr(async (c) => {
183
+ const data = await loadData();
184
+ c.get("page").title = data.title;
185
+
186
+ return () => <Page data={data} />;
187
+ });
188
+
189
+ app.get("/", () => html(() => <Page />));
190
+ ```
191
+
192
+ Do async work in the handler before returning the render function. Do not make the render function itself `async`; Solid SSR expects synchronous JSX evaluation.
193
+
194
+ ### v0.9.0 migration
195
+
196
+ This is a breaking change in v0.9.0. In v0.8.x and earlier, examples often returned already-created JSX:
197
+
198
+ ```tsx
199
+ // v0.8.x and earlier
200
+ export default ssr(async () => <Page />);
201
+
202
+ app.get("/", () => html(<Page />));
203
+ ```
204
+
205
+ In v0.9.0, wrap JSX creation in a render function:
206
+
207
+ ```tsx
208
+ // v0.9.0+
209
+ export default ssr(async () => () => <Page />);
210
+
211
+ app.get("/", () => html(() => <Page />));
212
+ ```
213
+
214
+ This ensures Solid primitives such as `createUniqueId()` run inside `renderToString()`, where the SSR context exists.
215
+
177
216
  ## `createConfig` options
178
217
 
179
218
  ```ts
@@ -206,7 +245,7 @@ export const { config, html } = createConfig({
206
245
  // docs-app.ts
207
246
  const docsApp = new Hono()
208
247
  .route("/_ssr", routes(config))
209
- .get("/", () => html(<DocsHome />));
248
+ .get("/", () => html(() => <DocsHome />));
210
249
 
211
250
  // host-app.ts
212
251
  export default new Hono().route("/docs", docsApp);
@@ -234,7 +273,7 @@ await Bun.build({
234
273
 
235
274
  - initializes `c.get("page")` as typed page options
236
275
  - accepts middlewares/validators before final handler
237
- - lets handlers return either JSX or `Response`
276
+ - lets handlers return either a synchronous render function or `Response`
238
277
 
239
278
  ## Dev mode tools
240
279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentinkolb/ssr",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "description": "Minimal SSR framework for SolidJS and Bun",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -23,7 +23,7 @@ type Routes = Record<string, RouteHandler>;
23
23
  * serve({
24
24
  * routes: {
25
25
  * ...routes(config),
26
- * "/": () => html(<Home />),
26
+ * "/": () => html(() => <Home />),
27
27
  * },
28
28
  * });
29
29
  * ```
@@ -20,7 +20,7 @@ import {
20
20
  * import { routes } from "@valentinkolb/ssr/adapter/elysia";
21
21
  * new Elysia()
22
22
  * .use(routes(config))
23
- * .get("/", () => html(<Home />))
23
+ * .get("/", () => html(() => <Home />))
24
24
  * .listen(3000);
25
25
  * ```
26
26
  */
@@ -5,8 +5,7 @@
5
5
  import { Hono } from "hono";
6
6
  import { createFactory } from "hono/factory";
7
7
  import type { Context, Env, Handler, MiddlewareHandler, TypedResponse } from "hono";
8
- import type { JSX } from "solid-js";
9
- import type { SsrConfig, HtmlFn } from "../index";
8
+ import type { SsrConfig, HtmlFn, RenderFn } from "../index";
10
9
  import { getSsrDir, getCacheHeaders, createReloadResponse, safePath } from "./utils";
11
10
 
12
11
  // ============================================================================
@@ -20,8 +19,8 @@ type PageEnv<T extends object> = {
20
19
  };
21
20
  };
22
21
 
23
- /** SSR handler return type - JSX element or Response (for redirects etc.) */
24
- type SsrHandlerResult = JSX.Element | Response | TypedResponse;
22
+ /** SSR handler return type - render function or Response (for redirects etc.) */
23
+ type SsrHandlerResult = RenderFn | Response | TypedResponse;
25
24
 
26
25
  /** SSR handler function signature */
27
26
  type SsrHandler<E extends Env, T extends object> = (
@@ -41,7 +40,7 @@ type SsrHandlers = [MiddlewareHandler, ...MiddlewareHandler[], Handler];
41
40
  * Features:
42
41
  * - Full compatibility with Hono middlewares/validators
43
42
  * - Type-safe page options via `c.get("page")`
44
- * - Return JSX directly or Response for redirects
43
+ * - Return a render function or Response for redirects
45
44
  * - No manual `html()` call needed
46
45
  *
47
46
  * @example
@@ -88,7 +87,7 @@ type SsrHandlers = [MiddlewareHandler, ...MiddlewareHandler[], Handler];
88
87
  * c.get("page").title = room.name;
89
88
  * c.get("page").description = `Welcome to ${room.name}`;
90
89
  *
91
- * return <RoomView room={room} />;
90
+ * return () => <RoomView room={room} />;
92
91
  * }
93
92
  * );
94
93
  * ```
@@ -125,7 +124,7 @@ export const createSSRHandler = <T extends object>(html: HtmlFn<T>) => {
125
124
  await next();
126
125
  });
127
126
 
128
- // Wrapped handler: JSX → html(), Response → passthrough
127
+ // Wrapped handler: render function → html(), Response → passthrough
129
128
  const wrappedHandler: Handler = async (c) => {
130
129
  const result = await finalHandler(c as Context<E & PageEnv<T>>);
131
130
 
@@ -134,8 +133,15 @@ export const createSSRHandler = <T extends object>(html: HtmlFn<T>) => {
134
133
  return result;
135
134
  }
136
135
 
137
- // Otherwise treat as JSX element and render with html()
138
- return html(result as JSX.Element, c.get("page") as T);
136
+ if (typeof result !== "function") {
137
+ throw new Error("[ssr] ssr() handlers must return a render function: return () => <Page />");
138
+ }
139
+
140
+ if (result.constructor.name === "AsyncFunction") {
141
+ throw new Error("[ssr] ssr() render functions must be synchronous: return () => <Page />");
142
+ }
143
+
144
+ return html(result as RenderFn, c.get("page") as T);
139
145
  };
140
146
 
141
147
  // Return tuple for spread: .get('/path', ...ssr(handler))
@@ -155,7 +161,7 @@ export const createSSRHandler = <T extends object>(html: HtmlFn<T>) => {
155
161
  * import { routes } from "@valentinkolb/ssr/adapter/hono";
156
162
  * const app = new Hono()
157
163
  * .route("/_ssr", routes(config))
158
- * .get("/", () => html(<Home />));
164
+ * .get("/", () => html(() => <Home />));
159
165
  * ```
160
166
  */
161
167
  export const routes = (config: SsrConfig) => {
package/src/index.ts CHANGED
@@ -68,7 +68,9 @@ export type SsrConfig = {
68
68
  ssrPath: string;
69
69
  };
70
70
 
71
- export type HtmlFn<T extends object> = (element: JSX.Element, options?: T) => Promise<Response>;
71
+ export type RenderFn = () => JSX.Element;
72
+
73
+ export type HtmlFn<T extends object> = (render: RenderFn, options?: T) => Promise<Response>;
72
74
 
73
75
  type PluginFn = () => BunPlugin;
74
76
 
@@ -151,8 +153,12 @@ export const createConfig = <T extends object = object>(options: SsrOptions<T> =
151
153
  const devConfigScript = `<script>globalThis.__SSR_CONFIG=${JSON.stringify({ ssrPath })}</script>`;
152
154
 
153
155
  // HTML renderer
154
- const html: HtmlFn<T> = async (element, opts = {} as T) => {
155
- const body = renderToString(() => element);
156
+ const html: HtmlFn<T> = async (render, opts = {} as T) => {
157
+ if (render.constructor.name === "AsyncFunction") {
158
+ throw new Error("[ssr] html() expects a synchronous render function: html(() => <Page />)");
159
+ }
160
+
161
+ const body = renderToString(render);
156
162
 
157
163
  // Framework-injected assets
158
164
  let scripts = `${islandDisplayStyle}\n${hydrationScript}`;