Skip to content
⚡
@tuvix.js/vite
Viteアイランドアセット
アイランドが必要とするスタイルシートとJSチャンクをサーバーレンダラーに伝えるViteプラグインです。サーバーレンダリングされたheadがそれらをリンクするため、アイランドは最初の描画時からスタイル適用済みになります。

インストール ​

bash
npm install -D @tuvix.js/vite
# or pnpm add -D @tuvix.js/vite

このプラグインにはVite 6または7が必要です(peer range ^6.0.0 || ^7.0.0)。テストスイートはVite 6で実行されます。

使いどころ ​

アイランド(React、Vue、Svelte、Angular、または素のDOM)をサーバーレンダリングすると、そのマークアップはJavaScriptより先にブラウザに届きます。ViteはCSS Modules、Vue SFCの<style>ブロック、SvelteのスタイルをそのJavaScript経由で配信するため、ブラウザはバンドルが実行されるまでアイランドをスタイルなしで描画します。@tuvix.js/viteを使うと、サーバーは「src/islands/footer/index.tsが必要とするスタイルシートはどれか」を問い合わせ、それらの<link>タグを<head>に出力します。

アイランドをクライアントでのみレンダリングする場合や、すべてのアイランドをすでにカバーするグローバルスタイルシートを1つリンクしている場合は、使う必要はありません。

クイックスタート ​

クライアントビルドとSSRビルドに使うViteの設定にプラグインを追加します。

ts
// vite.config.ts
import { defineConfig } from 'vite';
import { tuvix } from '@tuvix.js/vite';

export default defineConfig({
  plugins: [tuvix()],
});

仮想モジュールの型をtsconfig.jsonに追加します。

json
{
  "compilerOptions": {
    "types": ["vite/client", "@tuvix.js/vite/client"]
  }
}

サーバーエントリーでアセットを要求し、<head>に出力します。

ts
// src/entry-server.ts
/// <reference types="@tuvix.js/vite/client" />
import { getAssets, renderAssetTags } from 'virtual:tuvix/assets';
import { renderFooter } from './islands/footer/render';

export async function render(): Promise<string> {
  const assets = await getAssets(['src/islands/footer/index.ts']);
  return `<!doctype html>
<html>
  <head>${renderAssetTags(assets)}</head>
  <body><footer data-tuvix-app="footer">${await renderFooter()}</footer></body>
</html>`;
}

先にクライアントをビルドし、次にサーバーをビルドします。SSRビルドはクライアントのマニフェストを読み取ります。

bash
vite build --outDir dist/client
vite build --ssr src/entry-server.ts --outDir dist/server

API ​

tuvix(options?) ​

ts
function tuvix(options?: TuvixViteOptions): Plugin[];

3つのViteプラグインを返します。

  • tuvix:assetsはvirtual:tuvix/assetsを提供します。
  • tuvix:client-build-info(ビルド時のみ)は、クライアントのマニフェストの隣に.vite/tuvix-client.jsonを書き込みます。本番ビルドを参照してください。
  • tuvix:dev-css-handoff(devサーバーのみ)は、スタイルの適用をサーバーレンダリングされた<link>からVite自身の<style>タグへ引き継ぎます。

返された配列は、上のようにpluginsに入れてください。Viteはネストしたプラグイン配列をフラットにします。

オプション型デフォルト意味
clientOutDirstring'dist/client'クライアントビルドの出力ディレクトリ。Viteのルートからの相対パスまたは絶対パスです。SSRビルドは<clientOutDir>/.vite/manifest.jsonを読み取り、なければ<clientOutDir>/manifest.jsonにフォールバックします。
basestring仮想モジュールをロードするビルドまたはdevサーバーのVite baseブラウザがクライアントのアセットをロードする公開base。vinxiの/_buildのように、クライアントがサーバーバンドルとは異なるbaseで配信される場合に設定します。

TanStack Startとvinxiは、クライアントルーターのビルドを/_build配下の別フォルダーに書き込みます。

ts
import { tuvix } from '@tuvix.js/vite';

export const islandAssets = tuvix({ clientOutDir: '.vinxi/build/client/_build', base: '/_build' });

getAssets(entries) ​

virtual:tuvix/assetsからエクスポートされます。

ts
function getAssets(entries: string[]): Promise<TuvixAssets>;

アイランドのエントリーモジュールを、'src/islands/footer/index.ts'のようにViteのルートからの相対パスで渡します。先頭の./や/は無視されます。複数のエントリーを渡すと、エントリーの順にリストをマージし、重複を取り除きます。

devサーバーは各モジュールのimportを読み取る前にそのモジュールを変換する必要があるため、getAssetsはPromiseを返します。サーバーコードからのみ呼び出してください。

renderAssetTags(assets) ​

virtual:tuvix/assetsからエクスポートされます。

ts
function renderAssetTags(assets: TuvixAssets): string;

assets.cssのURLごとに<link rel="stylesheet">タグを1つ、続いてassets.modulepreloadのURLごとに<link rel="modulepreload" crossorigin>タグを1つ返します。hrefの値はHTMLエスケープされます。サーバーでもブラウザでも動作します。

<head>がReactコンポーネントの場合は、リストを自分でレンダリングします。

tsx
import type { TuvixAssets } from '@tuvix.js/vite';

export function IslandAssets({ assets }: { assets: TuvixAssets }) {
  return (
    <>
      {assets.css.map((href) => (
        <link key={href} rel="stylesheet" href={href} />
      ))}
      {assets.modulepreload.map((href) => (
        <link key={href} rel="modulepreload" crossOrigin="" href={href} />
      ))}
    </>
  );
}

型 ​

型エクスポート元意味
TuvixViteOptions@tuvix.js/vitetuvix()のオプション:clientOutDir?、base?。
TuvixAssets@tuvix.js/vite、virtual:tuvix/assets{ css: string[]; modulepreload: string[] }。cssはカスケード順です。devではmodulepreloadは空です。
@tuvix.js/vite/client型エントリーvirtual:tuvix/assetsモジュールを宣言します。compilerOptions.typesに追加するか、/// <reference types="@tuvix.js/vite/client" />で参照してください。

本番ビルド ​

  • クライアントビルドでは、自分でbuild.manifestを設定していない限り、プラグインがbuild.manifestを有効にします。
  • クライアントビルドは、マニフェストの隣に{ modules, sharedCss }という形の.vite/tuvix-client.jsonも書き込みます。modulesは、各エントリーとimport()の対象(Viteのルートからの相対パス)を、Rollupがそれを入れたチャンクに対応付けます。sharedCssには、build.cssCodeSplit: falseのビルドの単一スタイルシートが記載されます。
  • SSRビルドでは、クライアントのマニフェストとtuvix-client.jsonを読み取り、エントリー(isEntry)または動的エントリー(isDynamicEntry)であるマニフェストのすべてのキーからそのURLへのマップをインライン化します。
  • Rollupは、2つの動的エントリーを1つのチャンクにまとめることがあります。たとえば、ページと、アダプターがimport()でロードするフレームワークのランタイムです。するとViteのマニフェストはそのチャンクを_Page-<hash>.jsというキーで記録し、どちらのソースも記載しません。プラグインはtuvix-client.jsonのmodulesを通じてそのチャンクを見つけます。
  • build.cssCodeSplit: falseの場合、ViteはすべてのCSSを、どのマニフェストのチャンクにも記載されない1つのstyle-<hash>.cssに書き込みます。getAssetsは、すべてのエントリーについて、エントリー自身のCSSより前にそのファイルを返します。
  • tuvix-client.jsonがない場合(tuvix()なしでビルドしたクライアント)、解決されるのは独自のマニフェストキーを持つモジュールだけです。どのチャンクにも記載されないstyle.cssのマニフェストアセットは、引き続きcssCodeSplit: falseの単一スタイルシートとして扱われます。
  • 各エントリーについて、エントリーチャンクと、それが静的にimportするすべてのチャンク(推移的に)、およびそれらのCSSを収集します。importされたチャンクのCSSは、エントリー自身のCSSより前に来ます。これはViteがロードする順序です。
  • modulepreloadには、エントリーチャンクとその静的importが記載されます。URLはbase + fileです。
  • Viteはimport()の対象のチャンクとそのCSSを、importが実行されたときにのみロードするため、マップはそれらをスキップします。遅延モジュールがサーバーでレンダリングされる場合は、そのパスを別のエントリーとしてgetAssetsに渡してください。
  • 後でブラウザがアイランドのチャンクをロードするとき、Viteのpreloadヘルパーはサーバーが出力した<link>を見つけ、2つ目を追加しません。

devサーバー ​

  • getAssetsは、devサーバーのクライアントモジュールグラフを各エントリーから静的importに沿ってimport順にたどります。/src/islands/footer/footer.module.css?directのようなURLを返します。Viteは?directのリクエストに素のCSSで応答します。Vueのブロックは/src/App.vue?direct&vue&type=style&index=0&...&lang.cssとして返ります。
  • アイランドが仮想モジュール経由で到達するCSSも収集されます。\0virtual:theme.cssのような仮想スタイルシートは/@id/__x00__virtual:theme.css?directとして返るため、それを提供するプラグインはクエリ付きのidをロードできなければなりません。
  • アイランドのJavaScriptが実行されると、ViteはCSSモジュールごとに独自の<style>を注入します。するとtuvix:dev-css-handoffプラグインが、同じhrefを持つサーバーレンダリングされた<link>にlink.sheet.disabled = trueを設定するため、古いルールのコピーがHMRによる編集を上書きすることはありません。<link>要素はDOMに残るため、ハイドレートされたReactの<head>からは変化が見えません。
  • CSSを編集すると、tuvix:dev-css-handoffは?directのスタイルシートをHMRの更新から除外します。するとViteは、ページをリロードせず、<link>を差し替えることもなく、自身の<style>を更新します。制限が2つあります。
    • CSS Modulesの編集はハッシュ付きのクラス名を変えるため、アイランド自身のHMR(React Fast Refresh、Vue)が再レンダリングする必要があります。
    • ページがまだJavaScriptをロードしていないアイランドは、リロードするまでサーバーのスタイルシートを使い続けます。
  • devではmodulepreloadは常に空です。
  • getAssetsは同じNodeプロセス内のdevサーバーとやり取りします。ssrLoadModuleとViteのデフォルトのmodule runnerで動作し、server.restart()をまたいでも動作し続けます。

エッジケース ​

  • ビルドの入力でもクライアントビルドのimport()の対象でもないエントリーは、[tuvix] "<key>" is not an entry or a dynamically imported module in <manifest>でrejectされます。アイランドをbuild.rollupOptions.inputに追加するか、クライアントコードからimport()でロードしてください。
  • クライアントのマニフェストがないと、SSRビルドは[tuvix] No client manifest in <dir>で失敗します。先にクライアントをビルドするか、clientOutDirを修正してください。
  • devでは、SSRのコードが別のプロセスやワーカーランタイムで実行される場合、またはdevサーバーが閉じた後、getAssetsはfound no running Vite dev server with the tuvix() plugin in this processでrejectされます。
  • virtual:tuvix/assetsのブラウザ向けビルドでは、getAssetsはruns during server rendering onlyでrejectされます。クライアントバンドルは自身のマニフェストを知ることができません。
  • devでは、走査は/node_modules/配下のモジュールには入りません。自分のコードがパッケージからimportするスタイルシートは収集されますが、パッケージが自身のJavaScriptからimportするスタイルシートは収集されません。
  • './'のような相対的なbaseは相対URLを生成し、ネストしたルートで壊れます。絶対的なbaseを使ってください。
  • Svelte:vite-plugin-svelteのデフォルトであるemitCss: trueを維持してください。compilerOptions.css: 'injected'ではスタイルがJavaScript内に置かれ、プラグインはリンクすべきスタイルシートを見つけられません。

しないこと ​

  • HTMLにタグを書き込みません。renderAssetTagsの出力をどこに置くかはあなたが決めます。
  • import()の連鎖はたどりません。サーバーでレンダリングされる遅延モジュールは、それぞれ独立したエントリーとして渡してください。
  • カスタムのマニフェストファイル名(build.manifest: 'name.json')はサポートしません。trueを使ってください。
  • アイランドのレンダリングやハイドレーションは行いません。@tuvix.js/react(TuvixIsland)、@tuvix.js/vue、@tuvix.js/svelteなどのアダプターと組み合わせてください。