前言#
最近読みました vite のドキュメントで、ライブラリモード
がパッケージ化に便利であることに気づき、ブログにその過程を記録します。
基本配置#
以下のコマンドを実行して、React + TypeScript のプロジェクトを作成します。
pnpm create vite
src と public フォルダーを削除し、example と packages フォルダーを作成します。example にはコンポーネントの例やデバッグ用のコンポーネントを格納し、packages にはコンポーネントのソースコードを格納します。また、ルートディレクトリの index.html の script
パスを変更するのを忘れないでください。
├── 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>
注:関連する eslint prettier tsconfig の設定は末尾の git リポジトリを参照してください。これはこの記事の重点ではありません。
次に vite.config.ts
を開き、パッケージ化の設定を行います(最初に @types/node をインストールするのを忘れずに)。
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: {
// 出力フォルダー
outDir: 'dist',
lib: {
// コンポーネントライブラリのソースコードのエントリーファイル
entry: resolve('packages/index.tsx'),
// コンポーネントライブラリ名
name: 'demo-design',
// ファイル名, パッケージ化結果の例: suemor.cjs
fileName: 'suemor',
// パッケージ化フォーマット
formats: ['es', 'cjs'],
},
rollupOptions: {
// 関連のない依存関係を除外
external: ['react', 'react-dom', ...Object.keys(globals)],
},
},
})
この時点で packages/index.tsx
フォルダー内で任意のコードを export すれば、CommonJS と ESM に正しくパッケージ化されるはずです。
コンポーネント作成#
簡単のために、型サポートがあり、色を切り替えられる Tag コンポーネントを作成します。

依存関係をインストールします。
pnpm i less clsx -D
以下の react コードについては説明しません。
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 タグの背景色を設定
* @en The background color of Tag
*/
color?: Colors
}
type Colors = 'red' | 'orange' | 'green' | 'blue'
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 }
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;
}
});
}
packages/Tag/style/index.ts
を作成します。
import './index.less';
packages/index.tsx
を作成します。
export type { TagProps } from './Tag/interface'
export { default as Tag } from './Tag'
注意:この時点でパッケージ化を行うとエラーが発生します。なぜなら @rollup/plugin-typescript
プラグインをインストールしていないため、ts 型をパッケージ化できず、d.ts を生成できないからです。
pnpm i @rollup/[email protected] -D //ここで最新バージョンには少し奇妙な問題があるようなので、まずは 8.5.0 バージョンをインストールします。
vite.config.ts
にプラグインをインポートします。
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,
}),
],
この時点で pnpm build
を実行すると、パッケージ化が完了し、以下のディレクトリが生成されます。
npm に公開#
しかし、この時点でパッケージを npm に公開しても、ユーザーは依然として使用できません。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"
},
// npm にアップロードするフォルダーを指定
"files": [
"dist"
],
...
}
完了したら、実行して npm に公開します。
npm publish
その後、他のプロジェクトでインポートすれば、正常に表示され、TypeScript の型ヒントも利用できます。
import { Tag } from "@suemor/demo-design";
import '@suemor/demo-design/style'
const App = () => {
return (
<div>
<Tag color="orange">私はタグです</Tag>
</div>
);
};
export default App;

これでシンプルなコンポーネントライブラリの主体部分が開発完了しました(まだ不完全ですが)。次に単体テストを導入します。
単体テストの追加#
私たちは vitest を使用して単体テストを行います:
pnpm i vitest jsdom @testing-library/react -D
vite.config.ts
ファイルを開き、ファイルの最初の行に型宣言を追加し、defineConfig
にいくつかの設定を加えて、rollup
が .test
ファイルを処理できるようにします。
/// <reference types="vitest" />
test: {
globals: true,
environment: 'jsdom',
coverage: {
reporter: [ 'text', 'json', 'html' ]
}
}
次に package.json
に npm コマンドを追加します:
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest"
}
一般的に、単体テストのコードは __test__
フォルダーに置くので、packages/Tag/__test__/index.test.tsx
を新しく作成し、コードは以下の通りです:
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()
})
})
pnpm test
を実行すれば、正常に単体テストが行えます。
完全なコード#
完全なコードリポジトリ: https://github.com/suemor233/suemor-design-demo