Transformando seu código de negócio em biblioteca versionada

Um problema comum no Frontend é o gerenciamento e organização do código e as últimas tecnologias nos últimos anos nos ajudam a resolver.

Caso

Tenho um código que depende de mapas em várias telas do sistema e organizei um componente que pode ser importado em vários arquivos. Como esse componente tem um ciclo de desenvolvimento diferente, organizei como submódulo no git.

mapa1

Para facilitar, os devs importam o projeto principal com parâmetro pra já carregar os submódulos, mas no final de contas fica tudo num mesmo lugar.

git clone --recursive -j8 [email protected]:cmilfont/beerswarm.git

Problema

O código está blindado para importar as dependências sem se preocupar com estrutura e organização, já que nos foi dada com a nova abstração na linguagem — transpilada pelo Babeljs — para funcionar em todos os navegadores, algo como:

import { BMap, Toolbar, GoogleMutant, GoogleApiLoader } from '/components/maps';

O problema mesmo é no ciclo de versão desses componentes. Se um desenvolvedor move um método de lugar e o projeto não tem 100% de cobertura em testes (se tratando de legado é o normal e esperado) corre o risco provável de detectar apenas em produção.

Problema dobrado, uso o Leaflet como gerenciador de mapas, cada fornecedor representa uma camada (layer) a ser exibido, o default é o Open Street Maps.

Para usar o mapa do Google teria que implementar uma layer e carregar a API, causou o problema com outra tela que precisava além do Mapa também carregar a biblioteca de places que faz parte dos opcionais. Daí nasce o submódulo do submódulo pra gerenciar.

Problemas secundários que também tomam tempo da equipe: merges desmotivantes.

Solução

Transformar esses códigos genéricos em bibliotecas com seu próprio ciclo de versionamento usando o próprio gerenciador de pacotes do Nodejs, o npm, como fazem as bibliotecas OpenSource da linguagem.

Hospedando o projeto da lib

Começamos por isolar a Layer do Google a ser usada no Leaflet como uma lib.
Usando o create-react-app (troque pelo seu boilerplate preferido) eu criei um projeto novo https://github.com/produtoreativo/react-leaflet-googlemutant, adicionei repositorio no github.

Formato 1: Open Source
Se não tem diferencial pro meu negócio (como uma Layer para o mapa do Google) o melhor é deixar o mundo ajudar a manter, reservando um repositório no npmjs.com.

Formato 2: Privado no npmjs.com
O registro global de libs no nodejs tem planos pagos para libs privadas, tem também que colocar private: true no package.json do projeto. Conveniente por ser a mesma coisa do OpenSource, mas talvez um preço salgado dependendo do tamanho da sua companhia.

Formato 3: Privado e hospedado in-house.
Caso queira hospedar seu próprio repositório de bibliotecas, pode usar uma opção como o Nexus e apontar no package.json o caminho como a própria documentação da ferramenta ensina.

Formato 4: Privado sem gerenciador de repositórios de libs
Caso ainda não queira configurar um servidor privado ou pagar o registro público global, pode também indicar no package.json o caminho ao github diretamente, algo como:

{
  "name": "beerswarm",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    /* libs ... */
    "react-leaflet-googlemutant": "github:produtoreativo/react-leaflet-googlemutant",
  },
  "devDependencies": {
    "react-scripts": "0.9.5",
  },
  "scripts": {
    "start": "NODE_PATH=src react-scripts start",
    "build": "NODE_PATH=src react-scripts build && sw-precache --config=sw-precache-config.js",
    "test": "NODE_PATH=src react-scripts test --env=jsdom --coverage",
    "eject": "react-scripts eject"
  }
}

Os devs que tiveram em uma máquina com permissão para puxar desse repositório no seu fornecedor git bastarão executar o npm install (ou yarn install) para ter as libs no projeto principal como dependência.
Lembrando que esse último caso de apontar diretamente para o github (ou gitlab) só funciona — como demonstra a própria documentação — informando qual commit ou branch no final da url com o pattern #commit-ish.

A versão informada no package.json fica só simbólica nesse caso e não define que versão real foi puxada.

Estrutura da Lib

Estrutura da Lib

Com o seu boilerplate preferido — no meu caso o create-react-app — crie a estrutura do seu projeto e reserve uma pasta (sugestão, chamei de lib) para o resultado final que será empacotado.
Basicamente precisamos configurar adequadamente o package.json e usar um Module Bundler (construtor que vai transformar seu código em um projeto executável no navegador) como Webpack.

Quero transformar o import em algo como:

import { GoogleMutant, GoogleApiLoader } from 'react-leaflet-googlemutant';

Configurando o package.json

Duas coisas importantes a configurar logo de início, o número de versão da lib e de forma recomendável o script que vai ser carregado quando essa lib for importada.

{
  "name": "react-leaflet-googlemutant",
  "description": "React leaflet wrapper to GoogleMutant plugin",
  "version": "0.1.6",
  "main": "./lib/react-leaflet-googlemutant.js",
  "private": false
}

Ou seja, quando o usuário faz um import from ‘lib’. Essa Lib vai ser carregada no que você informar em main.

Utilizei a lib standard-version para gerenciar as mudanças do número de versão de forma automática.
Outro fator importante de configuração é definir o que vai ficar em dependencies, devDependencies ou peerDependencies. Lembrando que para o arquivo final que será importando num projeto Frontend você pode fazer o ajuste tranquilamente no Webpack.

Configurando o Webpack
Além de instalar com yarn add -D webpack, criei um arquivo webpack.conf.js na raiz da pasta para iniciar a configuração.
Para importar as classes que estão no caminho src/components/ eu criei um index.js que exporta todas as interfaces públicas dessa lib.

export { default as GoogleApiLoader} from 
'components/googleapiloader.js';
export { default as GoogleMutant } from 
'components/googlemutant.js';

Ou seja, você pode importar a Lib inteira como

import ReactLeafletGoogleMutant from 'react-leaflet-googlemutant';

ou as duas classes exportadas como no código mostrado anteriormente:

import { GoogleMutant, GoogleApiLoader } from 'react-leaflet-googlemutant';

Esse arquivo de export vai ser nosso Entry Point. A ponte de entrada para todos os componentes da lib.

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: "./src/react-leaflet-googlemutant/index.js"
};

Definimos as referências dinâmicas para os locais que o código deve encontrar as fontes já que estamos usando caminhos absolutos a partir de um source (src).

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: "./src/react-leaflet-googlemutant/index.js",
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  }
};

Em seguida definimos qual vai ser aquela saída que os projetos importarão:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: "./src/react-leaflet-googlemutant/index.js",
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },
  output: {
    path: path.join(__dirname, 'lib'),
    filename: "react-leaflet-googlemutant.js",
    library: "ReactLeafletGoogleMutant",
    libraryTarget: "amd"
  }
};

Escolhi o formato amd como padrão da biblioteca gerada por ser um dos mais comuns que funciona para todos os ambientes, seja web ou node, o que facilita testes e integrações.

Um fator importante é definir as dependências e não levar junto no build gerado, exemplo, esse projeto de lib assume que o React e o Leaflet vai existir para ele funcionar, não faz sentido empacotar já que os projetos já o farão, você pode definir o que é externo:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: "./src/react-leaflet-googlemutant/index.js",
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },
  output: {
    path: path.join(__dirname, 'lib'),
    filename: "react-leaflet-googlemutant.js",
    library: "ReactLeafletGoogleMutant",
    libraryTarget: "amd"
  },
  externals: {
    react: {
        root: 'React',
        amd: 'react'
    },
    'react-dom': {
        root: 'ReactDOM',
        amd: 'react-dom'
    },
    'prop-types': {
      root: 'PropTypes',
      amd: 'prop-types'
    },
    'react-leaflet': {
      amd: 'react-leaflet'
    },
    'leaflet': {
      amd: 'leaflet'
    }
  }
};

Existem várias preferências sobre qual formato de source map (aqui definido pela propriedade devtool), adotei qualquer uma porque essa lib é intermediária e não componente final a ser aplicado em projeto:

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: "./src/react-leaflet-googlemutant/index.js",
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },
  output: {
    path: path.join(__dirname, 'lib'),
    filename: "react-leaflet-googlemutant.js",
    library: "ReactLeafletGoogleMutant",
    libraryTarget: "amd"
  },
  externals: {
    react: {
        root: 'React',
        amd: 'react'
    },
    'react-dom': {
        root: 'ReactDOM',
        amd: 'react-dom'
    },
    'prop-types': {
      root: 'PropTypes',
      amd: 'prop-types'
    },
    'react-leaflet': {
      amd: 'react-leaflet'
    },
    'leaflet': {
      amd: 'leaflet'
    }
  },
  devtool: 'sourcemap'
};

Por fim configurei um plugin para aplicar variáveis de ambiente (vou colocar o Version Number da lib dentro do código) e o babel configurado para stage 2 e praticamente traduzindo apenas ES7, já que espero não usar diretamente no navegador e sim já incluído na tradução de outra lib/projeto. Ler mais sobre configuração do Babel nesse artigo.

var path = require('path');
var webpack = require('webpack');

module.exports = {
  entry: "./src/react-leaflet-googlemutant/index.js",
  resolve: {
    modules: [path.resolve(__dirname, "src"), "node_modules"]
  },
  output: {
    path: path.join(__dirname, 'lib'),
    filename: "react-leaflet-googlemutant.js",
    library: "ReactLeafletGoogleMutant",
    libraryTarget: "amd"
  },
  externals: {
    react: {
        root: 'React',
        amd: 'react'
    },
    'react-dom': {
        root: 'ReactDOM',
        amd: 'react-dom'
    },
    'prop-types': {
      root: 'PropTypes',
      amd: 'prop-types'
    },
    'react-leaflet': {
      amd: 'react-leaflet'
    },
    'leaflet': {
      amd: 'leaflet'
    }
  },
  devtool: 'sourcemap',
  plugins: [
    new webpack.EnvironmentPlugin(['__VERSION__'])
  ],
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /(node_modules|bower_components)/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: ["react", "stage-2"],
            plugins: [
              ['transform-runtime', {
                "helpers": false,
                "polyfill": false,
                "regenerator": false,
                "moduleName": "babel-runtime"
              }]
            ]
          }
        }
      }
    ]
  }
};

Criei uma task script no package.json chamada transpile com o comando webpack e o resultado final fica extremamente enxuto (já que não transpila classes e outras features do es6 também).

webpack
Criei um bash script pra rodar todas as tasks necessárias pra gerar uma versão, chamado publish.sh

#!/bin/sh -x

yarn release #incrementa a versão, consultar a lib standard-version
VERSION=`node -e "console.log(require('./package.json').version);"`
NODE_ENV=production __VERSION__=$VERSION yarn transpile
git commit -a -m $VERSION
git push --follow-tags origin master
npm publish

Debugger e atualizações

Uma realidade é que precisamos muitas vezes debugar o código dessa sublib enquanto detectamos alguma evidência de problema (atire o primeiro unit test que nunca fez isso), além de atualizar a versão conforme novas necessidades no projeto principal.

Estamos trabalhando no BeerSwarm 1.0 que precisará da versão v0.1.9 do react-leaflet-googlemutant que ainda se encontra na v0.1.8.

Para tornar isso possível o npm fornece um mecanismo simples para linkar um package local e fazer referência direta.

Na raiz do projeto, execute o comando npm link.

[cmilfont@MacBook-Pro-de-Christiano:/Users/cmilfont/projetos/react-leaflet-googlemutant:master:a635622:]
$ npm link
/Users/cmilfont/.nvm/versions/node/v8.0.0/lib/node_modules/react-leaflet-googlemutant -> /Users/cmilfont/projetos/react-leaflet-googlemutant

Agora no projeto da lib você vai executar a task transpile com o parâmetro -w (de watch) pra ficar observando todas as alterações:

webpack2

Toda alteração no source muda automaticamente no Webpack que produz aquela saída que planejamos.

No projeto que importa essa lib rode o mesmo comando, mas indicando qual lib foi linkada npm link react-leaflet-googlemutant.

E rode o seu toolset de desenvolvimento hot-loader, no meu caso o yarn start para o create-react-app, todas as modificações na lib original causarão um “touch” no projeto que linkou causando a imediata recompilação.

Ao final de desenvolvimento execute o script bash de geração da nova versão, coloque o número correto no package.json que vai receber (um yarn upgrade nome-da-lib resolve) e evite quebrar o código de outros projetos que continuarão com a versão anterior.

Espero que esse pequeno tutorial tenha sido útil como foi pra mim, quaisquer dúvidas comenta ou manda issues no github 🙂

Fonte: Medium

Gostou do conteúdo? Tem alguma dúvida? Entre em contato com nossos Especialistas Mandic Cloud, ficamos felizes em ajudá-lo.