Skip to content
🔥
@tuvix.js/svelte
Svelteバインディング
Svelte 3、4、5のコンポーネントをTuvix.jsのマイクロアプリに変換します。コンテナごとに1つのインスタンス、propsのその場での更新、サーバーのマークアップのハイドレーション、CSS付きのサーバーレンダリングを提供します。

インストール ​

bash
npm install @tuvix.js/svelte svelte
# or pnpm add @tuvix.js/svelte svelte

ピア範囲はSvelte 3、4、5をカバーします。このパッケージには2つのエントリーポイントがあります。ブラウザ用の@tuvix.js/svelteと、サーバーレンダリング用の@tuvix.js/svelte/serverです。

使用する場面 ​

SvelteコンポーネントをTuvix.jsのマイクロアプリとして動かす場合にこのパッケージを使います。オーケストレーターはルートに応じてそれをコンテナにマウントし、propsを渡し、更新し、ルートが変わるとアンマウントします。アイランドにも対応します。サーバーでコンポーネントをレンダリングしてHTMLをページに入れ、クライアントがそれをハイドレートします。

Svelteがページ全体を担う場合(SvelteKit)には不要です。その場合はSvelte自身がマウントを行い、やり取りするオーケストレーターは存在しません。

クイックスタート ​

propsを持つSvelte 5コンポーネント:

svelte
<!-- Profile.svelte -->
<script lang="ts">
  let { name, theme = 'light' }: { name: string; theme?: string } = $props();
</script>

<p class={theme}>Hello {name}</p>

マイクロアプリのエントリーでこれをラップします。SvelteMapを渡すと、propsの更新時にコンポーネントがその場で再レンダリングされます。

ts
// profile-app.ts
import { SvelteMap } from 'svelte/reactivity';
import { createSvelteMicroApp } from '@tuvix.js/svelte';
import Profile from './Profile.svelte';

export default createSvelteMicroApp({ name: 'profile', App: Profile, SvelteMap });

シェルはアプリを登録し、オーケストレーターを起動します。

ts
// shell.ts
import { createOrchestrator } from '@tuvix.js/core';

const orchestrator = createOrchestrator();

orchestrator.register({
  name: 'profile',
  entry: () => import('./profile-app'),
  container: '#profile',
  activeWhen: '/profile',
  props: { name: 'Ada' },
});

await orchestrator.start();

// Later: the component re-renders in place, no remount.
await orchestrator.updateAppProps('profile', { name: 'Grace', theme: 'dark' });

クライアントバンドルはsvelteをブラウザ用ビルドに解決する必要があります。@sveltejs/vite-plugin-svelteがこれを行います。セットアップがbrowserエクスポート条件を落とすと、Svelte 5のmount()はlifecycle_function_unavailableを投げ、onMountは実行されません。

API ​

createSvelteMicroApp ​

ts
function createSvelteMicroApp(config: SvelteMicroAppConfig): MicroAppModule;
オプション型デフォルト説明
namestring必須アプリ名です。アダプターは@tuvix.js/loaderのregisterModuleでこの名前にモジュールを登録します。
Appunknown必須ルートコンポーネントです。コンパイル済みのSvelte 5コンポーネント(関数)またはSvelte 3/4のコンポーネントクラスです。
bootstrap() => void | Promise<void>なし一度だけ実行するセットアップです。モジュールのbootstrap()はこれをawaitします。
ssrbooleanfalseコンテナが要素または空白以外のテキストを含む場合、サーバーのマークアップをハイドレートします。それ以外は新規にレンダリングします。
SvelteMapsvelte/reactivityのSvelteMapクラスなしSvelte 5専用です。指定するとupdate()はその場で再レンダリングします。指定しないと、Svelte 5のupdate()はコンポーネントを再マウントします。Svelte 3/4では無視されます。

最初のマウント時に、アダプターはsvelteをインポートし、mountエクスポートの有無を確認します。結果はページ単位でキャッシュされます。

  • Svelte 5(mountがある):mount()でレンダリングし、hydrate()でハイドレートし、unmount()で破棄します。update()の動作はSvelteMapによって変わります。
    • SvelteMapがある場合、コンポーネントはSvelteMapに裏付けられたpropsオブジェクトを受け取ります。update()はそのマップに新しい値を書き込むため、変更されたpropを読むテンプレート部分だけが再レンダリングされ、コンポーネントはDOMノードと状態を保持します。これは$props()コンポーネントと、Svelte 5でコンパイルされたレガシーなexport letコンポーネントの両方で動作します。
    • ない場合、update()はインスタンスをアンマウントし、コンテナを空にして、新しいpropsで新しいインスタンスをマウントし、hydrated: falseを付けて再びtuvix:mountedをディスパッチします。DOMノード、フォーカス、コンポーネントの状態は失われます。
  • Svelte 3/4(mountがない):new App({ target, props, hydrate })、$set()、$destroy()を使います。

返されるモジュールはコンテナ単位で動作します。コンテナごとに独自のインスタンスを持つため、1つのモジュールを3つのコンテナに同時にレンダリングできます。

  • mount({ container, props })はコンテナを空にし(ハイドレートする場合を除く)、レンダリングし、onMountコールバックとエフェクトを実行してから、コンテナ上でhydrated: true | falseを付けてtuvix:mountedをディスパッチします。失敗するとtuvix:errorをディスパッチし、元のエラーでrejectします。
  • update({ props, container? })はcontainer内のインスタンスを更新します。containerを省略するとすべてのインスタンスを更新します。各更新はprops全体を置き換えます。新しいpropsにないキーは、Svelte 5ではコンポーネントのデフォルト値に戻り、Svelte 3/4ではundefinedになります。
  • unmount({ container })はインスタンスを破棄し、コンテナを空にしてtuvix:unmountedをディスパッチします。
ts
import { SvelteMap } from 'svelte/reactivity';
import { createSvelteMicroApp } from '@tuvix.js/svelte';
import { onLifecycle } from '@tuvix.js/loader';
import Cart from './Cart.svelte';

const cart = createSvelteMicroApp({ name: 'cart', App: Cart, SvelteMap });

onLifecycle('mounted', ({ name, hydrated }) => {
  console.log(`${name} mounted`, { hydrated });
});

const header = document.querySelector<HTMLElement>('#cart-header')!;
const sidebar = document.querySelector<HTMLElement>('#cart-sidebar')!;

await cart.mount({ container: header, props: { compact: true } });
await cart.mount({ container: sidebar, props: { compact: false } });

// Updates the sidebar instance only.
await cart.update?.({ props: { compact: true }, container: sidebar });

createSsrSvelteMicroApp ​

ts
function createSsrSvelteMicroApp(config: Omit<SvelteMicroAppConfig, 'ssr'>): MicroAppModule;

createSvelteMicroApp({ ...config, ssr: true })と同じです。サーバーがrenderSvelteToStringまたはrenderSvelteでレンダリングしたアイランドに対して、クライアントで使います。

  • Svelte 5:hydrate(App, { target, props })。ハイドレーションはsvelte/serverが書き込むコメントマーカー(<!--[-->)を読むため、マークアップはsvelte/serverから得たものである必要があります。
  • Svelte 3/4:new App({ target, props, hydrate: true })。コンポーネントはhydratable: trueでコンパイルします。

ハイドレーションはサーバーのDOMノードを保持し、それにイベントハンドラーを付けます。空白またはコメントのみのコンテナには新規にレンダリングします。

ts
import { createSsrSvelteMicroApp } from '@tuvix.js/svelte';
import Footer from './Footer.svelte';

export default createSsrSvelteMicroApp({ name: 'footer', App: Footer });

renderSvelteToString ​

ts
// import from '@tuvix.js/svelte/server'
function renderSvelteToString(
  App: unknown,
  props?: Record<string, unknown>,
  options?: RenderSvelteOptions
): Promise<string>;
オプション型デフォルト説明
renderSvelteServerRenderなしsvelte/serverのrenderです。省略すると、アダプターは実行時にsvelte/serverをインポートします。

サーバーでコンポーネントをレンダリングし、その<style>タグとそれに続くマークアップを返します。この文字列をアイランドのコンテナ内に入れ、createSsrSvelteMicroAppでマウントします。文字列には<svelte:head>の内容は含まれません。それが必要な場合はrenderSvelteを使います。

この関数は実行時にSvelteのバージョンを判定します。renderを渡した場合は常にSvelte 5の経路を取り、render(App, { props })を呼び出します。渡さない場合、アダプターはsvelteをインポートし、そのモジュールがmountをエクスポートしていればsvelte/serverを読み込んでSvelte 5でレンダリングします。そうでなければApp.render(props)(generate: 'ssr'でコンパイルされたSvelte 3/4)を呼び出し、css.codeを<style>タグで囲みます。dev: trueでコンパイルされたSvelte 5コンポーネント(serveモードのvite-plugin-svelte)は例外を投げるApp.renderスタブを持つため、アダプターはApp.renderの有無で判定することはありません。

ブラウザでも実行されるコードからではなく、サーバー関数から呼び出します。ブラウザでは''を返します。

ts
import { render } from 'svelte/server';
import { renderSvelteToString } from '@tuvix.js/svelte/server';
import Footer from './Footer.svelte';

export async function renderFooterIsland(): Promise<string> {
  const html = await renderSvelteToString(Footer, { year: 2026 }, { render });
  return `<footer id="footer" data-tuvix-app="footer">${html}</footer>`;
}

renderSvelte ​

ts
// import from '@tuvix.js/svelte/server'
function renderSvelte(
  App: unknown,
  props?: Record<string, unknown>,
  options?: RenderSvelteOptions
): Promise<SvelteRenderResult>;

renderSvelteToStringと同じ引数とオプションを取り、各部分を個別に返します。

フィールド説明
htmlコンポーネントのマークアップです。Svelte 5の出力にはハイドレーション用のコメントが含まれます。
headコンポーネントの<style>タグを除いた<svelte:head>の内容です。ドキュメントの<head>に入れます。
css<style>タグとしてのコンポーネントCSSです。コンパイラーが何も出力しなかった場合は''です。

ブラウザでは3つのフィールドすべてが''になります。

ts
import { render } from 'svelte/server';
import { renderSvelte } from '@tuvix.js/svelte/server';
import Article from './Article.svelte';

export async function renderArticlePage(slug: string): Promise<string> {
  const { html, head, css } = await renderSvelte(Article, { slug }, { render });
  return `<!doctype html>
<html>
  <head>${head}${css}</head>
  <body>
    <main id="article" data-tuvix-app="article">${html}</main>
    <script type="module" src="/client.js"></script>
  </body>
</html>`;
}

型 ​

型エントリー説明
SvelteMicroAppConfig@tuvix.js/sveltecreateSvelteMicroAppのオプションです。
SvelteProps@tuvix.js/svelteRecord<string, unknown>:オーケストレーターがコンポーネントに渡すpropsです。
RenderSvelteOptions@tuvix.js/svelte/server{ render?: SvelteServerRender }。
SvelteServerRender@tuvix.js/svelte/serversvelte/serverのSvelte 5 renderの型です。
SvelteRenderResult@tuvix.js/svelte/serverrenderSvelteが返す{ html, head, css }です。

サーバーレンダリング ​

アイランドは3つのステップを経ます。サーバーでレンダリングし、HTMLをコンテナ内に入れて送り、クライアントでハイドレートします。

  1. サーバー関数でrenderSvelteToString(またはrenderSvelte)を呼び出し、結果をコンテナ要素に入れます。
  2. createSsrSvelteMicroApp({ name, App })をエクスポートするクライアントエントリーを読み込みます。
  3. オーケストレーターが同じコンテナにアプリをマウントします。アダプターはマークアップを検出してハイドレートします。

CSS。 Svelte 5がコンポーネントCSSをレンダリング出力に含めるのは、css: 'injected'でコンパイルした場合だけです。デフォルトのcss: 'external'では、バンドラーがCSSをファイルに書き出し、文字列には<style>タグが含まれません。Viteでは次のようにします。

ts
// vite.config.ts
import { defineConfig } from 'vite';
import { svelte } from '@sveltejs/vite-plugin-svelte';

export default defineConfig({
  plugins: [svelte({ compilerOptions: { css: 'injected' } })],
});

css: 'external'のままにする場合は、生成されたスタイルシートをページのheadからリンクします。@tuvix.js/viteは、アイランドのエントリーが必要とするスタイルシートをサーバーに伝えます。

Svelteのコピーは1つにします。 サーバーバンドルがSvelteを含む場合(Nitro、Vinxi、noExternalを使ったVite SSRビルド)は、svelte/serverのrenderを渡します。これにより、コンポーネントとレンダラーが1つのSvelteのコピーを共有します。このオプションがないと、アダプターは実行時にsvelte/serverをインポートし、node_modulesから2つ目のコピーを読み込む可能性があります。Svelteのサーバーレンダラーはinstanceofチェックを使うため、コンポーネントとレンダラーが別のコピーから来ていると失敗します。

エッジケース ​

  • 同じモジュールのインスタンスをすでに保持しているコンテナへのmountは、まず古いインスタンスをアンマウントします。
  • mountがまだsvelteを読み込んでいる間に呼ばれたunmountは、マウントの完了を待ってからインスタンスを破棄します。孤立したコンポーネントは残りません。
  • このモジュールがマウントしていない、またはマウントに失敗したコンテナに対するunmountは何もしません。コンテナの子要素は保持され、イベントも発生しません。
  • マウントの保留中に呼ばれたupdateは、その完了を待ってからpropsを適用します。失敗したマウントに対する更新は何もしません。
  • Svelte 3/4では、新しいpropsにないpropはundefinedになります。export let name = 'x'のデフォルト値には戻りません。Svelte 5ではSvelteMapの有無にかかわらずデフォルト値が適用されます。
  • アダプター自身はsvelte/reactivityをインポートしません。Svelte 3/4にはこのモジュールがなく、それを参照するSvelte 3/4アプリはバンドラーがビルドに失敗するため、インポートはSvelte 5のエントリー側に置きます。
  • Svelte 5の再マウント(SvelteMapなし)は再びtuvix:mountedをディスパッチしますが、その間にtuvix:unmountedはディスパッチしません。新しいレンダリングが例外を投げた場合、update()はそのエラーでrejectし、古いインスタンスはすでに破棄されています。
  • ssrなしの場合、mountはレンダリング前にコンテナを空にするため、読み込み中のプレースホルダーは消えます。
  • ssrありの場合、アダプターはコンテナに要素または空白以外のテキストがあるときにハイドレートします。コメントと空白だけの場合は新規にレンダリングします。
  • アダプターはSvelteのバージョンをページごとに一度だけ判定します。import('svelte')が失敗した場合は、次のマウントで再試行します。
  • サーバーでのSvelte 5のインポートエラーやレンダリングエラーは、rejectされたPromiseとして返ります。アダプターはApp.renderにフォールバックしません。
  • Svelte 5のrenderはPromiseを返す場合があります(非同期サーバーレンダリング)。両方のサーバー関数がこれをawaitします。

対象外の機能 ​

  • .svelteファイルはコンパイルしません。それはバンドラーが@sveltejs/vite-plugin-svelteまたはsvelte/compilerで行います。
  • バンドラーが選んだSvelteのビルドは確認しません。svelteをサーバー用ビルドに解決するクライアントバンドルは、マウント時に失敗します。
  • 渡すのはpropsだけです。スロット、スニペット、コンポーネントイベントにはホストからの経路がありません。アプリ間の通信には@tuvix.js/event-busを使います。
  • <svelte:head>の内容をドキュメントに書き込みません。renderSvelteがそれを返すので、サーバーが<head>に入れます。
  • css: 'external'でコンパイルされたCSSはリンクしません。@tuvix.js/viteを参照してください。
  • サーバーでデータを待ちません。先にデータを読み込み、propsとして渡します。

0.4からの移行 ​

  • Svelte 5が動作します。 0.4のアダプターはすべてのコンポーネントでnew App()を呼び出していたため、Svelte 5コンポーネントは失敗し、createSsrSvelteMicroAppはハイドレートせずに2つ目のコピーをレンダリングしていました。マウントにコードの変更は不要です。propsをその場で更新するには、設定にsvelte/reactivityのSvelteMapを渡します。渡さない場合、Svelte 5の各更新はコンポーネントを再マウントします。
  • マウント時にコンテナが空になります。 ssrなしの場合、Svelte 3/4は以前、コンテナにすでにあるマークアップの後ろにコンポーネントを追加していました。現在はアダプターが先にコンテナを空にします。コンポーネントの隣にマークアップが残ることに依存していた場合は、それをコンテナの外に移します。
  • サーバーでの暗黙のフォールバックはありません。 renderSvelteToStringは以前、svelte/serverの読み込みに失敗するとApp.renderにフォールバックしていました。現在はrenderを渡した場合またはsvelteがmountをエクスポートしている場合にSvelte 5の経路を取り、App.renderはSvelte 3/4でのみ呼び出し、Svelte 5のインポートエラーやレンダリングエラーではrejectします。ViteのdevモードSSRにおけるSvelte 5の開発ビルドは、App.renderスタブを持っていてもSvelte 5の経路を取ります。
  • 未知のコンテナに対するunmountは何もしません。 モジュールがマウントしていないコンテナに対するunmount({ container })は、以前はそのコンテナを空にしていました。現在はReact、Vue、Angularのアダプターと同様に、コンテナに手を加えません。
  • renderを渡します。 サーバーバンドルがSvelteを含む場合は、第3引数としてsvelte/serverの{ render }を追加します。
ts
import { render } from 'svelte/server';
import { renderSvelteToString } from '@tuvix.js/svelte/server';
import Footer from './Footer.svelte';

// 0.4: await renderSvelteToString(Footer, { year: 2026 });
const html = await renderSvelteToString(Footer, { year: 2026 }, { render });

完全なアプリについてはSvelteガイドを参照してください。