@svebcomponents/ssr-vue
Server-side rendering support for Svelte-built custom elements inside Vue apps.
This is the Vue counterpart to @svebcomponents/ssr’s Svelte integration. The
element renderers, the DOM shim, the renderer registry and the client-side
hydration machinery are all shared — this package only supplies the two pieces
that are host-framework specific: finding custom element tags in Vue templates,
and emitting declarative shadow DOM around them.
This package is experimental. Its API may change before it is released alongside the rest of the toolchain.
What It Provides
@svebcomponents/ssr-vue/vite: a Vite plugin that rewrites custom element tags in SFC templates to the wrapper component.@svebcomponents/ssr-vue: the wrapper component and a Vue plugin that registers it.
App-author Flow
Install the Vite plugin ahead of @vitejs/plugin-vue:
import vue from "@vitejs/plugin-vue";import svebcomponentsVue from "@svebcomponents/ssr-vue/vite";import { defineConfig } from "vite";
export default defineConfig({ plugins: [svebcomponentsVue(), vue()],});Register the wrapper on both the server and client app instances:
import { svebcomponents } from "@svebcomponents/ssr-vue";
app.use(svebcomponents());Then load the component package’s browser entry (which defines the custom
element) and its /ssr entry (which registers the renderer), and render the
custom element in any SFC:
<template> <my-component title="Hello" :count="5" /></template>Any Custom Element, Not Just Svelte-built Ones
This integration depends only on Lit’s ElementRenderer contract, so it will
server-render any custom element that has a renderer registered — including
Lit elements:
import { LitElementRenderer } from "@lit-labs/ssr/lib/lit-element-renderer.js";import { ElementRendererRegistry } from "@svebcomponents/ssr";
ElementRendererRegistry.use(LitElementRenderer);use() registers a renderer that selects its own elements through Lit’s
static matchesClass hook, so that one line covers every LitElement in the
app. e2e/ssr-vue renders a plain Lit element through this package to keep
that honest.
Async Components
Unlike the Svelte integration, there is no sync/async wrapper split. Vue’s
renderToString fully awaits an async setup() and emits its output in
order, so a component that awaits while rendering — or whose index.ssr.ts
exports an async SsrPrepare hook — renders through the same wrapper a
synchronous one does.
There is one catch, and it applies to any non-Svelte host. Svelte gates async
SSR behind a module-global flag that is normally flipped by a Svelte app
compiled with compilerOptions.experimental.async. A Vue app has no such
app, so it must opt in explicitly on the server:
import "@svebcomponents/ssr/enable-async";Without it, render() runs synchronously even when awaited and an async
component throws await_invalid.
Requirements
@svebcomponents/ssr calls Svelte’s server renderer, so svelte must be
installed as a server-side dependency of the Vue app even though no Svelte
components appear in it. The component package’s own server bundle ships its
Svelte runtime, but the renderer entry point does not.
How It Works
On the server the wrapper resolves the custom element’s registered
ElementRenderer, applies the incoming attributes and properties, and renders:
<my-component title="Hello"> <template shadowrootmode="open"><!-- shadow content --></template> <!-- light dom children --></my-component>The <template shadowrootmode> must be the element’s first child, because the
HTML parser adopts it into a shadow root and removes it from the light DOM
before any script runs. In the browser the wrapper therefore renders the same
element without it — what Vue hydrates against is the post-parse child list.
Both branches build the child list identically (slot content is spread, never
nested), because a nested array becomes a Vue Fragment and Vue SSR brackets a
Fragment in <!--[--> / <!--]--> anchors. Emitting those on one side only is
a hydration mismatch.
Why a Vite Plugin Instead of a Compiler Transform
Vue exposes compilerOptions.nodeTransforms, which looks like the natural
place to retag custom elements. It does not work for SSR: user node transforms
run after the built-in ones on enter, and @vue/compiler-ssr resolves a
component’s identity during ssrTransformComponent’s enter phase. A transform
that renames the tag is honored by the client compile and silently ignored by
the SSR compile, producing an app that renders correctly in the browser and
not at all on the server.
Rewriting the SFC source before @vitejs/plugin-vue sees it means both
compiles observe the same template.
Current Limitations
- Experimental; the API may change.
- Custom element tags are detected structurally (a dash, minus the HTML spec’s
reserved SVG/MathML names). Pass
tagsto the Vite plugin to narrow it. - Only SFC
<template>blocks are rewritten. Custom elements written directly in a render function or JSX must useCustomElementWrapperby hand. - The consuming app must import the browser custom element module and register the matching SSR renderer, exactly as with the Svelte integration.