🎁

ReactコンポーネントのnpmライブラリをTypescriptで作成する

2022/01/09に公開

ReactコンポーネントのnpmライブラリをTypescriptで作成する

前回の続き
https://zenn.dev/marumarumeruru/articles/c06e27deb95b71

前回作成したJavascriptで作成したコンポーネントをTypescriptに変更します

typescript install

npm install --save-dev typescript

rollup plugin

rollup.config.jsで使用します

npm install --save-dev @rollup/plugin-typescript

eslint install

create-react-app --template typescriptでinstallされるものを設定しました
https://zenn.dev/marumarumeruru/articles/54a979bb0b08ef

npm install --save-dev @typescript-eslint/eslint-plugin  @typescript-eslint/parser  eslint  eslint-config-airbnb eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react eslint-plugin-react-hooks

typesync

package.jsonを見て足りない型定義パッケージがあれば自動で追加してくれるパッケージです
https://github.com/jeffijoe/typesync

typesync
npm install

下記がinstallされた

package.json
+    "@types/babel__core": "^7.1.18",
+    "@types/babel__plugin-transform-runtime": "^7.9.2",
+    "@types/babel__preset-env": "^7.9.2",
+    "@types/eslint": "^8.2.2",

tsconfig.json

create-react-app --template typescriptで作成したものを同じ内容を設定しました

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}

.npmignore

.npmignore追記
tsconfig.json

rollup.config.js

rollup.config.js
react-component $ git diff rollup.config.js
diff --git a/rollup.config.js b/rollup.config.js
index 8f3fbfd..956d2f5 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -4,12 +4,13 @@ import styles from "rollup-plugin-styles";
 import babel from '@rollup/plugin-babel';
 import sourcemaps from 'rollup-plugin-sourcemaps';
 import del from 'rollup-plugin-delete';
+import typescript from '@rollup/plugin-typescript';
 
 
 const autoprefixer = require('autoprefixer');
 
 const conf = {
-    input: 'src/index.js',
+    input: 'src/index.tsx',
     output: {
         file: `dist/index.cjs.js`,
         format: "cjs",
@@ -18,6 +19,7 @@ const conf = {
     // this externelizes react to prevent rollup from compiling it
     external: ["react", /@babel\/runtime/],
     plugins: [
+        typescript(),
         // these are babel comfigurations
         babel({
             exclude: 'node_modules/**',

拡張子変更してTypescriptに変更

jsx -> tsx
index.js -> index.tsx

Javascriptで型チェックを行うために、prop-typesを使用してましたが、Typescriptに変更したので、prop-typesは不要になるため削除しました

変更後のソース

index.tsx
import { Header } from './stories/Header'

const returnLibrary = () => {
    return {
        Header: Header
        // you can add here other components that you want to export
    }
}
export default returnLibrary()
Button.tsx
import './button.css';

type Props = {
  primary?: boolean,
  size:  'small'|'medium'|'large',
  backgroundColor?: string,
  label?: string,
  onClick?: React.MouseEventHandler<HTMLButtonElement>
}

/**
 * Primary UI component for user interaction
 */
export const Button: React.VFC<Props> = ({
    primary = false,
    size = 'medium',
    backgroundColor,
    label,
    onClick
  }) => {
  const mode = primary ? 'storybook-button--primary' : 'storybook-button--secondary';
  const className = ['storybook-button', `storybook-button--${size}`, mode].join(' ');

  const style: React.CSSProperties = {
    background: backgroundColor
  };

  return (
    <button onClick={onClick} className={className} style={style}>{label}</button>
  );
};
Button.stories.tsx
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { Button } from './Button';

// More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export
export default {
  title: 'Example/Button',
  component: Button,
  // More on argTypes: https://storybook.js.org/docs/react/api/argtypes
  argTypes: {
    backgroundColor: { control: 'color' },
  },
} as ComponentMeta<typeof Button>;

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

export const Primary = Template.bind({});
// More on args: https://storybook.js.org/docs/react/writing-stories/args
Primary.args = {
  primary: true,
  label: 'Button',
};

export const Secondary = Template.bind({});
Secondary.args = {
  label: 'Button',
};

export const Large = Template.bind({});
Large.args = {
  size: 'large',
  label: 'Button',
};

export const Small = Template.bind({});
Small.args = {
  size: 'small',
  label: 'Button',
};
Header.tsx
import { Button } from './Button';
import './header.css';

type Props = {
  user?: boolean,
  onLogin?: React.MouseEventHandler<HTMLButtonElement>,
  onLogout?: React.MouseEventHandler<HTMLButtonElement>,
  onCreateAccount?: React.MouseEventHandler<HTMLButtonElement>
}

export const Header: React.VFC<Props> = ({
    user,
    onLogin,
    onLogout,
    onCreateAccount
  }) => (
  <header>
    <div className="wrapper">
      <div>
        <svg width="32" height="32" viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
          <g fill="none" fillRule="evenodd">
            <path
              d="M10 0h12a10 10 0 0110 10v12a10 10 0 01-10 10H10A10 10 0 010 22V10A10 10 0 0110 0z"
              fill="#FFF"
            />
            <path
              d="M5.3 10.6l10.4 6v11.1l-10.4-6v-11zm11.4-6.2l9.7 5.5-9.7 5.6V4.4z"
              fill="#555AB9"
            />
            <path
              d="M27.2 10.6v11.2l-10.5 6V16.5l10.5-6zM15.7 4.4v11L6 10l9.7-5.5z"
              fill="#91BAF8"
            />
          </g>
        </svg>
        <h1>Acme</h1>
      </div>
      <div>
        {user ? (
          <Button size="small" onClick={onLogout} label="Log out" />
        ) : (
          <>
            <Button size="small" onClick={onLogin} label="Log in" />
            <Button primary size="small" onClick={onCreateAccount} label="Sign up" />
          </>
        )}
      </div>
    </div>
  </header>
);
Header.stories.tsx
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { Header } from './Header';

export default {
  title: 'Example/Header',
  component: Header,
} as ComponentMeta<typeof Header>;

const Template: ComponentStory<typeof Header> = (args) => <Header {...args} />;

export const LoggedIn = Template.bind({});
LoggedIn.args = {
  user: true,
};

export const LoggedOut = Template.bind({});
LoggedOut.args = {};
Page.tsx
import { Header } from './Header';
import './page.css';

export type PageType = {
  user?: boolean,
  onLogin?: React.MouseEventHandler<HTMLButtonElement>,
  onLogout?: React.MouseEventHandler<HTMLButtonElement>,
  onCreateAccount?: React.MouseEventHandler<HTMLButtonElement>
}

export const Page = ({
      user,
      onLogin,
      onLogout,
      onCreateAccount
    }: PageType) => (
  <article>
    <Header user={user} onLogin={onLogin} onLogout={onLogout} onCreateAccount={onCreateAccount} />

    <section>
      <h2>Pages in Storybook</h2>
      <p>
        We recommend building UIs with a{' '}
        <a href="https://componentdriven.org" target="_blank" rel="noopener noreferrer">
          <strong>component-driven</strong>
        </a>{' '}
        process starting with atomic components and ending with pages.
      </p>
      <p>
        Render pages with mock data. This makes it easy to build and review page states without
        needing to navigate to them in your app. Here are some handy patterns for managing page data
        in Storybook:
      </p>
      <ul>
        <li>
          Use a higher-level connected component. Storybook helps you compose such data from the
          "args" of child component stories
        </li>
        <li>
          Assemble data in the page component from your services. You can mock these services out
          using Storybook.
        </li>
      </ul>
      <p>
        Get a guided tutorial on component-driven development at{' '}
        <a href="https://storybook.js.org/tutorials/" target="_blank" rel="noopener noreferrer">
          Storybook tutorials
        </a>
        . Read more in the{' '}
        <a href="https://storybook.js.org/docs" target="_blank" rel="noopener noreferrer">
          docs
        </a>
        .
      </p>
      <div className="tip-wrapper">
        <span className="tip">Tip</span> Adjust the width of the canvas with the{' '}
        <svg width="10" height="10" viewBox="0 0 12 12" xmlns="http://www.w3.org/2000/svg">
          <g fill="none" fillRule="evenodd">
            <path
              d="M1.5 5.2h4.8c.3 0 .5.2.5.4v5.1c-.1.2-.3.3-.4.3H1.4a.5.5 0 01-.5-.4V5.7c0-.3.2-.5.5-.5zm0-2.1h6.9c.3 0 .5.2.5.4v7a.5.5 0 01-1 0V4H1.5a.5.5 0 010-1zm0-2.1h9c.3 0 .5.2.5.4v9.1a.5.5 0 01-1 0V2H1.5a.5.5 0 010-1zm4.3 5.2H2V10h3.8V6.2z"
              id="a"
              fill="#999"
            />
          </g>
        </svg>
        Viewports addon in the toolbar
      </div>
    </section>
  </article>
);
Page.stories.tsx
import { ComponentStory, ComponentMeta } from '@storybook/react';

import { Page , PageType} from './Page';
import * as HeaderStories from './Header.stories';

export default {
  title: 'Example/Page',
  component: Page,
};

const Template: ComponentStory<typeof Page> = (args) => <Page {...args} />;

export const LoggedIn = Template.bind({});
LoggedIn.args = {
  // More on composing args: https://storybook.js.org/docs/react/writing-stories/args#args-composition
  ...HeaderStories.LoggedIn.args,
};

export const LoggedOut = Template.bind({});
LoggedOut.args = {
  ...HeaderStories.LoggedOut.args,
};

package.json

最終的にpackage.jsonはこの様になりました

package.josnの最終形
{
  "name": "@marumarumeruru/react-component",
  "version": "2.0.0",
  "description": "",
  "main": "dist/index.cjs.js",
  "scripts": {
    "build": "rollup -c",
    "storybook": "start-storybook -p 6006",
    "build-storybook": "build-storybook"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "@babel/cli": "^7.16.7",
    "@babel/core": "^7.16.7",
    "@babel/plugin-transform-runtime": "^7.16.7",
    "@babel/preset-env": "^7.16.7",
    "@babel/preset-react": "^7.16.7",
    "@rollup/plugin-babel": "^5.3.0",
    "@rollup/plugin-commonjs": "^21.0.1",
    "@rollup/plugin-node-resolve": "^13.1.3",
    "@rollup/plugin-typescript": "^8.3.0",
    "@storybook/addon-actions": "^6.4.9",
    "@storybook/addon-essentials": "^6.4.9",
    "@storybook/addon-links": "^6.4.9",
    "@storybook/react": "^6.4.9",
    "@types/babel__core": "^7.1.18",
    "@types/babel__plugin-transform-runtime": "^7.9.2",
    "@types/babel__preset-env": "^7.9.2",
    "@types/eslint": "^8.2.2",
    "@typescript-eslint/eslint-plugin": "^5.9.0",
    "@typescript-eslint/parser": "^5.9.0",
    "autoprefixer": "^10.4.2",
    "babel-loader": "^8.2.3",
    "eslint": "^8.6.0",
    "eslint-config-airbnb": "^19.0.4",
    "eslint-plugin-import": "^2.25.4",
    "eslint-plugin-jsx-a11y": "^6.5.1",
    "eslint-plugin-react": "^7.28.0",
    "eslint-plugin-react-hooks": "^4.3.0",
    "rollup": "^2.63.0",
    "rollup-plugin-delete": "^2.0.0",
    "rollup-plugin-sourcemaps": "^0.6.3",
    "rollup-plugin-styles": "^3.14.1",
    "typescript": "^4.5.4"
  },
  "dependencies": {
    "@babel/runtime": "^7.16.7"
  }
}

build

react-component $ npm run build

> @marumarumeruru/react-component@1.0.14 build
> rollup -c


src/index.tsx → dist/index.cjs.js...
created dist/index.cjs.js in 4s

storybook

react-component $ npm run storybook
最終的な構成
react-component $ tree -a -I "node_modules|.git|storybook-static"
.
├── .DS_Store
├── .babelrc
├── .gitignore
├── .npmignore
├── .storybook
│   ├── main.js
│   └── preview.js
├── README.md
├── dist
│   └── index.cjs.js
├── package-lock.json
├── package.json
├── rollup.config.js
├── src
│   ├── index.tsx
│   └── stories
│       ├── Button.stories.tsx
│       ├── Button.tsx
│       ├── Header.stories.tsx
│       ├── Header.tsx
│       ├── Introduction.stories.mdx
│       ├── Page.stories.tsx
│       ├── Page.tsx
│       ├── assets
│       │   ├── code-brackets.svg
│       │   ├── colors.svg
│       │   ├── comments.svg
│       │   ├── direction.svg
│       │   ├── flow.svg
│       │   ├── plugin.svg
│       │   ├── repo.svg
│       │   └── stackalt.svg
│       ├── button.css
│       ├── header.css
│       └── page.css
└── tsconfig.json

5 directories, 31 files

作成したパッケージを利用する

前回と同じ方法で利用できます(JavascriptのままでOK)
https://zenn.dev/marumarumeruru/articles/c06e27deb95b71

Discussion