@valentinkolb/ssr 0.7.3 → 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
@@ -49,6 +49,7 @@ Use the libraries you already prefer. This package only handles SSR and islands
49
49
  - Adapters for Bun, Hono, and Elysia
50
50
  - Type-safe Hono page helper via `createSSRHandler`
51
51
  - Monorepo support via `rootDir`
52
+ - Public path mounting via `basePath` for microfrontends
52
53
  - Stable file-path-based island IDs (collision-safe across workspace packages)
53
54
  - Production chunk cache busting (`/_ssr/*.js?v=<buildTimestamp>`)
54
55
 
@@ -56,7 +57,6 @@ Use the libraries you already prefer. This package only handles SSR and islands
56
57
 
57
58
  ```bash
58
59
  bun add @valentinkolb/ssr solid-js
59
- bun add -d @babel/core @babel/preset-typescript babel-preset-solid
60
60
 
61
61
  # choose adapter deps you need
62
62
  bun add hono
@@ -95,6 +95,8 @@ export const { config, plugin, html } = createConfig<PageOptions>({
95
95
  dev: process.env.NODE_ENV === "development",
96
96
  // For monorepos with separated packages:
97
97
  // rootDir: "/path/to/workspace-root",
98
+ // For microfrontends mounted under /docs:
99
+ // basePath: "/docs",
98
100
  template: ({ body, scripts, title, description }) => `
99
101
  <!doctype html>
100
102
  <html>
@@ -143,7 +145,7 @@ import Counter from "../components/Counter.island";
143
145
 
144
146
  export default ssr(async (c) => {
145
147
  c.get("page").title = "Home";
146
- return <Counter initial={5} />;
148
+ return () => <Counter initial={5} />;
147
149
  });
148
150
  ```
149
151
 
@@ -172,6 +174,45 @@ NODE_ENV=development bun --watch --preload=./scripts/preload.ts src/server.ts
172
174
  - Hono: `@valentinkolb/ssr/hono`
173
175
  - Elysia: `@valentinkolb/ssr/elysia`
174
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
+
175
216
  ## `createConfig` options
176
217
 
177
218
  ```ts
@@ -179,6 +220,7 @@ createConfig({
179
220
  dev?: boolean; // default: false
180
221
  verbose?: boolean; // default: !dev
181
222
  rootDir?: string; // default: process.cwd()
223
+ basePath?: string; // default: "", example: "/docs"
182
224
  external?: string[]; // passed to Bun.build for island bundle
183
225
  template?: ({ body, scripts, ...custom }) => string | Promise<string>;
184
226
  })
@@ -187,8 +229,30 @@ createConfig({
187
229
  ### Notes
188
230
 
189
231
  - `rootDir` is important in monorepos where server entrypoint and island files live in different packages.
232
+ - `basePath` moves SSR assets and dev endpoints under that prefix, e.g. `/docs/_ssr`.
190
233
  - In production, hydration imports include a build timestamp query (`?v=...`) for cache busting.
191
234
 
235
+ ## Microfrontend mount example
236
+
237
+ Use `basePath` when the SSR app is mounted under a sub-path:
238
+
239
+ ```ts
240
+ // config.ts
241
+ export const { config, html } = createConfig({
242
+ basePath: "/docs",
243
+ });
244
+
245
+ // docs-app.ts
246
+ const docsApp = new Hono()
247
+ .route("/_ssr", routes(config))
248
+ .get("/", () => html(() => <DocsHome />));
249
+
250
+ // host-app.ts
251
+ export default new Hono().route("/docs", docsApp);
252
+ ```
253
+
254
+ With this setup, hydration chunks and dev endpoints are served from `/docs/_ssr/...`.
255
+
192
256
  ## Build for production
193
257
 
194
258
  ```ts
@@ -209,7 +273,7 @@ await Bun.build({
209
273
 
210
274
  - initializes `c.get("page")` as typed page options
211
275
  - accepts middlewares/validators before final handler
212
- - lets handlers return either JSX or `Response`
276
+ - lets handlers return either a synchronous render function or `Response`
213
277
 
214
278
  ## Dev mode tools
215
279
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@valentinkolb/ssr",
3
- "version": "0.7.3",
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",
@@ -32,15 +32,15 @@
32
32
  }
33
33
  },
34
34
  "dependencies": {
35
+ "@babel/core": "^7.24.0",
36
+ "@babel/preset-typescript": "^7.24.0",
37
+ "babel-preset-solid": "^1.8.0",
35
38
  "seroval": "^1.0.0"
36
39
  },
37
40
  "devDependencies": {
38
- "@babel/core": "^7.24.0",
39
- "@babel/preset-typescript": "^7.24.0",
40
41
  "@elysiajs/static": "^1.2.0",
41
42
  "@types/babel__core": "^7.20.5",
42
43
  "@types/bun": "latest",
43
- "babel-preset-solid": "^1.8.0",
44
44
  "elysia": "^1.2.0",
45
45
  "hono": "^4.6.14",
46
46
  "solid-js": "^1.9.0",
@@ -23,26 +23,26 @@ 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
  * ```
30
30
  */
31
31
  export const routes = (config: SsrConfig): Routes => {
32
- const { dev } = config;
32
+ const { dev, ssrPath } = config;
33
33
  const ssrDir = getSsrDir(config);
34
34
 
35
35
  const devRoutes: Routes = dev
36
36
  ? {
37
- "/_ssr/_reload": () => createReloadResponse(),
38
- "/_ssr/_ping": () => new Response("ok"),
37
+ [`${ssrPath}/_reload`]: () => createReloadResponse(),
38
+ [`${ssrPath}/_ping`]: () => new Response("ok"),
39
39
  }
40
40
  : {};
41
41
 
42
42
  return {
43
43
  ...devRoutes,
44
44
 
45
- "/_ssr/*.js": async (req) => {
45
+ [`${ssrPath}/*.js`]: async (req) => {
46
46
  const filename = new URL(req.url).pathname.split("/").pop()!;
47
47
  const path = safePath(ssrDir, filename);
48
48
  if (!path) return notFound();
@@ -3,8 +3,10 @@ if (!window.__ssr_reload) {
3
3
  window.__ssr_reload = true;
4
4
 
5
5
  (function () {
6
+ const ssrPath = globalThis.__SSR_CONFIG?.ssrPath || "/_ssr";
7
+
6
8
  // Settings
7
- const STORAGE_KEY = "_ssr";
9
+ const STORAGE_KEY = `_ssr:${ssrPath}`;
8
10
  const defaults = {
9
11
  autoReload: true,
10
12
  highlightIslands: false,
@@ -24,9 +26,9 @@ if (!window.__ssr_reload) {
24
26
 
25
27
  const highlightCSS = (tag, color) => `
26
28
  ${tag} {
27
- display: block;
29
+ display: block !important;
28
30
  box-shadow: 0 0 0 1px ${color} !important;
29
- position: relative;
31
+ position: relative !important;
30
32
  }
31
33
  ${tag}::before {
32
34
  content: attr(data-file);
@@ -178,7 +180,7 @@ if (!window.__ssr_reload) {
178
180
  const start = () => {
179
181
  if (es) return;
180
182
  try {
181
- es = new EventSource("/_ssr/_reload");
183
+ es = new EventSource(`${ssrPath}/_reload`);
182
184
  stopAnimation();
183
185
  badge.innerText = "[ssr]";
184
186
  } catch {
@@ -191,7 +193,7 @@ if (!window.__ssr_reload) {
191
193
  startAnimation();
192
194
  if (!settings.autoReload) return;
193
195
  reconnectInterval = setInterval(() => {
194
- fetch("/_ssr/_ping")
196
+ fetch(`${ssrPath}/_ping`)
195
197
  .then((r) => r.ok && location.reload())
196
198
  .catch(() => {});
197
199
  }, 300);
@@ -20,22 +20,22 @@ 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
  */
27
27
  export const routes = (config: SsrConfig) => {
28
- const { dev } = config;
28
+ const { dev, ssrPath } = config;
29
29
  const ssrDir = getSsrDir(config);
30
30
 
31
31
  return new Elysia({ name: "ssr" })
32
32
  .use(
33
33
  staticPlugin({
34
34
  assets: ssrDir,
35
- prefix: "/_ssr",
35
+ prefix: ssrPath,
36
36
  headers: { "Cache-Control": getCacheHeaders(dev) },
37
37
  }),
38
38
  )
39
- .get("/_ssr/_reload", () => (dev ? createReloadResponse() : notFound()))
40
- .get("/_ssr/_ping", () => (dev ? new Response("ok") : notFound()));
39
+ .get(`${ssrPath}/_reload`, () => (dev ? createReloadResponse() : notFound()))
40
+ .get(`${ssrPath}/_ping`, () => (dev ? new Response("ok") : notFound()));
41
41
  };
@@ -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) => {
@@ -5,6 +5,27 @@
5
5
  import { dirname, join, resolve } from "path";
6
6
  import type { SsrConfig } from "../index";
7
7
 
8
+ /**
9
+ * Normalize a public app base path.
10
+ * - undefined, "", "/" => ""
11
+ * - "/docs/" => "/docs"
12
+ * - values without leading slash are rejected
13
+ */
14
+ export const normalizeBasePath = (input?: string): string => {
15
+ const value = input?.trim() ?? "";
16
+ if (!value || value === "/") return "";
17
+ if (!value.startsWith("/")) {
18
+ throw new Error(`[ssr] basePath must start with "/" or be empty. Received: ${JSON.stringify(input)}`);
19
+ }
20
+ return value.replace(/\/+$/, "");
21
+ };
22
+
23
+ /**
24
+ * Public HTTP path prefix used for SSR assets/endpoints.
25
+ */
26
+ export const toSsrPath = (basePath: string): string =>
27
+ basePath ? `${basePath}/_ssr` : "/_ssr";
28
+
8
29
  /**
9
30
  * Get the _ssr directory path based on dev/prod mode.
10
31
  * Dev: uses config.rootDir (fallback process.cwd())
package/src/build.ts CHANGED
@@ -16,6 +16,57 @@ const getSelector = (type: ComponentType, id: string) =>
16
16
 
17
17
  const fmt = (ms: number) => (ms >= 1000 ? `${(ms / 1000).toFixed(2)}s` : `${Math.round(ms)}ms`);
18
18
 
19
+ /**
20
+ * Workaround for Bun bundler bug: duplicate export statements in shared chunks
21
+ * when splitting=true. We only run this in production because rewriting chunk
22
+ * source in dev can affect evaluation order in cyclic-sensitive modules.
23
+ */
24
+ export const dedupeSharedChunkExports = async (outdir: string, verbose: boolean): Promise<number> => {
25
+ let fixedChunks = 0;
26
+ const chunkGlob = new Glob("chunk-*.js");
27
+ for await (const chunkFile of chunkGlob.scan({ cwd: outdir, absolute: true })) {
28
+ const src = await Bun.file(chunkFile).text();
29
+ // Collect all export blocks and deduplicate.
30
+ const exportRegex = /^export\s*\{[^}]*\}\s*;?\s*$/gm;
31
+ const exportBlocks = [...src.matchAll(exportRegex)].map((m) => m[0]);
32
+ if (exportBlocks.length > 1) {
33
+ // Parse all exported names from all blocks.
34
+ const seenNames = new Set<string>();
35
+ const uniqueBlocks: string[] = [];
36
+ for (const block of exportBlocks) {
37
+ const names =
38
+ block
39
+ .match(/\{([^}]*)\}/)?.[1]
40
+ ?.split(",")
41
+ .map((n) => n.trim())
42
+ .filter(Boolean) ?? [];
43
+ const newNames = names.filter((n) => !seenNames.has(n));
44
+ if (newNames.length > 0) {
45
+ uniqueBlocks.push(`export { ${newNames.join(", ")} };`);
46
+ newNames.forEach((n) => seenNames.add(n));
47
+ }
48
+ }
49
+ // Replace all export blocks with deduplicated ones.
50
+ let fixed = src;
51
+ for (const block of exportBlocks) {
52
+ fixed = fixed.replace(block, "");
53
+ }
54
+ // Append single combined export before any debugId comment.
55
+ const debugIdIdx = fixed.lastIndexOf("//# debugId=");
56
+ const combined = uniqueBlocks.join("\n") + "\n";
57
+ if (debugIdIdx !== -1) {
58
+ fixed = fixed.slice(0, debugIdIdx) + combined + fixed.slice(debugIdIdx);
59
+ } else {
60
+ fixed = fixed.trimEnd() + "\n" + combined;
61
+ }
62
+ await Bun.write(chunkFile, fixed);
63
+ fixedChunks += 1;
64
+ if (verbose) console.log(` Fixed duplicate exports in ${chunkFile.split("/").pop()}`);
65
+ }
66
+ }
67
+ return fixedChunks;
68
+ };
69
+
19
70
  export const buildIslands = async (options: {
20
71
  pattern: string;
21
72
  outdir: string;
@@ -130,49 +181,10 @@ export const buildIslands = async (options: {
130
181
  ],
131
182
  });
132
183
 
133
- // Workaround for Bun bundler bug: duplicate export statements in shared chunks
134
- // when splitting: true. Scan chunk files and deduplicate export lines.
135
- if (result.success) {
136
- const chunkGlob = new Glob("chunk-*.js");
137
- for await (const chunkFile of chunkGlob.scan({ cwd: outdir, absolute: true })) {
138
- const src = await Bun.file(chunkFile).text();
139
- // Collect all export blocks and deduplicate
140
- const exportRegex = /^export\s*\{[^}]*\}\s*;?\s*$/gm;
141
- const exportBlocks = [...src.matchAll(exportRegex)].map((m) => m[0]);
142
- if (exportBlocks.length > 1) {
143
- // Parse all exported names from all blocks
144
- const seenNames = new Set<string>();
145
- const uniqueBlocks: string[] = [];
146
- for (const block of exportBlocks) {
147
- const names =
148
- block
149
- .match(/\{([^}]*)\}/)?.[1]
150
- ?.split(",")
151
- .map((n) => n.trim())
152
- .filter(Boolean) ?? [];
153
- const newNames = names.filter((n) => !seenNames.has(n));
154
- if (newNames.length > 0) {
155
- uniqueBlocks.push(`export { ${newNames.join(", ")} };`);
156
- newNames.forEach((n) => seenNames.add(n));
157
- }
158
- }
159
- // Replace all export blocks with deduplicated ones
160
- let fixed = src;
161
- for (const block of exportBlocks) {
162
- fixed = fixed.replace(block, "");
163
- }
164
- // Append single combined export before any debugId comment
165
- const debugIdIdx = fixed.lastIndexOf("//# debugId=");
166
- const combined = uniqueBlocks.join("\n") + "\n";
167
- if (debugIdIdx !== -1) {
168
- fixed = fixed.slice(0, debugIdIdx) + combined + fixed.slice(debugIdIdx);
169
- } else {
170
- fixed = fixed.trimEnd() + "\n" + combined;
171
- }
172
- await Bun.write(chunkFile, fixed);
173
- if (verbose) console.log(` Fixed duplicate exports in ${chunkFile.split("/").pop()}`);
174
- }
175
- }
184
+ if (result.success && !dev) {
185
+ await dedupeSharedChunkExports(outdir, verbose);
186
+ } else if (result.success && dev && verbose) {
187
+ console.log("Skipped shared-chunk export rewrite in dev mode.");
176
188
  }
177
189
 
178
190
  if (verbose) {
package/src/index.ts CHANGED
@@ -12,6 +12,7 @@ import { transform } from "./transform";
12
12
  import { buildIslands } from "./build";
13
13
  import { join, dirname, resolve } from "path";
14
14
  import { resolveIslandImport } from "./island-resolve";
15
+ import { normalizeBasePath, toSsrPath } from "./adapter/utils";
15
16
  // @ts-ignore - Bun text import
16
17
  import devClientCode from "./adapter/client.js" with { type: "text" };
17
18
 
@@ -46,6 +47,8 @@ export type SsrOptions<T extends object = object> = {
46
47
  verbose?: boolean;
47
48
  /** Project root for island discovery and dev _ssr assets (default: process.cwd()) */
48
49
  rootDir?: string;
50
+ /** Public app mount path for SSR assets and dev endpoints (default: "") */
51
+ basePath?: string;
49
52
  /** Modules to exclude from the island bundle (passed to Bun.build) */
50
53
  external?: string[];
51
54
  /** HTML template function (optional, has default) */
@@ -61,9 +64,13 @@ export type SsrConfig = {
61
64
  dev: boolean;
62
65
  verbose?: boolean;
63
66
  rootDir?: string;
67
+ basePath: string;
68
+ ssrPath: string;
64
69
  };
65
70
 
66
- 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>;
67
74
 
68
75
  type PluginFn = () => BunPlugin;
69
76
 
@@ -105,8 +112,10 @@ export type SsrResult<T extends object> = {
105
112
  * ```
106
113
  */
107
114
  export const createConfig = <T extends object = object>(options: SsrOptions<T> = {}): SsrResult<T> => {
108
- const { dev = false, verbose, external, template, rootDir: rootDirOption } = options;
115
+ const { dev = false, verbose, external, template, rootDir: rootDirOption, basePath: basePathOption } = options;
109
116
  const rootDir = resolve(rootDirOption ?? process.cwd());
117
+ const basePath = normalizeBasePath(basePathOption);
118
+ const ssrPath = toSsrPath(basePath);
110
119
 
111
120
  // Default template if none provided
112
121
  const htmlTemplate =
@@ -130,23 +139,33 @@ export const createConfig = <T extends object = object>(options: SsrOptions<T> =
130
139
  dev,
131
140
  verbose,
132
141
  rootDir,
142
+ basePath,
143
+ ssrPath,
133
144
  };
134
145
 
135
146
  const buildVersion = getBuildVersion(dev);
136
147
 
148
+ const islandDisplayStyle =
149
+ "<style>solid-client,solid-island{display:contents}</style>";
150
+
137
151
  // Hydration script - dynamically loads island/client bundles based on DOM
138
- const hydrationScript = `<script type="module">const v=${JSON.stringify(buildVersion)};document.querySelectorAll('solid-island,solid-client').forEach(e=>import('/_ssr/'+e.dataset.id+'.js'+(v?'?v='+v:'')));</script>`;
152
+ const hydrationScript = `<script type="module">const p=${JSON.stringify(ssrPath)};const v=${JSON.stringify(buildVersion)};document.querySelectorAll('solid-island,solid-client').forEach(e=>import(p+'/'+e.dataset.id+'.js'+(v?'?v='+v:'')));</script>`;
153
+ const devConfigScript = `<script>globalThis.__SSR_CONFIG=${JSON.stringify({ ssrPath })}</script>`;
139
154
 
140
155
  // HTML renderer
141
- const html: HtmlFn<T> = async (element, opts = {} as T) => {
142
- 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);
143
162
 
144
- // Component scripts
145
- let scripts = hydrationScript;
163
+ // Framework-injected assets
164
+ let scripts = `${islandDisplayStyle}\n${hydrationScript}`;
146
165
 
147
166
  // Add dev tools script in dev mode (inlined)
148
167
  if (dev) {
149
- scripts += `\n<script type="module">${devClientCode}</script>`;
168
+ scripts += `\n${devConfigScript}\n<script type="module">${devClientCode}</script>`;
150
169
  }
151
170
 
152
171
  const content = await htmlTemplate({