Creating a reusable Design System between React and React Native with Tamagui
Tags: react, reactnative, javascript, designsystem
Published: 2024-03-27 01:35:42

### TLDR;

You can check the repository generated by this article [here](https://github.com/alvarogfn/tamagui-design-system-vite-example).

## 🚀 Introduction

Many times, while developing mobile applications with React Native, I have thought about the possibility of reusing components in both web and mobile contexts. Recently, I came across a library called [Tamagui](https://tamagui.dev/) that allows components to be shared in both React Web and React Native.

So I had the challenge of creating a separate component library that could be used in both React Web and React Native.

## 📦 Starting the project with Vite

Let's start by creating a template with [vite](https://vitejs.dev/guide/). `According to the Vite documentation, it is a build tool that aims to provide a faster and leaner development experience for modern web projects.`

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

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

This setup will give us an initial configuration with React and TypeScript geared towards web application development. However, what we really need is a configuration that allows us to develop components to be distributed and used by other applications.

To achieve this, let's adjust our `vite.config.ts` to configure the development mode for a library.

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

// https://vitejs.dev/config/
export default defineConfig({
  build: {
    lib: { 
      // defining the entry point of our component library.
      entry: "src/index.ts", 
        // defining the distribution formats of our library (CommonJS and ESM).
        formats: ["cjs", "es"]  
      },
      rollupOptions: {
        // defining the external dependencies of our library.
        // These dependencies will not be included in the final bundle.
      external: ["react", "react/jsx-runtime", "react-dom"], 
    },
  },
});
```

In the file specified in `entry`, let's create the entry point of our library.

```ts
// /src/index.ts
export * from "./components";
```
let's create an example component.

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

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

our file structure will look like this:

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

When the `pnpm vite build` command is executed, some files will be generated inside the `dist` folder with the name of your library.

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

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

Note that there are no external dependencies included in the final bundle because we defined them as external in `vite.config.ts`. For this reason, we need to specify in the `package.json` that these dependencies should come from outside (those installing the library should have them as dependencies). To do this, let's move React and ReactDOM to the `devDependencies` field since we only need these dependencies during development, and add them to the `peerDependencies` section of our `package.json`.

```json
  "peerDependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
  },
  "devDependencies": {
    "react": "18.2.0",
    "react-dom": "18.2.0"
    // other dependencies
  }
```

_This article by [Naveera](https://sentry.io/answers/npm-difference-between-dependencies/) explains the difference between each type of dependency._

Let's also update the scripts section of the `package.json` to include the vite build command:

```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"
  }
```
## ⛓️ Integrating with Typescript

When observing the distribution folder (dist) generated by Vite when running the build command, you may notice that our TypeScript types are not present. This happens because Vite transforms TypeScript into JavaScript to run in the context of browsers, where types are not necessary. However, in our case, as we are building the project to share with other developers, types are essential for bug prevention and improved development experience.

Let's address this issue by installing some new plugins for 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: [
    // The dtsPlugin will generate type declaration files for each file inside the src folder.
    dtsPlugin(),
  ],
  build: {
    lib: { 
      entry: "src/index.ts", 
      // Setting the distribution formats for our library (CommonJS and ESM).
      formats: ["cjs", "es"] 
    },
    rollupOptions: {
      // Setting the external dependencies for our library. 
      // These dependencies will not be included in the final bundle.
      external: ["react", "react/jsx-runtime", "react-dom"],
    },
  },
});
```

We will also make an adjustment in the tsconfig.json and package.json to configure the generation of sourcemaps from the generated types. This will allow the editor to provide the original source code instead of the compiled code when we search for references to imported libraries.

```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

By default, Vite merges all source files into a single .js file to be served to the browser. You can observe this behavior by looking at the generated files in the distribution folder:

```
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
```

All JavaScript content has been merged into a single file, while the types remain isolated from each other. However, this behavior may not be ideal in the context of library development. With all source code in a single file, when someone uses a component or function generated by your library, all JavaScript content will be included in the final bundle, even if they are only using a few components. This increases the final size of the application and consequently the weight of your application/website. In this section, we will configure Vite to consider each file in the project as a separate module that imports other modules. This will allow bundlers like Vite and webpack to remove unused components in the final build.

To configure each file as an entry point, we will use the `glob` library, which allows us to find each file within the `src` folder using a minimatch expression.

```bash
pnpm add -D glob @types/node
```
_The @types/node library will allow us to use internal node libraries with TypeScript, such as `node:url` and `node:path`._

Let's update the rollup configuration in `vite.config.ts` so that it maps all files within the `src` folder and adds them as entry points in the rollup configuration.

```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";

// Absolute path of the current directory from the root of the file system.
const __dirname = dirname(fileURLToPath(import.meta.url)); // /home/user/folder/library or C:\Users\user\folder\library

const computeAllSrcFiles = (): Record<string, string> => {
  // Find all .ts and .tsx files within the src folder.
  const files = glob.sync(["src/**/*.{ts,tsx}"]);

  const paths = files.map((file) => [
    // Remove the file extension and calculate the relative path from the src folder.
    /* key: */ relative(
      "src",
      file.slice(0, file.length - extname(file).length)
    ),

    // Convert the file path to an absolute path.
    /* value: */ fileURLToPath(new URL(file, import.meta.url)),
  ]);

  return Object.fromEntries(paths);
  // Convert the array of paths into an object.
};

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [dtsPlugin()],
  build: {
    lib: {
      // Set the entry point of our component library for Vite.
      entry: resolve(__dirname, "src/main.ts"),
      // Set the distribution formats for our library (CommonJS and ESM).
      formats: ["cjs", "es"],
      // Set the output file name.
      // `entryName` is the file name without the extension,
      // and `format` is the distribution format.
      fileName(format, entryName) {
        if (format === "es") return `${entryName}.js`;
        return `${entryName}.${format}`;
      },
    },
    rollupOptions: {
      // Set the external dependencies for our library.
      external: ["react", "react/jsx-runtime", "react-dom"],
      // The `input` configuration in rollupOptions allows you to provide an object
      // specifying output files pointing to source code files.
      input: computeAllSrcFiles(),
    },
  },
});
```

Now, our distribution folder will contain the following generated files:

```
/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
```


## 💅 Setting up Storybook

Storybook is a tool for building components and pages in isolation, which is very useful for testing and documenting interfaces and components. When developing a component library, it is common to want to have a catalog of our components and documentation of their properties and behaviors, streamlining the development and usage process of the components.

We can set up our initial Storybook file with the following command:

```bash
# At the time of writing this article, the version of Storybook is 8.0.4
npx storybook@latest init
```

This will generate a folder called `.storybook` with files named `main.ts` and `preview.ts`, along with a folder of examples called `stories`.

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

The stories folder contains components and examples of how you can use Storybook, so it can be safely removed. Let's delete the stories folder and improve our `Text` component by including a `text.stories.tsx` file and a typings file `text.types.ts`.

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

import { ComponentPropsWithoutRef, ReactNode } from "react";

export type TextProps = ComponentPropsWithoutRef<"p"> & {
  // The ComponentPropsWithoutRef interface provides the base typing
  // for the <p> element without adding a 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 = {
  // Here, we are configuring Storybook to render the text component and name it as Default.
  name: "Default",
  render: (props) => <Text {...props} />,
};
```
If you are unsure about the content within the stories file, the [official documentation](https://storybook.js.org/docs/writing-stories) of Storybook is a good starting point.

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

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

export const Text = ({ children, ...props }: TextProps) => (
  <p {...props}>{children}</p>
);
```
_All this structuring is optional, and you can organize the files and styles however you like._

If you start Storybook with the command `pnpm storybook`, you will notice that our text component has already been rendered on the screen.

## 🧪 Adding Vitest with React Testing Library

[vitest](https://vitest.dev/) is a testing framework similar to [jest](https://vitest.dev/) that integrates well with projects using Vite. It allows us to reuse plugins and configurations already set up in the `vite.config.ts`, making the test setup process easier.

Since we are testing the front-end, we will also rely on the [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/), which provides support for rendering components and custom queries in the DOM.

Let's start by installing the necessary dependencies and creating a `vitest.config.ts` file in the project root:

```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"],
    // In this file, we will configure the integration of vitest
    // with the React Testing Library
    setupFiles: ["./src/setup-tests.ts"],
  },
});
```
```ts
// /src/setup-tests.ts
import "@testing-library/jest-dom/vitest";
```
Some adjustments will also need to be made in the `tsconfig.json` so that TypeScript recognizes the global variables provided by vitest:

```json
// /tsconfig.json
  "compilerOptions": {
    // add reference to vitest/globals in the types property.
    "types": ["vitest/globals"], 
    // ...
```

Finally, let's add a command in the `scripts` section of the `package.json` to run the tests more easily:
```json
// /package.json
  "scripts": {
    "test": "vitest"
  },
```

Finally, it's time to create a test for our text component and see the result in the 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
```

## 👾 Adding Tamagui

[Tamagui](https://tamagui.dev/) is a tool that enables code sharing between different platforms that support React (web, android, ios), making styling in the React environment more pleasant and faster.

### ⚠️ Before we continue: Limitations regarding [PNPM](https://pnpm.io/npmrc#node-linker)

There are some limitations when using `pnpm` to configure environments involving React Native, and even Storybook, due to the way the package manager resolves its dependencies. Unfortunately, React Native doesn't work well with the structure created to resolve dependencies using symbolic links. For this reason, we will use a flag in the `.npmrc` file that tells pnpm to create a file structure similar to `npm` and `yarn` in the node_modules folder:

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

It will also be necessary to remove the `type: "module"` declaration inside the `package.json` file so that @tamagui can function correctly.

After this configuration, it is necessary to use `pnpm install` so that it recreates the node_modules with the new structure.

### Continuing...

Let's start by installing the necessary dependencies to configure the `tamagui` environment in our library.

```bash
pnpm add expo expo-linear-gradient -D react-native @tamagui/core @tamagui/vite-plugin
```
_For some reason, tamagui has a dependency on expo-linear-gradient internally, so it is necessary to install both expo and expo-linear-gradient libraries._

Some adjustments also need to be made to the structure of our `peerDependencies` to include @tamagui and react-native as external dependencies. Since our library works in different contexts, some dependencies are not necessarily required:

```json
// /package.json

 "peerDependencies": {
    "@tamagui/core": "1.91.4",
    "@tamagui/vite-plugin": "1.91.4",
    // The metro-plugin is responsible for making tamagui work in the react-native context.
    "@tamagui/metro-plugin": "1.91.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-native": "0.73.5"
  },
  // We will use this field to define peer dependencies that are not required.
  "peerDependenciesMeta": {
    // We will not use react-dom in the react-native context.
    "react-dom": {
      "optional": true
    },
    // We will not use react-native in the web context.
    "react-native": {
      "optional": true
    },
    // Using vite as a bundler is not mandatory.
    "@tamagui/vite-plugin": {
      "optional": true
    },
    // Using metro as a bundler is not mandatory.
    "@tamagui/metro-plugin": {
      "optional": true
    }
  },
```

Let's also configure the `vite.config.ts` to include `@tamagui/core` and `react-native` as external dependencies:

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

    rollupOptions: {
      // Setting up the external dependencies of our library.
      external: [
        "react",
        "react/jsx-runtime",
        "react-dom",
        "react-native",
        "@tamagui/core",
      ],
```

It will also be necessary to create an initial configuration file for tamagui called `tamagui.config.ts`. I will create this file in the `src/themes` folder.

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

// You are free to define tamagui's tokens and themes as you wish.
// In our case, we will define only two colors and two themes.
const config = createTamagui({
  fonts: {},
  shorthands: {},
  themes: {
    // It is necessary to configure at least one theme in the application.
    night: { color: "#005" },
    sun: { color: "#FA0" },
  },
  tokens: {
    color: {
      primary: "#000",
      secondary: "#FFF",
    },
    radius: {},
    size: {},
    space: {},
    zIndex: {},
  },
});

export default config;
```

After defining the tokens, let's configure TypeScript to recognize the custom types defined in tamagui.config.ts by creating a file `src/types.d.ts` and adding the following configuration:

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

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

export type AppConfig = typeof config;

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

We extract the config type and assign it to the TamaguiCustomConfig type. This will allow TypeScript to recognize the constant types defined in `src/themes/tamagui.config.ts`.

To make the tamagui token and theme system work, it is necessary to create a provider that injects the tamagui context into the application. Let's create a file called `theme-provider.tsx` in the `src/themes` folder.

```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>
  );
}
```
This provider should be used by Storybook, tests, and the library consumer in order for the tokens defined in tamagui.config.ts to work.

### 👾🧪 Tamagui with Vitest

To make Tamagui work in the context of tests with Vitest, it is necessary to add the plugin responsible for processing Tamagui components and add the `ThemeProvider` in each render() call.

Let's install the Vite plugin and add it to `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"],
      // The tamagui plugin is added to the plugins section of vitest, pointing to our custom token configuration.
      config: "src/themes/tamagui.config.ts",
    }),
  ],
  test: {
    environment: "jsdom",
    globals: true,
    server: {
      // Since the tests run in the node context, this configuration is necessary to remove ESM imports and exports.
      deps: {
        inline: ["@tamagui"],
      },
    },
    include: ["src/**/*.test.ts", "src/**/*.test.tsx"],
    setupFiles: ["./src/setup-tests.ts"],
  },
});
```

To add the provider to all tests in a practical way, we can create a custom render that will be used by all tests instead of the traditional render exported by the `@testing-library/react` library. Let's create a file in the `src/__tests__/setup.tsx` directory that will contain our custom render. This render should be imported instead of the render from `@testing-library/react` to ensure that our tests work.

```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 };
```

After these adjustments, our project is now configured to run tests even with Tamagui components. Finally, let's integrate our text component with 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();
  });
});
```

When running the test, you will see that the integration is already working:

```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
```


### 👾💅 Configuring with Storybook

To make Tamagui work within the context of Storybook, the steps are similar to those for tests. You need to inject the Tamagui plugin into Storybook and add the provider to each story.

We can add the plugin to Storybook as follows:

```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,
    // Setting the tamagui target to render for 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 = {
      // Environment variables required for @Tamagui to work correctly
      ...config.define,
      "process.env.NODE_ENV":
        configType === "PRODUCTION" ? "production" : "development",
      "process.env.STORYBOOK": true,
    };

    config.plugins!.push(
      tamaguiPlugin({
        // Reference from the absolute path to the 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) => (
      // We add the provider to render in all stories:
      <ThemeProvider>
        <Story />
      </ThemeProvider>
    ),
  ],
  parameters: {
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /date$/i,
      },
    },
  },
};

export default preview;
```

After these configurations, we can run `pnpm storybook` and see the component working in the Storybook itself.

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

The `changesets` library is designed to automate the generation of changelogs, publishing, and version updating of the library. This ensures that users who download your design system will be able to handle version management based on semantic versions.

### 🧹 Cleaning up before publishing

If you tried running the library build command before reaching this step, you may have noticed that there are some files in our distribution folder related to unit tests and the Storybook itself. The build command may not have even worked on its own.

This happens because we defined all files in the `/src` folder as an entry point for a module in our `vite.config.ts`, which is causing it to also consider test and documentation files when generating the files.

We can adjust this within the `computeAllSrcFiles` function in `vite.config.ts`, configuring it to ignore all files that are not source code:

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

const computeAllSrcFiles = (): Record<string, string> => {
  // Find all .ts and .tsx files within the src folder.
  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 patterns we want the glob to ignore.
  });
```
It is also necessary to configure the dts plugin to ignore these files in the generation of d.ts and d.ts.map files:

```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"],
    }),
  ],
  // ...
```

You may also notice that some files like `button.types.js` were generated without any JavaScript content. This is because these files only contain type declarations in their source code, which cannot be transpiled to JavaScript.

We can remove these empty generated files by creating a custom plugin for 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(),
  ],
  // ...
```

This function will check if any data chunk is empty or only contains the "use strict;" declaration. If true, this content will be removed from the final bundle.

At the end of this step, our `vite.config.ts` will look like this:

```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";

// Absolute path of the current directory from the root of the file system.
const __dirname = dirname(fileURLToPath(import.meta.url)); // /home/user/folder/library or C:\Users\user\folder\library

const computeAllSrcFiles = (): Record<string, string> => {
  // Find all .ts and .tsx files within the src folder.
  const files = glob.sync(["src/**/*.{ts,tsx}"], {
    ignore: ["src/**/*.stories.tsx", "src/**/*.test.tsx", "src/setup-tests.ts"],
  });

  const paths = files.map((file) => [
    // Remove the file extension and calculate the relative path from the src folder.
    /* key: */ relative(
      "src",
      file.slice(0, file.length - extname(file).length)
    ),

    // Convert the file path to an absolute path.
    /* value: */ fileURLToPath(new URL(file, import.meta.url)),
  ]);

  return Object.fromEntries(paths);
  // Convert the array of paths into an object.
};

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: {
      // Setting the entry point of our component library.
      entry: resolve(__dirname, "src/main.ts"),
      // Setting the distribution formats of our library (CommonJS and ESM).
      formats: ["cjs", "es"],
      // Setting the output file name.
      // EntryName is the file name without the extension,
      // and format is the distribution format.
      fileName(format, entryName) {
        if (format === "es") return `${entryName}.js`;
        return `${entryName}.${format}`;
      },
    },
    rollupOptions: {
      // Setting the external dependencies of our library. (They won't be included in the bundle)
      external: [
        "react",
        "react/jsx-runtime",
        "react-dom",
        "@tamagui/core",
        "@tamagui/vite-plugin",
      ],
      input: computeAllSrcFiles(),
    },
  },
});
```

### Continuing...

To install the `changesets` library, run this command in the terminal:

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

After installing the library, some files were generated in the `.changeset` folder, and some commands need to be added to our application's `package.json`:


```json
// package.json
  // ...
   scripts: {
  // ...
  // The changeset library's add command creates a new version entry
  // in the .changeset folder with the major, minor, or patch scopes
  // representing semantic versioning.
  "changeset": "changeset add", 
  // The publish command is responsible for publishing the library
  // to artifacts such as npm, Azure, AWS, etc.
  "publish":  "changeset publish",
  // The version command is used to consume all the entries created
  // by the changeset command and calculate the resulting version.
  // This command also generates a CHANGELOG file containing each
  // version already published by the library.
  "version":  "changeset version"
  // ...
``` 

We will also need to adjust our `package.json` to point to the correct files in our distribution folder:

```json
// /package.json
{
  // The name to be published for your library,
  // if it's in a public registry, the name must be unique.
  "name": "library",
  // Indicates whether the library can be published to public registries:
  "private": false,
  // The changeset library will automatically increment the version
  // whenever the changeset version command is used:
  "version": "0.0.0",
  // We point to the original source code:
  "source": "./src/index.ts",
  // We point to the transpiled code in CommonJS format:
  "main": "dist/index.cjs",
  // We point to the transpiled code in ESM format:
  "module": "dist/index.js",
  // We include all the folders that should be added when publishing
  // the library:
  "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", // To publish publicly, set it to `public`
  "baseBranch":  "main", // Set it to the name of your main branch
  "updateInternalDependencies":  "patch",
  "ignore":  []
}
```

After these adjustments, you can generate a `changeset` using the following commands in the 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 · First version of my library
🦋  
🦋  === 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
```

This command will generate a randomly named markdown file inside the `.changeset` folder called `changeset`. You can modify it as you prefer before actually generating a new version, and you can also accumulate as many `changesets` as needed before releasing new versions.

To generate a new version of the library, you should run the `version` command. This command will calculate how many `changesets` exist inside the `.changeset` folder and update the `version` key inside the package.json file, as well as document each of these files inside the `CHANGELOG.md` file.

```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

- First version of my library
```

This changelog file contains the textual content of all the changesets generated within the `.changeset` folder.

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

The version of our library has also been updated in the `package.json`, according to the semantic scope defined in each `changeset` (major, minor, or patch).

Finally, to publish the library, you should run the command `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"
```

This will cause the contents specified within the `files` field in our package.json to be published to the npm registry. You should authenticate with npm and follow the steps provided in the terminal to publish your library. :D

## Thank you for reading !!!!

You can see all the code generated during the development of this library in my Github repository:

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

I hope this article has been helpful to you in some way, and I appreciate any suggestions for improvement!