Fonte: https://dropbox.tech/frontend/the-great-coffeescript-to-typescript-migration-of-2017
Pré-história: adoção de CoffeeScript
- Em 2012, o Dropbox era uma startup com 150 funcionários.
- ES5 e jQuery dominavam o desenvolvimento JavaScript.
- A codebase consistia de cerca de 100 mil linhas de JavaScript empacotada por simples concatenação de arquivos.
- Dois engenheiros migraram tudo para CoffeeScript em uma semana.
- Em 2013, adotaram o sistema de módulos RequireJS (AMD) já que CommonJS era restrita a Node e, portanto, não projetada para uso no browser.
Rumores de uma mudança de linguagem
- Ao final de 2015, ES6 já tinha mais funcionalidades que CoffeeScript.
- Alguns times começaram a adotar ES6 em projetos isolados.
- A codebase era difícil de manter pois as técnicas de codificação defensiva eram insuficientes e prejudicavam a legibilidade do código.
- Por CoffeeScript ser menos estrita que Python no uso de whitespaces, alguns engenheiros verificavam manualmente o código JavaScript gerado; é citado um bug sério no outono de 2013 causado por um espaço em branco mal-posicionado.
- Uma pesquisa interna em novembro de 2015 verificou que 62% dos desenvolvedores gostariam de mudar a linguagem utilizada.
- Problemas com CoffeeScript apontados:
- Falta de delimitadores
- Syntactic sugar muito opinativo
- Falta de suporte da comunidade
- Leitura difícil pois a sintaxe é muito densa
- Sujeita a erros por causa de ambiguidades sintáticas
- O ferramental de TypeScript parecia melhor que vanilla ES6 + Flow no final de 2015.
- No primeiro semestre de 2016, um engenheiro integrou Babel e TypeScript.
- Tanto o time quanto a codebase (330 mil linhas) haviam crescido significativamente para uma migração indolor.
Um plano de migração otimista
- Foram estabelecidas cinco milestones a serem cumpridas até julho de 2017: M1. Suporte básico a TypeScript com interoperabilidade com CoffeeScript e ferramentas de i18n, lint e testes; M2. Documentar boas práticas e guias de migração e migrar as principais bibliotecas e módulos para TypeScript ser a linguagem padrão de desenvolvimento; M3. Consolidar a milestone anterior migrando o restante de módulos e bibliotecas; M4. Migrar os arquivos mais modificados ao longo do tempo no projeto; M5. Remover o compilador de CoffeeScript.
- M1, M2 e M3 foram executadas no segundo semestre de 2016.
- M4 e M5 foram mais problemáticas; esperava-se que o código ficaria a cargo dos times que o desenvolveram originalmente.
- 20% do time de produto foi alocado para trabalhar nas "fundações" da codebase.
Interoperabilidade de CoffeeScript/TypeScript
- Para cada arquivo de CoffeeScript (
*.coffee
), foi criado um arquivo de declarações (*.d.ts
) exportando os módulos comoany
. - Módulos em TS com
export default value;
eram importados nos módulos AMD como objetos do tipo{ default: value }
; named exports foram migrados com poucos problemas. - Em módulos cujos exports eram dinamicamente determinados, exportou-se todos os identificadores possíveis mas com valores
undefined
dinamicamente determinados.
Banindo novos arquivos em CoffeeScript
- Foi criado um teste automático para barrar a adição de novos arquivos.
- Este teste quebrou quando uma migração paralela para Bazel como sistema de build foi executada, pois a lista de arquivos
*.coffee
estava vazia. - Como aprendizado, testes passaram a fazer asserções em suposições (neste caso, de que a lista nunca seria vazia).
- A redução da whitelist de arquivos permitidos gerava pequenos atrasos no code review.
Experiência inicial: não perdemos o syntactic sugar do CoffeeScript
- A perda de optional chaining e nullish coalescing foi compensada com a adição de tipagem.
Prioridades concorrentes
- No final de 2016, foi criado um time para redesign e reescrita do website em React ("Maestro").
- O time do Maestro não conseguiu cumprir o prazo do primeiro trimestre, entregando ao término do segundo o projeto completo em React e TypeScript.
- A lista de arquivos que deveria ser eliminada na M4 estagnou em 100 arquivos, mas na prática ainda haviam 2000 arquivos em CoffeeScript com manutenção constante.
Adiando a M5
- Interpretou-se erroneamente que os scripts restantes deveriam ser substituídos pelos arquivos compilados, causando problemas com lint e i18n.
- Não ficou claro que o objetivo da M5 era reduzir o custo (já pago) de manter o ferramental de CoffeeScript e TypeScript.
- A migração perdeu seu ETA.
- A estimativa inicial da migração era de 1000 linhas por dia demandando um ano de trabalho de um único engenheiro; na prática, 100 linhas por dia eram convertidas, significando um tempo de 10 anos ou o trabalho de 10 engenheiros em um ano.
- Não havia clareza sobre o que era o trabalho "fundamental" que demandaria 20% do time: se infraestrutura ou pagamento de dívida técnica.
- Uma migração de sistemas em produção para uma nova distribuição do Ubuntu tomou boa parte da mão-de-obra de infraestrutura.
Um novo plano com decaffeinate
- Em janeiro de 2017, foi sugerido o uso de
decaffeinate
para as conversões. - Uma função de ordenação com i18n não-testada quebrou completamente uma página no browser Safari.
- A longa lista de bugs semelhantes no
decaffeinate
obrigou se adotar uma conversão mista (manual e automática) que eventualmente ocasiona erros a nível de semântica do código. - Seis meses depois,
decaffeinate
parecia suficientemente maduro em comparação à conversão manual. - O time entrou em acordo: TypeScript apenas com o tipo
any
era preferível a CoffeeScript não-tipado e habilitaria os times a introduzirem tipos no seu próprio ritmo.
Um plano em duas fases
- A pipeline de migração de código consistia de múltiplas etapas:
- Conversão para JavaScript ES6;
- Transformações com
codemod
, especialmente de funções com binding para arrow functions; - Transformação da API legada de React para JSX;
- Conversão de AMD para módulos ES (especial cuidado com exports);
- Anotação de parâmetros de função com
any
; - Adição de declaração de membros de classes;
- Anotação de componentes com tipos específicos de React;
- Adição de comentário explicado como olhar o código CoffeeScript original através do
git
;
- A etapa final de correção dos tipos envolveu escrever scripts para interpretar os erros vindos do type checking e aplicar as correções automaticamente.
Mantendo o foco
- O compromisso de manter uma ferramenta suficientemente boa tornou trivial resolver manualmente problemas como variáveis mortas.
- Agrupar a quantidade de erros do
tsc
por código de erro gerou uma métrica para a migração e confiança de que estavam na direção correta. - A taxa de erros por arquivo ficou entre 0,5 e 1.
Ganhando confiança nas suas ferramentas
- Bugs pré-existentes eram capturados rodando testes antes e após as conversões.
- Se um erro ocorria em muitos lugares, uma asserção era adicionada à pipeline de migração em vez de realizar correção manual.
- Um bug notável destes é relacionado a CoffeeScript não ter suporte a variable shadowing.
- O código foi inteiramente portado para o strict mode sem problemas como atribuição em propriedades read-only.
- As primeiras conversões em massa foram feitas nos testes automáticos.
- Uma boa configuração do itest permitia checar rapidamente quais modificações eram a origem dos problemas.
- Foi importante ter rigor na escrita das traduções de código, cobrindo todos os corner cases e explicitando quais casos eram ignorados.
A última parte
- Nas últimas semanas as ferramentas eram capazes de converter entre 100 e 200 arquivos numa única passagem.
- Um dos truques para iterar rapidamente é rodar o typecheck incremental (
tsc --noEmit --watch
). - O número de arquivos em CoffeeScript era mantido atualizado num quadro branco.
- Apenas dois bugs entraram em produção.
- Ao time mais resistente à mudança foi prometido que erros ficariam a cargo dos engenheiros envolvidos na migração.
- Ao se admitir como produto um código TypeScript não-idiomático, foram gastos 2 meses de trabalho de três engenheiros (cerca de 19 engenheiros-semana).
- "Nós devemos economizar nosso capital político e organizacional com trabalhos que não podemos automatizar para todos"; assim, a conversão manual foi descartada.
- Atualmente, o time da Dropbox mantém 2kk linhas de TypeScript.