Installation
npm install -D @tuvix.js/vite
# or pnpm add -D @tuvix.js/viteDas Plugin braucht Vite 6 oder 7 (Peer-Bereich ^6.0.0 || ^7.0.0). Die Testsuite läuft auf Vite 6.
Wann du es verwendest
Du renderst ein Island serverseitig (React, Vue, Svelte, Angular oder reines DOM), und sein Markup erreicht den Browser vor seinem JavaScript. Vite liefert CSS Modules, <style>-Blöcke aus Vue SFCs und Svelte-Styles über dieses JavaScript aus, daher zeichnet der Browser das Island ungestylt, bis das Bundle läuft. Mit @tuvix.js/vite fragt dein Server „Welche Stylesheets braucht src/islands/footer/index.ts?“ und gibt <link>-Tags dafür in <head> aus.
Lass es weg, wenn deine Islands nur auf dem Client rendern oder wenn du ein globales Stylesheet verlinkst, das bereits jedes Island abdeckt.
Schnellstart
Füge das Plugin der Vite-Konfiguration hinzu, die du für den Client-Build und den SSR-Build verwendest:
// vite.config.ts
import { defineConfig } from 'vite';
import { tuvix } from '@tuvix.js/vite';
export default defineConfig({
plugins: [tuvix()],
});Füge die Typen des virtuellen Moduls zu tsconfig.json hinzu:
{
"compilerOptions": {
"types": ["vite/client", "@tuvix.js/vite/client"]
}
}Frage die Assets in deinem Server-Entry ab und gib sie in <head> aus:
// 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>`;
}Baue zuerst den Client, dann den Server. Der SSR-Build liest das Client-Manifest:
vite build --outDir dist/client
vite build --ssr src/entry-server.ts --outDir dist/serverAPI
tuvix(options?)
function tuvix(options?: TuvixViteOptions): Plugin[];Gibt drei Vite-Plugins zurück:
tuvix:assetsstelltvirtual:tuvix/assetsbereit.tuvix:client-build-info(nur Builds) schreibt.vite/tuvix-client.jsonneben das Client-Manifest. Siehe Produktions-Builds.tuvix:dev-css-handoff(nur Dev-Server) übergibt das Styling vom serverseitig gerenderten<link>an Vites eigene<style>-Tags.
Lege das zurückgegebene Array wie oben gezeigt in plugins; Vite flacht verschachtelte Plugin-Arrays ab.
| Option | Typ | Standard | Bedeutung |
|---|---|---|---|
clientOutDir | string | 'dist/client' | Ausgabeverzeichnis des Client-Builds, relativ zum Vite-Root oder absolut. Der SSR-Build liest <clientOutDir>/.vite/manifest.json und greift ersatzweise auf <clientOutDir>/manifest.json zurück. |
base | string | Vite-base des Builds oder Dev-Servers, der das virtuelle Modul lädt | Öffentliche Base, von der der Browser Client-Assets lädt. Setze sie, wenn der Client unter einer anderen Base ausgeliefert wird als das Server-Bundle, etwa unter /_build bei vinxi. |
TanStack Start und vinxi schreiben den Build des Client-Routers in einen eigenen Ordner unter /_build:
import { tuvix } from '@tuvix.js/vite';
export const islandAssets = tuvix({ clientOutDir: '.vinxi/build/client/_build', base: '/_build' });getAssets(entries)
Exportiert von virtual:tuvix/assets.
function getAssets(entries: string[]): Promise<TuvixAssets>;Übergib Entry-Module von Islands als Pfade relativ zum Vite-Root, etwa 'src/islands/footer/index.ts'. Die Funktion ignoriert ein führendes ./ oder /. Bei mehreren Entries führt sie die Listen in Entry-Reihenfolge zusammen und verwirft Duplikate.
getAssets gibt ein Promise zurück, weil der Dev-Server jedes Modul transformieren muss, bevor er dessen Imports lesen kann. Rufe es nur aus Server-Code auf.
renderAssetTags(assets)
Exportiert von virtual:tuvix/assets.
function renderAssetTags(assets: TuvixAssets): string;Gibt ein <link rel="stylesheet">-Tag pro URL in assets.css zurück, danach ein <link rel="modulepreload" crossorigin>-Tag pro URL in assets.modulepreload. Es maskiert die href-Werte für HTML. Es funktioniert auf dem Server und im Browser.
Ist dein <head> eine React-Komponente, renderst du die Listen selbst:
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} />
))}
</>
);
}Typen
| Typ | Exportiert aus | Bedeutung |
|---|---|---|
TuvixViteOptions | @tuvix.js/vite | Optionen von tuvix(): clientOutDir?, base?. |
TuvixAssets | @tuvix.js/vite, virtual:tuvix/assets | { css: string[]; modulepreload: string[] }. css steht in Kaskadenreihenfolge. modulepreload ist im Dev-Modus leer. |
@tuvix.js/vite/client | Typ-Entry | Deklariert das Modul virtual:tuvix/assets. Füge es zu compilerOptions.types hinzu oder referenziere es mit /// <reference types="@tuvix.js/vite/client" />. |
Produktions-Builds
- Im Client-Build schaltet das Plugin
build.manifestein, sofern dubuild.manifestnicht selbst setzt. - Der Client-Build schreibt außerdem
.vite/tuvix-client.jsonneben das Manifest, in der Form{ modules, sharedCss }.modulesordnet jeden Entry und jedesimport()-Ziel, relativ zum Vite-Root, dem Chunk zu, in den Rollup es gelegt hat.sharedCsslistet das einzige Stylesheet eines Builds mitbuild.cssCodeSplit: false. - Im SSR-Build liest es das Client-Manifest und
tuvix-client.jsonund bettet eine Zuordnung ein, die jeden Manifest-Schlüssel, der ein Entry (isEntry) oder ein dynamischer Entry (isDynamicEntry) ist, auf seine URLs abbildet. - Rollup führt manchmal zwei dynamische Entries in einem Chunk zusammen, zum Beispiel eine Seite und eine Framework-Runtime, die ein Adapter mit
import()lädt. Vites Manifest führt diesen Chunk dann unter dem Schlüssel_Page-<hash>.jsund nennt keine der beiden Quellen. Das Plugin findet den Chunk übermodulesintuvix-client.json. - Mit
build.cssCodeSplit: falseschreibt Vite das gesamte CSS in eine einzigestyle-<hash>.css, die kein Chunk im Manifest listet.getAssetsgibt diese Datei für jeden Entry zurück, vor dem eigenen CSS des Entry. - Ohne
tuvix-client.json(ein Client, der ohnetuvix()gebaut wurde) werden nur Module mit eigenem Manifest-Schlüssel aufgelöst. Einstyle.css-Asset im Manifest, das kein Chunk listet, gilt trotzdem als das einzige Stylesheet voncssCodeSplit: false. - Für jeden Entry sammelt es den Entry-Chunk und jeden Chunk, den dieser statisch importiert, transitiv, samt deren CSS. Das CSS importierter Chunks steht vor dem eigenen CSS des Entry, in der Reihenfolge, in der Vite sie lädt.
modulepreloadlistet den Entry-Chunk und seine statischen Imports. URLs sindbase + file.- Die Zuordnung überspringt
import()-Ziele, weil Vite diese Chunks und ihr CSS erst lädt, wenn der Import ausgeführt wird. Rendert ein Lazy-Modul auf dem Server, übergib seinen Pfad als separaten Entry angetAssets. - Lädt der Browser später den Chunk des Islands, findet Vites Preload-Helper das
<link>, das dein Server ausgegeben hat, und fügt kein zweites hinzu.
Dev-Server
getAssetsdurchläuft den Client-Modulgraphen des Dev-Servers von jedem Entry aus entlang statischer Imports, in Import-Reihenfolge. Es gibt URLs wie/src/islands/footer/footer.module.css?directzurück; Vite beantwortet?direct-Anfragen mit reinem CSS. Vue-Blöcke kommen als/src/App.vue?direct&vue&type=style&index=0&...&lang.csszurück.- CSS, das ein Island über ein virtuelles Modul erreicht, wird ebenfalls gesammelt. Ein virtuelles Stylesheet wie
\0virtual:theme.csskommt als/@id/__x00__virtual:theme.css?directzurück, daher muss das Plugin, das es bereitstellt, IDs mit Query laden können. - Sobald das JavaScript des Islands läuft, injiziert Vite für jedes CSS-Modul ein eigenes
<style>. Das Plugintuvix:dev-css-handoffsetzt dannlink.sheet.disabled = trueauf dem serverseitig gerenderten<link>mit demselben href, damit eine veraltete Kopie der Regeln deine HMR-Änderungen nicht überschreibt. Das<link>-Element bleibt im DOM, daher sieht ein hydrierter React-<head>keine Änderung. - Bei einer CSS-Änderung lässt
tuvix:dev-css-handoffdas?direct-Stylesheet aus dem HMR-Update heraus. Vite aktualisiert dann sein<style>, ohne die Seite neu zu laden und ohne das<link>auszutauschen. Dabei gelten zwei Grenzen:- Eine Änderung an CSS Modules ändert die gehashten Klassennamen, daher muss das eigene HMR des Islands (React Fast Refresh, Vue) es neu rendern.
- Ein Island, dessen JavaScript die Seite noch nicht geladen hat, behält das Server-Stylesheet, bis du neu lädst.
modulepreloadist im Dev-Modus immer leer.getAssetsspricht mit dem Dev-Server im selben Node-Prozess. Es funktioniert mitssrLoadModuleund dem Standard-Module-Runner von Vite und funktioniert auch nachserver.restart()weiter.
Sonderfälle
- Ein Entry, der weder ein Build-Input noch ein
import()-Ziel des Client-Builds ist, lehnt mit[tuvix] "<key>" is not an entry or a dynamically imported module in <manifest>ab. Füge das Island zubuild.rollupOptions.inputhinzu oder lade es mitimport()aus Client-Code. - Ein fehlendes Client-Manifest lässt den SSR-Build mit
[tuvix] No client manifest in <dir>scheitern. Baue zuerst den Client oder korrigiereclientOutDir. - Im Dev-Modus lehnt
getAssetsmitfound no running Vite dev server with the tuvix() plugin in this processab, wenn dein SSR-Code in einem anderen Prozess oder einer Worker-Runtime läuft oder nachdem der Dev-Server geschlossen wurde. - Im Browser-Build von
virtual:tuvix/assetslehntgetAssetsmitruns during server rendering onlyab. Das Client-Bundle kann sein eigenes Manifest nicht kennen. - Im Dev-Modus steigt der Walker nicht in Module unter
/node_modules/hinab. Ein Stylesheet, das dein eigener Code aus einem Paket importiert, wird gesammelt; ein Stylesheet, das ein Paket aus seinem eigenen JavaScript importiert, nicht. - Eine relative
basewie'./'ergibt relative URLs, die auf verschachtelten Routen brechen. Verwende eine absolute Base. - Svelte: Behalte den Standardwert
emitCss: truevonvite-plugin-sveltebei. MitcompilerOptions.css: 'injected'stecken die Styles im JavaScript, und das Plugin findet kein Stylesheet zum Verlinken.
Was es nicht tut
- Es schreibt keine Tags in dein HTML. Du entscheidest, wohin die Ausgabe von
renderAssetTagskommt. - Es folgt keinen
import()-Ketten. Übergib Lazy-Module, die auf dem Server rendern, als eigene Entries. - Es unterstützt keinen eigenen Dateinamen für das Manifest (
build.manifest: 'name.json'). Verwendetrue. - Es rendert und hydriert keine Islands. Kombiniere es mit einem Adapter wie
@tuvix.js/react(TuvixIsland),@tuvix.js/vueoder@tuvix.js/svelte.