From 7335d3ac6d9fa8dbdd4192cb28b5bc03cd7f3f8d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 19 Oct 2026 11:06:40 +0800 Subject: [PATCH] fix: compile server transforms with the SSR generate when ssr is off - Without `ssr: true`, modules loaded on the server were compiled to DOM output and threw "Client-only API called on the server side". - Server transforms now use `generate: 'ssr'` with `hydratable: false`, so a client-rendered app can prerender pages with `ssrLoadModule`. - Client transforms are unchanged. Fixes #64 Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/ssr-codegen-without-ssr-option.md | 5 ++ README.md | 6 +- src/index.ts | 11 ++- src/ssr-codegen.test.ts | 70 ++++++++++++++++++++ 4 files changed, 89 insertions(+), 3 deletions(-) create mode 100644 .changeset/ssr-codegen-without-ssr-option.md create mode 100644 src/ssr-codegen.test.ts diff --git a/.changeset/ssr-codegen-without-ssr-option.md b/.changeset/ssr-codegen-without-ssr-option.md new file mode 100644 index 0000000..5d190ff --- /dev/null +++ b/.changeset/ssr-codegen-without-ssr-option.md @@ -0,0 +1,5 @@ +--- +'@solidjs/vite-plugin': patch +--- + +Without `ssr: true`, modules loaded on the server now compile to non-hydratable SSR output instead of DOM output. Prerendering a client-rendered app with `ssrLoadModule` no longer throws "Client-only API called on the server side" (#64). diff --git a/README.md b/README.md index 74508fc..48d9e9a 100644 --- a/README.md +++ b/README.md @@ -192,7 +192,11 @@ Whether the app is server-rendered — one meaning everywhere. Without [`start`](#optionsstart), `ssr: true` enables the SSR transforms (hydratable client code, SSR server code); you provide the entries and the -server yourself, as before. With `start`, the boolean selects the start +server yourself, as before. With `ssr: false`, client code is +non-hydratable DOM output. Modules that Vite loads on the server, for +example through `ssrLoadModule` to prerender static pages, get +non-hydratable SSR output, so they render to HTML without hydration keys. +With `start`, the boolean selects the start mode: `ssr: true` is SSR start mode, `ssr: false`/omitted is client start mode — see below. diff --git a/src/index.ts b/src/index.ts index 7b9ab11..6237ea5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -341,7 +341,10 @@ export interface Options { * * Without {@link start}: the legacy transform-only flag, unchanged. * `true` enables the SSR transforms (hydratable client code, SSR server - * code) — you provide the entries and the server yourself. + * code) — you provide the entries and the server yourself. With `false`, + * client code is non-hydratable DOM output, and modules loaded on the + * server (for example through `ssrLoadModule` to prerender a page) get + * non-hydratable SSR output. * * With {@link start}: selects the start mode. `true` is SSR start mode * (per-request streaming render + hydration); `false`/omitted is client @@ -672,7 +675,11 @@ function getSolidOptions( solidOptions = { generate: 'dom', hydratable: true }; } } else { - solidOptions = { generate: 'dom', hydratable: false }; + // A client-rendered app can still load modules on the server, for + // example to prerender static pages with `ssrLoadModule`. DOM output + // cannot run there, so server transforms use the SSR generate. Nothing + // hydrates the result, so it stays non-hydratable. + solidOptions = { generate: isSsr ? 'ssr' : 'dom', hydratable: false }; } // Server components (serverFunctions.components) turn on the SSR-side diff --git a/src/ssr-codegen.test.ts b/src/ssr-codegen.test.ts new file mode 100644 index 0000000..bc11532 --- /dev/null +++ b/src/ssr-codegen.test.ts @@ -0,0 +1,70 @@ +import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { createServer, type ViteDevServer } from 'vite'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; +import solidPlugin from './index.js'; + +// https://github.com/solidjs/solid-vite-plugin/issues/64 +describe('JSX codegen per transform', () => { + let root: string; + + async function serve(options: Parameters[0]): Promise { + return createServer({ + root, + configFile: false, + appType: 'custom', + logLevel: 'silent', + server: { middlewareMode: true, hmr: false, watch: null }, + optimizeDeps: { noDiscovery: true, include: [] }, + plugins: [solidPlugin(options)], + }); + } + + async function transform(options: Parameters[0]) { + const server = await serve(options); + try { + const client = await server.environments.client.transformRequest('/src/Page.tsx'); + const ssr = await server.environments.ssr.transformRequest('/src/Page.tsx'); + const { renderToString } = await server.ssrLoadModule('@solidjs/web'); + const { default: Page } = await server.ssrLoadModule('/src/Page.tsx'); + const html = renderToString(() => Page({ name: 'x' })) as string; + return { client: client!.code, ssr: ssr!.code, html }; + } finally { + await server.close(); + } + } + + beforeAll(async () => { + root = await mkdtemp(path.join(tmpdir(), 'solid-vite-plugin-ssr-codegen-')); + await symlink( + fileURLToPath(new URL('../node_modules', import.meta.url)), + path.join(root, 'node_modules'), + 'dir', + ); + await mkdir(path.join(root, 'src')); + await writeFile( + path.join(root, 'src/Page.tsx'), + 'export default (props: { name: string }) =>
Hello {props.name}
;\n', + ); + }); + + afterAll(async () => { + await rm(root, { recursive: true, force: true }); + }); + + it('compiles server transforms with the SSR generate when `ssr` is off', async () => { + const { client, ssr, html } = await transform({ hot: false }); + expect(client).toContain('template'); + expect(ssr).not.toContain('template'); + expect(html).toBe('
Hello x
'); + }); + + it('keeps hydration keys in server output when `ssr` is on', async () => { + const { client, ssr, html } = await transform({ hot: false, ssr: true }); + expect(client).toContain('template'); + expect(ssr).not.toContain('template'); + expect(html).toMatch(/_hk=/); + }); +});