TypeScriptでcomponentのscaffoldを作ろう
はじめに
ふといつものように React でコンポーネントを作っていたらうちの TL の@kichionが毎回同じような構成のファイル作るの面倒だわ
と言い出し Golang でコンポーネントを scaffold する CLI を作りよった。
大層便利なその CLI はmake scaffold COMPONENT=atoms/buttons NAME=TextButton
みたいなコマンドを実行するとsrc/components/atoms/buttons/TextButton
ディレクトリにコンポーネント用・storybook 用の tsx ファイル、css modules 用の css ファイルをよしなに作成してくれおる。
大層感銘を受けた hada だったがフロントエンドのリポジトリに golang のコードが入ってるのもなぁ、どうせなら TypeScript で完結したいと思い、golang で書かれた script を TypeScript で書き直す決意をしたのであった。
記事のターゲット
- 新しいコンポーネント作ろーっと → 行数少ないこのコンポーネントから構成コピペして持ってこよう → 名前書き換えよう(めんどくさいなぁ)と思ってる人
環境
関係ありそうなとこだけ
{
"dependencies": {
"react": "16.14.0",
"react-dom": "16.14.0",
},
"devDependencies": {
"@types/node": "14.14.6",
"ts-node": "9.1.1",
"typescript": "4.0.5"
}
}
前提
対象のプロジェクトでは atomic design が採用されており、一つのコンポーネント作るには下記のような構成になっている
src/components/atoms/texts/Txt
├── index.stories.tsx // storybookの定義
├── index.tsx // componentの定義
└── styles.ts // styled-componentの定義
それぞれのファイルで下記で示すように共通する書き方があり、これらをすでにあるコンポーネントから毎回コピペで持ってきて名前を差し替えるのが面倒であった。
component
import classnames from 'classnames';
import React from 'react';
import { Div } from './styles';
export type TxtProps = {
name: string;
};
export const Txt: React.FC<TxtProps> = ({ name }: TxtProps) => (
<Div>
<p>Enjoy {name} component life!!</p>
</Div>
);
storybook
import { storiesOf } from '@storybook/react';
import React from 'react';
import { Txt } from '.';
storiesOf('${path}/${name}', module).add('default', () => <Txt name="name" />);
styled-component
import styled from 'styled-components';
// theme colorとか使うときに呼び出す
// import { ThemeProps } from 'styles/Theme';
export const Div = styled.div``;
いざ実装
TypeScript+node.js を実行できるようにする
ts-node を使うと TypeScript ファイルの実行と node.js の REPL をやってくれるのでインストールする
TypeScript execution and REPL for node.js, with source map support.
https://github.com/TypeStrong/ts-node
Next.js や React を使ってる場合、tsconfig がもしかしたら node.js で使う型を扱えないかもしれないので今回作るスクリプト用の tsconfig を設定する
(今回はルート直下に scripts ディレクトリを作り、そこにスクリプトを作る想定)
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"isolatedModules": false,
"types": ["ts-node"]
},
"include": ["./**/*.ts"]
}
スクリプトの実装
簡単 3 ステップだよ
- ディレクトリとコンポーネント名を引数から受け取る
- 作成したいファイルの中身を定義する
- ファイルを指定したディレクトリに作成する
コードの全容は一番最後に記載してやす
コマンドの実装
下記のようなコマンドを package.json に登録して
"scripts": {
"scaffold": "ts-node --project scripts/tsconfig.json scripts/component_scaffold/index.ts --"
},
yarn scaffold --component=atoms/buttons --name=FooButton
するとよしなにコンポーネントを作ってくれる
ポイント
- ts-node の
--project
オプションでどの tsconfig 使うか指定できるよ - ts-node の型を script で呼び出したい時は tsconfig の
compilerOptions
のtypes
でts-node
を指定しよう - ts-node でのカスタム引数の指定する前に
--
をいれるts-node --project scripts/tsconfig.json scripts/component_scaffold/index.ts -- --component=atoms/buttons --name=FooButton
- 引数を受け取る
process.argv
が独特なので気をつけて -
fs
のexistsSync
でファイルやディレクトリの存在確認ができるよ -
fs
のmkdirSync
でディレクトリの作成ができるよ -
fs
のwriteFileSync
でファイルの作成ができるよ-
writeFileSync
はすでにディレクトリがないとエラーを返すので要注意
-
参考: process.argv の返り値
[
'/Users/yhada/dev/project-name/node_modules/.bin/ts-node',
'/Users/yhada/dev/project-name/scripts/component_scaffold/index.ts',
'--',
'--component=atoms/texts',
'--name=Txt'
]
以下、コード全容
スクリプト全容
import { existsSync, mkdirSync, writeFileSync } from 'fs';
import {
createComponentTemplate,
createStorybookTemplate,
createStyleTemplate
} from './templates';
const getArgValue = (arg: string): { name: string; value: string } => {
const initStr = arg.slice(0, 2);
if (initStr !== '--') {
throw new Error('引数がおかしいでヤンス。「--」で指定してほしいでヤンス');
}
const equalIndex = arg.indexOf('=');
if (equalIndex === -1) {
throw new Error(
'引数の指定方法がおかしいでヤンス。「--component=atoms --name=ComponentName」にしてほしいでやんす'
);
}
const name = arg.slice(2, equalIndex);
if (name.length === 0) {
throw new Error('引数がないでヤンス');
}
if (name !== 'component' && name !== 'name') {
throw new Error(
'引数が違うでヤンス。「--component=atoms --name=ComponentName」にしてほしいでやんす'
);
}
const value = arg.slice(equalIndex + 1, arg.length);
if (value.length === 0) {
throw new Error('引数の値がないでヤンス');
}
return { name, value };
};
const validateArgs = (args: string[]) => {
if (args.length !== 5) {
throw new Error(
'引数の数が正しくないでゲス。--componentと--nameを指定してほしいでゲス'
);
}
};
const main = () => {
try {
validateArgs(process.argv);
const pathName = getArgValue(process.argv[3]);
const componentName = getArgValue(process.argv[4]);
// HACK: 雑な実装です
const path = `src/components/${pathName.value}/${componentName.value}`;
if (!existsSync(path)) {
mkdirSync(path, {
recursive: true
});
}
writeFileSync(
`${path}/index.tsx`,
createComponentTemplate(componentName.value)
);
writeFileSync(`${path}/styles.ts`, createStyleTemplate());
writeFileSync(
`${path}/index.stories.tsx`,
createStorybookTemplate(pathName.value, componentName.value)
);
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
};
main();
テンプレート生成部分
// コンポーネントのテンプレート
export const createComponentTemplate = (
name: string
): string => `// import classnames from 'classnames';
import React from 'react';
import { Div } from './styles';
export type ${name}Props = {
name: string;
};
export const ${name}: React.FC<${name}Props> = ({ name }: ${name}Props) => (
<Div>
<p>Enjoy {name} component life!!</p>
</Div>
);
`;
// storybookのテンプレート
export const createStorybookTemplate = (
path: string,
name: string
): string => `import { storiesOf } from '@storybook/react';
import React from 'react';
import { ${name} } from '.';
storiesOf('${path}/${name}', module).add('default', () => <${name} name="name" />);
`;
// styled-componentのテンプレート
export const createStyleTemplate = (): string => `import styled from 'styled-components';
// import { ThemeProps } from 'styles/Theme';
export const Div = styled.div\`\`;
`;
以上、意外と簡単にできる scaffold のお話でした!
Discussion