Introduction#
Recently, I read the documentation for Vite and found that it has a library mode
which is quite convenient for packaging, so I decided to write a blog post to document the process.
Basic Configuration#
Run the following command to create a React + TypeScript project
pnpm create vite
Delete the src and public folders, and create example and packages folders, where example stores component examples or debugging components, and packages stores component source code. Also, don't forget to modify the script
path in the root directory index.html.
├── node_modules
├── packages
├── example
├── index.html
├── package.json
├── pnpm-lock.yaml
├── tsconfig.json
├── tsconfig.node.json
└── vite.config.ts
// index.html
<script type="module" src="/example/main.tsx"></script>
Note: For related eslint, prettier, and tsconfig configurations, please refer to the end of the git repository, as this is not the focus of this article.
Next, let's open vite.config.ts
and configure the packaging (remember to install @types/node first).
import { readFileSync } from 'fs'
import path from 'path'
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
const packageJson = JSON.parse(
readFileSync('./package.json', { encoding: 'utf-8' }),
)
const globals = {
...(packageJson?.dependencies || {}),
}
function resolve(str: string) {
return path.resolve(__dirname, str)
}
export default defineConfig({
plugins: [react()],
build: {
// Output folder
outDir: 'dist',
lib: {
// Entry file for the component library source code
entry: resolve('packages/index.tsx'),
// Component library name
name: 'demo-design',
// File name, packaging result example: suemor.cjs
fileName: 'suemor',
// Packaging format
formats: ['es', 'cjs'],
},
rollupOptions: {
// Exclude unrelated dependencies
external: ['react', 'react-dom', ...Object.keys(globals)],
},
},
})
At this point, you can export some code in the packages/index.tsx
folder, and it should be correctly packaged into CommonJS and ESM.
Component Development#
To keep it simple, we will create a Tag component that has type support and can change colors.

Install dependencies
pnpm i less clsx -D
The following React code will not be introduced.
Create packages/Tag/interface.ts
import { CSSProperties, HTMLAttributes } from 'react'
/**
* @title Tag
*/
export interface TagProps
extends Omit<HTMLAttributes<HTMLDivElement>, 'className' | 'ref'> {
style?: CSSProperties
className?: string | string[]
/**
* @zh Set the background color of the tag
* @en The background color of Tag
*/
color?: Colors
}
type Colors = 'red' | 'orange' | 'green' | 'blue'
Create packages/Tag/index.tsx
import clsx from 'clsx'
import { forwardRef } from 'react'
import './style'
import { TagProps } from './interface'
const Tag: React.ForwardRefRenderFunction<HTMLDivElement, TagProps> = (
props,
ref,
) => {
const { className, style, children, color, ...rest } = props
return (
<div
ref={ref}
style={style}
{...rest}
className={clsx(className,'s-tag', `s-tag-${color}`)}
>
{children}
</div>
)
}
const TagComponent = forwardRef<unknown, TagProps>(Tag)
TagComponent.displayName = 'Tag'
export default TagComponent
export { TagProps }
Create packages/Tag/style/index.less
@colors: red, orange, green, blue;
.s-tag {
display: inline;
padding: 2px 10px;
each(@colors, {
&-@{value} {
background-color: @value;
color: #fff;
}
});
}
Create packages/Tag/style/index.ts
import './index.less';
Create packages/index.tsx
export type { TagProps } from './Tag/interface'
export { default as Tag } from './Tag'
Note: At this point, if we try to package, it will throw an error because we have not installed the @rollup/plugin-typescript
plugin, which is needed to package TypeScript types and generate d.ts files.
pnpm i @rollup/[email protected] -D // The latest version seems to have some strange issues, so we will install version 8.5.0 first.
Go to vite.config.ts
and import the plugin
import typescript from '@rollup/plugin-typescript'
plugins: [
react(),
typescript({
target: 'es5',
rootDir: resolve('packages/'),
declaration: true,
declarationDir: resolve('dist'),
exclude: resolve('node_modules/**'),
allowSyntheticDefaultImports: true,
}),
],
Now, when we execute pnpm build
, the packaging is complete, generating the following directory
Publish to npm#
However, at this point, if we publish the package to npm, users still won't be able to use it. We also need to define some basic entry information and type declarations in package.json
:
{
"name": "@suemor/demo-design",
"version": "0.0.1",
"type": "module",
"main": "./dist/suemor.cjs",
"module": "./dist/suemor.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"require": "./dist/suemor.cjs",
"import": "./dist/suemor.js"
},
"./style": "./dist/style.css"
},
"publishConfig": {
"access": "public"
},
// Specify the folder you want to upload to npm
"files": [
"dist"
],
...
}
After completing this, execute the following command to publish to npm.
npm publish
Then, in your other projects, you can import it, and it will display normally with TypeScript type hints.
import { Tag } from "@suemor/demo-design";
import '@suemor/demo-design/style'
const App = () => {
return (
<div>
<Tag color="orange">I am a tag</Tag>
</div>
);
};
export default App;

Thus, the main part of a simple component library has been developed (though it is quite imperfect), and now we will introduce unit testing.
Adding Unit Tests#
We will use Vitest for unit testing:
pnpm i vitest jsdom @testing-library/react -D
Open the vite.config.ts
file and add type declarations at the first line of the file, and add a few lines of configuration in defineConfig
to let rollup
handle .test
files:
/// <reference types="vitest" />
test: {
globals: true,
environment: 'jsdom',
coverage: {
reporter: [ 'text', 'json', 'html' ]
}
}
Next, open package.json
and add npm commands:
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest"
}
Generally, we will place the test code in the __test__
folder, so create packages/Tag/__test__/index.test.tsx
with the following code:
import { describe, expect, it, vi } from 'vitest'
import { fireEvent, render, screen } from '@testing-library/react'
import { Tag, TagProps } from '../..'
const defineColor: Array<Pick<TagProps, 'color'> & { expected: string }> = [
{ color: 'red', expected: 's-tag-red' },
{ color: 'orange', expected: 's-tag-orange' },
{ color: 'green', expected: 's-tag-green' },
{ color: 'blue', expected: 's-tag-blue' },
]
const mountTag = (props: TagProps) => {
return render(<Tag {...props}>Hello</Tag>)
}
describe('tag click', () => {
const handleCallback = vi.fn()
const tag = mountTag({ onClick: handleCallback })
it('tag click event executed correctly', () => {
fireEvent.click(tag.container.firstChild as HTMLDivElement)
expect(handleCallback).toHaveBeenCalled()
})
})
describe.each(defineColor)('Tag color test', ({ color, expected }) => {
it('tag color', () => {
const tag = mountTag({ color })
const element = tag.container.firstChild as HTMLDivElement
expect(element.classList.contains(expected)).toBeTruthy()
})
})
Run pnpm test
to perform unit testing successfully.
Complete Code#
Complete code repository: https://github.com/suemor233/suemor-design-demo