TLDR;
You can check the repository generated by this article here.
🚀 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 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. According to the Vite documentation, it is a build tool that aims to provide a faster and leaner development experience for modern web projects.
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.
// /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.
// /src/index.ts
export * from "./components";
let's create an example component.
// /src/components/Text/Text.tsx
export const Text = () => {
return <p>Text</p>;
};
our file structure will look like this:
/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.
// /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
.
"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 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:
// /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:
pnpm add -D vite-plugin-dts
// /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.
// /tsconfig.json
// ...
"skipLibCheck": true,
"declaration": true,
"declarationMap": true,
/* Bundler mode */
"moduleResolution": "bundler",
// ...
// /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.
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.
// /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:
# 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
.
.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
.
// /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;
};
// /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 of Storybook is a good starting point.
// /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 is a testing framework similar to jest 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, 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:
pnpm add -D vitest @testing-library/jest-dom @testing-library/react jsdom
// /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"],
},
});
// /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:
// /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:
// /package.json
"scripts": {
"test": "vitest"
},
Finally, it's time to create a test for our text component and see the result in the terminal:
// /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();
});
});
✓ 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 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
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:
// /.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.
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:
// /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:
// /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.
// /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:
// 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.
// /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
:
pnpm add -D @tamagui/vite-plugin
// /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.
// /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:
// /src/components/text/text.styles.ts
import { Text, styled } from "@tamagui/core";
export const StyledText = styled(Text, {
color: "$black",
});
// /src/components/text/text.types.tsx
import { GetProps } from "@tamagui/core";
import type { StyledText } from "./text.styles";
export type TextProps = GetProps<typeof StyledText>;
// /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>
)
// /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:
✓ 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:
// /.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;
// /.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
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:
// /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:
// 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:
// 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:
// /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:
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
:
// 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:
// /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"
],
// ...
},
// .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:
╰─± 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.
pnpm run version
🦋 All files have been updated. Review them and commit at your leisure
// / 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.
// /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
.
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!
Top comments (3)
Thank you for the comprehensive guide on setting up a cross-platform component library with Tamagui. Your step-by-step instructions are incredibly valuable for developers aiming to create shared components across both web and mobile platforms.
I’d like to add some insights from Mohammed Sohail's article on Tamagui, which complement your guide:
@tamagui/core
,@tamagui/static
, andtamagui
—to optimize development workflows.Your guide provides an excellent practical implementation of Tamagui, while Sohail’s article offers a broader perspective on its features and position within the cross-platform development landscape. Together, these resources give developers a well-rounded understanding of how to leverage Tamagui in their projects.
For those interested in a deeper dive, I recommend checking out this insightful article by Mohammed Sohail: Tamagui Overview. It offers an in-depth look at how Tamagui can enhance your cross-platform development efforts.
Thank you again for contributing such a detailed and helpful guide to the community!
Does this require including
react-native
as a dependency on the web side?it should not be required, have you encountered any problems?