Menu
6/9 Boas práticasTodas as páginas

Tutoriais · Plugins

Boas práticas e armadilhas

Sem dependências em runtime, pasta de dados, logs, idempotência e as armadilhas que mais derrubam plugins.

Nesta página

Sem dependências em runtime

O plugin precisa funcionar só com os arquivos que você publicou, num Node “puro”. Nenhuma origem de instalação roda o seu build nem entrega node_modules completo: a origem npm instala o pacote numa pasta temporária, com --ignore-scripts, e copia só a pasta do pacote; git, zip e folder copiam exatamente o que estiver lá.

  • Use o que o Node traz: fetch global, node:readline, node:fs/promises, node:net, node:tls, node:crypto e, no Node 22+, node:sqlite.
  • Escreveu em TypeScript ou precisa de uma biblioteca? Compile/empacote antes de publicar e versione ou publique o resultado (por exemplo dist/): é o que o plugin ClickUp faz, com o dist/ compilado no repositório, para que um import via git funcione sem nenhum passo de build.
  • Não use scripts de instalação (preinstall, install, postinstall): eles não rodam na instalação pelo app, e o Marketplace avisa quando encontra algum.
  • Declare o Node mínimo em engines; o command: "node" vem do PATH do usuário.
HTTP só com fetch, com timeout
async function getJson(url, token) {
  const res = await fetch(url, {
    headers: { authorization: `Bearer ${token}` },
    signal: AbortSignal.timeout(10_000), // nunca espere I/O sem timeout
  })
  if (!res.ok) throw new Error(`HTTP ${res.status} em ${new URL(url).pathname}`) // sem query, sem token
  return res.json()
}

Estado só em ADE_PLUGIN_DATA_DIR

A pasta do plugin (userData/plugins/<id>) é descartável: reimportar ou atualizar apaga a pasta inteira e copia a nova. Qualquer arquivo que o seu sidecar escreva ali some na próxima atualização. O lugar correto para estado, cache e temporários é ADE_PLUGIN_DATA_DIR — o app cria a pasta antes de iniciar o processo e a reimportação não mexe nela.

state.mjs — estado com escrita atômica
import { mkdir, readFile, rename, writeFile } from 'node:fs/promises'
import { join } from 'node:path'

const dataDir = process.env.ADE_PLUGIN_DATA_DIR ?? '.ade-data'

export async function loadState() {
  try {
    return JSON.parse(await readFile(join(dataDir, 'state.json'), 'utf8'))
  } catch {
    return { cards: {} } // primeira execução, ou arquivo ilegível: recomeça do zero
  }
}

export async function saveState(state) {
  await mkdir(dataDir, { recursive: true })
  const tmp = join(dataDir, 'state.json.tmp')
  await writeFile(tmp, JSON.stringify(state), 'utf8')
  await rename(tmp, join(dataDir, 'state.json')) // atômico: nunca um arquivo pela metade
}
  • Escrita atômica (arquivo temporário + rename): um crash ou um SIGTERM no meio da escrita não deixa um JSON truncado.
  • Arquivo ilegível ≠ erro fatal. Recomece do zero e deixe a idempotência (abaixo) evitar o trabalho repetido.
  • O fallback ?? '.ade-data' só serve para rodar o sidecar à mão, fora do app.

Logs

  • Use mensagens { type: 'log', level, message }. Só JSON de protocolo no stdout; texto solto ali vira log info, e o stderr vira warn.
  • Cada mensagem é cortada em 500 caracteres e o painel guarda as últimas 50: escreva mensagens curtas, com o essencial (o que, qual id, qual resultado).
  • Não registre a cada evento. Um sidecar tagarela enche o painel e esconde o que importa. Logue transições e falhas.
  • Nunca registre segredos — nem o objeto de configurações inteiro, nem URLs com token na query (repare que o getJson acima só nomeia o caminho).
  • Use error para uma falha que o usuário precisa ver (token inválido) e siga vivo.

Idempotência e replays

O sidecar vai reiniciar: ao salvar configurações, no botão Reiniciar, ao abrir o app, e depois de um crash (com backoff). Cada início traz um hello novo e um board.snapshot de cada missão com quadro. Por outro lado, o que acontece enquanto o processo está parado não é reenviado. Projete para os dois lados:

  • Trate o snapshot como “reconciliar”, não como “tudo é novo”: compare com o que já foi feito (um hash por card, o último at processado) e só aja no que mudou.
  • Prefira chamadas idempotentes no serviço externo (PUT por id em vez de POST).
  • Um card.note repetido aparece repetido na atividade do card — registre no estado o que já anotou.
  • Uma ação disparada por configuração precisa de trava persistida.
onCard — não repete o que já foi feito
import { createHash } from 'node:crypto'

/** Hash só do que importa para o espelho — o mesmo card, o mesmo hash. */
const hashOf = (card) =>
  createHash('sha256')
    .update(JSON.stringify([card.title, card.description, card.columnId]))
    .digest('hex')

export async function onCard(card, state, sync) {
  const seen = state.cards[card.id]
  const hash = hashOf(card)
  if (seen?.hash === hash) return // replay do snapshot / evento repetido: nada a fazer
  await sync(card) // ideal: uma chamada idempotente (PUT por id, não POST)
  state.cards[card.id] = { hash }
}

Ciclo de vida sem sustos

  • Saia em até 5 s ao receber shutdown. Salvar configurações também reinicia o sidecar: trabalho longo em andamento é cortado, então grave o progresso no estado.
  • Não use process.exit(1) para um erro recuperável (rede fora do ar, token errado). Cada saída inesperada conta para o limite de 5 em 10 minutos, depois do qual a integração vira error e para de tentar. Capture, envie um error e espere.
  • Nunca espere I/O sem timeout e nunca deixe uma exceção sem tratamento.
  • Envie ready cedo. Sem ele, o app espera 10 s e segue mesmo assim, com um aviso no log.
  • Filtre com events o que não precisa receber — e lembre que [] significa todos, não nenhum.

Armadilhas mais comuns

Armadilhas e como evitar
ArmadilhaO que aconteceFaça assim
type: "select" em settingsO manifesto é recusado (PLUGIN_INVALID_MANIFEST).Use string e valide.
Depender de node_modulesO plugin chega ao usuário sem as dependências.Empacote em um único arquivo e publique o resultado.
Escrever dentro da pasta do pluginSumiu na próxima atualização.Grave em ADE_PLUGIN_DATA_DIR.
Agir a cada helloA ação repete em cada restart e em cada tentativa após um crash.Trava persistida.
Erro de digitação em contributesIgnorado sem erro: a contribuição não existe.Confira o resumo de contribuições após importar.
events: [] achando que é “nenhum”Recebe todos os eventos.Liste os eventos desejados, ou mission.finished se quiser quase nada.
process.exit(1) em erro recuperávelBackoff e, ao passar de 5 saídas em 10 min, estado error.Capture, envie error, continue.
private: true no package.jsonnpm publish recusa o pacote.Remova a chave antes de publicar.
Logar as configuraçõesSegredos ficam visíveis no painel de log.Logue só o que precisa, sem valores.