Created
January 14, 2026 18:04
-
-
Save matths/4e8fbf97c450920837a141b53b56b014 to your computer and use it in GitHub Desktop.
Hydrate a Svelte SSR result in client
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { rollup } from 'rollup'; | |
| import svelte from 'rollup-plugin-svelte'; | |
| import resolve from '@rollup/plugin-node-resolve'; | |
| export const rollupBundle = async (target: 'server' | 'client', source: string, clientEntry?: string) => { | |
| return await rollup({ | |
| input: clientEntry ? 'client-entry.js' : 'entry.svelte', | |
| onwarn(warning, warn) { | |
| if ( | |
| warning.code === 'CIRCULAR_DEPENDENCY' && | |
| warning.message.includes('svelte') | |
| ) { | |
| return; | |
| } | |
| warn(warning); | |
| }, | |
| plugins: [ | |
| { | |
| name: 'resolve-files', | |
| resolveId(id) { | |
| if (id === 'client-entry.js') return id; | |
| if (id === 'entry.svelte') return id; | |
| }, | |
| load(id) { | |
| if (id === 'client-entry.js' && clientEntry) return clientEntry; | |
| if (id === 'entry.svelte' && source) return source; | |
| } | |
| }, | |
| svelte({ | |
| compilerOptions: { | |
| generate: (() => { | |
| console.log('Generating for', target); | |
| return target; | |
| })(), | |
| runes: true, | |
| } | |
| }), | |
| resolve() | |
| ] | |
| }); | |
| } | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import { render } from 'svelte/server'; | |
| import { rollupBundle } from './rollup-bundle.js'; | |
| import fs from 'fs/promises'; | |
| import path from 'path'; | |
| const source = ` | |
| <script> | |
| let { name = '' } = $props(); | |
| </script> | |
| <h1>Hello {name}!</h1> | |
| `; | |
| const clientEntry = `import { hydrate } from 'svelte'; | |
| import App from 'entry.svelte'; | |
| export { hydrate, App }; | |
| `; | |
| const buildClient = async () => { | |
| const bundle = await rollupBundle('client', source, clientEntry); | |
| const { output } = await bundle.generate({ | |
| format: 'esm' | |
| }); | |
| const clientCode = output[0].code; | |
| return clientCode; | |
| }; | |
| const buildSSR = async ( | |
| { name }: { name: string } | |
| ): Promise<string> => { | |
| const clientCodeForHydration = await buildClient(); | |
| const encodedClientCode = encodeURIComponent(clientCodeForHydration).replace(/'/g, "\\'"); | |
| const ssrBundle = await rollupBundle('server', source); | |
| const { output } = await ssrBundle.generate({ format: 'esm' }); | |
| const code = output[0].code; | |
| const cwd = process.cwd(); | |
| const tmpDir = path.join(cwd, 'tmp'); | |
| const tmpPath = path.join(tmpDir, `svelte-ssr-${Date.now()}-${Math.random().toString(36).slice(2)}.mjs`); | |
| await fs.writeFile(tmpPath, code); | |
| const SSRModule = await import(`file://${tmpPath}`); | |
| const SSRComponent = SSRModule.default; | |
| const { head, body } = render(SSRComponent, { props: { name } }); | |
| await fs.unlink(tmpPath); | |
| return `<!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8" /> | |
| <title>Svelte SSR</title> | |
| ${head} | |
| </head> | |
| <body> | |
| <div id="app">${body}</div> | |
| <script type="module"> | |
| import { App, hydrate } from 'data:text/javascript;charset=utf-8,${encodedClientCode}'; | |
| const app = hydrate(App, { | |
| target: document.getElementById('app'), | |
| props: ${JSON.stringify({ name })} | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| `; | |
| }; | |
| export default buildSSR; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment