Você está desenvolvendo um Design System em um monorepo com Nx e está encontrando dificuldades para compartilhar os estilos CSS entre os projetos?

Neste artigo, falaremos sobre esse problema que pode acontecer com você e fornecer soluções dependendo de qual caso de uso você se encaixa.

Se você nunca ouviu falar sobre esses temas antes, você pode começar com os artigos Design Systems: exemplos práticos e Monorepos descomplicados: explorando o NX!

Abordaremos os seguintes tópicos:

  • O problema da repetição de código CSS;
  • Programação defensiva com valores fallback de variáveis CSS;
  • Configurações de projetos Nx para inclusão de novos arquivos;
  • Publicação de bibliotecas no NPM.

Vamos nessa?

O problema: repetição do código CSS

Digamos que você esteja utilizando o Nx para construir uma biblioteca para cada componente do seu Design System, resultando em um biblioteca apenas para o botão, outra apenas para um input, outra apenas para o modal, e assim por diante. Essa abordagem permite que o Design System fique modularizado em diferentes bibliotecas e pacotes, otimizando os componentes para quem for consumi-los.

A versão inicial do CSS do seu botão está assim:

button {
  --ab-primary-color: #2d5bff;
  --ab-white: #fff;
  --ab-border-radius-botao: 0.5rem;

  background: var(--ab-primary-color);
  color: var(--ab-white);
  border-radius: var(--ab-border-radius-botao);
  padding: 0.75rem 2rem 0.75rem 2rem;
  font-size: 1.125rem;
  border: none;
  text-align: center;
}

.violet {
  --ab-primary-color: #af4bfe;
}

As variáveis CSS estão utilizando um prefixo em seus nomes (ab). Isso é uma prática que pode ser aplicada em um Design System, pois assim essas variáveis não vão entrar em conflito com outras já existentes em um projeto. Nesse caso, o prefixo faz referência a alfabit, uma empresa fictícia utilizada nos cursos da Alura.

Note que podemos utilizar variáveis CSS para padronizar valores do Design System e até para mudar de maneira prática o tema do componente. No caso do código acima, quando o botão tiver a classe violet, sua cor primária será alterada.

Agora, digamos que você começa a desenvolver o componente de input e inicia com esse CSS:

input {
  --ab-inputs: #f8f8f8;
  --ab-primary-text-color: #181818;
  --ab-border-radius-input: 0.5rem;
  --ab-primary-color: #2d5bff;

  box-sizing: border-box;
  width: 15.625rem;
  padding: 1rem;
  background: var(--ab-inputs);
  color: var(--ab-primary-text-color);
  border-radius: var(--ab-border-radius-input);
  outline: 1px solid var(--ab-primary-color);
  border: 0;
  font-size: 0.875rem;
}

.violet {
  --ab-primary-color: #af4bfe;
}

Agora perceba que algumas informações estão começando a se repetir: a variável --ab-primary-color possui o mesmo valor em ambos os componentes, já que é um token (um valor fixo) do Design System, além de sua variante no tema violeta também ser a mesma.

Sabemos que repetição de código é sempre um problema, pois dificulta a manutenção do projeto no futuro. E se um dia for necessário mudar o valor de um ou mais tokens? Nós precisaríamos mudar a definição das variáveis CSS em todos os componentes que as utilizam.

Com esse cenário, vamos explorar algumas soluções para a repetição do código CSS?

Banner promocional da Alura, com um design futurista em tons de azul, apresentando dois blocos de texto, no qual o bloco esquerdo tem os dizeres:

Preparando o ambiente para as soluções

A primeira solução que podemos imaginar para evitar a repetição de código é centralizar as variáveis CSS em um arquivo separado, e os componentes consumiriam as variáveis a partir deste arquivo.

E esse é o caminho correto! Então a primeira coisa que podemos fazer é remover as definições das variáveis dentro dos componentes. Dessa forma, o componente de botão, por exemplo, ficaria assim:

button {
  background: var(--ab-primary-color);
  color: var(--ab-white);
  border-radius: var(--ab-border-radius-botao);
  padding: 0.75rem 2rem 0.75rem 2rem;
  font-size: 1.125rem;
  border: none;
  text-align: center;
}

.violet {
  --ab-primary-color: #af4bfe;
}

Porém, perceba que agora o botão está dependente da declaração dessas variáveis! É claro que, uma vez que elas sejam importadas corretamente, o CSS do botão voltará ao normal.

No entanto, em um Design System, é importante que cada componente seja autossuficiente. Ou seja, mesmo que a importação das variáveis falhe por algum motivo, o ideal é que o componente não deixe de funcionar.

Para isso, podemos utilizar valores fallback (ou valores alternativos) nas variáveis CSS. Vamos conferir como ficaria o novo código para ambos os componentes:

button {
  background: var(--ab-primary-color, #2d5bff);
  color: var(--ab-white, #fff);
  border-radius: var(--ab-border-radius-botao, 0.5rem);
  padding: 0.75rem 2rem 0.75rem 2rem;
  font-size: 1.125rem;
  border: none;
  text-align: center;
}

.violet {
  --ab-primary-color: #af4bfe;
}
input {
  box-sizing: border-box;
  width: 15.625rem;
  padding: 1rem;
  background: var(--ab-inputs, #f8f8f8);
  color: var(--ab-primary-text-color, #181818);
  border-radius: var(--ab-border-radius-input, 0.5rem);
  outline: 1px solid var(--ab-primary-color, #2d5bff);
  border: 0;
  font-size: 0.875rem;
}

.violet {
  --ab-primary-color: #af4bfe;
}

A sintaxe de valores fallback funciona assim: se a variável que colocamos como primeiro parâmetro da função var() não estiver disponível, a função irá utilizar o valor que colocamos no segundo parâmetro.

Ou seja, se a variável --ab-primary-color não estiver declarada, a função irá utilizar o valor alternativo: #2d5bff. Além disso, essa solução é compatível com o código que aplica o tema violeta nos componentes!

Utilizando os valores fallback da função var(), aplicamos uma técnica de programação defensiva; este conceito diz que devemos aplicar técnicas para evitar ao máximo falhas e bugs no nosso software.

Com essa modificação, estamos com o ambiente preparado para discutir a implementação das nossas soluções de compartilhamento de código CSS.

Conferindo um monorepo real

Para aplicar as soluções, vamos tomar como base o monorepo desenvolvido na Formação Angular Design System, que constrói um Design System com componentes Angular e Storybook.

Se você quiser me acompanhar a partir desse ponto, você pode acessar o projeto do GitHub!

Os aplicativos e bibliotecas do monorepo estão organizados dessa forma:

monorepo
├───apps
│   └───alfabit
└───libs
    ├───storybook-host
    └───ui
         ├───button
         └───input

Temos uma aplicação chamada alfabit, duas bibliotecas button e input organizadas dentro da pasta ui e uma biblioteca chamada storybook-host.

Dependendo da forma que queremos compartilhar o código CSS, temos que tomar diferentes abordagens. Uma das principais questões que temos que refletir é:

Os componentes do Design System serão compartilhados com projetos apenas dentro do monorepo ou também serão compartilhados com aplicações fora do monorepo?

Vamos explorar essa questão nas nossas soluções.

Solução 1: criar um novo arquivo CSS no monorepo

Essa solução deve ser escolhida no seguinte caso de uso:

  • Você deseja utilizar os componentes apenas dentro dos projetos do monorepo. Ou seja, esses componentes não serão publicados e, portanto, aplicações fora do monorepo não poderão consumir seu Design System.

Para essa situação, vamos criar um novo arquivo global.css dentro da pasta ui:

monorepo
├───apps
│   └───alfabit
└───libs
    ├───storybook-host
    └───ui
         ├───button
         ├───input
         └───global.css (adicionado)

E esse arquivo poderá conter todas as variáveis CSS do Design System dentro da pseudo-classe :root, para que fiquem disponibilizadas para qualquer componente. Aqui vai um exemplo de conteúdo:

:root {
  --ab-primary-text-color: #181818;
  --ab-secondary-text-color: #5c5c5c;
  --ab-tertiary-text-color: #747474;
  --ab-boxes: #f8f8f8;
  --ab-inputs: var(--ab-boxes);
  --ab-white: #fff;

  --ab-primary-color: #2d5bff;
  --ab-secondary-color: #6284fd;
  --ab-tertiary-color: #96adff;
  --ab-quaternary-color: #ecf0ff;
  --ab-hover-color: #1b4af0;
  --ab-click-color: #002ed0;
  --ab-focus-color: #af4bfe;

  --ab-border-radius-checkboxes: 0.25rem;
  --ab-border-radius-botao: 0.5rem;
  --ab-border-radius-input: 0.5rem;
}

Em seguida, você incluirá esse arquivo na configuração dos projetos que irão consumir os componentes.

Por exemplo, para incluir o global.css na aplicação alfabit, você irá abrir o arquivo apps/alfabit/project.json, localizar a tarefa build e adicionar (ou modificar) as opções styles e stylePreprocessorOptions:

{
  // configurações omitidas
  "targets": {
    "build": {
      "executor": "@angular-devkit/build-angular:application",
      "outputs": ["{options.outputPath}"],
      "options": {
        // opções omitidas

        "styles": ["apps/alfabit/src/styles.css", "libs/ui/global.css"], // adicionado
        "stylePreprocessorOptions": {
          "includePaths": ["libs/ui/global.css"] // adicionado
        }
      }
    }

    // tarefas omitidas
  }
}

Nota: a propriedade stylePreprocessorOptions surge das configurações do Angular e permite que você possa trabalhar com pré-processadores como SASS e LESS.

E agora a aplicação alfabit já pode importar e consumir os componentes de botão e input! O código do repositório já está utilizando esses componentes. No arquivo apps/alfabit/src/app/app.component.html, já temos o seguinte:

<ab-button text="Botão no app do monorepo"></ab-button>

<ab-input label="Input do app" [id]="'teste'"></ab-input>

<app-nx-welcome></app-nx-welcome> <router-outlet></router-outlet>

Então podemos executar a aplicação:

nx serve alfabit

E já é possível conferir os componentes no navegador!

Botão azul com o texto "Botão no app do monorepo". Abaixo, há um campo de texto com a legenda "Input do app" e dentro dele a palavra "Text".

E sim, eles funcionariam mesmo sem o arquivo global.css, por conta dos valores fallback que inserimos no CSS dos componentes! No entanto, para conferir que funcionou, você pode alterar alguma das variáveis do global.css e conferir a mudança na aplicação.

Como desafio, inclua o arquivo global.css na biblioteca storybook-host também! A documentação necessária pode ser conferida na página Configurando estilos para projetos Angular com uma configuração do Storybook.

Com esse solução, ganhamos os seguintes benefícios:

  • Centralização e documentação de informações: as variáveis CSS do Design System ficam centralizadas e documentadas em um único local;
  • Facilidade de manutenção: se um dia for necessário alterar algum token do Design System, como uma cor ou espaçamento, só é necessário mudar no arquivo global.css.

O código dessa solução pode ser conferido na branch solucao-arquivo-css.

Porém, essa abordagem não funciona para o caso de uso onde você quer compartilhar o seu Design System com o restante do mundo, ou seja, para aplicações fora do monorepo.

Vamos conferir esse cenário?

Solução 2: criar uma biblioteca CSS

Essa solução deve ser escolhida no seguinte caso de uso:

  • Você deseja compartilhar os componentes para aplicações fora do monorepo. Ou seja, esses componentes são publicáveis e, portanto, aplicações fora do monorepo poderão consumir seu Design System.

Para essa situação, não só os componentes devem ser publicados, mas o arquivo CSS também deve ser publicado para que ele seja importado pelos componentes.

O monorepo que estamos utilizando já está configurado para publicar os componentes no NPM com o Nx Release, uma ferramenta do Nx que automatiza a publicação de múltiplos pacotes. Mas como publicar uma biblioteca apenas com um arquivo CSS?

Você poderia pensar em criar essa biblioteca dentro do monorepo, no entanto, o Nx fornece recursos oficiais apenas para trabalhar com bibliotecas JS/TS, então é bastante difícil de criar uma biblioteca apenas com um arquivo CSS e que se encaixe no ambiente do Nx.

Dito isso, nada nos impede de criar e publicar uma biblioteca manualmente no NPM! Vamos descobrir como fazer isso?

Primeiro, crie uma nova pasta no seu computador e crie o arquivo global.css, que pode ter o mesmo conteúdo que utilizamos na solução 1:

:root {
  --ab-primary-text-color: #181818;
  --ab-secondary-text-color: #5c5c5c;
  --ab-tertiary-text-color: #747474;
  --ab-boxes: #f8f8f8;
  --ab-inputs: var(--ab-boxes);
  --ab-white: #fff;

  --ab-primary-color: #2d5bff;
  --ab-secondary-color: #6284fd;
  --ab-tertiary-color: #96adff;
  --ab-quaternary-color: #ecf0ff;
  --ab-hover-color: #1b4af0;
  --ab-click-color: #002ed0;
  --ab-focus-color: #af4bfe;

  --ab-border-radius-checkboxes: 0.25rem;
  --ab-border-radius-botao: 0.5rem;
  --ab-border-radius-input: 0.5rem;
}

Agora, execute o seguinte comando para criar um arquivo package.json:

npm init

O terminal irá fazer algumas perguntas. As únicas que você precisará digitar algo específico são essas:

  • package name: @alfabit-alura/global-css
  • entry point: global.css

O nome (package name) que eu dei para o pacote está na sintaxe dos pacotes de escopo do NPM, onde alfabit-alura é o nome da organização do NPM e global-css é o nome do pacote. Se você nomear o seu pacote seguindo essa mesma sintaxe, você deve primeiro criar sua própria organização no NPM para que o seu pacote seja publicado dentro do escopo definido.

E sobre o ponto de entrada da aplicação (entry point), colocamos global.css. Dessa forma, o arquivo CSS poderá ser disponibilizado corretamente pelo NPM.

Confirmando todas as perguntas do terminal, esperamos que o seguinte arquivo package.json seja criado:

{
  "name": "@alfabit-alura/global-css",
  "version": "1.0.0",
  "main": "global.css",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": ""
}

Você pode modificar os outros campos do arquivo conforme sua necessidade!

Agora um ponto de atenção: como este é um pacote de escopo, o padrão é que ele seja publicado de forma privada no NPM, e isso não é gratuito. Para modificar a forma de publicação para pública e publicar nosso pacote gratuitamente, vamos adicionar a propriedade publishConfig com a configuração de acesso público:

{
  "name": "@alfabit-alura/global-css",
  "version": "1.0.0",
  "main": "global.css",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC",
  "description": "",
  "publishConfig": {
    "access": "public"
  }
}

E agora podemos publicar o pacote! Para isso, faça login na sua conta do NPM pelo terminal:

npm login

Siga o passo a passo para ser autenticado. Em seguida, publique o pacote:

npm publish

Se tudo der certo, o terminal irá dizer que o pacote foi publicado com sucesso! Você pode acessar o pacote @alfabit-alura/global-css no NPM.

Além disso, os pacotes @alfabit-alura/button e @alfabit-alura/input também já estão publicados, caso você queira conferir.

E finalmente, para consumir o pacote CSS, você pode importá-lo diretamente dentro de uma aplicação. Outra vantagem dessa abordagem é que ela funciona para aplicações que estão dentro ou fora do monorepo!

Para consumir o pacote, vamos primeiro instalá-lo:

npm i @alfabit-alura/global-css

E, em seguida, importá-lo em um arquivo JS ou TS da sua aplicação:

import '@alfabit-alura/global-css';

No caso de uma aplicação Angular, você pode fazer essa importação no arquivo main.ts, que vem por padrão. Faça o teste e veja que funciona!

O código dessa solução pode ser conferido na branch solucao-lib-css.

Essa solução possui os mesmos benefícios da anterior, com a adição de que também funciona para apps fora do monorepo e qualquer pessoa do mundo agora é capaz de consumir nosso Design System!

Análise, implementação e impacto das soluções

Ambas as soluções que abordamos utilizam a mesma ideia principal: reutilizar o código CSS para evitar a repetição de código. No entanto, cada uma segue uma direção diferente, dependendo do caso de uso.

Sabendo disso, antes de implementar qualquer solução, é importante analisar o cenário que você se enquadra para tomar a decisão que mais faz sentido para o seu projeto. Ao escolher a solução mais apropriada, você vai evitar o retrabalho de ter que trocar a abordagem no meio do caminho.

Além disso, é importante analisar o impacto das suas soluções. Em um primeiro momento, remover a declaração das variáveis CSS dos componentes para centralizá-las em um novo local faria os componentes não serem mais autossuficientes.

Ou seja, cada ação feita no código possui impactos, podendo ser positivos e/ou negativos. No nosso caso, para neutralizar os possíveis impactos negativos, implementamos uma técnica de programação defensiva com valores fallback para que os componentes se tornassem autossuficientes.

Resumo

Ufa! Aprendemos muita coisa, não foi? Segue um resumo do que conferimos neste artigo:

  • Repetição do código CSS: em um Design System, teremos vários tokens que irão se repetir pelos componentes, o que reflete na repetição do código CSS;

  • Uso de valores fallback nas variáveis CSS: os valores fallback garantem que os componentes funcionem mesmo que a importação das variáveis CSS falhe, aplicando assim um técnica de programação defensiva;

  • Solução 1: criação do arquivo global.css: essa solução funciona para projetos dentro do monorepo, que precisam ser configurados para importar o arquivo;

  • Solução 2: criação de uma biblioteca CSS: essa solução funciona para projetos dentro e fora do monorepo, permitindo que aplicações externas consumam o Design System.

Se você deseja se aprofundar ainda mais em Design System, Storybook, Nx e monorepos, confira a Formação Angular Design System!

Antônio Evaldo
Antônio Evaldo

Instrutor e Desenvolvedor de Software nas escolas de Front-end e de Programação da Alura. Estudei Engenharia Elétrica na Universidade Federal do Piauí. Sou apaixonado por programação e por compartilhar conhecimento de forma encantadora. No tempo livre, assisto séries, filmes e animes.

Veja outros artigos sobre Front-end