🧚🏼‍♂️

はじめてのnpmパッケージを作って公開(コンポーネントライブラリ)

2022/02/17に公開1

はじめに

前々から一度はやってみたかったnpmパッケージを作って公開するをやってみました。

TL;DR

  • npmパッケージを作って公開する手順を知りたかったので、調べてわかったことを書きました。(実際に簡単なものを作って公開した手順をひととおり書きました)
  • 作ったパッケージは、react, typescript, rollup, storybook, emotion を使ったコンポーネントライブラリのセットアップといった内容です(簡単なTextコンポーネント作っただけです)
  • npmのパッケージを公開してみたい、簡単なコンポーネントライブラリ作ってみたい、という人の参考になるかもです。

作ったもの

https://www.npmjs.com/package/react-component-lib-sample

https://github.com/shimabukuromeg/react-component-lib-sample

やったこと

以下、パッケージ作成からnpm公開までにやったことです。

  • npmパッケージを公開するプロジェクトの作成
  • 必要なパッケージ導入する
  • 実装する(コンポーネントライブラリの実装)
  • ビルドする
  • npmパッケージ公開する
  • 別プロジェクトで公開したパッケージを使ってみる

プロジェクトの作成

react-component-lib-sample の部分などは、適宜変更してください。

$ mkdir react-component-lib-sample
$ cd react-component-lib-sample
$ yarn init
$ mkdir src
$ touch .gitignore
.gitignore
.DS_Store
*.log
node_modules
# Dist and query are both build output folders
dist*/
# But don't ignore the RTK Query source
lib
es

.yalc
yalc.lock

.idea/
.vscode/
temp/
.tmp-projections
build/
.rts2*
coverage/

typesversions
.cache
.yarnrc
.yarn/*
!.yarn/patches
!.yarn/releases
!.yarn/plugins
!.yarn/sdks
!.yarn/versions
.pnp.*
*.tgz

.yalc
yalc.lock
yalc.sig
example

必要なパッケージ導入する

  • 公開するnpmパッケージを開発する際に、必要なパッケージの導入します。
  • 後続の説明で、パッケージを追加する手順もありますが、この記事での最終的な package.json は以下の通りです。
pacakge.json
{
  "name": "react-component-lib-sample",
  "version": "1.0.0",
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  "author": "[author]",
  "license": "MIT",
  "dependencies": {
    "@emotion/react": "^11.7.1",
    "@emotion/styled": "^11.6.0",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  },
  "devDependencies": {
    "@babel/core": "^7.17.4",
    "@rollup/plugin-commonjs": "^21.0.1",
    "@rollup/plugin-typescript": "^8.3.0",
    "@storybook/addon-actions": "^6.4.19",
    "@storybook/addon-essentials": "^6.4.19",
    "@storybook/addon-interactions": "^6.4.19",
    "@storybook/addon-links": "^6.4.19",
    "@storybook/react": "^6.4.19",
    "@storybook/testing-library": "^0.0.9",
    "@types/react": "^17.0.39",
    "babel-loader": "^8.2.3",
    "eslint": "^8.9.0",
    "eslint-config-prettier": "^8.3.0",
    "prettier": "^2.5.1",
    "rollup": "^2.67.2",
    "rollup-plugin-dts": "^4.1.0",
    "tslib": "^2.3.1",
    "typescript": "^4.5.5"
  },
  "peerDependencies": {
    "@emotion/react": "^11.0.0",
    "@emotion/styled": "^11.6.0",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  "files": [
    "package.json",
    "README.md",
    "LICENSE",
    "types",
    "dist"
  ],
  "scripts": {
    "build": "rollup -c",
    "lint": "eslint --ext ts,tsx .",
    "format": "prettier --write .",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  }
}
$ yarn add react react-dom
$ yarn add -D typescript @types/react
$ node_modules/.bin/tsc --init
tsconfig.json
{
  "compilerOptions": {
    "target": "es5",
    "module": "esnext",
    "jsx": "react",
    "sourceMap": false,
    "outDir": "dist",
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationDir": "types",
    "emitDeclarationOnly": true,
    "rootDir": "./src"
  }
}

  • eslint導入
$ yarn add -D eslint
$ yarn eslint --init
$ yarn add -D eslint-config-prettier
.eslintrc.js
module.exports = {
  env: {
    browser: true,
    es2021: true,
  },
  extends: ['plugin:react/recommended', 'prettier'],
  parser: '@typescript-eslint/parser',
  parserOptions: {
    ecmaFeatures: {
      jsx: true,
    },
    ecmaVersion: 12,
    sourceType: 'module',
  },
  plugins: ['react'],
  settings: {},
  rules: {
    'react/prop-types': 0,
    'react/jsx-props-no-spreading': [
      2,
      {
        html: 'enforce',
        custom: 'ignore',
        explicitSpread: 'enforce',
      },
    ],
  },
};

package.jsonのスクリプトを更新する

...
  "lint": "eslint --ext ts,tsx .",
...
  • prettier 導入
$ yarn add -D prettier
$ touch .prettierrc.json
.prettierrc.json
{
  "singleQuote": true
}

package.jsonのスクリプトを更新する

...
  "format": "prettier --write .",
...
  • rollup導入
rollupとは
$ yarn add -D rollup @rollup/plugin-commonjs @rollup/plugin-typescript tslib rollup-plugin-dts
$ touch rollup.config.js
rollup.config.js
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import dts from 'rollup-plugin-dts';
const pkg = require('./package.json');

export default [
  {
    input: 'src/index.ts',
    output: [
      {
        file: pkg.main,
        format: 'cjs',
        sourcemap: false,
      },
      {
        file: pkg.module,
        format: 'esm',
        sourcemap: false,
      },
    ],
    plugins: [
      commonjs({
        include: ['node_modules/**'],
      }),
      typescript({
        tsconfig: './tsconfig.json',
        exclude: ['**/__tests__/**'],
      }),
    ],
    external: ['react', 'react-dom', '@emotion/styled', '@emotion/react'],
  },
  {
    input: 'dist/cjs/types/index.d.ts',
    output: [{ file: 'dist/cjs/index.d.ts', format: 'cjs' }],
    plugins: [dts()],
  },
  {
    input: 'dist/esm/types/index.d.ts',
    output: [{ file: 'dist/esm/index.d.ts', format: 'esm' }],
    plugins: [dts()],
  },
];

バンドルするときに使うコマンドを package.json のスクリプトに追記する

...
  "build": "rollup -c",
...
  • emotion導入する(今回作成するコンポーネントで利用するため)
$ yarn add @emotion/react @emotion/styled
  • storybookを導入する
$ npx sb init
  • storybookを起動する
$ yarn run storybook

  • 導入時の example コンポーネントは不要なので削除しておきます。
$ rm -rf stories/

コンポーネントライブラリの実装する

Textコンポーネントを実装してみます。

$ mkdir -p src/constants
$ touch src/constants/index.ts
$ mkdir -p src/components/Text
$ touch src/components/Text/Text.tsx
$ touch src/components/Text/index.ts
$ touch src/components/Text/Text.stories.tsx
$ touch src/components/index.ts
$ touch src/index.ts
src/constants/index.ts
export const fontSize = {
  s: '10px',
  m: '16px',
  l: '24px',
} as const;
src/components/Text/Text.tsx
import React, { VFC } from 'react';
import styled from '@emotion/styled';
import { fontSize } from '../../constants';

export type TextProps = {
  text: string;
  className?: string;
};

export const Text: VFC<TextProps> = ({ text, className = '' }) => {
  return <Wrapper className={className}>{text}</Wrapper>;
};

const Wrapper = styled.p`
  font-size: ${fontSize.m};
`;
src/components/Text/index.ts
export { TextProps, Text } from './Text';
src/components/Text/Text.stories.tsx
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { Text } from './Text';

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
  title: 'Text/Text',
  component: Text,
} as ComponentMeta<typeof Text>;

// More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args
const Template: ComponentStory<typeof Text> = (args) => <Text {...args} />;

export const Default = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Default.args = {
  text: 'hello, world',
};
src/components/index.ts
export { TextProps, Text } from './Text';
src/index.ts
export { TextProps, Text } from './components';
  • storybook を立ち上げて作成したコンポーネントを見てみる
$ yarn run storybook

以上で、公開用のパッケージ作成は終わりです。続いて、作成したパッケージを公開していきましょう。

ビルドする

  • package.json のエントリーポイントを以下のように修正します。
  "main": "dist/cjs/index.js",
  "module": "dist/esm/index.js",
  • ビルドコマンドを実行します。dist ディレクトリ配下にファイルが生成されてること
$ yarn run build
yarn run v1.22.11
$ rollup -c

src/index.ts → dist/cjs/index.js, dist/esm/index.js...
created dist/cjs/index.js, dist/esm/index.js in 2.4s

dist/cjs/types/index.d.ts → dist/cjs/index.d.ts...
created dist/cjs/index.d.ts in 18ms

dist/esm/types/index.d.ts → dist/esm/index.d.ts...
created dist/esm/index.d.ts in 11ms

パッケージ公開する

  • npmに公開するファイルを、package.jsonに記述します。
  "files": [
    "package.json",
    "README.md",
    "LICENSE",
    "types",
    "dist"
  ],
  • 作成したパッケージ(Textコンポーネント)は emotion に依存しているので、package.json に以下を記述します。
  "peerDependencies": {
    "@emotion/react": "^11.0.0",
    "@emotion/styled": "^11.6.0",
    "react": "^16.13.1",
    "react-dom": "^16.13.1"
  },
  • npmパッケージを公開は下記のサイトから行います。まずはアカウントが必要なのでsingupページから新規登録します。

  • アカウント作成したらログインします。

$ npm login
  • パッケージ公開
$ npm publish
  • パッケージ検索
$ npm search react-component-lib-sample
  • パッケージ削除
$ npm unpublish react-component-lib-sample --force

別プロジェクトに公開したパッケージをインストールして使ってみる

  • 今回公開したパッケージを実際に動かしてみましょう。動かしてみる用のプロジェクトを作成し、導入して動かしてみます。以下、create-next-appして、nextjsで確認してみます。
$ npx create-next-app@latest --typescript example
$ cd example
  • 公開したパッケージ導入します
$ yarn add react-component-lib-sample @emotion/react @emotion/styled
  • package.jsonの差分を見ると、公開したパッケージが追加されてることを確認できます。
   "dependencies": {
     "next": "12.0.10",
     "react": "17.0.2",
+    "react-component-lib-sample": "^1.0.0",
     "react-dom": "17.0.2"
   },
  • テキストコンポーネントを呼び出してみます。pages/index.tsx を以下のように編集します。
import type { NextPage } from 'next'
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import { Text } from 'react-component-lib-sample'

const Home: NextPage = () => {
  return (
    <div>
      <Text text="hello, world" />
    </div>
  )
}

export default Home

  • 起動します。
$ yarn run dev
  • hello, worldが表示されること

おわりに

  • 作ったライブラリ自体は簡単なものでしたが、npm公開の仕組みがわかってよかったです。
  • 公開の仕方がわかったので、今度はもっと有益な何かを作って出したいです。
  • yarn add react-component-lib-sample できたのは感動した。

参考

参考にしたリンクたち

https://github.com/kufu/smarthr-ui

https://qiita.com/knjname/items/0c521a81ff2695a94368

https://zenn.dev/yuki0410/articles/74f80c4243919ea2a247-2

https://zenn.dev/antez/articles/a9d9d12178b7b2#アカウント作成

https://dev.to/siddharthvenkatesh/component-library-setup-with-react-typescript-and-rollup-onj

https://zenn.dev/lidqqq/articles/1ed2ca8a1bb41b3d002d

https://rollupjs.org/guide/en/

https://zenn.dev/manycicadas/books/b6f2d99b5208e9/viewer/f37864

GitHubで編集を提案

Discussion