ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • create-react-app 처럼 cli를 하나 만들어보자
    Node 2024. 3. 25. 18:50

     

    Create-react-app와 같이 cli를 만들 필요가 있어 create-react-app repository를 풀 받아서 분석했습니다.

    Npm package를 만들고 npm에 배포하면서 얻은 노하우를 공유드립니다.

     

    https://pingfanzhilu.tistory.com/entry/Vuejs-Vue-CLI-UI%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%B4%EC%84%9C-Vue-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8%EB%A5%BC-%EC%83%9D%EC%84%B1%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

     

    [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없이 실행할 수 있습니다.

    댓글

Designed by Tistory.