Pular para o conteúdo
charmotion

Rive em uma extensão do Chrome (Manifest V3): empacote o WASM

Por equipe Charmotion · Atualizado em

Ilustração de capa do guia: Rive em uma extensão do Chrome (Manifest V3): inclua o WASM no pacote, com personagens de demonstração do Charmotion

O Rive falha em uma extensão Manifest V3 porque o runtime web baixa o rive.wasm do unpkg por padrão, e o Chrome proíbe código hospedado remotamente, incluindo WebAssembly. Empacote o @rive-app/canvas, copie os arquivos WASM para o pacote, chame RuntimeLoader.setWasmUrl() com chrome.runtime.getURL() antes de criar a animação, inclua o .riv dentro da extensão e adicione 'wasm-unsafe-eval' à CSP.

Principais pontos

  • O Chrome define código hospedado remotamente como qualquer coisa executada que seja carregada de fora dos arquivos da própria extensão, e cita WASM explicitamente.
  • A CSP padrão das extensões desativa o WebAssembly; a política de que você precisa é script-src 'self' 'wasm-unsafe-eval'; object-src 'self';.
  • O runtime busca o rive.wasm no unpkg e um fallback no jsDelivr; substitua os dois por URLs locais.
  • Mantenha os arquivos WASM e o pacote npm exatamente na mesma versão; segundo o Rive, uma diferença pode quebrar a renderização.
  • Electron e Tauri precisam do mesmo WASM hospedado localmente, além de 'wasm-unsafe-eval' em qualquer CSP que você definir.

Já tem um personagem ou logo? Veja seu personagem em telas reais de app: telas de splash, onboarding, carregamento e estado vazio. É grátis, roda no navegador e não envia nada.

Por que o Rive não carrega na minha extensão do Chrome?

Duas regras do Manifest V3 entram em conflito com os padrões do runtime.

1. Código remoto é proibido. O Chrome define código hospedado remotamente como "qualquer coisa executada pelo navegador que seja carregada de algum lugar que não sejam os arquivos da própria extensão" e cita "coisas como JavaScript e WASM" como exemplos. As extensões devem "empacotar todo o código que usam dentro da própria extensão". O runtime web do Rive, por padrão, baixa o motor de uma CDN. No @rive-app/canvas 2.47.1 o padrão é https://unpkg.com/@rive-app/canvas@2.47.1/rive.wasm e, se isso falhar, ele tenta o rive_fallback.wasm do jsDelivr.

2. A CSP padrão desativa o WebAssembly. Sem uma chave content_security_policy, as páginas da extensão recebem script-src 'self'; object-src 'self';, e a documentação do Chrome diz que, com ela, "o WebAssembly será desativado". A política mais permissiva que o Chrome aceita para páginas de extensão é script-src 'self' 'wasm-unsafe-eval'; object-src 'self';.

Então os sintomas são uma requisição de rede bloqueada para o rive.wasm ou um erro de CSP sobre a compilação do WebAssembly, seguidos de um canvas em branco. Corrija os dois.

Qual CSP o manifest precisa para o WebAssembly?

Adicione 'wasm-unsafe-eval' a extension_pages. Essa é a política mínima da documentação do Chrome, então também é o máximo que você pode afrouxá-la:

{
  "manifest_version": 3,
  "name": "Mascot",
  "version": "1.0.0",
  "action": { "default_popup": "popup.html" },
  "content_security_policy": {
    "extension_pages": "script-src 'self' 'wasm-unsafe-eval'; object-src 'self';"
  }
}
  • Não adicione 'unsafe-eval' nem um host de CDN; o Chrome rejeita na instalação políticas mais frouxas que a mínima.
  • Essa política vale para as páginas da extensão: o popup, o painel lateral, a página de opções e a substituição da nova guia.
  • O próprio .riv é buscado na origem da extensão, o que 'self' permite.

Como empacotar o rive.wasm e o .riv na extensão?

Instale uma versão exata do runtime, copie os dois arquivos WASM para a pasta da extensão e aponte o runtime para eles antes de criar a primeira animação. O pacote npm traz os dois arquivos (na 2.47.1, o rive.wasm tem 2.165.388 bytes e o rive_fallback.wasm, 2.191.884 bytes).

npm install --save-exact @rive-app/canvas@2.47.1
mkdir -p extension/vendor
cp node_modules/@rive-app/canvas/rive.wasm extension/vendor/
cp node_modules/@rive-app/canvas/rive_fallback.wasm extension/vendor/

Depois, no script do popup (empacotado com Vite, esbuild, Rollup ou webpack, já que as páginas da extensão não resolvem imports npm diretos):

import { Rive, RuntimeLoader } from "@rive-app/canvas";

// Must run before the first new Rive(): no CDN requests from now on.
RuntimeLoader.setWasmUrl(chrome.runtime.getURL("vendor/rive.wasm"));
RuntimeLoader.setWasmFallbackUrl(chrome.runtime.getURL("vendor/rive_fallback.wasm"));

const canvas = document.getElementById("mascot");
const r = new Rive({
  src: chrome.runtime.getURL("assets/character.riv"),   // shipped in the package
  canvas,
  stateMachine: "CharacterViewModelMachine",
  autoplay: !matchMedia("(prefers-reduced-motion: reduce)").matches,
  autoBind: true,
  onLoad: () => r.resizeDrawingSurfaceToCanvas(),
});

document.addEventListener("visibilitychange", () => {
  if (document.hidden) r.pause(); else r.play();
});
  • setWasmUrl é a chamada que o Rive documenta para hospedagem própria. A documentação do Rive acrescenta que a versão do WASM "deve corresponder à versão do pacote @rive-app", porque os dois são gerados em par; atualize os arquivos copiados no mesmo commit de qualquer mudança de versão.
  • setWasmFallbackUrl existe no código-fonte do runtime a partir da 2.40.1 (verificamos da 2.40.1 até a 2.47.1). Apontá-la para o fallback empacotado faz com que até a nova tentativa continue local; em versões mais antigas, essa chamada não existe.
  • Inclua o .riv no pacote e carregue-o com chrome.runtime.getURL(). Assim ele funciona offline e não há nada remoto para um revisor questionar.

O código da biblioteca ainda contém as URLs padrão da CDN como strings, embora elas nunca sejam usadas depois que você as substitui. O guia do Chrome sugere procurar https:// no código gerado para encontrar violações; então, se um revisor apontar isso, mostre a chamada setWasmUrl.

Onde uma animação Rive pode rodar: popup, painel lateral ou content script?

As páginas da extensão são o caso fácil: popup, painel lateral, opções e nova guia rodam sob a CSP da sua extensão, então a configuração acima funciona sem mudanças.

  • Popup: o Chrome ajusta o tamanho ao conteúdo, entre 25×25 e 800×600 pixels. Defina largura e altura explícitas em CSS para o canvas, senão o popup se fecha em volta de um canvas vazio.
  • Painel lateral e opções: o mesmo código; redimensione a superfície de desenho quando a largura do painel mudar.
  • Em páginas web: em vez de rodar o runtime dentro de um content script na página de outra pessoa, injete um iframe que aponte para uma página da sua extensão. Liste essa página em web_accessible_resources para que os sites possam exibi-la em frame; assim a animação roda na origem da sua extensão, sob a sua CSP, isolada dos scripts e estilos do site.
"web_accessible_resources": [
  {
    "resources": ["mascot-frame.html"],
    "matches": ["https://*.example.com/*"]
  }
]

Use a lista matches mais restrita possível; ela controla quais sites podem carregar o recurso.

Como manter rápido um mascote na nova guia?

Uma substituição da nova guia (chrome_url_overrides) carrega toda vez que alguém abre uma guia, então pequenos custos se acumulam.

  • Pré-carregue o WASM. O guia de pré-carregamento do Rive usa <link rel="preload" as="fetch" crossorigin>; a URL do preload precisa ser exatamente igual à URL de setWasmUrl(), senão o arquivo é baixado duas vezes. Um caminho relativo vendor/rive.wasm em uma página da extensão resolve para a mesma URL que chrome.runtime.getURL("vendor/rive.wasm").
  • Pause quando a página estiver oculta. O exemplo pausa no visibilitychange, para que guias em segundo plano não continuem animando.
  • Uma instância. Use um único personagem com humores, não vários canvas.
  • Respeite o movimento reduzido. A reprodução automática fica desligada quando o usuário pede menos movimento; mostre o primeiro frame ou uma imagem estática no lugar.

O Rive também recomenda @rive-app/webgl2 para a maioria dos projetos web; as mesmas chamadas de RuntimeLoader valem, mas copie os arquivos WASM desse pacote.

A mesma configuração funciona no Firefox?

Sim, com as mesmas regras. A referência de manifest do MDN diz que, no Manifest V3, os únicos valores permitidos em script-src para páginas de extensão são 'self', 'none' e 'wasm-unsafe-eval', e que "o Manifest V3 não permite URLs remotas em script-src". Também observa que as políticas de add-ons do Firefox não permitem scripts remotos. O WASM incluído no pacote, o .riv local e a mesma string de CSP continuam valendo.

Para add-ons em Manifest V2, o MDN diz que o Firefox atualmente consegue rodar WebAssembly sem 'wasm-unsafe-eval' por compatibilidade retroativa, mas que "esse comportamento não é garantido", e recomenda declará-lo.

Apps em Electron e Tauri precisam da mesma correção?

O princípio é idêntico: inclua o rive.wasm no app, chame RuntimeLoader.setWasmUrl() com uma URL local e permita WebAssembly na sua CSP.

  • Electron. O checklist de segurança do Electron alerta contra carregar e executar código remoto e recomenda servir páginas locais a partir de um protocolo personalizado em vez de file://. Aponte setWasmUrl para uma URL nesse protocolo. Se você definir uma CSP (com uma meta tag para páginas locais), inclua 'wasm-unsafe-eval' em script-src.
  • Tauri. A CSP do Tauri só é aplicada se você a definir na configuração; quando você faz isso, a documentação diz para "incluir 'wasm-unsafe-eval' como script-src" e desaconselha scripts de CDN. O runtime baixa o WASM e o .riv com fetch, então garanta que connect-src (ou default-src) permita a origem de onde você os serve.

O que muda em um .riv do Charmotion dentro de uma extensão?

Nada no arquivo. O .riv do Charmotion (17 animações a partir de uma imagem, com um view model Character: um número mood de 0 a 9 e os triggers success, error, yes, no, enter e exit) é autossuficiente, então funciona offline a partir do pacote. O código para web que vem com ele carrega o runtime de uma CDN; para uma extensão, substitua essa tag script pelo import empacotado e pelas duas chamadas de RuntimeLoader mostradas acima. O .riv faz parte de todos os planos pagos (a partir de US$ 20/mês).

Diagrama de uma state machine do Rive: um estado de entrada, dez estados de loop escolhidos por um número mood e estados de reação única por trigger que voltam ao loop atual
Como um .riv do Charmotion é conectado: o número mood escolhe um loop, e os triggers tocam reações únicas a partir de qualquer estado.

Como rodar uma animação Rive em uma extensão do Chrome com Manifest V3

  1. Instale o runtime. Instale uma versão exata de @rive-app/canvas e empacote o script do popup.
  2. Copie o WASM. Copie rive.wasm e rive_fallback.wasm de node_modules para a pasta da extensão.
  3. Aponte o runtime para eles. Chame RuntimeLoader.setWasmUrl e setWasmFallbackUrl com chrome.runtime.getURL antes de new Rive().
  4. Inclua o .riv no pacote. Coloque o .riv no pacote e carregue-o com chrome.runtime.getURL.
  5. Defina a CSP. Adicione script-src 'self' 'wasm-unsafe-eval'; object-src 'self'; em content_security_policy.extension_pages.
  6. Verifique a rede. Abra o DevTools do popup e confirme que nenhuma requisição vai para unpkg ou jsDelivr.

Perguntas frequentes

Minha extensão pode baixar arquivos .riv de um servidor?

As regras do Chrome sobre código remoto se aplicam a código executado, e o guia diz que dados como JSON ou CSS não são código remoto. Um .riv é principalmente dados, mas arquivos que usam scripting do Rive contêm código, e um arquivo incluído no pacote funciona offline e evita questionamentos na revisão. Inclua-o no pacote, a menos que você realmente precise atualizar o conteúdo remotamente.

Por que meu popup com Rive está minúsculo ou vazio?

O Chrome dimensiona o popup de acordo com o conteúdo, entre 25×25 e 800×600 pixels. Um canvas sem dimensões em CSS dá pouco para medir. Defina largura e altura explícitas no canvas e chame resizeDrawingSurfaceToCanvas() em onLoad para que o buffer de desenho corresponda.

Preciso de web_accessible_resources para o rive.wasm?

Não para páginas da extensão, como o popup ou o painel lateral, que carregam os próprios arquivos do pacote. Você precisa de web_accessible_resources quando um site precisa carregar algo da sua extensão, como uma página da extensão injetada como iframe em um site.

Respostas rápidas deste guia

Fontes

Guias relacionados