Instalação
npm install -D @tuvix.js/vite
# or pnpm add -D @tuvix.js/viteO plugin precisa do Vite 6 ou 7 (faixa de peer ^6.0.0 || ^7.0.0). A suíte de testes roda no Vite 6.
Quando usar
Você renderiza uma ilha no servidor (React, Vue, Svelte, Angular ou DOM puro) e o markup dela chega ao navegador antes do JavaScript. O Vite entrega CSS Modules, blocos <style> de SFCs Vue e estilos Svelte por meio desse JavaScript, então o navegador pinta a ilha sem estilo até o bundle rodar. Com @tuvix.js/vite, seu servidor pergunta "de quais folhas de estilo src/islands/footer/index.ts precisa?" e escreve as tags <link> correspondentes no <head>.
Dispense o plugin quando suas ilhas forem renderizadas apenas no cliente, ou quando você vincular uma única folha de estilo global que já cobre todas as ilhas.
Início rápido
Adicione o plugin à config do Vite que você usa para o build do cliente e para o build SSR:
// vite.config.ts
import { defineConfig } from 'vite';
import { tuvix } from '@tuvix.js/vite';
export default defineConfig({
plugins: [tuvix()],
});Adicione os tipos do módulo virtual ao tsconfig.json:
{
"compilerOptions": {
"types": ["vite/client", "@tuvix.js/vite/client"]
}
}Peça os assets na entry do servidor e escreva-os no <head>:
// 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>`;
}Faça o build do cliente primeiro e depois o do servidor. O build SSR lê o manifest do cliente:
vite build --outDir dist/client
vite build --ssr src/entry-server.ts --outDir dist/serverAPI
tuvix(options?)
function tuvix(options?: TuvixViteOptions): Plugin[];Retorna três plugins do Vite:
tuvix:assetsfornecevirtual:tuvix/assets.tuvix:client-build-info(apenas em builds) escreve.vite/tuvix-client.jsonao lado do manifest do cliente. Veja Builds de produção.tuvix:dev-css-handoff(apenas no servidor de desenvolvimento) passa a responsabilidade pelos estilos do<link>renderizado no servidor para as próprias tags<style>do Vite.
Coloque o array retornado em plugins como mostrado acima; o Vite achata arrays de plugins aninhados.
| Opção | Tipo | Padrão | Significado |
|---|---|---|---|
clientOutDir | string | 'dist/client' | Diretório de saída do build do cliente, relativo à raiz do Vite ou absoluto. O build SSR lê <clientOutDir>/.vite/manifest.json e, como fallback, <clientOutDir>/manifest.json. |
base | string | base do Vite no build ou servidor de desenvolvimento que carrega o módulo virtual | Base pública a partir da qual o navegador carrega os assets do cliente. Defina quando o cliente é servido a partir de uma base diferente da do bundle do servidor, como o /_build do vinxi. |
O TanStack Start e o vinxi escrevem o build do roteador do cliente em uma pasta separada sob /_build:
import { tuvix } from '@tuvix.js/vite';
export const islandAssets = tuvix({ clientOutDir: '.vinxi/build/client/_build', base: '/_build' });getAssets(entries)
Exportado por virtual:tuvix/assets.
function getAssets(entries: string[]): Promise<TuvixAssets>;Passe os módulos de entry das ilhas como caminhos relativos à raiz do Vite, como 'src/islands/footer/index.ts'. A função ignora um ./ ou / inicial. Com várias entries, ela junta as listas na ordem das entries e remove duplicatas.
getAssets retorna uma promise porque o servidor de desenvolvimento precisa transformar cada módulo antes de conseguir ler os imports dele. Chame-a apenas a partir de código de servidor.
renderAssetTags(assets)
Exportado por virtual:tuvix/assets.
function renderAssetTags(assets: TuvixAssets): string;Retorna uma tag <link rel="stylesheet"> para cada URL em assets.css e, em seguida, uma tag <link rel="modulepreload" crossorigin> para cada URL em assets.modulepreload. Ela escapa os valores de href para HTML. Funciona no servidor e no navegador.
Se o seu <head> for um componente React, renderize as listas você mesmo:
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} />
))}
</>
);
}Tipos
| Tipo | Exportado de | Significado |
|---|---|---|
TuvixViteOptions | @tuvix.js/vite | Opções de tuvix(): clientOutDir?, base?. |
TuvixAssets | @tuvix.js/vite, virtual:tuvix/assets | { css: string[]; modulepreload: string[] }. css está na ordem da cascata. modulepreload fica vazio em desenvolvimento. |
@tuvix.js/vite/client | entry de tipos | Declara o módulo virtual:tuvix/assets. Adicione-o a compilerOptions.types ou referencie-o com /// <reference types="@tuvix.js/vite/client" />. |
Builds de produção
- No build do cliente, o plugin ativa
build.manifest, a menos que você mesmo definabuild.manifest. - O build do cliente também escreve
.vite/tuvix-client.jsonao lado do manifest, no formato{ modules, sharedCss }.modulesmapeia cada entry e cada alvo deimport(), relativos à raiz do Vite, para o chunk em que o Rollup os colocou.sharedCsslista a folha de estilo única de um build combuild.cssCodeSplit: false. - No build SSR, ele lê o manifest do cliente e o
tuvix-client.json, e embute um mapa de cada chave do manifest que seja uma entry (isEntry) ou uma entry dinâmica (isDynamicEntry) para as URLs dela. - Às vezes o Rollup junta duas entries dinâmicas em um único chunk, por exemplo uma página e um runtime de framework que um adaptador carrega com
import(). O manifest do Vite então usa a chave_Page-<hash>.jspara esse chunk e não lista nenhuma das duas fontes. O plugin encontra o chunk por meio demodulesnotuvix-client.json. - Com
build.cssCodeSplit: false, o Vite escreve todo o CSS em um únicostyle-<hash>.cssque nenhum chunk do manifest lista.getAssetsretorna esse arquivo para todas as entries, antes do CSS da própria entry. - Sem
tuvix-client.json(um cliente cujo build foi feito semtuvix()), só resolvem os módulos que têm a própria chave no manifest. Um assetstyle.cssdo manifest que nenhum chunk lista continua contando como a folha de estilo única decssCodeSplit: false. - Para cada entry, ele coleta o chunk da entry e todos os chunks que ela importa estaticamente, de forma transitiva, além do CSS deles. O CSS dos chunks importados vem antes do CSS da própria entry, que é a ordem em que o Vite os carrega.
modulepreloadlista o chunk da entry e os imports estáticos dele. As URLs sãobase + file.- O mapa ignora alvos de
import(), porque o Vite carrega esses chunks e o CSS deles só quando o import é executado. Se um módulo lazy é renderizado no servidor, passe o caminho dele paragetAssetscomo uma entry separada. - Quando o navegador carrega o chunk da ilha depois, o helper de preload do Vite encontra o
<link>que seu servidor escreveu e não adiciona um segundo.
Servidor de desenvolvimento
getAssetspercorre o grafo de módulos do cliente no servidor de desenvolvimento a partir de cada entry, seguindo os imports estáticos, na ordem dos imports. Ele retorna URLs como/src/islands/footer/footer.module.css?direct; o Vite responde requisições?directcom CSS puro. Blocos Vue voltam como/src/App.vue?direct&vue&type=style&index=0&...&lang.css.- O CSS que uma ilha alcança por meio de um módulo virtual também é coletado. Uma folha de estilo virtual como
\0virtual:theme.cssvolta como/@id/__x00__virtual:theme.css?direct, então o plugin que a fornece precisa carregar ids que trazem uma query. - Assim que o JavaScript da ilha roda, o Vite injeta seu próprio
<style>para cada módulo CSS. O plugintuvix:dev-css-handoffentão definelink.sheet.disabled = trueno<link>renderizado no servidor com o mesmo href, para que uma cópia desatualizada das regras não sobrescreva suas edições via HMR. O elemento<link>continua no DOM, então um<head>React hidratado não vê nenhuma mudança. - Em uma edição de CSS,
tuvix:dev-css-handoffdeixa a folha de estilo?directfora da atualização de HMR. O Vite então atualiza seu<style>sem recarregar a página e sem trocar o<link>. Há dois limites:- Uma edição em CSS Modules muda os nomes de classe com hash, então o próprio HMR da ilha (React Fast Refresh, Vue) precisa renderizá-la de novo.
- Uma ilha cujo JavaScript a página ainda não carregou mantém a folha de estilo do servidor até você recarregar.
modulepreloadfica sempre vazio em desenvolvimento.getAssetsconversa com o servidor de desenvolvimento no mesmo processo Node. Funciona comssrLoadModulee com o module runner padrão do Vite, e continua funcionando depois deserver.restart().
Casos extremos
- Uma entry que não é input do build nem alvo de
import()no build do cliente rejeita com[tuvix] "<key>" is not an entry or a dynamically imported module in <manifest>. Adicione a ilha abuild.rollupOptions.input, ou carregue-a comimport()a partir do código do cliente. - A ausência do manifest do cliente faz o build SSR falhar com
[tuvix] No client manifest in <dir>. Faça o build do cliente primeiro, ou corrijaclientOutDir. - Em desenvolvimento,
getAssetsrejeita comfound no running Vite dev server with the tuvix() plugin in this processquando seu código SSR roda em outro processo ou em um runtime de worker, ou depois que o servidor de desenvolvimento foi fechado. - No build para navegador de
virtual:tuvix/assets,getAssetsrejeita comruns during server rendering only. O bundle do cliente não tem como conhecer o próprio manifest. - Em desenvolvimento, o percurso não desce em módulos sob
/node_modules/. Uma folha de estilo que seu próprio código importa de um pacote é coletada; uma folha de estilo que um pacote importa a partir do próprio JavaScript não é. - Uma
baserelativa, como'./', gera URLs relativas, que quebram em rotas aninhadas. Use uma base absoluta. - Svelte: mantenha o padrão
emitCss: truedovite-plugin-svelte. ComcompilerOptions.css: 'injected', os estilos ficam dentro do JavaScript e o plugin não encontra nenhuma folha de estilo para vincular.
O que ele não faz
- Não escreve tags no seu HTML. Você decide onde a saída de
renderAssetTagsvai. - Não segue cadeias de
import(). Passe módulos lazy que são renderizados no servidor como entries próprias. - Não suporta um nome de arquivo de manifest personalizado (
build.manifest: 'name.json'). Usetrue. - Não renderiza nem hidrata ilhas. Combine-o com um adaptador como
@tuvix.js/react(TuvixIsland),@tuvix.js/vueou@tuvix.js/svelte.