Clones em node_modules te deixaram mal? Elimine a duplicação do seu sistema de arquivos

(14 de março de 2019)

Quantas cópias de [email protected] seriam você gosta? (Foto de 浮萍 闪电 em Unsplash )

ATUALIZAÇÃO: a implementação descrita abaixo tem problemas com arquivos que possuem dependências (declarações require / import ) Devido à estrutura de node\_modules/, um arquivo idêntico em dois locais pode obter versões diferentes de suas próprias dependências, portanto, para ser mais seguro, você deve se limitar a desduplicar arquivos com zero dependências.

Um dos problemas que encontramos na Tradle em vários grandes projetos de aplicativos Node.js / React Native / web é que node\_modules acaba com cópias duplicadas de certas dependências. Quando o aplicativo é inicializado, ele analisa, executa e, em seguida, desperdiça memória no mesmo módulo várias vezes.

No Node.js antigo normal, geralmente não há uma penalidade de desempenho grande o suficiente para reclamar, mas ainda dói saber que está acontecendo. No celular / web, choramingar é obrigatório. A certa altura, acho que tínhamos 10 cópias de uma versão de readable-stream em nosso aplicativo React Native e não sou bom em emojis, então vou deixar Wat dizer isso por mim:

Wat ensina inglês a Chaucer

Quer sentir a dor? Aqui está um exemplo de estrutura de dependência que causa o problema:

# assume for simplicity that deps specify exact versions, i.e.:
# myProject"s package.json specifies "lodash": "4.0.0"
# depA and depB"s package.json specifies "lodash": "3.0.0"myProject/
node\_modules/
lodash # 4.0.0
depA
node\_modules/lodash # 3.0.0
depB
node\_modules/lodash # 3.0.0 <- not cool

Seu projeto depende de [email protected], mas dois de seus dependências dependem de [email protected]. Seu gerenciador de pacotes não pode desduplicar [email protected] içando-o até a raiz porque [email protected] está lá. Então, você acaba com duas cópias de [email protected], uma em depA e uma em depB . Eles são idênticos. E não há nada que você possa fazer sobre isso, a menos que você tenha tempo para enviar PRs aos autores de depA e depB para atualizá-los para [email protected] (PRs que podem levar meses para serem mesclados) e espere que sejam mesclados. Isso ou manter garfos de ambos os módulos. Então você vai resolver outro problema.

Não sei como ele sabia de tudo o há muito tempo

Parabéns! Agora você adiou o problema de desduplicação para seu bundler. Webpack, Browserify e Metro agora precisam resolvê-lo independentemente. Aqui está o problema aberto no Webpack. Ele está aberto desde 2017 e, se eu fosse um Webpack, ficaria chateado com as pessoas que tentam torná-lo meu problema.

Esse problema não é novo e tem sorrido pacientemente como muitos encarnações de gerenciadores de pacotes e bundlers para Node.js e o navegador exibido por. Provavelmente ainda existirá por mais algum tempo, como uma barata digital vigorosa.

Felizmente, com os recentes cortes de impostos, agora podemos acender a luz no fim do túnel. (Mas apenas para os ricos).

A luz no fim do túnel

Para podar as cópias idênticas das dependências no pacote do aplicativo React Native do Tradle, decidimos não tentar adicionar suporte ao Metro Packager ou apelar para a equipe do Metro Packager. Também fornecemos nosso aplicativo com React Native for Web, que se agrupa com Webpack, e queríamos um truque que funcionasse para ambos.

Decidimos direcionar o que todos os gerenciadores de pacotes e empacotadores têm em comum: Algoritmo resolve() do Node.js. Este é o algoritmo que é executado quando você executa require("some-module"), para encontrar o caminho absoluto de um módulo em seu sistema de arquivos. Tanto os gerenciadores de pacotes quanto os bundlers contam com esse algoritmo, o primeiro a construir a node\_modules estrutura de diretório, o segundo a percorrê-la para construir o bundle.

A solução é simples: atravesse node\_modules, identifique todas as duplicatas, escolha uma delas como canônica (por exemplo, a primeira em ordem alfabética) e reescreva os arquivos *.js em todos os outros apontam para o canônico. Os *.js arquivos nas pastas duplicadas ficam assim:

// deduped by dedupe-deps
module.exports = require("../../../changes-feed/node\_modules/readable-stream/duplex.js")

Parece um pouco sujo, mas ele funciona e torna nosso pacote menor e, portanto, mais rápido de carregar. O módulo é de código aberto, sinta-se à vontade para experimentá-lo: https://github.com/tradle/dedupe-deps

Deve haver uma maneira melhor

Alguns de vocês provavelmente estão revirando os olhos agora e gritando “basta usar pnpm!“Cuidado, revirar os olhos pode ser viciante. E você não quer saber o que eles vão prescrever para diminuir a retirada de revirar os olhos.

pnpm é um empacotador relativamente novo em Cidade. Ele usa uma combinação de links físicos e links simbólicos para garantir que um módulo esteja presente apenas uma vez em um determinado node\_modules do projeto. Na verdade, ele garante que estará presente apenas uma vez em todo o seu sistema de arquivos! Isso é muito legal. Algum dia todos nós usaremos pnpm e tudo funcionará magicamente, mas tenho a sensação de que hoje ainda haverá problemas de compatibilidade com alguns empacotadores.

… Sim, acabei de verificar e ainda é um problema no Metro, embora pareça que eles estão se aproximando de uma solução: https://github.com/facebook/metro/ pull / 257

Indo um passo adiante

Então, de volta à nossa solução baseada em sistema de arquivos. Podemos dar um passo adiante e matar mais pássaros com a mesma pedra. Sem dúvida, são pássaros ainda maiores do que o original. Vamos imaginar que temos esta árvore de dependência:

myProject/
node\_modules/
lodash # 4.0.0
depA
node\_modules/lodash # 3.0.5
depB
node\_modules/lodash # 3.0.0 <- not cool

Aqui temos duas versões diferentes de [email protected]. No entanto, dependendo dos intervalos semver especificados para lodash em depA e depB, eles podem muito bem ser desduplicáveis. Atualmente, o módulo dedupe-deps não aborda esse cenário, mas estamos felizes em aceitar PRs.