-
create-react-app 처럼 cli를 하나 만들어보자Node 2024. 3. 25. 18:50
Create-react-app와 같이 cli를 만들 필요가 있어 create-react-app repository를 풀 받아서 분석했습니다.
Npm package를 만들고 npm에 배포하면서 얻은 노하우를 공유드립니다.
[Vue.js] - Vue CLI UI를 사용해서 Vue 프로젝트를 생성하는 방법
#Vue UI란? Vue UI는 Vue CLI를 사용하여 생성된 프로젝트를 관리하고 시각적으로 구성할 수 있는 그래픽 사용자 인터페이스입니다. Vue CLI는 Vue.js 애플리케이션을 개발하기 위한 공식적인 명령줄 인
pingfanzhilu.tistory.com
처럼 ui를 이용해서 프로젝트를 생성하는 방법도 추후 공유드리겠습니다.
소스: https://github.com/biglol10/tistory_source/tree/main/cli-for-tistory
cli-ui 링크: (not yet)
# 필요한 패키지
"dependencies": {"chalk": "^4.1.2","commander": "^4.1.1","download-github-repo": "^0.1.4","fs-extra": "^11.2.0","prompts": "^2.4.2","execa": "^1.0.0"}cra를 분석하고 로컬에서 개발해보면 패키지 버전이 중요하다는 것을 알 수 있습니다.
commander, chalk, execa등은 버전이 많이 바뀌면서 common.js는 지원하지 않거나 사용방법이 바꼈습니다.
create-react-app이랑 로직을 비슷하게 구성할 것이기 때문에 버전은 동일하게 가져가겠습니다.
# package.json 구성
npm init를 하여 package.json을 생성해줍니다
{ "name": "create-react-app-for-tistoryblog", "version": "1.0.1", "description": "Example of sample cli like create-react-app", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "bin": { "create-react-app-for-tistoryblog": "./bin/index.js" }, "author": "biglol", "license": "MIT", "dependencies": { "chalk": "^4.1.2", "commander": "^4.1.1", "download-github-repo": "^0.1.4", "fs-extra": "^11.2.0", "prompts": "^2.4.2", "execa": "^1.0.0" } }
name은 패키지를 식별하는 이름이고 bin은 커맨드에서 직접 호출할 수 있는 명령어 + 실행될 내용의 파일 위치입니다.
# index.js 작성
#!/usr/bin/env node const currentNodeVersion = process.versions.node; const semver = currentNodeVersion.split("."); const major = semver[0]; if (major < 14) { console.error( "You are running Node " + currentNodeVersion + ".\n" + "This cli requires Node 14 or higher. \n" + "Please update your version of Node." ); process.exit(1); } const init = require("./createReactAppForTistory"); init();
#!/usr/bin/env node 는 해당 파일을 node환경에서 실행하라는 뜻이고 우선 노드 버전을 체크해줍니다.
노드 버전이 14 미만이면 에러를 뱉어내고 끝냅니다.
# createReactAppForTistory.js 작성
## 1. command 작성
const { Command } = require("commander"); const program = new Command(); let projectName; program .version(packageJson.version) .description("script sample (tistory)") .arguments("<project-directory-folder>") .usage(`${chalk.yellow("<project-directory-folder>")} [options]`) .option("--author", "The author of the project") .option("--info", "Print project info") .option( "--template <path-to-template>", "specify the type of the project (e.g typescript)" ) .option( "--templateFolder <folder-of-template>", "specify the folder name of the template" ) .action((name) => { projectName = name; }) .on("--help", () => { console.log( chalk.yellow( "Description about help can be added directly using on('--help')" ) ); console.log(chalk.yellow("The project is about Microfrontend")); }); program.parse(process.argv);
arguments엔 npx create-react-app-for-tistoryblog {project-directory-folder} 처럼 어떤 값들을 받을지 명시하는 것입니다.
options엔 사용자가 원하는 옵션들을 설정할 수 있는데
저는 npx create-react-app-for-tistoryblog --author, npx create-react-app-for-tistoryblog --info 를 입력했을 때 만든 사람에 대한 정보, 패키지에 대한 정보를 표시하기 위해 추가했습니다.
--template, --templateFolder는 npx create-react-app-for-tistoryblog --template typescript --templateFolder sampleFolder를 입력할 수 있게끔 추가했습니다.
## 2. option에 대한 로직 작성
if (program.author) { console.log(chalk.green("Author: biglol")); console.log(chalk.green("Tistory")); process.exit(0); }
--author을 입력했을 때 관련 정보를 출력하고 프로세스를 종료합니다. --info에 대한 것도 동일합니다.
## 3. projectName을 입력하지 않았을 때의 대처
if (typeof projectName === "undefined") { console.log(chalk.red("Enter projectName")); console.log( chalk.red("npx create-react-app-for-tistoryblog {projectName}") ); process.exit(1); }
프로젝트명을 제대로 입력했을 경우 1번의 action에 projectName에 값이 들어가며 그렇지 않을 경우 undefined로 초기화 되어있으니 여기에서 판별해서 프로세스를 종료해줍니다.
## 4. 사용자 입력을 받을 수 있도록 로직 작성
React랑 Vue의 경우 inquirer와 prompts 라이브러리를 쓰는데 prompts가 조금 더 가볍고 직관적이라 prompts를 썼습니다
const modePrompt = { type: "toggle", name: "mode", message: "Would you like to copy template or git pull?", initial: false, active: "Template copy", inactive: "Git pull", }; const { mode } = await prompts(modePrompt); // copy template if (mode) { let craTemplateName = "cra-template"; if (program.template) { craTemplateName += `-${program.template}`; } copyTemplate(craTemplateName, program.templateFolder ?? projectName); } else { gitRepositoryPull(); }
커맨드 실행 시 위 사용자 입력창이 뜨며 template copy를 선택하면 옵션으로 입력한 cra-template, cra-template-typescript 폴더 중 하나를 복사합니다.
Git pull을 하면 특정 git repo를 clone하고 dependency를 install합니다.
## 5. gitRepositoryPull
function isUsingYarn() { return (process.env.npm_config_user_agent || "").indexOf("yarn") === 0; } function gitRepositoryPull(isYarn) { const currentNodeWorkingDirectory = process.cwd(); const projectPath = path.resolve(currentNodeWorkingDirectory, projectName); download("biglol10/NodeJsCrash", projectPath, async (err) => { if (err) { console.log(chalk.red("Failed to download repository")); process.exit(1); } console.log(chalk.green("Project pulled successfully")); const command = isYarn ? "yarn" : "npm"; const args = ["install"]; try { await execa(command, args, { cwd: projectPath, stdio: "inherit", }); console.log(chalk.green("Dependency install finished")); process.exit(0); } catch (err) { console.log(chalk.red("Dependency install failed")); console.log(err); process.exit(1); } }); }
repository를 다운로드하는 library들이 여러개 있지만 download-github-repo를 쓴 이유는 dependency관련 warning이 뜨지 않아서 사용했습니다.
process.cwd()를 이용하여 현재 노드 프로세스가 실행되고 있는 디렉토리의 경로를 얻고 projectName과 결합합니다 -> 해당 폴더에 소스를 다운로드 합니다.
yarn을 쓸 경우 yarn install을 실행하고 그게 아닐 경우 npm install을 execa를 통해 실행합니다.
stdio: "inherit"이기에 부모 프로세스와 입출력을 공유하고 dependency 설치 과정을 볼 수 있습니다.
## 6. 불확실한 부분
child_process의 spawn을 이용해 npm install 스크립트를 실행할 수 있지만 window 환경에서 spawn을 이용해 yarn install 을 할 경우
Error: spawn yarn ENOENT at ChildProcess._handle.onexit (node:internal/child_process:283:19) at onErrorNT (node:internal/child_process:476:16) at process.processTicksAndRejections (node:internal/process/task_queues:82:21) Emitted 'error' event on ChildProcess instance at: at ChildProcess._handle.onexit (node:internal/child_process:289:12) at onErrorNT (node:internal/child_process:476:16) at process.processTicksAndRejections (node:internal/process/task_queues:82:21) { errno: -4058, code: 'ENOENT', syscall: 'spawn yarn', path: 'yarn', spawnargs: [ 'install' ] }
와 같은 에러가 발생하니 execa library를 이용하여 스크립트를 실행했습니다.
## 7. npm 배포
이후 npm publish를 실행하면 아래 그림처럼 npm에 배포된 것을 확인할 수 있습니다 (npm login, package.json버전 체크 필수)
npx create-react-app-for-tistoryblog projectName 을 실행하여 확인하면 끝입니다.
## Note
배포하기 전 로컬에서 테스트하기 위해선 npx를 써도 됩니다. npm에 먼저 검색하고 없으면 로컬에 있는걸 실행합니다.
매번 npx를 실행하기 힘드시면 npm link를 하여 어디에서나 create-react-app-for-tistoryblog를 npx없이 실행할 수 있습니다.