Criando um Design System reutilizável entre React e React Native com Tamagui
Tags: react, reactnative, javascript, designsystem
Published: 2024-03-27 00:14:50

### TLDR;

Você pode conferir o repositório gerado por esse artigo [clicando aqui](https://github.com/alvarogfn/tamagui-design-system-vite-example).

## 🚀 Introdução

Muitas vezes, no desenvolvimento de aplicações mobile com React Native me veio a cabeça a possibilidade de reutilização de componentes no contexto web/mobile. Recentemente, conheci uma biblioteca chamada [Tamagui](https://tamagui.dev/) que permite que os componentes sejam compartilhados tanto no React Web quando no React Native.

Então tive o desafio de criar uma biblioteca de componentes separada que pudesse ser utilizada tanto no React Web quanto no React Native.

## 📦 Iniciando o projeto com vite

Vamos começar criando um template com [vite](https://vitejs.dev/guide/). De acordo com a documentação do Vite, ele é `uma ferramenta de construção que tem como objetivo fornecer uma experiência de desenvolvimento mais rápida e leve para projetos web modernos.`

```bash
pnpm create vite@latest
```

```
╰─○ pnpm create vite@latest
✔ Project name: library
✔ Select a framework: › React
✔ Select a variant: › TypeScript + SWC
```

Esse setup vai nos dar uma configuração inicial com React e TypeScript orientado para o desenvolvimento de aplicações web, porém, o que precisamos mesmo é de uma configuração que permita o desenvolvimento de componentes para serem distribuídos para serem utilizados por outras aplicações.

Para isso, vamos ajustar o nosso `vite.config.ts` para configurar o modo de desenvolvimento de uma biblioteca.

```ts
// /vite.config.ts
import { defineConfig } from "vite";

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    lib: { 
      // definindo o ponto inicial da nossa biblioteca de componentes.
      entry: "src/index.ts", 
      // definindo os formatos de distribuição da nossa biblioteca (CommonJS e ESM).
      formats: ["cjs", "es"]  
    },
    rollupOptions: {
      // definindo as dependências externas da nossa biblioteca.
      // Essas dependências não serão incluídas no bundle final.
      external: ["react", "react/jsx-runtime", "react-dom"], 
    },
  },
});
```

No arquivo especificado em `entry`, vamos criar o ponto de entrada da nossa biblioteca.

```ts
// /src/index.ts
export * from "./components";
```

e vamos criar um componente de exemplo.

```tsx
// /src/components/Text/Text.tsx

export const Text = () => {
  return <p>Text</p>;
};
```

No final, nossa estrutura de arquivos se parecerá com isso:

```json
  /src
    /components
      /text
        text.tsx
        index.ts
      index.ts
    index.ts
  vite.config.ts
```

Quando o comando `pnpm vite build` for executado, alguns arquivos serão gerados dentro da pasta `dist` com o nome da sua biblioteca.

```js
// /dist/library.js

import { jsx as t } from "react/jsx-runtime";
const i = () => /* @__PURE__ */ t("text", { children: "Text!" });
export {
  i as Text
};
```

Note que não há nenhuma dependência externa incluída no bundle final, pois definimos as dependências como externas no `vite.config.ts`. Por esse motivo, precisamos informar no `package.json` que essas dependências devem vir de fora (quem está instalando a biblioteca deve ter elas como dependência). Para isso, vamos mover o React e o ReactDOM para o campo de `devDependencies`, pois precisamos dessas dependências apenas enquanto desenvolvemos, e adicionar elas na seção de `peerDependencies` do nosso `package.json`.

```json
  "peerDependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
    // outras dependências
  }
```

_Esse artigo da [Naveera](https://sentry.io/answers/npm-difference-between-dependencies/) explica bem a diferença entre cada tipo de dependência._

Vamos também atualizar a seção de scripts do `package.json` para incluir o comando de build do vite:

```json
// /package.json
  "scripts": {
    "build": "vite build"
  },
  "devDependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "typescript": "5.4.3",
    "vite": "5.2.3",
    "vite-plugin-dts": "3.7.3"
  },
  "peerDependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
  }
```
## ⛓️ Integrando com o Typescript

Ao observar a pasta de distribuição (dist) gerada pelo vite ao rodar o comando de build, você pode notar que os nossos tipos do typescript não estão presentes, isso acontece pois o vite transforma o Typescript em Javascript para rodar no contexto dos navegadores, onde os tipos não são necessários. No nosso caso que estamos buildando o projeto para compartilhar com outros desenvolvedores, os tipos acabam sendo essenciais para a prevenção de bugs e melhora na experiência de desenvolvimento. 

Vamos ajustar esse detalhe instalando alguns plugins novos para o vite:
```bash
  pnpm add -D vite-plugin-dts
```

```ts
// /vite.config.ts
import { defineConfig } from "vite";
import dtsPlugin from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    // O plugin dtsPlugin vai gerar arquivos de 
    // tipagem para cada arquivo dentro da pasta src.
    dtsPlugin(),
  ],
  build: {
    lib: { 
      entry: "src/index.ts", 
      // Definindo os formatos de distribuição da nossa biblioteca (CommonJS e ESM).
      formats: ["cjs", "es"] 
    },
    rollupOptions: {
      // Definindo as dependências externas da nossa biblioteca. 
      // Essas dependências não serão incluídas no bundle final.
      external: ["react", "react/jsx-runtime", "react-dom"],
    },
  },
});
```

Também vamos fazer um ajuste no tsconfig.json e no package.json para configurar a geração de sourcemaps a apartir dos tipos gerados. Isso vai permitir que o editor forneça o código fonte original ao invés do código compilado quando buscarmos as referências das bibliotecas importadas.

```json
// /tsconfig.json
    // ...
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,

    /* Bundler mode */
    "moduleResolution": "bundler",
    // ...
```

```json
// /package.json
  // ...
  "source": "./src/index.ts",
  "files": [
    "dist",
    "src"
  ],
  // ...
```

### ♟  Tree Shaking

Por padrão, o vite tem o comportamento de mesclar todos os arquivos fonte do seu código em um único arquivo .js para ser servido para o navegador. Vocês podem notar isso ao observar os arquivos gerados na pasta de distribuição:
```
dist/
  components/
    Text/
      text.d.ts
      text.d.ts.map
      index.d.ts
      index.d.ts.map
    index.d.ts
    index.d.ts.map
  index.d.ts
  index.d.ts.map
  library.js
  library.cjs
```
Todo conteúdo javascript foi mesclado dentro de um único arquivo, enquanto os tipos permaneceram isolados entre sí, porém esse comportamento pode não ser o ideal no contexto do desenvolvimento de bibliotecas. Com todo código fonte dentro de um único arquivo, quando alguém utilizar algum componente ou função gerado pela sua biblioteca, todo o conteúdo javascript será incluído no bundle final, mesmo que você esteja utilizando apenas alguns componentes. Isso aumenta o tamanho final da aplicação e consequentemente o peso do seu aplicativo/site. Nessa seção, vamos configurar o vite para que ele considere cada arquivo do projeto como se fosse um módulo separado, que importa outros módulos, isso vai permitir que bundlers como `vite` e `webpack` removam os componentes que não estão sendo utilizados no build final.

Para configurar cada arquivo como um entry point, vamos utilizar a biblioteca `glob`, que vai nos permitir encontrar cada arquivo dentro da pasta `src` a partir de uma expressão minimatch.

```bash
pnpm add -D glob @types/node
```
_A biblioteca @types/node vai permitir que usemos bibliotecas internas do node com typescript, como `node:url` e `node:path`._

Vamos atualizar as configurações do rollup no `vite.config.ts` para que ele mapeie todos os arquivos dentro da pasta `src` e os adicione como entry point nas configurações do rollup.

```ts
// /vite.config.ts

import { defineConfig } from "vite";
import dtsPlugin from "vite-plugin-dts";
import { dirname, extname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { glob } from "glob";

// Caminho absoluto do diretório atual a partir da raiz do sistema de arquivos.
const __dirname = dirname(fileURLToPath(import.meta.url)); // /home/user/pasta/library ou C:\Users\user\pasta\library

const computeAllSrcFiles = (): Record<string, string> => {
  // Encontra todos os arquivos .ts e .tsx dentro da pasta src.
  const files = glob.sync(["src/**/*.{ts,tsx}"]);

  const paths = files.map((file) => [
    // Remove a extensão do arquivo e calcula o caminho relativo a partir da pasta src.
    /* key: */ relative(
      "src",
      file.slice(0, file.length - extname(file).length)
    ),

    // Converte o caminho do arquivo para um caminho absoluto.
    /* value: */ fileURLToPath(new URL(file, import.meta.url)),
  ]);

  return Object.fromEntries(paths);
  // Converte o array de caminhos em um objeto.
};

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [dtsPlugin()],
  build: {
    lib: {
      // Definindo o ponto inicial da nossa biblioteca de componentes para o vite.
      entry: resolve(__dirname, "src/main.ts"),
      // Definindo os formatos de distribuição da nossa biblioteca (CommonJS e ESM).
      formats: ["cjs", "es"],
      // Definindo o nome do arquivo de saída. 
      // `entryName` é o nome do arquivo sem a extensão, 
      // e `format` é o formato de distribuição.
      fileName(format, entryName) {
        if (format === "es") return `${entryName}.js`;
        return `${entryName}.${format}`;
      },
    },
    rollupOptions: {
      // Definindo as dependências externas da nossa biblioteca. 
      external: ["react", "react/jsx-runtime", "react-dom"],
      // A configuração `input` do rollupOptions permite que você forneça um objeto
      // específicando arquivos de saída apontando para arquivos do código fonte.
      input: computeAllSrcFiles(),
    },
  },
});
```

Agora, a nossa pasta de distribuição vai conter os seguintes arquivos gerados:
```
  /dist
    /components
      Button.cjs
      Button.d.ts
      Button.d.ts.map
      Button.js
      Button.cjs
      index.d.ts
      index.d.ts.map
      index.js
    index.cjs
    index.d.ts
    index.d.ts.map
    index.js
```


## 💅 Configurando o Storybook

O Storybook é uma ferramenta de construção de componentes e páginas de forma isolada, muito útil para construção de testes e documentação de interfaces e componentes. Ao desenvolver uma biblioteca de componentes, é comum querermos ter a disposição um catalogo dos nossos componentes e a documentação das suas propriedades e comportamentos, agilizando o processo de desenvolvimento e utilização dos componentes.

Podemos configurar o nosso arquivo inicial do Storybook com o comando:

```bash
# No momento de escrita desse artigo, a versão do storybook é a 8.0.4
npx storybook@latest init
```

Isso vai gerar uma pasta chamada `.storybook` com arquivos chamados `main.ts` e `preview.ts`, além de uma pasta de exemplos chamada `stories`.

```json
  .storybook/
    main.ts
    preview.ts
  src/ 
    stories/
```

A pasta de stories contém componentes e exemplos de como você pode utilizar o storyboook, então ela pode ser removida sem medo. Vamos excluir a pasta stories, e melhorar o nosso componente de `Text` para incluir 
um arquivo de `text.stories.tsx`, e um arquivo de tipagens `text.types.ts`.

```ts
// /src/components/text/text.types.ts

import { ComponentPropsWithoutRef, ReactNode } from "react";

export type TextProps = ComponentPropsWithoutRef<"p"> & {
  // A interface ComponentPropsWithoutRef fornece a tipagem base 
  // do elemento <p> sem adicionar um ref.
  children?: ReactNode;
};
```

```tsx
// /src/components/text/text.stories.tsx

import type { Meta, StoryObj } from "@storybook/react";
import { Text } from "./text";

export default {
  component: Text,
  title: "Components/Text",
} satisfies Meta<typeof Text>;

type Story = StoryObj<typeof Text>;

export const StoryDefault: Story = {
  // Aqui, estamos configurando o storybook para 
  // renderizar o componente de texto e nomeá-lo como Default.
  name: "Default",
  render: (props) => <Text {...props} />,
};
```
Se você está em dúvida sobre conteúdo dentro do arquivo de stories, a [própria documentação](https://storybook.js.org/docs/writing-stories) do storybook é um bom começo.

```tsx
// /src/components/text/text.tsx

import { TextProps } from "./text.types";

export const Text = ({ children, ...props }: TextProps) => (
  <p {...props}>{children}</p>
);
```
_Toda essa estruturação é opcional, e você pode organizar os arquivos e estilos do jeito que bem entender._

Se você iniciar o Storybook com o comando `pnpm storybook`, vai notar que o nosso componente de texto já foi renderizado na tela.



## 🧪 Adicionando Vitest com React Testing Library

O [vitest](https://vitest.dev/) é um framework de testes semelhante ao [jest](https://vitest.dev/), que integra muito bem a ambientes com o vite configurado. Podemos reutilizar os plugins e configurações já inseridas dentro do `vite.config.ts`, facilitando o processo de configuração dos testes. 

Como estamos em um contexto de testes para o front-end, vamos contar também com o apoio da biblioteca [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) que fornece suporte para a renderização de componentes e queries customizadas na DOM.

Vamos começar instalando as dependências necessárias e criando um `vitest.config.ts` na raiz do projeto:

```bash
pnpm add -D vitest @testing-library/jest-dom @testing-library/react jsdom
```

```ts
// /vitest.config.ts

import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";

export default defineConfig({
  plugins: [react()],
  test: {
    environment: "jsdom",
    globals: true,
    include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
    // Nesse arquivo, vamos configurar a integração do vitest
    // com a biblioteca React Testing Library
    setupFiles: ["./src/setup-tests.ts"],
  },
});
```
```ts
// /src/setup-tests.ts
import "@testing-library/jest-dom/vitest";
```
Também será necessário fazer alguns ajustes no `tsconfig.json` para que o Typescript reconheça as variáveis globais fornecidas pelo vitest:

```json
// /tsconfig.json
  "compilerOptions": {
    // adicione referencia ao vitest/globals na propriedade types.
    "types": ["vitest/globals"], 
    // ...
```

Por fim, vamos adicionar um comando na seção de `scripts` do `package.json` para rodar os testes com maior facilidade:
```json
// /package.json
  "scripts": {
    "test": "vitest"
  },
```

Finalmente, é hora de criar um teste para o nosso componente de texto e ver o resultado no terminal:

```tsx
// /src/components/text/text.test.tsx

import { render, screen } from "@testing-library/react";

import { Text } from "./text";

describe("[Components]: Text", () => {
  it("renders without crash", () => {
    render(<Text>Hello World</Text>)
    const component = screen.getByText("Hello World");
    expect(component).toBeDefined();
  });
});
```
```bash
 ✓ src/components/text/text.test.tsx (1)
   ✓ [Components]: Text (1)
     ✓ renders without crash

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  15:29:37
   Duration  383ms
```

## 👾 Adicionando Tamagui

O [Tamagui](https://tamagui.dev/) é uma ferramenta que possibilita o compartilhamento de código entre diferentes plataformas que suportam o React (web, android, ios), tornando a estilização no ambiente React mais agradável e rápido.

### ⚠️ Antes de continuarmos: Limitações em relação ao [PNPM](https://pnpm.io/npmrc#node-linker)

Existem algumas limitações em relação ao uso de `pnpm` ao configurar ambientes que envolvem o react native, e até mesmo o storybook devido a forma com que o gerenciador de pacotes resolve suas dependências. Infelizmente, o React Native não funciona muito bem com a estrutura criada para resolver as dependências a partir de links simbólicos. Por esse motivo, vamos utilizar de uma flag no `.npmrc` diz ao pnpm que queremos uma estrutura de arquivos semelhante ao `npm` e `yarn` na node_modules:

```json
// /.npmrc
# https://pnpm.io/npmrc#node-linker
node-linker=hoisted
```

Também será necessário remover a declaração de `type: "module"` dentro do arquivo `package.json` para que o @tamagui possa funcionar corretamente.

Após essa configuração, é necessário utilizar `pnpm install` para que ele re-crie a node_modules com a nova estrutura.

### Continuando...

Vamos começar instalando as dependências necessárias para configurar o ambiente do `tamagui` na nossa biblioteca.

```bash
pnpm add expo expo-linear-gradient -D react-native @tamagui/core @tamagui/vite-plugin
```
_Por algum motivo, o tamagui tem dependência do expo-linear-gradient internamente, então é necessário instalar ambas as bibliotecas expo e expo-linear-grandient._

Também será necessário fazer alguns ajustes na estrutura das nossas `peerDependencies` para incluir o @tamagui e o react-native como dependências externas. Como nossa biblioteca funciona em contextos diferentes, algumas dependências não são necessariamente obrigatórias:

```json
// /package.json

 "peerDependencies": {
    "@tamagui/core": "1.91.4",
    "@tamagui/vite-plugin": "1.91.4",
    // O metro-plugin é responsável por fazer o tamagui funcionar no contexto do react-native.
    "@tamagui/metro-plugin": "1.91.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.73.5"
  },
  // Vamos usar esse campo para definir as peer dependências que não são obrigatórias.
  "peerDependenciesMeta": {
    // Não usaremos react-dom no contexto do react-native.
    "react-dom": {
      "optional": true
    },
    // não usaremos o react-native no contexto do web.
    "react-native": {
      "optional": true
    },
    // Não é obrigatório utilizar o vite como bundler;
    "@tamagui/vite-plugin": {
      "optional": true
    },
    // Não é obrigatório utilizar o metro como bundler;
    "@tamagui/metro-plugin": {
      "optional": true
    }
  },
```

Vamos configurar também o `vite.config.ts` para incluir o `@tamagui/core` e o `react-native` como uma dependência externa:

```ts
// /vite.config.ts

    rollupOptions: {
      // Definindo as dependências externas da nossa biblioteca.
      external: [
        "react",
        "react/jsx-runtime",
        "react-dom",
        "react-native",
        "@tamagui/core",
      ],
```

Também será necessário criar um arquivo de configuração inicial para o tamagui chamado `tamagui.config.ts`, irei criar esse arquivo na pasta `src/themes`.

```ts
// /src/themes/tamagui.config.ts
import { createTamagui } from "@tamagui/core";

// Você é livre para definir os tokens e temas do tamagui como quiser.
// no nosso caso, vamos definir apenas duas cores e dois temas.
const config = createTamagui({
  fonts: {},
  shorthands: {},
  themes: {
	// É necessário configurar ao menos um tema na aplicação.
    night: { color: "#005" },
    sun: { color: "#FA0" },
  },
  tokens: {
    color: {
      primary: "#000",
      secondary: "#FFF",
    },
    radius: {},
    size: {},
    space: {},
    zIndex: {},
  },
});

export default config;
```

Após a definição dos tokens, vamos configurar o Typescript para reconhecer os tipos customizados definidos no tamagui.config.ts criando um arquivo `src/types.d.ts` e adicionando a seguinte configuração:

```ts
// src/types.d.ts

import config from "./themes/tamagui.config";

export type AppConfig = typeof config;

declare module "@tamagui/core" {
  interface TamaguiCustomConfig extends AppConfig {}
}
```

Nós extraímos o tipo de config e atribuímos ao tipo de TamaguiCustomConfig. Isso vai permitir que o typescript reconheça os tipos constantes definidos no `src/themes/tamagui.config.ts`.

Para que o sistema de tokens e temas do tamagui funcione, é necessário criar um provider que injete o contexto do tamagui na aplicação. Vamos criar um arquivo chamado `theme-provider.tsx` na pasta `src/themes`.

```tsx
// /src/themes/theme-provider.tsx
import { TamaguiProvider, TamaguiProviderProps } from "@tamagui/core";
import { PropsWithChildren } from "react";
import appConfig from "./tamagui.config";

type ThemeProviderProps = PropsWithChildren<TamaguiProviderProps>;

export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
  return (
    <TamaguiProvider config={appConfig} {...props}>
      {children}
    </TamaguiProvider>
  );
}
```
Esse provider deverá ser utilizado pelo storybook, testes e pelo consumidor da biblioteca para que os tokens definidos pelo tamagui.config.ts funcionem.

### 👾🧪 Tamagui com Vitest

Para que o Tamagui funcione no contexto dos testes com o vitest, é necessário adicionar o plugin responsável pelo processamento dos componentes do tamagui e adicionar o `ThemeProvider` em cada chamada render().

Vamos instalar o plugin do vite e adicioná-lo no `vitest.config.ts`:

```bash
pnpm add -D @tamagui/vite-plugin
```

```tsx
// /vitest.config.ts

import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react-swc";
import { createRequire } from "module";

const require = createRequire(import.meta.url);
const { tamaguiPlugin } = require("@tamagui/vite-plugin");

export default defineConfig({
  plugins: [
    react(),
    tamaguiPlugin({
      components: ["@tamagui/core"],
      // O plugin do tamagui foi colocado na seção de plugins do vitest, apontando para nossa configuração personalizada de tokens.
      config: "src/themes/tamagui.config.ts",
    }),
  ],
  test: {
    environment: "jsdom",
    globals: true,
    server: {
      // Como os testes rodam no contexto do node, essa configuração é necessária para remover os imports e exports do ESM.
      deps: {
        inline: ["@tamagui"],
      },
    },
    include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
    setupFiles: ["./src/setup-tests.ts"],
  },
});
```

Para adicionar o provider em todos os testes de forma prática, podemos criar um render customizado que será utilizado por todos os testes ao invés do render tradicional exportado pela biblioteca `@testing-library/react`. vamos criar um arquivo no diretório `src/__tests__/setup.tsx` que conterá o nosso render customizado. Esse render deve ser importado no lugar do render do `@testing-library/react` para garantir que nossos testes funcionem.

```tsx
// /src/__tests__/setup.tsx

import {
  queries,
  Queries,
  render as nativeRender,
  RenderOptions,
  RenderResult,
} from "@testing-library/react";
import "@testing-library/jest-dom";
import { TamaguiProvider } from "@tamagui/core";
import { ReactElement } from "react";
import config from "../themes/tamagui.config";

const render = <
  Q extends Queries = typeof queries,
  Container extends DocumentFragment | Element = HTMLElement,
  BaseElement extends DocumentFragment | Element = Container,
>(
  ui: ReactElement,
  renderOptions?: RenderOptions<Q, Container, BaseElement>
): RenderResult<Q, Container, BaseElement> =>
  nativeRender(<TamaguiProvider config={config}>{ui}</TamaguiProvider>, {
    ...renderOptions,
  });

export * from "@testing-library/react";
export { render };
```

Após esses ajustes, o nosso projeto já está configurado para rodar os testes mesmo com os componentes do Tamagui. finalmente, vamos integrar o nosso componente de texto com o Tamagui:

```ts
// /src/components/text/text.styles.ts

import { Text, styled } from "@tamagui/core";

export const StyledText = styled(Text, {
  color: "$black",
});
```

```tsx
// /src/components/text/text.types.tsx
import { GetProps } from "@tamagui/core";
import type { StyledText } from "./text.styles";

export type TextProps = GetProps<typeof StyledText>;
```

```tsx
// /src/components/text/text.tsx

import { forwardRef } from 'react';
import { TamaguiElement } from '@tamagui/core';
import type { TextProps } from './text.types';
import { StyledText } from '.';

export const Text = (props: TextProps) => (
   <StyledText {...props}>
     {children}
   </StyledText>
)
```

```tsx
// /src/components/text/text.test.tsx

import { Text } from "./text";

import { render, screen } from "../../__tests__/setup";

describe("[Components]: Text", () => {
  it("renders without crash", () => {
    render(<Text>Hello World</Text>);
    const component = screen.getByText("Hello World");
    expect(component).toBeDefined();
  });
});
```

Ao rodar o teste, você verá que a integração já está funcionando:

```tsx
✓ src/components/text/text.test.tsx (1)
   ✓ [Components]: Text (1)
     ✓ renders without crash

 Test Files  1 passed (1)
      Tests  1 passed (1)
   Start at  21:36:14
   Duration  460ms
```


### 👾💅 Configurando com Storybook

Para fazer o Tamagui funcionar no contexto do storybook, os passos são semelhantes aos dos testes, você precisa injetar o plugin do tamagui no storybook e adicionar o provider em cada story.

Podemos adicionar o plugin ao storybook da seguinte forma:

```tsx
// /.storybook/main.ts

import { tamaguiPlugin } from "@tamagui/vite-plugin";
import type { StorybookConfig } from "@storybook/react-vite";
import tsconfigPaths from "vite-tsconfig-paths";

const config: StorybookConfig = {
  addons: [
    "@storybook/addon-links",
    "@storybook/addon-essentials",
    "@storybook/addon-interactions",
  ],
  docs: {
    autodocs: true,
  },
  env: (config) => ({
    ...config,
    // Definindo o target do tamagui para renderizar para web
    TAMAGUI_TARGET: "web",
  }),
  framework: {
    name: "@storybook/react-vite",
    options: {},
  },
  stories: [
    "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)",
    "../src/**/*.mdx",
    "../docs/**/*.mdx",
  ],
  viteFinal: (config, { configType }) => {
    config.define = {
      // variáveis de ambiente necessárias para que o @Tamagui entenda
      ...config.define,
      "process.env.NODE_ENV":
        configType === "PRODUCTION" ? "production" : "development",
      "process.env.STORYBOOK": true,
    };

    config.plugins!.push(
      tamaguiPlugin({
        // Referencia a partir do caminho absoluto para o tamagui.config.ts
        config: "/src/themes/tamagui.config.ts",
      }),
    );

    return config;
  },
};
export default config;
```
```tsx
// /.storybook/preview.tsx

import type { Preview } from "@storybook/react";

import { ThemeProvider } from "../src/themes/theme-provider";

const preview: Preview = {
  decorators: [
    (Story) => (
      // Colocamos o provider para renderizar em todos os stories:
      <ThemeProvider>
        <Story />
      </ThemeProvider>
    ),
  ],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /date$/i,
      },
    },
  },
};

export default preview;
```

Após essas configurações, já podemos rodar `pnpm storybook` e ver o componente funcionando no próprio storybook.

## 🦋 Publicando com [changesets](https://github.com/changesets/changesets)

A biblioteca `changesets` é desenhada para automatizar a geração de changelogs, publicação e atualização da versão da biblioteca. Isso garante que os usuários que baixarem o seu design system serão capazes de lidar com o gerenciamento de versão a partir de versões semânticas. 

### 🧹 Fazendo uma limpeza antes da publicação

Se você tentou rodar o comando de build da biblioteca antes de chegar nessa etapa, você pode ter notado que existem alguns arquivos na nossa pasta de distribuição relacionados aos testes de unidade e ao próprio storybook. Talvez o comando de build nem tenha funcionado por sí só.

Isso acontece pois definimos que todos os arquivos da pasta `/src` como um entry-point para um módulo no nosso `vite.config.ts`, o que está fazendo com que ele também considere os arquivos de teste e documentação ao gerar os arquivos.

Podemos ajustar isso dentro da função `computeAllSrcFiles` no `vite.config.ts`, configurando-o para ignorar todos os arquivos que não são código fonte:

```ts
// /vite.config.ts

const computeAllSrcFiles = (): Record<string, string> => {
  // Encontra todos os arquivos .ts e .tsx dentro da pasta src.
  const files = glob.sync(["src/**/*.{ts,tsx}"], {
    ignore: [
    "src/**/*.stories.tsx",
	"src/**/__tests/**",
	"src/**/*.test.{ts,tsx}",
	"src/setup-tests.ts",
	"types.d.ts",
	], // minimatch que queremos que o glob ignore.
  });
```
Também é necessário configurar o plugin dts para ignorar esses arquivos na geração de arquivos d.ts e d.ts.map:

```ts
// vite.config.ts
 
// ...

export default defineConfig({
  plugins: [
    dtsPlugin({
      exclude: [
	    "node_modules",     
	    "src/**/*.stories.tsx",
		"src/**/__tests/**",
		"src/**/*.test.{ts,tsx}",
		"src/setup-tests.ts",
		"types.d.ts"
	  ],
      include: ["src"],
    }),
  ],
  // ...
```

Você também pode notar que alguns arquivos como o `button.types.js` foram gerados sem conteúdo javascript nenhum. Isso é porque esses arquivos só contém incluem declarações de tipo no seu código fonte, o que não é possível transpilar para javascript.

Podemos remover esses arquivos vazios gerados criando um plugin customizado para o vite:

```ts
// vite.config.ts
const removeEmptyFiles = (): PluginOption => ({
  generateBundle(_, bundle) {
    for (const name in bundle) {
      const file = bundle[name];
      if (file.type !== "chunk") return;

      if (file.code.trim() === "") delete bundle[name];
      if (file.code.trim() === '"use strict";') delete bundle[name];
    }
  },
  name: "remove-empty-files",
});

export default defineConfig({
  plugins: [
    dtsPlugin({
      exclude: [
	    "node_modules",     
	    "src/**/*.stories.tsx",
		"src/**/__tests/**",
		"src/**/*.test.{ts,tsx}",
		"src/setup-tests.ts",
		"types.d.ts"
	  ],
      include: ["src"],
    }),
    removeEmptyFiles(),
  ],
  // ...
```

Essa função vai verificar se algum `chunk` de dado está vazio ou se apenas contém a declaração de "use strict;", se verdadeiro, esse conteúdo será removido do bundle final.

Ao final dessa etapa, o nosso `vite.config.ts` estará assim:

```ts
// /vite.config.ts

import { PluginOption, defineConfig } from "vite";
import dtsPlugin from "vite-plugin-dts";
import tsconfigPaths from "vite-tsconfig-paths";
import { dirname, extname, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { glob } from "glob";

// Caminho absoluto do diretório atual a partir da raiz do sistema de arquivos.
const __dirname = dirname(fileURLToPath(import.meta.url)); // /home/user/pasta/library ou C:\Users\user\pasta\library

const computeAllSrcFiles = (): Record<string, string> => {
  // Encontra todos os arquivos .ts e .tsx dentro da pasta src.
  const files = glob.sync(["src/**/*.{ts,tsx}"], {
    ignore: ["src/**/*.stories.tsx", "src/**/*.test.tsx", "src/setup-tests.ts"],
  });

  const paths = files.map((file) => [
    // Remove a extensão do arquivo e calcula o caminho relativo a partir da pasta src.
    /* key: */ relative(
      "src",
      file.slice(0, file.length - extname(file).length)
    ),

    // Converte o caminho do arquivo para um caminho absoluto.
    /* value: */ fileURLToPath(new URL(file, import.meta.url)),
  ]);

  return Object.fromEntries(paths);
  // Converte o array de caminhos em um objeto.
};

const removeEmptyFiles = (): PluginOption => ({
  generateBundle(_, bundle) {
    for (const name in bundle) {
      const file = bundle[name];
      if (file.type !== "chunk") return;

      if (file.code.trim() === "") delete bundle[name];
      if (file.code.trim() === '"use strict";') delete bundle[name];
    }
  },
  name: "remove-empty-files",
});

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [
    dtsPlugin({
      exclude: [
        "node_modules",
        "src/**/*.stories.tsx",
        "src/**/*.test.tsx",
        "src/setup-tests.ts",
      ],
      include: ["src"],
    }),
    removeEmptyFiles(),
  ],
  build: {
    lib: {
      // Definindo o ponto inicial da nossa biblioteca de componentes.
      entry: resolve(__dirname, "src/main.ts"),
      // Definindo os formatos de distribuição da nossa biblioteca (CommonJS e ESM).
      formats: ["cjs", "es"],
      // Definindo o nome do arquivo de saída.
      // EntryName é o nome do arquivo sem a extensão,
      // e format é o formato de distribuição.
      fileName(format, entryName) {
        if (format === "es") return `${entryName}.js`;
        return `${entryName}.${format}`;
      },
    },
    rollupOptions: {
      // Definindo as dependências externas da nossa biblioteca. (Que não serão incluídas no bundle)
      external: [
        "react",
        "react/jsx-runtime",
        "react-dom",
        "@tamagui/core",
        "@tamagui/vite-plugin",
      ],
      input: computeAllSrcFiles(),
    },
  },
});
```

### Continuando...

Para instalar a biblioteca `changesets`, rode esse comando no terminal:

```bash
pnpm install -D @changesets/cli && npx changeset init
```

Após a instalação da biblioteca, alguns arquivos foram gerados na pasta `.changeset`, e alguns comandos devem ser adicionados no `package.json` da nossa aplicação:


```json
// package.json
   scripts: {
	// ...
	// O comando add da biblioteca changeset
	// cria uma nova entrada de versão na pasta 
	// .changeset com os escopos major, minor ou patch
	// representando o versionamento semântico
	"changeset": "changeset add", 
	// O comando de publish é responsável por publicar
	// a biblioteca em artefatos como o `npm`, `azure`, 'aws`...
	"publish":  "changeset publish",
	// O comando version é usado para consumir
	// todas as entradas criadas pelo comando de changeset
	// e calcular a versão resultante. Esse comando também
	// gera um arquivo de CHANGELOG contendo cada versão
	// já publicada pela biblioteca. 
	"version":  "changeset version"
	// ...
``` 

Também será necessário ajustar o nosso `package.json` para apontar para os arquivos corretos na nossa pasta de distribuição:

```json
// /package.json
{
	// O nome a ser publicado da sua biblioteca,
	// caso seja em um registro público, o nome deve ser único.
	"name":  "library",
	// Indica que a biblioteca pode ser publicada em registros públicos:
	"private":  false, 
	// A biblioteca changeset aumentará a versão automaticamente sempre
	// que o comando changeset version for usado:
	"version":  "0.0.0",
	// Apontamos para o código fonte original:
	"source":  "./src/index.ts",
	// Apontamos para o código transpilado no formado Common Javascript:
	"main":  "dist/index.cjs",
	// Apontamos para o código transpilado no formado ESM:
	"module":  "dist/index.js",
	// Incluímos todas as pastas que devem ser adicionadas ao publicar 
	// a biblioteca:
	"files":  [
		"dist",
		"src",
		"package.json",
		"CHANGELOG",
		"README.md"
	],
	// ...
},
```
```js
// .changeset/config
{
	"$schema":  "https://unpkg.com/@changesets/config@3.0.0/schema.json",
	"changelog":  "@changesets/cli/changelog",
	"commit":  false,
	"fixed":  [],
	"linked":  [],
	"access":  "public", // Para publicar publicamente, defina como `public`
	"baseBranch":  "main", // Defina com o nome da sua branch principal
	"updateInternalDependencies":  "patch",
	"ignore":  []
}
```
Após esses ajustes, você pode gerar um `changeset` através dos comando no terminal:

```bash
╰─± pnpm changeset    
🦋  What kind of change is this for library? (current version is 0.0.0) · minor
🦋  Please enter a summary for this change (this will be in the changelogs).
🦋    (submit empty line to open external editor)
🦋  Summary · Primeira versão da minha biblioteca
🦋  
🦋  === Summary of changesets ===
🦋  minor:  library
🦋  
🦋  Is this your desired changeset? (Y/n) · true
🦋  Changeset added! - you can now commit it
🦋  
🦋  If you want to modify or expand on the changeset summary, you can find it here
🦋  info /home/user/documents/library/.changeset/pink-spoons-laugh.md
```

Esse comando vai gerar um arquivo markdown de nome aleatório dentro da pasta `.changeset` chamado de `changeset`, você pode modifica-lo do jeito que preferir antes de gerar de fato uma nova versão, e você também pode acumular quantos `changesets` precisar antes de lançar novas versões.

Para gerar uma nova versão da biblioteca, você deve executar o comando version, esse comando vai calcular quantos `changesets` existem dentro da pasta `.changeset` e fazer a atualização da chave de `version` dentro do package.json, além de documentar cada um desses arquivos dentro do arquivo `CHANGELOG.md`

```bash
pnpm run version
🦋  All files have been updated. Review them and commit at your leisure
```

```md
// / CHANGELOG.md

# library

## 0.1.0

### Minor Changes

- Primeira versão da minha biblioteca
```

Esse arquivo de changelog contém o conteúdo textual de todos os `changesets` gerados dentro da pasta `.changeset`.

```json
// /package.json
"name":  "library",
"private":  false,
"version":  "0.1.0",
```

A versão da nossa biblioteca também foi alterada no `package.json`,  de acordo com o escopo semântico definido em cada `changeset` (major, minor ou patch).

Finalmente, para publicar a biblioteca, você deve rodar o comando `pnpm run publish`

```bash
pnpm run publish
> changeset publish

🦋  info npm info library
🦋  warn Received 404 for npm info "library"
🦋  info library is being published because our local version (0.1.0) has not been published on npm
🦋  info Publishing "library" at "0.1.0"
```

Isso vai fazer com que os conteúdos especificados dentro do campo `files` no nosso package.json sejam publicados no registro do `npm`.  Você deve autenticar com  o `npm` e seguir os passos definidos no terminal para que sua biblioteca seja publicada. :D

## Obrigado por ler !!!!

Você pode ver todo o código gerado durante a codificação dessa biblioteca no meu repositório do Github:

https://github.com/alvarogfn/tamagui-design-system-vite-example

Espero que esse artigo tenha sido útil para você em algum nível, e agradeço a todas as sugestões de melhoria!