Debug de uma aplicação em CoffeeScript

O tópico de debug surge com frequência em discussões sobre CoffeeScript. Neste post vou dar um exemplo real de erros utilizando CoffeeScript e a dificuldade (dica: nenhuma) em rastrear a origem.

Este é um projeto de front-end em CoffeeScript, não é dos mais extensos (~1200 linhas) mas suficiente como exemplo. A aplicação está separada em uma dúzia de arquivos:

Estrutura de arquivos coffeescript

Estes são compilados via Cakefile utilizando flour:

task 'build:dev', ->
    flour.minifiers.js = null
    bundle [
        'scripts/util.coffee'
        'scripts/api.coffee'
        'scripts/modules.coffee'
        'scripts/user.coffee'
        'scripts/profile.coffee'
        'scripts/location.coffee'
        'scripts/activity.coffee'
        'scripts/stats.coffee'
        'scripts/main.coffee'
    ], 'resources/app.js'

Neste setup todos os arquivos .coffee são compilados e concatenados em um único arquivo resources/app.js, que é incluido no <head> do HTML. Durante o desenvolvimento a task cake watch se encarrega de re-compilar o JS a cada alteração.

Erros de sintaxe

Sabe aquele erro maldito de js que te faz voltar pro editor pra corrigir um caractere? Seja um ponto-e-vírgula no lugar errado, chave não fechada, vírgula, etc, esses erros praticamente não existem em coffeescript.

Em primeiro, porque a linguagem é muito simples e limpa, sem tantos caracteres de controle; segundo, porque a maioria desses errors são acusados na hora do compile, não da execução. Além disso as mensages de erro do compiler coffeescript são muito claras. Exemplo:

class AppView extends Backbone.View

    ...

    start: ->

        @loadTemplates()
        @root = $('#main'))

Consegue ver o erro? No momento em que o arquivo for salvo, a task watch que está rodando no terminal acusa o erro:

Erro de compilação

Basta ir até a linha em questão e remover o parêntese extra.

Erros de execução

Os erros de execução são a questão que mais preocupa quem ainda não usou coffeescript: receio de não conseguir identificar a linha de código que gerou o erro, pelo fato de o stack trace ser indicado em cima do Javascript compilado, não do fonte.

Vamos ver o quão difícil é na prática. Como exemplo vou usar esta função de inicialização do app:

class AppView extends Backbone.View

    initialize: ->
        @connect()
        @currentUser = new User.Model { key: session.get 'userkey' }

Agora vamos inserir um erro, uma tentativa de acesso a uma propriedade não-existente no objeto User:

    initialize: ->
        @connect()
        @currentUser = new User.model { key: session.get 'userkey' }

(se você não percebeu o erro: Model -> model)

Nesse caso a compilação ocorre normalmente - para o compiler o código está ok, ele não tem conhecimento sobre os objetos no momento da execução. Ao abrir o browser porém a aplicação não carregou:

Tela branca

Uncaught TypeError: undefined is not a function [app.js:1450].

"Linha 1450?? Jamais vou encontrar esse erro.". Na prática o fluxo é o mesmo de debugar javascript. Abra o stack:

Stack trace

A função culpada é App.initialize. É só abrir o arquivo app.coffee, encontrar a definição da função (por memória ou Cmd+F initialize:) e pronto! Se tu está boiando no projeto e não faz a menor idéia de em que arquivo a função está, bom... vai de Find in project ou pede ajuda pra um colega.

Tela branca

Source maps

Erros de lógica exigem reler/interpretar o trecho de código para encontrar o erro - portanto não há nenhuma necessidade de abrir o arquivo app.js compilado. Sendo um pré-processador, é por definição impossível que o compiler CoffeeScript gere erros de lógica (impossível nesse contexto == mais de 5200 linhas de testes).

No exemplo acima, a única diferença em relação a debugar javascript é não termos a linha exata que gerou o erro. Chrome e Firefox já estão implementando uma feature chamada source maps, que permite ao console mapear logs à linha de código do arquivo fonte, tanto para javascript (coffeescript, typescript, etc) quanto css (LESS, Sass, Stylus, etc).

Infelizmente a versão atual do coffeescript (1.4.0) não gera source maps, mas um rewrite já está em andamento e deve se tornar o compiler oficial nos próximos meses.

Node.js

Ao instalar o coffeescript na sua máquina com npm install coffee-script -g, o executável coffee é adicionado ao seu path. Ele nada mais é do que uma invocação do node com o módulo coffee-script carregado. Ou seja, coffee meuapp.coffee é o equivalente a executar:

require('coffee-script') // registra .coffee no require.extensions
require('./meuapp')

Este padrão é comum no deployment de aplicações em serviços como Heroku, dotCloud, nodejitsu, etc - em geral se exige um arquivo server/main/app.js para inicialização.

Como exemplo vou usar o código do jetcraft, jogo que desenvolvi junto com o @vitor42 para o Node Knockout 2013:

# app.coffee
express  = require 'express'
http     = require 'http'

app = express()

app.configure ->
  app.use app.router
  app.use express.static "#{__dirname}/public"
  
app.configure 'development', ->
  app.use express.errorHandler dumpExceptions: true, showStack: true

app.get '/', (req, res) ->
  res.render 'index', nada

Note que a variável nada não foi definida. Como lá em cima ativamos o express.errorHandler com dump e showStack, o erro é enviado para browser além de impresso no console:

Erro do Express no browser

Erro no terminal

Em ambos os casos o erro indica o arquivo .coffee que originou o erro:

ReferenceError: nada is not defined
    at /Users/ricardobeat/Projects/jetcraft/app.coffee:42:32

Assim, não é estritamente necessário ter um script de build ou tarefas de compile/watch. É util no caso de escrever código para ser compartilhado, como uma biblioteca ou módulo do NPM, para distribuição sem a dependência do módulo coffee-script.

Conclusões

Não tenha medo de utilizar CoffeeScript pela questão de debug. As vantagens da linguagem compensam e muito esse detalhe, que está por ser corrigido em breve. Ainda mais considerando o uso de TDD, build scripts também para o front-end (reduzindo o tamanho de cada source), e os padrões atuais de código, não ter a linha exata de um runtime error é a menor das preocupações.

Aprenda mais sobre CoffeeScript no site oficial ou com os livros (grátis):

Siga no twitter: @ricardobeat