Thành's Blog

Tự làm CLI với Nodejs và Typescript

April 14, 2019 • 5 mins

Bài viết này tập trung về việc tự tạo một CLI generate project mới từ template có sẵn.

Vấn đề

Hôm trước mình có viết một bài để setup một dự án React Native mới với Typescript. Nhưng mỗi lần bắt đầu một dự án mới lại đi copy/paste khá là mất công. Chưa kể nhỡ mà copy thiếu cái gì đó lại loạn lên. Nên hôm nay tranh thủ đang buổi ốm đau ở nhà, mình đành tự làm một cái CLI thử xem sao.

Todo

  • Tạo một CLI đơn giản bằng NodeJs và Typescript
  • Chạy được trên MacOS và Windows
  • Chọn được nhiều loại template khác nhau

Tàm tạm thế. Code thôi 🤖

1. Tạo project mới

Lại là lệnh lủng

mkdir simple-cli && cd $_
yarn init -y
yarn add commander inquirer shelljs
yarn add -D @types/{node,inquirer,shelljs} typescript shx

Có thể bạn biết rồi!

$_ sẽ đại diện cho argument cuối cùng của lệnh phía trước nó và sử dụng cho lệnh tiếp theo, giúp bạn đỡ cảm thấy mệt mỏi khi phải gõ quá nhiều 😎

$ echo a b && echo $_
# Output
# a b
# b

Tạo file tsconfig.json với nội dung

{
  "compilerOptions": {
    "target": "es6",
    "lib": ["es2017", "es2015", "dom", "es6"],
    "module": "commonjs",
    "outDir": "dist",
    "rootDir": ".",
    "sourceMap": false,
    "resolveJsonModule": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true
  },
  "exclude": ["node_modules", "./templates", "./dist"]
}

Sửa file package.json một chút

{
  // Đây là cách để đăng ký tên cho cli khi mà cài global.
  // Sau này chúng ta sẽ có cli: simple-cli init projectname. Đại loại vậy
  "bin": {
    "simple-cli": "dist/index.js"
  },
  // Thêm một số scripts
  "scripts": {
    "clean": "shx rm -rf dist",
    "copy:templates": "shx cp -R templates dist/templates",
    "build": "yarn clean && yarn copy:templates && tsc",
    "start": "tsc -w"
  }
}

2. Code thực sự đây

Đầu tiên, chạy yarn start để cho typescript build ts code sang js.

Tạo file src/index.ts

Việc đầu tiên cần làm là thêm quả shebang cho node. Ngay dòng đầu tiên luôn nhé 🤣

#!/usr/bin/env node

Giờ đến việc định nghĩa các command mà cli sẽ hỗ trợ.

- Kiểm tra version của CLI

import program from 'commander'
import pkg from './package.json'

program
  .version(pkg.version, '-v, --version')
  .description('Simple CLI')

program.parse(process.argv)

Giờ bạn chạy node dist/index.js -v hoặc node dist/index.js --version, terminal sẽ echo ra version mà bạn define trong file package.json. -v option Đấy! Mấy dòng code mà trông chuyên nghiệp rồi đấy!

- Lệnh init project với lựa chọn loại project

// other import
import inquirer from 'inquirer'

/* other code */

enum ProjectType {
  ReactTS = 'React Typescript',
  ReactNativeTS = 'React Native Typescript'
}

// Khai báo lệnh init
program
  .command('init <name>')
  .description('Init new project')
  .action(async (projectName: string) => {
    const answers = await inquirer.prompt<{ projectType: ProjectType }>([
      {
        type: 'list',
        name: 'projectType',
        message: 'Chọn một pô-dếch đi ông ơi!',
        choices: [ProjectType.ReactTS, ProjectType.ReactNativeTS],
      },
    ])
    console.log('simple-cli', { projectName, projectType: answers.projectType })
  })

program.parse(process.argv)

<name> là trường bắt buộc. Các bạn có thể thêm các trường khác không bắt buộc theo kiểu [optionalField]. Tất cả sẽ được truyền xuống callback trong chain action.

Chú ý: Nhớ đặt các command trước program.parse(process.argv).

Rồi, lại chạy thử xem nào node dist/index.js init Testing init command Tá đa! 🎉🎉🎉 Khi chạy lệnh init <name>, CLI sẽ hỏi bạn muốn init loại project nào với hai lựa chọn. Khi chọn một câu trả lời, CLI log ra projectName là folder name và loại project. Ngon rồi 🥳

Giờ tạo một template vớ vẩn gì đó xem nào.

mkdir templates && cd $_
mkdir simple-template && cd $_
yarn init -y
yarn add react
rm -rf node_modules && rm -rf yarn.lockcd ../..

Chú ý: Xóa node_modules cho nhẹ. Xóa yarn.lock để sau install lại các package được up to date.

Giờ, thử copy template sang project mới và chạy yarn để cài các package xem sao.

// other import
import path from 'path'
import shell from 'shelljs'

/* other code */

program
  .command('init <name>')
  .description('Init new project')
  .action(async (projectName: string) => {
    const answers = await inquirer.prompt<{ projectType: ProjectType }>([
      {
        type: 'list',
        name: 'projectType',
        message: 'Chọn một pô-dếch đi ông ơi!',
        choices: [ProjectType.ReactTS, ProjectType.ReactNativeTS],
      },
    ])
    const currentWorkingDir = path.join(process.cwd(), projectName)
    const templateDir = path.join(__dirname, `./templates/simple-template`)
    shell.cp('-R', templateDir, currentWorkingDir)
    shell.cd(currentWorkingDir)
    shell.exec('yarn')
    shell.echo(`Init project ${projectName} successful! 🥳`)
    shell.exit(1)
  })

Đầu tiên, copy templates vào folder dist bằng lệnh yarn copy:templates. Xong xuôi thì chạy thử thôi. node dist/index.js init Testing init command Ú ù ú ù. Chạy rồi anh em ơi 🥳

Để copy đúng template, anh em chỉ việc thay

const templateDir = path.join(__dirname, `./templates/simple-template`)

bằng

const templateDir = path.join(__dirname, `./templates/${answers.projectType}`)

Và anh em nhớ đặt tên folder template giống với ProjectType đã định nghĩa ở bên trên.

Ngoài ra, còn cần check chủng xem folder đã tốn tại chưa, kiểm tra command có đúng không. Mấy cái này khá dễ. Anh em chỉ việc if else tí là xong.

3. Cài CLI global

Không ai chạy cli kiểu node dist/index.js cả. Vì sang folder khác thì lại phải trỏ đúng file dist/index.js. Mà mình mong muốn chạy khắp nơi bằng lệnh đã định nghĩa ở trong package.json. simple-cli init MyApp. Giờ phải làm sao? Đơn giản thôi!

yarn build
yarn link

Thế là dùng được simple-cli -v rồi!

Thật ra anh em có thể cài global rồi vừa dev vừa dùng simple-cli để test CLI cũng được.

Chú ý: Nếu gặp lỗi permission denied. Thì file build của mình chưa có quyền execute. Anh em chạy chmod +x dist/index.js.

Tá đa. Xong! 😎

2018 © Thành
RSS