필자는 프로젝트를 할때마다 파일을 하나만들고 그 안에 스토리북과 tsx파일을 한곳에 모아서 작성하는 경향이 있다.
즉
<fileName>-----<fileName>.stories.tsx
|
|--<fileName>.tsx
import React from 'react'
const Test = () => {
return (
<div>Test</div>
)
}
export default Test
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import Test from './Test';
export default {
title: 'Components/Test',
component: Test,
} as ComponentMeta<typeof Test>;
export const CardDafault: ComponentStory<typeof Test> = () => (
<Test/>
);
이런 구조를 적용 시켜주는 편인데 파일을 하나 만들때마다 각자 세팅을 해주는데 파일을 하나 만들때마다 일일히 만들어주는 것은 굉장히 귀찮은 일이다.
그러다보니 이걸 자동화 해줄 수 있는 cli를 만들어주면 어떨까하는 생각이 들어 cli를 작성하였다.
// command.js
const { program } = require('commander');
const fs = require('fs');
const path = require('path');
const inquirer = require('inquirer');
const chalk = require('chalk');
const tsxTemplate = tsx => {
return `
import React from 'react'
const ${tsx} = () => {
return (
<div>${tsx}</div>
)
}
export default ${tsx}
`;
};
const storybookTemplate = tsx => {
return `
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import ${tsx} from './${tsx}';
export default {
title: 'Components/${tsx}',
component: ${tsx},
} as ComponentMeta<typeof ${tsx}>;
export const CardDafault: ComponentStory<typeof ${tsx}> = () => (
<${tsx}/>
);
`;
};
const exist = dir => {
try {
fs.accessSync(
dir,
fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK,
);
return true;
} catch (e) {
return false;
}
};
const makeStoybook = dir => {
const filePath = `./src/components/${dir}`;
const dirname = path
.relative('.', path.normalize(filePath))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
const makeTsxbook = dir => {
const filePath = `./src/components/${dir}`;
const dirname = path
.relative('.', path.normalize(filePath))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
console.log('pathBuilder', pathBuilder);
fs.mkdirSync(pathBuilder);
}
});
};
const makeStorybookTemplate = dir => {
const componentName = upperCase(dir);
makeStoybook(componentName);
const pathToFile = path.join(
`./src/components/${componentName}`,
`${componentName}.stories.tsx`,
);
if (exist(pathToFile)) {
console.error(chalk.bold.red('이미 해당 파일이 존재합니다'));
} else {
fs.writeFileSync(pathToFile, storybookTemplate(componentName));
console.log(chalk.green(pathToFile, '생성 완료'));
}
};
const makeTsxTemplate = dir => {
const componentName = upperCase(dir);
makeTsxbook(componentName);
const pathToFile = path.join(
`./src/components/${componentName}`,
`${componentName}.tsx`,
);
if (exist(pathToFile)) {
console.error(chalk.bold.red('이미 해당 파일이 존재합니다'));
} else {
fs.writeFileSync(pathToFile, tsxTemplate(componentName));
console.log(chalk.green(pathToFile, '생성 완료'));
}
};
const upperCase = str => {
const firstChar = str[0];
const firstCharUpper = firstChar.toUpperCase();
const leftChar = str.slice(1, str.length);
const result = firstCharUpper + leftChar;
return result;
};
program
.command('template ')
.usage(' --filename [filename] --path [path]')
.description('템플릿을 생성합니다.')
.alias('tmpl')
.option('-f, --filename [filename]', '파일명을 입력하세요.', 'index')
.action(options => {
// makeTemplate(type, options.filename, options.directory);
makeStorybookTemplate(options.filename);
console.log('options', options.filename);
makeTsxTemplate(options.filename);
});
program.version('0.0.1', '-v, --version').name('cli');
program
.action((cmd, args) => {
if (args) {
console.log(chalk.bold.red('해당 명령어를 찾을 수 없습니다.'));
program.help();
} else {
inquirer
.prompt([
{
type: 'input',
name: 'name',
message: '파일의 이름을 입력하세요.',
default: 'index',
},
])
.then(answers => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('터미널을 종료합니다.'));
}
});
}
})
.parse(process.argv);
부분적으로 나누어서 확인해보자
const tsxTemplate = tsx => {
return `
import React from 'react'
const ${tsx} = () => {
return (
<div>${tsx}</div>
)
}
export default ${tsx}
`;
};
우리가 만들어줄 tsx파일의 내부에 넣어줄 template을 넣어줄 코드 입니다.
fileName을 받아 넣어줄 수 있게 해주었습니다.
const storybookTemplate = tsx => {
return `
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import ${tsx} from './${tsx}';
export default {
title: 'Components/${tsx}',
component: ${tsx},
} as ComponentMeta<typeof ${tsx}>;
export const CardDafault: ComponentStory<typeof ${tsx}> = () => (
<${tsx}/>
);
`;
};
우리가 만들어줄 storybook파일의 내부에 넣어줄 template을 넣어줄 코드 입니다.
fileName을 받아 넣어줄 수 있게 해주었습니다.
const exist = dir => {
try {
fs.accessSync(
dir,
fs.constants.F_OK | fs.constants.R_OK | fs.constants.W_OK,
);
return true;
} catch (e) {
return false;
}
};
파일이 존재할 경우 true 값을 없을 경우에는 false값을 return해줍니다.
const makeStoybook = dir => {
const filePath = `./src/components/${dir}`;
const dirname = path
.relative('.', path.normalize(filePath))
.split(path.sep)
.filter(p => !!p);
dirname.forEach((d, idx) => {
const pathBuilder = dirname.slice(0, idx + 1).join(path.sep);
if (!exist(pathBuilder)) {
fs.mkdirSync(pathBuilder);
}
});
};
각 상수에 어떠한 값이 들어가는지 확인해 보면
dirName의 경우 ['src','components','${dir}']의 배열을 가지게 됩니다.
배열을 돌면서
파일이 존재하지 않을 경우 fs.mkdirSync에 의해서 파일을 만들어줍니다.
const upperCase = str => {
const firstChar = str[0];
const firstCharUpper = firstChar.toUpperCase();
const leftChar = str.slice(1, str.length);
const result = firstCharUpper + leftChar;
return result;
};
fileName을 받으면 맨 앞글자를 대문자로 바꿔주는 함수입니다. 이걸로 인해 소문자로 입력을 하더라도 파일명은 맨앞글자는 대문자가 됩니다.
const makeStorybookTemplate = dir => {
const componentName = upperCase(dir);
makeStoybook(componentName);
const pathToFile = path.join(
`./src/components/${componentName}`,
`${componentName}.stories.tsx`,
);
if (exist(pathToFile)) {
console.error(chalk.bold.red('이미 해당 파일이 존재합니다'));
} else {
fs.writeFileSync(pathToFile, storybookTemplate(componentName));
console.log(chalk.green(pathToFile, '생성 완료'));
}
};
이제 cli를 직접 실행해줄 함수를 제작해줍시다.
해당 함수에 fileName이 들어오면 해당 문자열의 첫번째 문자를 대문자로 바꿔줍니다.
그런 다음 components 폴더 안에 dir파일을 만들어주고 storybook파일 내부에 template을 넣어줍니다.
yarn add commander inquirer chalk
program
.command('template ')
.usage(' --filename [filename] --path [path]')
.description('템플릿을 생성합니다.')
.alias('tmpl')
.option('-f, --filename [filename]', '파일명을 입력하세요.', 'index')
.action(options => {
// makeTemplate(type, options.filename, options.directory);
makeStorybookTemplate(options.filename);
console.log('options', options.filename);
makeTsxTemplate(options.filename);
});
program.version('0.0.1', '-v, --version').name('cli');
program
.action((cmd, args) => {
if (args) {
console.log(chalk.bold.red('해당 명령어를 찾을 수 없습니다.'));
program.help();
} else {
inquirer
.prompt([
{
type: 'input',
name: 'name',
message: '파일의 이름을 입력하세요.',
default: 'index',
},
])
.then(answers => {
if (answers.confirm) {
makeTemplate(answers.type, answers.name, answers.directory);
console.log(chalk.rgb(128, 128, 128)('터미널을 종료합니다.'));
}
});
}
})
.parse(process.argv);
{
"scripts": {
"cli": "node ./command.js",
},
}
yarn cli template -f <fileName>
fileName을 입력하면 components file안에 폴더가 생성됩니다.