DEV Community

Eduardo Henrique Gris
Eduardo Henrique Gris

Posted on

Biblioteca de componentes React e typescript, parte 1: setup rollup e publicação

Introdução

Durante o ano passado criei artigos falando sobre alguns conteúdos referentes a React. A ideia agora é trazer a base da criação de uma lib React, dividida em diferentes partes, com a ideia de aplicar o que foi mostrado no ano passado e algumas partes novas: setup rollup e publicação, uso de styled-components, adição de typescript, testes unitários com Jest e testing-library, adição de eslint e prettier para padronização de código, documentação com storybook, pre-commit com Husky, autogeração de código com Hygen.
A medida que abordar temas que já escrevi artigos sobre, vou colocar como referência nos artigos referentes a lib, para os que uma nova versão trouxe maiores mudanças em relação ao que já foi escrito, vou produzir um artigo referente ao tema antes de seguir com a implementação na lib.
A parte 1 vai abordar como configurar a lib, criar os primeiros componentes de exemplo com typescript usando styled-components e publicar no npmjs para terem acesso a ela.

Rollup

É um empacotador de módulos para javascript que permite compilar pequenos pedaços de código em coisas maiores e mais complexas, como libs. Vai ser usado na criação da lib.

npmjs

Local onde vai ser publicada a lib e disponibilizada para outras apps usarem.

styled-components

Lib de estilização de componentes em aplicações React, que traz flexibilidade na definição da estilização a partir do recebimento de props. Criei um artigo ano passado explicando como funciona: Styled-components em React com Typescript

typescript

Adiciona typagem para app javascript, detectando erros em tempo de compilação.

Setup inicial

Criar uma pasta com o nome que vai ter a lib (aconselho verificar no npmjs se já tem uma lib com o mesmo nome, para evitar não conseguir adicionar lá), depois iniciar o projeto dentro dela:

yarn init

A princípio pode aceitar todos os default, depois será ajustado as informações do package.json gerado.

Adição do react e styled componentes como peer dependências, pois ao adicionarem a lib de componentes não será feito o bundle dessas libs (a app que vai usar a lib de componentes vai precisar ter elas adicionadas), e dev dependencies para uso durante o desenvolvimento da lib de componentes:

yarn add react react-dom styled-components --peer
yarn add react react-dom styled-components --dev

No momento desse artigo gerou as seguintes versões:

"react": "^19.0.0",
"react-dom": "^19.0.0",
"styled-components": "^6.1.14"
Enter fullscreen mode Exit fullscreen mode

Adição typescript

Adição do typescript como dev dependencies:

yarn add typescript @types/react --dev

No momento desse artigo gerou as seguintes versões:

"@types/react": "^19.0.8",
"typescript": "^5.7.3"
Enter fullscreen mode Exit fullscreen mode

Adição do arquivo de config do typescript, na raiz do projeto:

  • tsconfig.json
{
  "compilerOptions": {
    "target": "ES2016",
    "jsx": "react",
    "module": "ESNext",
    "moduleResolution": "node",
    "declaration": true,
    "emitDeclarationOnly": true,
    "outDir": "dist",
    "declarationDir": "types",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  }
}
Enter fullscreen mode Exit fullscreen mode
  • "target": "ES2016", especifica a versão ECMAScript alvo
  • "jsx": "react", transforma código jsx em React
  • "module": "ESNext", gera módulos js modernos para a lib
  • "moduleResolution": "node", segue as regras do node.js para encontrar módulos
  • "declaration": true, gera um arquivo .d.ts para os types da lib
  • "emitDeclarationOnly": true, somente exporta as declarações de typos, sem gerar js pois o rollup realizará isso
  • "outDir": "dist", diretório onde o projeto será gerado
  • "declarationDir": "types", aonde será colocado o arquivo .d.ts
  • "allowSyntheticDefaultImports": true, assume exports default se nenhum for criado manualmente
  • "esModuleInterop": true, permite uma melhor compatibilidade entre CommonJS e ES modules
  • "forceConsistentCasingInFileNames": true, verifica se o import de arquivos está consistente com o case-sensitive do nome do arquivo
  • "strict": true, traz uma grande quantidade de checagem de types que garante uma ótima integridade do código
  • "skipLibCheck": true, ignora a validação de types do arquivo d.ts

Criação componentes de exemplo

Serão criados dois componentes de exemplo, um simples para texto e outro para tag.
Primeiro será gerada na raiz do projeto uma pasta src, e dentro dela uma pasta components na qual serão colocados todos os componentes criados.

Começando pelo componente de texto, será criada a pasta Text dentro de components, com dois arquivos:

  • Text.tsx
import React from "react";
import styled from "styled-components";

export interface TextProps {
  children: React.ReactNode;
  color?: string;
  weight?: "normal" | "bold";
  fontWeight?: number;
  fontSize?: string;
  fontFamily?: string;
}

export interface StyledTextProps {
  $color?: string;
  $weight?: "normal" | "bold";
  $fontWeight?: number;
  $fontSize?: string;
  $fontFamily?: string;
}

export const StyledText = styled.span<StyledTextProps>`
  color: ${(props) => 
    props.$color 
      ? props.$color 
      : "#000"};
  font-size: ${(props) => 
    props.$fontSize 
      ? props.$fontSize 
      : "16px"};
  font-weight: ${(props) =>
    props.$fontWeight
      ? props.$fontWeight
      : props.$weight
        ? props.$weight
        : "normal"};
  font-family: ${(props) => 
    props.$fontFamily 
      ? props.$fontFamily 
      : "Arial"};
`;

const Text = ({
  children,
  color,
  weight,
  fontWeight,
  fontSize,
  fontFamily,
}: TextProps) => (
  <StyledText
    $color={color}
    $weight={weight}
    $fontWeight={fontWeight}
    $fontSize={fontSize}
    $fontFamily={fontFamily}
  >
    {children}
  </StyledText>
);

export default Text;
Enter fullscreen mode Exit fullscreen mode

Que aceita 5 props: chidren que vai representar o texto em si, color que vai definir a cor do texto, fontWeight que vai definir a font-weight do texto, fontSize que vai definir a font-size do texto e por fim a fontFamily que vai definir a font-family do texto. Caso algum deles não sejam passados, com exceção do children, o StyledText já tem um valor default definido para cada propriedade.

  • index.ts
export { default } from "./Text";
Enter fullscreen mode Exit fullscreen mode

Vai disponibilizar o componente.

Agora vendo o componente de tag, será criada a pasta Tag dentro de components, com dois arquivos:

  • Tag.tsx
import React from "react";
import styled from "styled-components";
import Text from "../Text/Text";

export interface TagProps {
  type?: "default" | "success" | "alert" | "error";
  text: string;
  textColor?: string;
  textWeight?: "normal" | "bold";
  textFontWeight?: number;
  textFontSize?: string;
  textFontFamily?: string;
  backgroundColor?: string;
  format?: "default" | "semiRounded" | "rounded";
  borderRadius?: string;
  size?: "small" | "medium" | "large";
  padding?: string;
}

export interface StyledTagProps {
  $type?: "default" | "success" | "alert" | "error";
  $textColor?: string;
  $textWeight?: "normal" | "bold";
  $textFontWeight?: number;
  $textFontSize?: string;
  $textFontFamily?: string;
  $backgroundColor?: string;
  $format?: "default" | "semiRounded" | "rounded";
  $borderRadius?: string;
  $size?: "small" | "medium" | "large";
  $padding?: string;
}

export const StyledTag = styled.div<StyledTagProps>`
  border: none;
  padding: ${(props) =>
    props.$padding
      ? props.$padding
      : props.$size === "large"
        ? "15px 20px"
        : props.$size === "medium"
          ? "10px 12px"
          : "7px"};
  background-color: ${(props) =>
    props.$backgroundColor
      ? props.$backgroundColor
      : props.$type === "error"
        ? "#e97451"
        : props.$type === "alert"
          ? "#f8de7e"
          : props.$type === "success"
            ? "#50c878"
            : "#d3d3d3"};
  pointer-events: none;
  border-radius: ${(props) =>
    props.$borderRadius
      ? props.$borderRadius
      : props.$format === "rounded"
        ? "30px"
        : props.$format === "semiRounded"
          ? "5px"
          : "0"};
  width: fit-content;
`;

const Tag = ({
  text,
  type,
  textColor,
  textWeight,
  textFontWeight,
  textFontSize,
  textFontFamily,
  backgroundColor,
  format,
  borderRadius,
  size,
  padding,
}: TagProps) => (
  <StyledTag
    $type={type}
    $backgroundColor={backgroundColor}
    $format={format}
    $borderRadius={borderRadius}
    $size={size}
    $padding={padding}
  >
    <Text
      color={textColor || "#fff"}
      weight={textWeight}
      fontWeight={textFontWeight}
      fontSize={textFontSize}
      fontFamily={textFontFamily}
    >
      {text}
    </Text>
  </StyledTag>
);

export default Tag;
Enter fullscreen mode Exit fullscreen mode

Que aceita 11 props.
Sendo 3 pre-definidas: size que define o padding (se large define 15px 20px, se medium define 10px 12px), type que define a background-color (se error define #e97451, se alert define #f8de7e, se success define #50c878) e format que define o border-radius (se rounded define 30px, se semiRounded define 5px).
E sendo 8 customizáveis: text que define o texto da tag, textColor que define a cor do texto, textFontWeight que define a font-weight do texto, textFontSize que define a font-size do texto, textFontFamily que define a font-family do texto, backgroundColor que define a background-color da tag, borderRadius que define o border-radius da tag e por fim o padding que define o padding da tag.
Caso algum deles não sejam passados, com exceção do text, o StyledTag já tem um valor default definido para cada propriedade.

  • index.ts
export { default } from "./Tag";
Enter fullscreen mode Exit fullscreen mode

Vai disponibilizar o componente.

Após a definição dos dois componentes, vai ser centralizado o export deles dentro da pasta src/components:

  • index.ts
export { default as Tag } from "./Tag";
export { default as Text } from "./Text";
Enter fullscreen mode Exit fullscreen mode

Por fim vai ser centralizado o que a lib irá disponibilizar quando adicionarem ela, dentro da pasta src:

  • index.ts
export * from "./components";
Enter fullscreen mode Exit fullscreen mode

Dessa forma se tem dois componentes definidos e centralizado a disponibilização deles, podendo já prosseguir para configuração do rollup.

Setup Rollup

Para utilizar o Rollup, primeiro vai ser adicionadas as seguintes libs:

yarn add rollup rollup-plugin-dts rollup-plugin-peer-deps-external @rollup/plugin-commonjs @rollup/plugin-node-resolve @rollup/plugin-typescript@11.1.6 @rollup/plugin-terser --dev

No momento desse artigo gerou as seguintes versões:

"@rollup/plugin-commonjs": "^28.0.2",
"@rollup/plugin-node-resolve": "^16.0.0",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "11.1.6",
"rollup": "^4.30.1",
"rollup-plugin-dts": "^6.1.1",
"rollup-plugin-peer-deps-external": "^2.2.4"
Enter fullscreen mode Exit fullscreen mode

Diferente das libs em geral que estão sendo adicionadas, foi definida uma versão específica para o @rollup/plugin-typescript, por ter uma issue que não permite utilizar ela em versões posteriores.

Depois vai ser necessário criar na raiz do projeto o arquivo de config do rollup, que vai usar as libs adicionadas:

  • rollup.config.js
import resolve from "@rollup/plugin-node-resolve";
import commonjs from "@rollup/plugin-commonjs";
import typescript from "@rollup/plugin-typescript";
import dts from "rollup-plugin-dts";
import terser from "@rollup/plugin-terser";
import peerDepsExternal from "rollup-plugin-peer-deps-external";

const packageJson = require("./package.json");

export default [
  {
    input: "src/index.ts",
    output: [
      {
        file: packageJson.main,
        format: "cjs",
      },
      {
        file: packageJson.module,
        format: "esm",
      },
    ],
    plugins: [
      peerDepsExternal(),
      resolve(),
      commonjs(),
      typescript({ tsconfig: "./tsconfig.json" }),
      terser(),
    ],
    external: ["react", "react-dom", "styled-components"],
  },
  {
    input: "src/index.ts",
    output: [{ file: "dist/index.d.ts", format: "esm" }],
    plugins: [dts.default()],
  },
];
Enter fullscreen mode Exit fullscreen mode

Nesse arquivo são usados as libs que foram adicionadas na app.
Em input: "src/index.ts" é definido o arquivo onde os componentes estão sendo exportados. Em output abaixo desse input, é definido a criação de dois arquivos de output, o primeiro com formato cjs e o segundo com formato esm, que vão ser especificados em package.json o caminho deles. Em peerDepsExternal() e em external: ["react", "react-dom", "styled-components"], são definidas as dependências externas que a lib possui, ou seja, que não vai acontecer o bundle delas na adição da lib. Em resolve() é usado o algoritmo do Node para encontrar módulos de terceiros em node_modules. Em commonjs() é convertido commonjs módulos em es módulos, permitindo o uso de ambos formatos. Em typescript({ tsconfig: "./tsconfig.json" }) é definida a integração entre o rollup e typescript, e é passado o local das configurações do typescript. Em terser() é reduzido o tamanho do bundle. No final a parte referente ao plugin dts.default() vai definir em output um arquivo com os types usados na lib.

package.json

Por agora o package.json deve estar parecido com o abaixo:

{
  "name": "react-example-lib", // nome da pasta onde iniciou o projeto
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "devDependencies": {
    "@rollup/plugin-commonjs": "^28.0.2",
    "@rollup/plugin-node-resolve": "^16.0.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.2",
    "@types/react": "^19.0.8",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "rollup": "^4.30.1",
    "rollup-plugin-dts": "^6.1.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "styled-components": "^6.1.14",
    "typescript": "^5.7.3"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.14"
  }
}
Enter fullscreen mode Exit fullscreen mode

Vai ser preciso fazer algumas alterações e adições nele, ficando dessa forma:

{
  "name": "react-example-lib",
  "version": "0.1.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "types": "dist/index.d.ts",
  "files": [
    "dist"
  ],
  "license": "MIT",
  "repository": {
    "type": "git",
    "url": "https://github.com/griseduardo/react-example-lib.git"
  },
  "scripts": {
    "build": "rollup -c --bundleConfigAsCjs"
  },
  "devDependencies": {
    "@rollup/plugin-commonjs": "^28.0.2",
    "@rollup/plugin-node-resolve": "^16.0.0",
    "@rollup/plugin-terser": "^0.4.4",
    "@rollup/plugin-typescript": "^12.1.2",
    "@types/react": "^19.0.8",
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "rollup": "^4.30.1",
    "rollup-plugin-dts": "^6.1.1",
    "rollup-plugin-peer-deps-external": "^2.2.4",
    "styled-components": "^6.1.14",
    "typescript": "^5.7.3"
  },
  "peerDependencies": {
    "react": "^19.0.0",
    "react-dom": "^19.0.0",
    "styled-components": "^6.1.14"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • name: está definido o nome da lib
  • version: a versão da lib, mudei de 1.0.0 para 0.1.0 por não estar pronta para utilização
  • main: o caminho de saída para o formato cjs
  • module: o caminho de saída para o formato esm
  • types: o caminho para os types da lib
  • files: a pasta onde vai estar contida toda a lib
  • repository: onde está a app caso a coloque no github também
  • scripts: definido o build, que vai executar o rollup

Arquivo CHANGELOG

A medida que a lib vai sendo modificada, se torna interessante mostrar o que foi mudado a cada subida de versão para quem for utilizar ela. Para isso será criado um arquivo na raiz:

  • CHANGELOG.md
## 0.1.0

_Jan. 29, 2025_

- initial config
Enter fullscreen mode Exit fullscreen mode

Arquivo README

Outra parte importante é adicionar um README explicando do que se trata a app, como adicionar ela e como utilizar seus componentes. Será adicionado na raiz:

  • README.md

Image description

Arquivo .gitignore

No caso de além de publicar a lib no npmjs, criar um repositório no github para ela, é interessante não subir algumas pastas para o github. Será adicionado na raiz do projeto:

  • .gitignore
dist
node_modules
Enter fullscreen mode Exit fullscreen mode

Estrutura de pastas

Por fim a estrutura inicial de pastas ficará da seguinte forma:

Image description

Publicação da lib

Primeiro passo a ser realizado é ver se a execução do rollup ocorre com sucesso. Para isso será executado o yarn build no terminal, que foi definido em package.json. Executando com sucesso, será criada automaticamente uma pasta dist na raiz do projeto.
Após isso será necessário criar uma conta em npmjs, local onde será publicado a lib.
Após a criação da conta no site, vai ser necessário fazer o login no terminal dentro do projeto:

npm login

Onde vai ser necessário colocar o username/email e senha (o mesmo referente ao que foi criado no site). Após o login é publicar a lib:

npm publish --access public

Executado com sucesso a publicação, no site do npmjs vai ser possível ver a lib publicada ali, ficando disponível para outras apps adicionarem e usarem os componentes que foram definidos dentro dela (Text e Tag). Como foi colocado em peer dependências o react, react-dom e styled-components, caso a app que adicionar a lib não tenha algum deles, vai ser informado que são dependências necessárias para execução da lib no momento da adição.

Conclusão

A ideia desse artigo foi trazer como criar e publicar uma lib de componentes em React com Typescript. Para isso, foi passado de forma geral como colocar o typescript e configurar o rollup, foram definidos dois componentes usando typescript com styled-components, foi apresentado alguns arquivos que acredito ser interessante para mostrar as alterações que forem sendo realizadas na lib e como usar ela, e por fim os passos para publicar no npmjs. A ideia agora é nos próximos artigos implementar coisas novas para ela, como testes, regras pre-commit, dentre outras que foram citadas na introdução. Estou colocando o repositório no github e publicando no npmjs para seguir o que estou escrevendo nos artigos.

Top comments (0)