Skip to content
Open
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 @@
---
'vite-plugin-solid': 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). Vitest keeps DOM output.
3 changes: 3 additions & 0 deletions .github/workflows/vitest.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
- name: Install project dependencies
run: pnpm i

- name: Run unit tests
run: pnpm run test:unit

- name: Install playwright browser
working-directory: examples/vite-6
run: pnpm exec playwright install chromium
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,10 @@ If set to false, it won't inject the runtime in dev.

This will force SSR code in the produced files.

With `ssr: true`, client code is hydratable and server code renders with hydration keys.

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. They render to HTML without hydration keys. Vitest is the exception and keeps DOM output.

#### options.babel

- Type: Babel.TransformOptions
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
"prepublishOnly": "pnpm build",
"release": "pnpm build && changeset publish",
"check": "package-check",
"test": "node scripts/test-examples.ts"
"test": "node scripts/test-examples.ts",
"test:unit": "vitest run"
},
"repository": {
"type": "git",
Expand Down Expand Up @@ -68,7 +69,8 @@
"rollup-plugin-cleaner": "^1.0.0",
"solid-js": "^1.9.3",
"typescript": "^5.2.2",
"vite": "^6.0.0"
"vite": "^6.0.0",
"vitest": "^3.0.7"
},
"peerDependencies": {
"@testing-library/jest-dom": "^5.16.6 || ^5.17.0 || ^6.0.0 || ^7.0.0",
Expand Down
37 changes: 36 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ export interface Options {
/**
* This will force SSR code in the produced files.
*
* With `true`, client code is hydratable and server code is hydratable SSR
* output. 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.
*
* @default false
*/
ssr?: boolean;
Expand Down Expand Up @@ -386,7 +391,15 @@ export default function solidPlugin(options: Partial<Options> = {}): Plugin {
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. Vitest keeps DOM
// output, since tests resolve the browser build of solid-js.
solidOptions = {
generate: isSsr && !isTestMode ? 'ssr' : 'dom',
hydratable: false,
};
}

// We need to know if the current file extension has a typescript options tied to it
Expand Down
85 changes: 85 additions & 0 deletions src/ssr-codegen.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { mkdtemp, mkdir, realpath, 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 InlineConfig } from 'vite';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import solidPlugin, { type Options } from './index.js';

// https://github.com/solidjs/solid-vite-plugin/issues/64
describe('JSX codegen per transform', () => {
let root: string;

async function transform(options: Partial<Options>, config: InlineConfig = {}) {
const server = await createServer({
root,
configFile: false,
appType: 'custom',
logLevel: 'silent',
server: { middlewareMode: true, hmr: false, watch: null },
optimizeDeps: { noDiscovery: true, include: [] },
plugins: [solidPlugin(options)],
...config,
});
try {
const client = await server.environments.client.transformRequest('/src/Page.tsx');
const ssr = await server.environments.ssr.transformRequest('/src/Page.tsx');
return { server, client: client!.code, ssr: ssr!.code };
} catch (error) {
await server.close();
throw error;
}
}

async function render(server: Awaited<ReturnType<typeof transform>>['server']) {
try {
const { renderToString } = await server.ssrLoadModule('solid-js/web');
const { default: Page } = await server.ssrLoadModule('/src/Page.tsx');
return renderToString(() => Page({ name: 'x' })) as string;
} finally {
await server.close();
}
}

beforeAll(async () => {
root = await realpath(await mkdtemp(path.join(tmpdir(), 'vite-plugin-solid-ssr-codegen-')));
await symlink(
fileURLToPath(new URL('../node_modules', import.meta.url)),
path.join(root, 'node_modules'),
'dir',
);
await writeFile(
path.join(root, 'package.json'),
JSON.stringify({ name: 'ssr-codegen-fixture', private: true, type: 'module' }),
);
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 { server, client, ssr } = await transform({ hot: false });
expect(client).toContain('template');
expect(ssr).not.toContain('template');
expect(await render(server)).toBe('<div class="page">Hello x</div>');
});

it('keeps hydration keys in server output when `ssr` is on', async () => {
const { server, client, ssr } = await transform({ hot: false, ssr: true });
expect(client).toContain('template');
expect(ssr).not.toContain('template');
expect(await render(server)).toMatch(/data-hk=/);
});

it('keeps DOM output for server transforms in test mode', async () => {
const { server, ssr } = await transform({ hot: false }, { mode: 'test' });
await server.close();
expect(ssr).toContain('template');
});
});
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"src", "cypress"
],
"exclude": [
"**/*.spec.ts"
"**/*.spec.ts", "**/*.test.ts"
],
"compilerOptions": {
"target": "ESNext",
Expand Down
10 changes: 10 additions & 0 deletions vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineConfig } from 'vitest/config';

// The plugin's own unit tests (`pnpm test:unit`). The example apps under
// examples/* carry their own vitest configs and run through `pnpm test`.
export default defineConfig({
test: {
include: ['src/**/*.test.ts'],
environment: 'node',
},
});
Loading