O primeiro app em Node
Ao terminar este guia você tem um app Node de pé na sua máquina, instalado num servidor VSSH e
aberto pelo menu do ambiente, com log estruturado, healthcheck e a ponte com o shell carregada. É
o mesmo app do guia em Python, com as mesmas peças e as mesmas rotas;
a escolha entre os dois templates é de linguagem, e mais nada. O ponto de partida é
templates/hello-vssh-app-node/ deste repositório.
Os nomes de verbo deste guia são os da referência gerada, a mesma que o SDK servido pelo sistema expõe.
1. O pacote
meu-app/
vssh-app.json
icon.svg
package.json sem dependência: as libs de backend são o runtime do servidor
frontend/
index.html
backend/
server.js o entrypoint; escuta no socket unix de $VSSH_APP_SOCKET
git clone https://github.com/colabhd/vssh-sdk
cp -r vssh-sdk/templates/hello-vssh-app-node ~/meu-app && cd ~/meu-app
O backend importa vssh, o runtime que todo servidor VSSH tem em /opt/vssh/sdk/node e que o
lançador põe no NODE_PATH do app. Nada disso viaja no pacote, e o package.json do template
não declara dependência nenhuma. Uma dependência sua entra ali como em qualquer pacote, e aí o
manifesto ganha um installCommand com npm ci.
2. O manifesto
{
"id": "meu-app",
"name": "Meu App",
"version": "1.0.0",
"icon": "icon.svg",
"category": "Utility",
"backend": {
"runtime": "node",
"entrypoint": "backend/server.js",
"healthcheckPath": "/saude"
},
"window": { "title": "Meu App", "width": 900, "height": 640 }
}
healthcheckPath aponta para o /saude que o portão do runtime responde antes de chamar o app.
A sondagem segura o clique de quem abriu o app, e vai com o token.
Um app com dependências próprias tem duas formas de levá-las ao servidor, e a diferença é só
quem faz a cópia: vendorizar (rodar npm ci uma vez e commitar node_modules/; o publish
empacota o que está versionado, por git archive, então a pasta entra no tarball) ou deixar
node_modules/ no .gitignore e reconstruir no alvo por um installCommand como
( [ "${VSSH_APP_REBUILD:-}" != 1 ] && test -d node_modules ) || npm ci --omit=dev, que roda
uma vez como root no install e uma por usuário no primeiro run.
3. O backend
'use strict';
const http = require('node:http');
const path = require('node:path');
const { servidor, web } = require('vssh');
const log = servidor.criarLog();
const spa = web.spa(path.join(__dirname, '..', 'frontend'), { tuff: true, aoAvisar: log });
// Base só para o parsing de URL relativa; este host nunca vai à rede. Num socket unix não existe
// porta, e uma base montada com `${PORT}` estouraria em toda requisição.
const BASE_URL = 'http://vssh-app.invalid';
const server = http.createServer(servidor.portao(async (req, res) => {
const url = new URL(req.url, BASE_URL);
try {
if (await spa(req, res, url)) return;
servidor.responderJson(res, 404, { error: 'Rota desconhecida.' });
} catch (err) {
log('request-failed', { path: url.pathname, message: err.message, stack: err.stack });
if (!res.headersSent) res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('erro interno\n');
}
}));
servidor.escutar(server)
.then(({ transporte, endereco }) => log('listening', { transporte, endereco }))
.catch((err) => {
if (err.code === servidor.JA_ESCUTANDO) process.exit(0);
log('listen-failed', { message: err.message, code: err.code });
servidor.registrar(`não consegui escutar: ${err.message}`);
process.exit(1);
});
process.on('uncaughtException', (err) => log('uncaught', { message: err.message, stack: err.stack }));
process.on('unhandledRejection', (err) => log('unhandled-rejection', { message: String(err) }));
O que cada peça faz:
servidor.escutar(server)lê$VSSH_APP_SOCKET, limpa um socket órfão por tentativa de conexão, põe o modo0600, anuncia[<id>] versão <v> escutando em <onde>no stdout e falha alto quando não veio endereço.servidor.JA_ESCUTANDOquer dizer que outra instância já atende, e sair com0é o contrato do ciclo de vida. Um app que binde uma porta em127.0.0.1não é proxiado por ninguém: healthcheck000, janela em branco, e nada no log dizendo por quê;servidor.portao(atender)fica na frente de toda rota: recusa o pedido sem oX-Vssh-App-Tokendo ambiente (403 comX-Vssh-Token: recusado, em tempo constante) e respondeGET /saudecom{ok, versao, pid}antes de chamar o app;servidor.criarLog()escreve em$VSSH_APP_DATA_DIR/app.log. Os doisprocess.ondo fim existem porque sem eles uma falha assíncrona derruba o processo sem deixar rastro, e o ciclo de vida só mostra que o app "não subiu";web.spa(raiz, { tuff: true })serve ofrontend/sob o prefixo do proxy e injeta no<head>do index a tag do SDK web (_sdk/vssh.js) e as do Tuff (_sdk/tuff/…). Quem responde esses caminhos é o sistema, de dentro do espaço de URL do app; o backend não os serve, e um pedido a_sdk/recebefalsecomo qualquer rota que não é da SPA. Cada script do próprio app injetado porscripts: [...]sai com o hash do conteúdo na URL, para uma reinstalação nunca servir a versão velha de nenhum cache do caminho.
No frontend, fetch('api/ping') com URL relativa, sem a barra inicial, para funcionar sob
/proxy/app/<id>/.
4. Rodar na sua máquina
source vssh-sdk/scripts/ambiente-de-dev.sh # NODE_PATH e PYTHONPATH apontando para runtime/
VSSH_APP_ID=meu-app VSSH_APP_DATA_DIR=/tmp/meu-app-data node backend/server.js --tcp 127.0.0.1:0
# [meu-app] versão dev escutando em 127.0.0.1:53017
curl -fsS http://127.0.0.1:53017/saude
O --tcp é da bancada: o servidor usa o socket unix de VSSH_APP_SOCKET, e é o que
servidor.escutar abre sem a opção. Roda no Windows também. Acrescente VSSH_APP_TOKEN=segredo
para exercitar o portão: a sondagem do portal vai com o cabeçalho X-Vssh-App-Token, então o
/saude é gateado como qualquer outra rota. O
guia do ambiente de desenvolvimento tem o resto: o SDK web
fora do shell, e o mesmo arranjo no CI.
5. Instalar no servidor
scp -r ~/meu-app servidor:/tmp/meu-app
ssh servidor sudo vssh-app-install /tmp/meu-app --force
O app aparece no menu iniciar na category declarada, sem reiniciar a sessão. O clique sobe o
backend como você, e a janela abre quando o healthcheck responde. Uma versão nova entra com
vssh-app-install <pacote> --force e uma version nova no manifesto; o script encerra as
instâncias que estiverem rodando, e cada pessoa sobe a nova na próxima abertura.
6. A primeira chamada ao ambiente
<script>
vssh.app.capacidades().then((caps) => {
console.log(caps.host, caps.shellVersion, caps.verbos);
});
</script>
caps.verbos é a tabela de exportação do shell em que o app caiu. Um app que precisa de um verbo
que não está na lista degrada ali, em vez de descobrir na primeira chamada que a promessa não
resolve. caps.shellVersion pode ser null, num shell antigo demais para se declarar; trate como
desconhecido.
7. Quando algo falha
~/.vssh-apps/meu-app/run.log (stdout e stderr, "Ver log do backend" no menu da janela) e
$VSSH_APP_DATA_DIR/app.log (o seu log). Os códigos do healthcheck estão no
ciclo de vida.
O que fazer em seguida
O template traz mais do que este guia usa: SSE por eventos.Difusor, o filesystem privado por
dados, bandeja, notificação e atividade por avisos, a GPU concedida por gpu, e a galeria,
uma peça por capacidade do ambiente, com a versão do shell ao lado, que é o que explica quase toda
ausência. Ao copiar para um app seu, apague frontend/galeria.js, as peças do index.html e as
rotas api/* que não forem suas.