Analista de NOC
Como construí um playground de Ruby para o meu blog

Como construí um playground de Ruby para o meu blog

Publicado em04 de março de 2025
Junior RibeiroJota Ribeiro
0Visualizações
7Minutos de leitura

Por que fiz isso

Tenho estudado Ruby e, enquanto explorava a linguagem e suas possibilidades, percebi que seria muito útil ter um ambiente interativo dentro do próprio blog. Eu queria um espaço onde pudesse:
  • Testar trechos de código Ruby em tempo real.
  • Compartilhar exemplos funcionais com os leitores.
  • Registrar a evolução dos meus estudos.
A maioria dos blogs de programação apresenta apenas blocos de código estáticos. Eu queria algo mais dinâmico: um playground em que qualquer pessoa pudesse executar, modificar e experimentar Ruby diretamente no navegador, sem precisar configurar nada localmente.

O resultado final

Antes de entrar nos detalhes técnicos, este é o resultado final:
<RubyPlayground initialCode="puts 'Olá, mundo!'" />
Playground de Ruby integrado ao blog, com editor de código e console de saída.

Tecnologias utilizadas

Estas foram as principais tecnologias usadas no projeto.

Front-end

  • React e TypeScript: para a interface e o gerenciamento de estado.
  • Next.js: como framework do blog.
  • Tailwind CSS: para a estilização.
  • Monaco Editor: o mesmo editor que equipa o VS Code, oferecendo uma experiência de edição mais completa.
  • react-resizable-panels: para criar painéis redimensionáveis.

WebAssembly

  • Ruby WASM: uma versão do Ruby compilada para WebAssembly, por meio do pacote ruby-head-wasm-wasi.
  • @wasmer/wasi: para emular o ambiente WASI no navegador.
  • @wasmer/wasmfs: para fornecer um sistema de arquivos virtual ao ambiente Ruby.

Utilitários

  • LZ-String: para compactar código nos parâmetros da URL.
  • Lucide React: para os ícones da interface.

Como construí o playground

1. Preparando o WebAssembly para executar Ruby

O primeiro desafio foi fazer o Ruby funcionar no navegador. Isso se tornou possível graças ao projeto Ruby WASM, que compila o interpretador da linguagem para WebAssembly.
browserVm.ts
1export class BrowserVm {
2 module: WebAssembly.Module;
3 vm: any;
4
5 async createVm(printToOutput: (line: string) => void) {
6 const response = await fetch(
7 'https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.3.0-2022-04-20-a/dist/ruby+stdlib.wasm'
8 );
9 const buffer = await response.arrayBuffer();
10 this.module = await WebAssembly.compile(buffer);
11
12 const { RubyVM } = await import('ruby-head-wasm-wasi/dist/index');
13 const { DefaultRubyVM } = await import('./browser');
14
15 const { vm } = await DefaultRubyVM(this.module, {
16 consolePrint: true,
17 consoleHandlers: { 1: printToOutput, 2: printToOutput },
18 });
19 this.vm = vm;
20 }
21}
Arquitetura simplificada da execução do Ruby por WebAssembly no navegador.

2. Criando a interface do editor

Para o editor, usei o Monaco Editor, o mesmo componente que serve de base para o VS Code. Com ele, consegui oferecer recursos como destaque de sintaxe, autocompletar e formatação de código Ruby.
CodeEditor.tsx
1const CodeEditor = ({
2 code,
3 onChange,
4 height = '300px',
5 isFullScreen = false,
6}: CodeEditorProps) => {
7 const monaco = useMonaco();
8
9 useEffect(() => {
10 if (monaco) {
11 monaco.languages.registerCompletionItemProvider('ruby', {
12 // Configuração do autocompletar para Ruby
13 // ...
14 });
15 }
16 }, [monaco]);
17
18 return (
19 <MonacoEditor
20 height={isFullScreen ? '70vh' : height}
21 language='ruby'
22 theme='vs-dark'
23 value={code}
24 onChange={onChange}
25 options={editorConfig}
26 onMount={handleEditorMount}
27 />
28 );
29};
Monaco Editor com destaque de sintaxe e autocompletar para Ruby.

3. Construindo o console de saída

Para exibir o resultado da execução, criei um componente de console que captura e formata a saída produzida pelo interpretador Ruby.
ConsoleOutput.tsx
1const ConsoleOutput = ({
2 output,
3 isError,
4 isFullScreen = false,
5}: ConsoleOutputProps) => {
6 return (
7 <div
8 className={clsx(
9 'bg-neutral-900 text-neutral-50 py-3 px-4 overflow-y-auto',
10 isFullScreen ? `h-[70vh]` : 'h-[500px]',
11 isError && `!text-red-400`
12 )}
13 >
14 <code
15 className={clsx('text-sm')}
16 style={{ whiteSpace: 'pre-wrap', overflowWrap: 'break-word' }}
17 >
18 {output}
19 </code>
20 </div>
21 );
22};

4. Reunindo tudo no componente principal

O componente Playground é o centro da aplicação. Ele gerencia o estado, inicializa a máquina virtual do Ruby e coordena a comunicação entre o editor e o console.
Playground.tsx
1const Playground = ({
2 id = undefined,
3 isHeading = false,
4 initialCode,
5}: PlaygroundProps) => {
6 const [code, setCode] = useState<string>(initialCode ?? '');
7 const [output, setOutput] = useState<string>('');
8 const [isError, setError] = useState<boolean>(false);
9 const [isFullScreen, setFullScreen] = useState<boolean>(false);
10 const [isLoading, setIsLoading] = useState<boolean>(true);
11 const vmRef = useRef<BrowserVm | null>(null);
12 const outputBufferRef = useRef<string[]>([]);
13
14 useEffect(() => {
15 const initVM = async () => {
16 setIsLoading(true);
17 try {
18 const vm = new BrowserVm();
19 await vm.createVm((line: string) => {
20 outputBufferRef.current.push(line);
21 setOutput(outputBufferRef.current.join(''));
22 });
23 vmRef.current = vm;
24 } catch (error) {
25 setError(true);
26 } finally {
27 setIsLoading(false);
28 }
29 };
30
31 initVM();
32 }, []);
33
34 const handleRunCode = async () => {
35 // Lógica para executar o código Ruby
36 // ...
37 };
38
39 return (
40 <>
41 {isHeading && <PlaygroundHeader />}
42 <CodePlayground
43 id={id}
44 onFullScreen={handleFullScreen}
45 code={code}
46 output={isLoading ? 'Olá, fã de Ruby... :)' : output}
47 isError={isError}
48 onRunCode={handleRunCode}
49 onSetCode={setCode}
50 onSetOutput={setOutput}
51 />
52 {/* Modal do modo de tela cheia */}
53 </>
54 );
55};
Layout responsivo com painéis redimensionáveis.

5. Desafios encontrados

Durante o desenvolvimento, encontrei alguns desafios importantes.

Gerenciamento de memória

O WebAssembly possui limitações de memória e o interpretador Ruby pode consumir bastante recurso. Foi necessário otimizar o uso de memória e implementar cuidados para evitar vazamentos.

Compatibilidade com bibliotecas Ruby

Nem todas as bibliotecas Ruby funcionam em um ambiente WebAssembly. Precisei adaptar alguns exemplos e garantir que os recursos fundamentais da linguagem estivessem disponíveis.

Desempenho

Executar Ruby por WebAssembly é mais lento do que executar a linguagem nativamente. Adicionei indicadores visuais para deixar claro quando o código está sendo processado e otimizei os pontos possíveis.
Indicador de carregamento durante a inicialização da máquina virtual Ruby.

Recursos interessantes

Compartilhamento de código pela URL

Implementei um sistema que permite compartilhar trechos de código por meio da URL. O LZ-String compacta o conteúdo para manter o endereço em um tamanho razoável.
CodePlayground.tsx
1const queryString = window.location.search;
2const urlParams = new URLSearchParams(queryString);
3urlParams.set('q', LZString.compressToEncodedURIComponent(codeEditor.getValue()));
4urlParams.delete('q2');
5history.replaceState('', '', '?' + urlParams.toString());

Modo de tela cheia

Para oferecer uma experiência mais imersiva, adicionei um modo que expande o playground para ocupar toda a tela.
PanelFooter.tsx
1const PanelFooter = ({
2 isFullScreen,
3 onCloseFullScreen,
4 onFullScreen,
5}: PanelFooterProps) => {
6 return (
7 <div className='flex items-center justify-between bg-neutral-900 border border-neutral-700 border-t-0 py-1 px-2 rounded-b-md'>
8 <div className='text-sm font-sora items-center text-neutral-500'>
9 &copy; <a href={siteMetadata.siteUrl}>{siteMetadata.siteShortTitle}</a>
10 </div>
11 {isFullScreen ? (
12 <Tooltip title='Fechar'>
13 <ExitFullScreenIcon
14 size={22}
15 onClick={onCloseFullScreen}
16 className='text-neutral-500 cursor-pointer'
17 />
18 </Tooltip>
19 ) : (
20 <Tooltip title='Tela cheia'>
21 <FullScreenIcon
22 size={22}
23 onClick={onFullScreen}
24 className='text-neutral-500 cursor-pointer'
25 />
26 </Tooltip>
27 )}
28 </div>
29 );
30};
Playground em modo de tela cheia.

Suporte a gráficos simples

Também adicionei suporte para desenhar gráficos básicos por meio de um buffer de pixels.
ruby.rb
1def set_pixel(x, y, color)
2 JS::eval("frameBuffer[#{y} * 100 + #{x}] = #{color}")
3end
4
5# Desenha um quadrado vermelho
610.times do |x|
7 10.times do |y|
8 set_pixel(x + 10, y + 10, 0xFF0000FF)
9 end
10end
Exemplo de gráfico gerado por um código Ruby.

Integração com o blog

Para integrar o playground ao blog construído com Next.js, criei um componente React que pode ser incluído facilmente em qualquer publicação MDX.
Playground.tsx
1// Exemplo de uso em uma publicação MDX
2<Playground
3 isHeading={true}
4 initialCode={`puts "Olá, mundo Ruby!"
5# Experimente modificar este código
6[1, 2, 3, 4, 5].each do |n|
7 puts n * n
8end`}
9/>

O que aprendi

Este projeto me ensinou bastante sobre:
  1. WebAssembly: como compilar e executar linguagens de programação no navegador.
  2. React avançado: gerenciamento de estados complexos e uso de referências.
  3. Otimização de desempenho: como lidar com operações intensivas no navegador.
  4. Experiência de ferramentas para desenvolvedores: como criar uma interface intuitiva para quem programa.

Próximos passos

Tenho alguns planos para melhorar o playground:
  1. Suporte a gems: permitir o uso de gems Ruby populares.
  2. Persistência: salvar o código no localStorage para continuar de onde o usuário parou.
  3. Exemplos prontos: criar uma biblioteca de exemplos voltados ao aprendizado.
  4. Melhor integração com Rails: adicionar exemplos específicos de Ruby on Rails.

Conclusão

Construir um playground de Ruby para o blog foi um desafio técnico muito interessante, porque uniu meu interesse pela linguagem ao que venho aprendendo sobre desenvolvimento web moderno. O resultado é uma ferramenta que me ajuda a estudar e experimentar Ruby e, ao mesmo tempo, melhora a experiência de quem lê o blog.
Espero que este texto ajude quem esteja pensando em criar algo parecido. O código-fonte completo está disponível no meu GitHub, e sugestões e contribuições são sempre bem-vindas.
Bom código em Ruby! 💎
Esta publicação faz parte da minha jornada de aprendizado em Ruby e desenvolvimento web. Compartilhe suas ideias e sugestões nos comentários.