Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/ssr-codegen-without-ssr-option.md
Original file line number Diff line number Diff line change
@@ -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).
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
11 changes: 9 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
70 changes: 70 additions & 0 deletions src/ssr-codegen.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof solidPlugin>[0]): Promise<ViteDevServer> {
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<typeof solidPlugin>[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 }) => <div class="page">Hello {props.name}</div>;\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('<div class="page">Hello x</div>');
});

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=/);
});
});
Loading