O desenvolvimento de aplicações web e mobile modernas frequentemente envolve o uso de monorepos, que permitem compartilhar código entre diferentes projetos. Ferramentas como o pnpm (performant npm) têm ganhado popularidade devido à sua eficiência e capacidade de otimizar o gerenciamento de dependências em monorepos. No entanto, a integração do pnpm com o React Native, especialmente com o bundler Metro, pode apresentar desafios inesperados. Este artigo explora um problema comum enfrentado por desenvolvedores que utilizam essa combinação e oferece uma solução prática para garantir um fluxo de trabalho suave.
O Desafio: pnpm e Metro no React Native
Ao trabalhar com um monorepo que inclui um projeto React Native e utiliza o pnpm para gerenciar as dependências, é possível encontrar erros relacionados à resolução de módulos. Um erro comum é o "Unable to resolve module @babel/runtime/helpers/interopRequireDefault", mesmo que o módulo @babel/runtime esteja instalado e presente na pasta node_modules do projeto React Native.
Este problema surge devido à forma como o pnpm organiza as dependências. Diferentemente do npm ou yarn, o pnpm não instala as dependências de forma "plana" na pasta node_modules. Em vez disso, ele cria um diretório virtual sob .pnpm/ e utiliza symlinks (links simbólicos) para conectar as dependências. Essa abordagem otimiza o espaço em disco e evita problemas de dependências duplicadas, mas pode confundir o Metro, que espera uma estrutura de node_modules mais tradicional.
Entendendo a Estrutura do pnpm
Para ilustrar, considere a seguinte estrutura de diretórios:
mobile/node_modules/@babel/runtime
Em um projeto pnpm, este diretório não contém os arquivos reais do @babel/runtime. Em vez disso, ele é um link simbólico que aponta para:
../../node_modules/.pnpm/@[email protected]/node_modules/@babel/runtime
O Node.js geralmente consegue seguir esses links sem problemas, mas o resolver do Metro, por vezes, não. Ele pode subir diretórios até encontrar um diretório node_modules, acabando por carregar módulos da raiz do workspace em vez da cópia local da aplicação mobile, causando o erro.
Soluções Tentativas e Frustrações
Antes de encontrar a solução ideal, muitos desenvolvedores tentam diversas abordagens para contornar o problema. Algumas das tentativas mais comuns incluem:
- Desabilitar o hoisting: Configurar o
hoistPatternpara um array vazio no arquivo.npmrc. - Isolar linkers: Utilizar a configuração
node-linker=isolated. - Hoisting "vergonhoso": Ativar a opção
shamefully-hoist=true.
Embora essas abordagens possam parecer promissoras, elas frequentemente causam outros problemas. Desabilitar o hoisting pode quebrar a construção do projeto mobile ou impedir que os pacotes compartilhados sejam corretamente linkados. O hoisting "vergonhoso", embora às vezes funcione, pode levar a comportamentos inesperados e é geralmente considerado uma solução de último recurso.
A frustração surge quando, após várias tentativas, nenhuma dessas soluções resolve o problema de forma consistente e confiável.
A Solução Definitiva: Adaptando o Metro ao pnpm
A verdadeira solução reside em adaptar o Metro para entender a estrutura de diretórios do pnpm, em vez de forçar o pnpm a se comportar como o npm. Isso pode ser feito configurando o arquivo metro.config.js do projeto React Native.
O arquivo metro.config.js permite personalizar o comportamento do Metro, incluindo a forma como ele resolve os módulos. Para resolver o problema de resolução de módulos com o pnpm, adicione a seguinte configuração ao arquivo metro.config.js:
const path = require('path');
const { getDefaultConfig, mergeConfig } = require('@react-native/metro-config');
/**
* Metro configuration for pnpm monorepos
* (do-not-stop project)
*
* - Lets Metro follow pnpm's symlinks
* - Always prefers the mobile app's own node_modules
* - Watches the monorepo root so shared packages rebuild correctly
*/
const defaultConfig = getDefaultConfig(__dirname);
const config = {
watchFolders: [path.resolve(__dirname, '..')],
resolver: {
...defaultConfig.resolver,
unstable_enableSymlinks: true,
unstable_enablePackageExports: true,
extraNodeModules: new Proxy(
{},
{
get: (_target, name) =>
path.join(__dirname, 'node_modules', String(name)),
}
),
},
};
module.exports = mergeConfig(defaultConfig, config);
Essa configuração faz o seguinte:
watchFolders: Adiciona o diretório raiz do monorepo à lista de diretórios observados pelo Metro. Isso garante que o Metro detecte mudanças nos pacotes compartilhados.unstable_enableSymlinks: true: Permite que o Metro siga os symlinks criados pelo pnpm.unstable_enablePackageExports: true: Habilita o suporte a package exports, uma feature moderna do Node.js.extraNodeModules: Cria um proxy que redireciona as requisições de módulos para a pastanode_moduleslocal do projeto React Native. Isso garante que o Metro sempre prefira os módulos instalados localmente em vez dos módulos da raiz do workspace.
Com essa configuração, o Metro consegue entender a estrutura de diretórios do pnpm e resolver corretamente os módulos, eliminando o erro "@babel/runtime".
Utilizando Pacotes Compartilhados
Uma das vantagens de usar um monorepo é a capacidade de compartilhar código entre diferentes projetos. Com a configuração do Metro descrita acima, é possível importar e utilizar pacotes compartilhados no projeto React Native sem problemas.
O watchFolders inclui a raiz do repositório, permitindo que o Metro detecte alterações em pacotes compartilhados como packages/shared-auth/. Para um controle mais refinado, você pode especificar os diretórios dos pacotes compartilhados individualmente:
watchFolders: [
path.resolve(__dirname, '..', 'packages', 'shared-auth'),
path.resolve(__dirname, '..', 'packages', 'utils'),
],
Conclusão
A integração do pnpm com o React Native pode apresentar desafios iniciais, mas a solução reside em adaptar as ferramentas para coexistirem harmoniosamente. Ao configurar o Metro para entender a estrutura de diretórios do pnpm, é possível resolver problemas de resolução de módulos e aproveitar os benefícios de um monorepo sem comprometer a estabilidade e o desempenho do projeto React Native.
No futuro, esperamos que as ferramentas de desenvolvimento web e mobile se tornem mais inteligentes e capazes de lidar automaticamente com diferentes estruturas de diretórios e sistemas de gerenciamento de dependências. Isso simplificará o desenvolvimento de aplicações multiplataforma e permitirá que os desenvolvedores se concentrem na criação de recursos e funcionalidades inovadoras, em vez de gastar tempo resolvendo problemas de configuração.
A adoção de abordagens como a descrita neste artigo não apenas resolve problemas imediatos, mas também contribui para um ecossistema de desenvolvimento mais flexível e adaptável.