Создание консольного приложения на TypeScript с помощью Node.js и Commander

Создание консольного приложения на TypeScript с помощью Node.js и Commander
Содержимое страницы

Командная строка имеет тысячи инструментов, таких как awk, sed, grep и find, доступных в вашем распоряжении, которые сокращают время разработки и автоматизируют утомительные задачи. Создание инструмента командной строки в Node.js не очень сложно благодаря такой мощной библиотеке, как Commander.js.

Перевод статьи stanulilic Building a TypeScript CLI with Node.js and Commander

Сочетание Node.js и TypeScript помогает выявлять ошибки на ранних этапах процесса разработки, что позволяет выпускать более надежные CLI с меньшим количеством ошибок.

В этом пособии мы поговорим о том, что такое CLI, а затем покажем, как использовать Commander.js с TypeScript для его создания. Затем мы сделаем CLI доступным глобально, чтобы пользователи могли получить к нему доступ из любой точки своей системы. Наконец, мы воспользуемся GitHub Actions, чтобы опубликовать его в npm, чтобы он был легко доступен всем разработчикам.

Предпосылки

Для прохождения этого пособия вам понадобится:

  • Установленный Node.js v≥16
  • Знакомство с тем, как писать асинхронный код на javascript
  • Практические навыки работы с Node.js и TypeScript
  • Аккаунт GitHub
  • Аккаунт npm

Почему Commander.js?

Интерфейс командной строки, часто называемый CLI, — это программа, которая позволяет пользователям вводить инструкции и взаимодействовать со скриптом, который обрабатывает ввод и выдает вывод. Node.js имеет много пакетов, которые позволяют вам создавать CLI. Вот некоторые примеры: Commander.js , minimist и oclif (возможно, stricli ).

Commander.js предоставляет множество функций, которые позволяют вам лаконично создавать интерфейсы командной строки. Кроме того, сообщество Node.js предоставляет библиотеки, такие как Chalk и Figlet, которые дополняют CLI Commander.js, чтобы сделать их визуально привлекательными.

Мы будем использовать Commander.js из-за следующих его особенностей:

  • Поддержка подкоманд

  • Поддержка различных параметров командной строки, таких как обязательные, переменные (вариативные) или необязательные

  • Пользовательские прослушиватели событий

  • Автоматизированный раздел помощи

Понимание интерфейса командной строки

Прежде чем приступить к созданию CLI, давайте рассмотрим, как работает существующий CLI.

Если вы следуете этому руководству, то, вероятно, на вашем компьютере установлен Node.js. Node.js предоставляет CLI, к которому можно получить доступ, введя следующую команду:

$> node

Это позволяет вам получить доступ к циклу чтения-вычисления-вывода Node.js (REPL), где вы можете вводить и выполнять код javascript.

Вы можете изменить Node.js CLI, чтобы сделать что-то еще с использованием флагов командной строки или опций. Выйдите из REPL, набрав CTRL+D, затем проверьте версию Node.js, например:

$> node -v
v22.9.0

Как вы можете видеть в выводе, передача опции -v изменила поведение CLI, чтобы показать версию Node.js. Вы также можете использовать длинные опции:

$> node -version
v22.9.0

Другие опции Node.js CLI требуют передачи аргумента вместе с опцией. Например, опция -e, которая является краткой формой --eval, принимает аргумент строки, содержащей код javascript. Node.js выполняет код и регистрирует результат в терминале:

node -e "console.log(4 * 2)"
8

Параметр -e возвращает ошибку, если аргумент не передан:

node -e
node: -e requires an argument

Теперь, когда у нас есть представление о том, как работает CLI. Давайте рассмотрим терминологию Commander.js для опций Node.js CLI, которые мы уже видели:

Булева опция: эти опции не требуют аргументов. -v — пример булевой опции; другие знакомые примеры — ls -l или sudo -i

Обязательная опция: Эти опции требуют аргументов. Например, node -e "console.log(4 * 2)" выдает ошибку, если аргумент не передан

Аргумент опции: Это аргументы, переданные в опцию. В команде node -e "console.log(4 * 2)" команда "console.log(4 * 2)" является аргументом опции; другой пример — git status -m "commit message", где "commit message" является аргументом для опции -m

Теперь, когда вы имеете представление о том, что такое CLI, мы создадим каталог и настроим его для использования TypeScript и Commander.js.

Начало работы и настройка TypeScript

В этом разделе мы создадим каталог для проекта, инициализируем его как пакет npm, установим все необходимые зависимости и настроим TypeScript.

Для начала создадим каталог для проекта:

$> mkdir directory_manager

Перейдите в каталог:

$> cd directory_manager

Инициализируйте каталог как проект npm:

$> npm init -y

Это создаст файл package.json, который содержит важную информацию о вашем проекте и отслеживает зависимости.

Далее выполните следующую команду:

$> npm install commander figlet @types/figlet

Commander.js — это библиотека для создания CLI, а Figlet будет использоваться для преобразования текста CLI в ASCII-графику. Также нам понадобятся типы

Далее загрузите пакеты TypeScript и ts-node:

$> npm install @types/node typescript --save-dev

Теперь создайте файл tsconfig.json в текстовом редакторе и добавьте следующие параметры конфигурации для TypeScript:

{
  "compilerOptions": {
    "rootDir": "src",
    "outDir": "dist",
    "strict": true,
    "target": "es6",
    "module": "commonjs",
    "sourceMap": true,
    "esModuleInterop": true,
    "moduleResolution": "node"
  }
}

Давайте рассмотрим некоторые опции:

  • rootDir: Каталог, содержащий файлы TypeScript (файлы .ts) для CLI, которые мы будем хранить в каталоге src
  • outDir: Каталог, содержащий исходный код javascript, скомпилированный на TypeScript. Мы будем использовать каталог dist
  • strict: отключает необязательную типизацию и гарантирует, что весь написанный вами код TypeScript имеет типы#!
  • target: версия ECMAScript, в которую TypeScript должен компилировать javascript
  • Для полного ознакомления со всеми вариантами посетите документацию TypeScript .

Далее в файле package.json создайте скрипт сборки, который вы будете использовать для компиляции TypeScript (опустите комментарии в файле JSON):

{
  ...
  "scripts": {
    // add the following line
    "build": "npx tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  ...
}

Чтобы скомпилировать TypeScript, позже в этом руководстве, вы запустите скрипт сборки с помощью npm run build, который выполнит команду npx tsc, компилирующую TypeScript в javascript.

И так мы настроили TypeScript и добавили скрипт для компиляции TypeScript. Далее мы начнем создавать CLI.

Создание CLI с помощью TypeScript

В этом разделе мы начнем создавать CLI с использованием TypeScript и Commander.js. Он будет выглядеть следующим образом:

screenshot

CLI будет использоваться для управления каталогом и будет иметь опцию -l, которая будет перечислять содержимое каталога в формате таблицы. Для каждого элемента он будет отображать его имя, размер и дату создания. Он также будет иметь опцию -m для создания каталогов и опцию -t для создания пустых файлов.

Теперь, когда у вас есть представление о том, что мы будем создавать, мы разобьем CLI на более мелкие части и начнем создавать каждую из них.

Создание имени CLI

В этом разделе мы создадим имя CLI и воспользуемся пакетом Figlet, чтобы превратить его в текст ASCII-арта.

По завершении это будет выглядеть так:

screenshot

В каталоге вашего проекта создайте каталог src и перейдите в него:

$> mkdir src && cd src

Этот каталог будет содержать файлы TypeScript. Вы можете вспомнить, что мы указали этот каталог в параметре rootDir, когда ранее в этом руководстве настраивали TypeScript с помощью файла tsconfig.js.

Далее создайте файл index.ts и добавьте в него следующее содержимое:

import figlet from "figlet";

console.log(figlet.textSync("Dir Manager"));

В первой строке мы импортируем модуль Figlet. Далее мы вызываем метод figlet.textSync() со строкой Dir Manager в качестве аргумента для преобразования текста в ASCII Art. Наконец, мы выводми текст в консоль.

Чтобы проверить, что изменения работают, сохраните файл. Скомпилируйте файл TypeScript в javascript с помощью следующей команды:

$> npm run build

Когда TypeScript завершит компиляцию, вы увидите следующий вывод:

$<
> building-typescript-cli-node-js-commander@1.0.0 build
> npx tsc

В случае успеха вы не увидите здесь никаких ошибок.

Вы можете вспомнить, что мы добавили опцию outDir и установили ее в каталог dist в файле tsconfig.json. После компиляции TypeScript каталог будет создан автоматически в корневом каталоге.

Перейдите в каталог dist:

$> cd ../dist

Содержимое каталога:

$> ls
$< index.js  index.js.map

Вы увидите, что файл index.js был создан. Вы можете запустить файл с Node.js следующим образом:

$> node index.js

После запуска команды вы увидите имя CLI в ASCII-коде:

screenshot

Теперь вернитесь в корневой каталог:

$> cd ..

В дальнейшем мы не будем входить в каталог dist для запуска файла. Мы сделаем это, находясь в корневом каталоге как node dist/index.js.

Теперь, когда вы можете создать имя CLI в тексте ASCII, мы создадим параметры CLI.

Создание параметров CLI с помощью Commander.js

В этом разделе мы будем использовать Commander.js для создания описания CLI и его параметров. Мы создадим следующие параметры:

screenshot

Опция -V вызовет метод Commander.js version(), а -h будет предоставлена ​​по умолчанию. Теперь нам осталось определить три опции:

  • -l / --ls : Изменяет CLI для отображения содержимого каталога в таблице. Также принимает необязательный аргумент пути к каталогу
  • -m / --mkdir: Используется для создания каталога. Он потребует аргумент, который является именем каталога, который будет создан
  • -t / --touch: Изменяет CLI для создания пустого файла. Для этого потребуется аргумент, который является именем файла

Теперь, когда мы знаем, какие параметры будем создавать, мы определим их с помощью Commander.js.

Определение параметров с помощью Commander.js

В текстовом редакторе откройте файл index.ts в каталоге src и добавьте следующий код для импорта и инициализации Commander.js:

import { Command } from 'commander'; // Добавить эту строку
import figlet from 'figlet';

const program = new Command(); // И эту

console.log(figlet.textSync('Dir Manager'));

В первой строке мы импортируем модуль Commander.js и извлекаем класс Command. Затем мы устанавливаем переменную программы в экземпляр класса Command. Класс предоставляет нам несколько методов, которые можно использовать для установки версии, описания и параметров CLI.

Далее определите параметры CLI в файле index.ts:

...
program
  .version('1.0.0')
  .description('An example CLI for managing a directory')
  .option('-l, --ls  [value]', 'List directory contents')
  .option('-m, --mkdir <value>', 'Create a directory')
  .option('-t, --touch <value>', 'Create a file')
  .parse(process.argv);

const options = program.opts();

В предыдущем коде мы используем переменную программы, содержащую экземпляр Commander, для вызова метода version(). Метод принимает строку, содержащую версию CLI, и Commander создает для вас опцию -V.

Далее мы связываем вызов метода description() с текстом, описывающим программу CLI. После этого вы связываете вызов метода option() пакета Commander, который принимает два аргумента: параметр и описание. Первый аргумент — это строка, которая указывает параметр -l и длинное имя --ls. Затем мы заключаем значение в [], чтобы параметр мог принимать необязательный аргумент. Второй аргумент — это текст справки, который будет отображаться, когда пользователи используют флаг -h.

После этого мы сцепляем другой вызов метода option() для определения опции -m / --mkdir. Скобки <> в <value> означает, что требуется аргумент. После этого мы сцепляем другой option() для определения опции -t и длинного имени --touch, которое также требует аргумента.

Затем мы связываем вызов метода parse(), который обрабатывает аргументы в process.argv, представляющем собой массив, содержащий аргументы, переданные пользователем. Первый аргумент — это node, второй аргумент — имя файла программы, а остальные — дополнительные аргументы.

Наконец, мы устанавливаем переменную options для вызова program.opts(), который возвращает объект. Объект имеет CLI-опции в качестве свойств, значениями которых являются аргументы, переданные пользователем.

На этом этапе файл index.ts будет выглядеть следующим образом:

import { Command } from 'commander';
import figlet from 'figlet';

const program = new Command();

console.log(figlet.textSync('Dir Manager'));

program
  .version('1.0.0')
  .description('An example CLI for managing a directory')
  .option('-l, --ls  [value]', 'List directory contents')
  .option('-m, --mkdir <value>', 'Create a directory')
  .option('-t, --touch <value>', 'Create a file')
  .parse(process.argv);

const options = program.opts();

Закончив вносить изменения, сохраните файл, затем скомпилируйте TypeScript:

$> npm run build

Запустите index.js с опцией -h, чтобы увидеть страницу справки CLI:

$> node dist/index.js -h

После выполнения команды страница будет выглядеть так:

screenshot

Давайте также попробуем опцию -V:

$> node dist/index.js -V
  ____  _        __  __                                   
|  _ \(_)_ __  |  \/  | __ _ _ __   __ _  __ _  ___ _ __ 
| | | | | '__| | |\/| |/ _` | '_ \ / _` |/ _` |/ _ \ '__|
| |_| | | |    | |  | | (_| | | | | (_| | (_| |  __/ |   
|____/|_|_|    |_|  |_|\__,_|_| |_|\__,_|\__, |\___|_|   
                                          |___/           
1.0.0

Пока что опции -h и -V работают без проблем. Если вы попробуете другие опции, которые мы определили, вы увидите только имя CLI:

$> node dist/index.js -l

screenshot

Это происходит потому, что мы не определили действия для других вариантов.

Создание действий для CLI

До сих пор мы определили параметры для CLI, но с ними не связаны никакие действия. В этом разделе мы создадим действия для параметров, чтобы при использовании параметров пользователем CLI выполнял соответствующую задачу. Начнем с параметра -l. Мы хотим, чтобы CLI отображал содержимое каталога в таблице со следующими полями:

  • Filename
  • Size(KB)
  • created_at

Пользователь также может указать необязательный путь к каталогу:

$> node dist/index.js -l /home/username/Documents

Если пользователь не передаст ни одного аргумента для опции, CLI покажет только содержимое в расположении файла index.js, который мы выполняем:

$> node dist/index.js -l

В файле index.ts импортируйте модули fs и path:

import { Command } from 'commander';
import figlet from 'figlet';
// Добавить то, что ниже
import fs from 'fs';
import path from 'path';

Определите функцию listDirContents() с обработчиком исключений в конец файла:

import { Command } from 'commander';
...

const options = program.opts();
// Добавить то, что ниже
async function listDirContents(filepath: string) {
  try {
  } catch (error) {
    console.error('Error occurred while reading the directory!', error);
  }
}

Асинхронная функция listDirContents() принимает параметр filepath, который имеет тип string понятный для TypeScript. Тип гарантирует, что функция принимает только строки в качестве аргументов, а ключевое слово async, которое вы добавляете, делает функцию асинхронной. Это позволит нам использовать ключевое слово await внутри функции, что мы скоро и сделаем.

Внутри функции мы определяем блок try, который пока пуст. Он будет содержать функционал, который выводит список содержимого каталога и форматирует результат в таблицу. После этого мы определяем блок catch, который будет регистрировать сообщение в консоли, если код, содержащийся в блоке try, имеет исключение.

Давайте добавим код, который выводит список содержимого каталога в функцию listDirContents():

async function listDirContents(filepath: string) {
  try {
    // Добавить следующие строки
    const files = await fs.promises.readdir(filepath);
    const detailedFilesPromises = files.map(async (file: string) => {
      const fileDetails = await fs.promises.lstat(path.resolve(filepath, file));
      const { size, birthtime } = fileDetails;
      return { filename: file, 'size(KB)': size, created_at: birthtime };
    });
  } catch (error) {
    console.error('Error occurred while reading the directory!', error);
  }
}

Сначала мы вызываем fs.promises.readdir() со значением в параметре filepath, чтобы прочитать содержимое каталога. Функция возвращает обещание, поэтому мы добавляем к нему префикс await, чтобы дождаться его разрешения. После разрешения filesis устанавливается в массив.

Во-вторых, мы перебираем каждый элемент в массиве файлов и возвращаем новый массив с помощью метода map(), который принимает асинхронный обратный вызов. Обратный вызов принимает параметр файла. В обратном вызове мы вызываем fs.promises.lstat() с полным путем к файлу, чтобы получить больше подробностей о файле, таких как размер, время рождения и информация. Затем мы извлекаем свойства размера и времени рождения и возвращаем объект с именем файла, размером (КБ) и created_at в массив, который метод map() возвращает в переменную detailFilesPromise.

Теперь добавьте следующий код в конец блока try, чтобы создать таблицу, отображающую содержимое каталога:

async function listDirContents(filepath: string) {
  try {
    const files = await fs.promises.readdir(filepath);
    const detailedFilesPromises = files.map(async (file: string) => {
      const fileDetails = await fs.promises.lstat(path.resolve(filepath, file));
      const { size, birthtime } = fileDetails;
      return { filename: file, 'size(KB)': size, created_at: birthtime };
    });
    const detailedFiles = await Promise.all(detailedFilesPromises);
    console.table(detailedFiles);
  } catch (error) {
    console.error('Error occurred while reading the directory!', error);
  }
}

Теперь каждый элемент в detailFilesPromise вернет обещание и оценит объект после разрешения. Чтобы дождаться разрешения всех из них, мы вызываем метод Promise.all().

Наконец, мы вызываем console.table() с массивом detailFiles для записи данных в консоль.

Давайте теперь определим действие для опции -m. Для этого определите функцию createDir() под функцией listDirContents():

async function listDirContents(filepath: string) {
...
}

function createDir(filepath: string) {
  if (!fs.existsSync(filepath)) {
    fs.mkdirSync(filepath);
    console.log('The directory has been created successfully');
  }
}

В функции createDir() мы проверяем, существует ли заданный путь к каталогу. Если он не существует, мы вызываем fs.mkdirSync() для создания каталога, а затем регистрируем сообщение об успешном завершении.

Прежде чем вызывать функцию, определите функцию createFile() для флага -t:

async function listDirContents(filepath: string) {
...
}

function createDir(filepath: string) {
...
}

function createFile(filepath: string) {
  fs.openSync(filepath, 'w');
  console.log('An empty file has been created');
}

В функции createFile() мы вызываем fs.openSync() для создания пустого файла по указанному пути. Затем мы выводим подтверждающее сообщение на терминал.

Пока что мы создали три функции, но не вызывали их. Для этого нам нужно проверить, использовал ли пользователь опцию, чтобы мы могли вызвать подходящую функцию.

Чтобы проверить, использовал ли пользователь опцию -l или --ls, добавьте в index.ts следующее:

...
function createFile(filepath: string) {
...
}

if (options.ls) {
  const filepath = typeof options.ls === 'string' ? options.ls : process.cwd();
  listDirContents(filepath);
}

Если options.ls установлен на значение, мы устанавливаем переменную filepath на путь, предоставленный пользователем, если option.ls является строкой; в противном случае он устанавливается на путь файла index.js в каталоге dist. После этого мы вызываем listDirContents() с переменной filepath.

Теперь давайте вызовем функции createDir() и createFile(), когда пользователь использует соответствующую опцию:

...
if (options.ls) {
...
}

if (options.mkdir) {
  createDir(path.resolve(process.cwd(), options.mkdir));
}
if (options.touch) {
  createFile(path.resolve(process.cwd(), options.touch));
}

Если пользователь использует флаг -m и передает аргумент, мы вызываем createDir() с полным путем к файлу index.js для создания каталога.

Если пользователь использует флаг -t и передает аргумент, мы вызываем функцию createFile() с полным путем к расположению index.js.

На этом этапе полный файл index.ts будет выглядеть так:

import { Command } from 'commander';
import figlet from 'figlet';
import fs from 'fs';
import path from 'path';

const program = new Command();

console.log(figlet.textSync('Dir Manager'));

program
  .version('1.0.0')
  .description('An example CLI for managing a directory')
  .option('-l, --ls  [value]', 'List directory contents')
  .option('-m, --mkdir <value>', 'Create a directory')
  .option('-t, --touch <value>', 'Create a file')
  .parse(process.argv);

const options = program.opts();

async function listDirContents(filepath: string) {
  try {
    const files = await fs.promises.readdir(filepath);
    const detailedFilesPromises = files.map(async (file: string) => {
      const fileDetails = await fs.promises.lstat(path.resolve(filepath, file));
      const { size, birthtime } = fileDetails;
      return { filename: file, 'size(KB)': size, created_at: birthtime };
    });
    const detailedFiles = await Promise.all(detailedFilesPromises);
    console.table(detailedFiles);
  } catch (error) {
    console.error('Error occurred while reading the directory!', error);
  }
}

function createDir(filepath: string) {
  if (!fs.existsSync(filepath)) {
    fs.mkdirSync(filepath);
    console.log('The directory has been created successfully');
  }
}

function createFile(filepath: string) {
  fs.openSync(filepath, 'w');
  console.log('An empty file has been created');
}

if (options.ls) {
  const filepath = typeof options.ls === 'string' ? options.ls : process.cwd();
  listDirContents(filepath);
}

if (options.mkdir) {
  createDir(path.resolve(process.cwd(), options.mkdir));
}
if (options.touch) {
  createFile(path.resolve(process.cwd(), options.touch));
}

Сохраните файл и скомпилируйте TypeScript:

$> npm run build

Давайте проверим, что опции работают. В вашем терминале введите следующее, чтобы попробовать опцию -l:

$> node dist/index.js -l

Содержимое каталога вы увидите в таблице, которая выглядит примерно так:

screenshot

Далее передайте путь к каталогу по вашему выбору в качестве аргумента:

$> node dist/index.js -l /home/<your_username>/

В выводе вы увидите содержимое каталога по выбранному вами пути:

screenshot

Используя опцию -m, создайте новый каталог с любым именем по вашему желанию:

$> node dist/index.js -m new_directory
The directory has been created successfully

Давайте также создадим пустой файл с помощью опции -t:

$> node dist/index.js -t empty_file.txt
An empty file has been created

После этого давайте проверим, были ли созданы каталог и пустой файл, с помощью следующей команды:

$> node dist/index.js -l

screenshot

В выводе отображаются файлы new_directory и empty_file.txt, подтверждающие, что они были созданы.

Теперь, если вы используете команду node dist/index.js без каких-либо опций, она покажет имя CLI:

$> node dist/index.js

screenshot

Отображение страницы справки

Было бы неплохо показать страницу справки, когда не было передано ни одного параметра. В файле index.ts добавьте следующее в конец файла:

...
if (!process.argv.slice(2).length) {
  program.outputHelp();
}

Если количество переданных аргументов равно двум — то есть, process.argv имеет только узел и имя файла в качестве аргумента — вы можете вызвать outputHelp(), чтобы отобразить вывод.

Как и при любых изменениях, скомпилируйте TypeScript в JavaScript:

$> npm run build

Выполните следующую команду:

$> node dist/index.js

screenshot

Обеспечение глобальной доступности CLI

На этом этапе наш CLI готов. Вы могли заметить, что использование CLI утомительно. Ежедневно нам пришлось бы менять каталог на каталог проекта CLI, а затем вызывать index.js, чтобы использовать его. Было бы проще, если бы мы могли дать ему имя, например dirmanager, которое будет работать в любой точке нашей системы, например:

$> dirmanager -l

Для этого откройте файл package.json и добавьте следующее:

{
  ...
  "main": "dist/index.js",    // <- Обновить это
  // и добавить следующие строки
  "bin": {
    "dirmanager": "./dist/index.js"
  },
  ...
}

В коде выше мы обновляем путь main до скомпилированного файла index.js. Затем добавляем bin с объектом в качестве его значения. В объекте мы устанавливаем dirmanager в ./dist/index.js, что является местоположением скомпилированного скрипта. Мы будем использовать dirmanager для доступа к CLI, но вы можете использовать любое имя, которое вам нравится.

Затем откройте файл index.ts и добавьте следующую строку в начало файла:


#! /usr/bin/env node

import { Command } from 'commander';
import figlet from 'figlet';

Строка называется шебанг (shebang line), которая сообщает ОС о необходимости запустить файл с nodeinterpreter. Сохраните файл и скомпилируйте TypeScript еще раз:

$> npm run build

Выполните следующую команду:

$> npm install -g .

Параметр -g указывает npm установить пакет глобально.

На этом этапе вы можете открыть новый терминал или использовать текущий терминал, а затем ввести следующую команду:

$> dirmanager

screenshot

Вы также можете попробовать другие варианты, и они будут работать нормально:

$> dirmanager -l

Теперь мы успешно создали TypeScript CLI, который работает в любой точке системы.

Публикация в npm с использованием GitHub Actions

Теперь, когда у нас есть CLI, давайте опубликуем его в npm, чтобы другие разработчики могли использовать его в любом месте своих систем. Сначала мы создадим репозиторий GitHub и отправим исходный код в репозиторий. Затем мы настроим токен доступа в нашей учетной записи npm, а затем настроим действия GitHub для публикации репозитория в npm с использованием токена доступа. После завершения пользователи смогут получить доступ к приложению в любом месте.

Создание Git-репозитория

Для начала введите следующий код в корневой каталог проекта, чтобы создать репозиторий git:

$> git init

Далее создайте файл .gitignore и добавьте в него следующее содержимое:

# Dependency directories
node_modules
dist

Файл .gitignore содержит файлы и каталоги, которые git должен игнорировать. Мы добавили несколько каталогов для краткости, но в реальном проекте обязательно добавьте все содержимое файла Node.gitignore на GitHub .

Далее создайте в корневом каталоге файл README.md с кратким описанием проекта:

A CLI directory manager that can be used to create directories, list directory contents and create files

Теперь зафиксируйте файлы в репозитории:

$> git add .

Сделайте первый коммит в репозиторий:

$> git commit -am "initial commit"

Затем откройте браузер и перейдите по адресу github.com/new , чтобы создать удаленный репозиторий с именем directory_manager, и нажмите Создать репозиторий:

screenshot

  • На странице репозитория скопируйте команды из раздела «…или отправьте существующий репозиторий из командной строки» и вставьте их в терминал:

screenshot

Если по какой-то причине вы не можете скопировать эти команды, просто скопируйте и вставьте приведенный ниже код и замените его своим именем пользователя GitHub для этого и последующих примеров в этой статье:

git remote add origin git@github.com:<username>/directory_manager.git
git branch -M main
git push -u origin main

Вы увидите, что содержимое локального каталога было добавлено в репозиторий на GitHub.

Создание токена доступа

Теперь давайте создадим токен доступа в вашем аккаунте npm. Мы будем использовать токен для создания секрета GitHub Actions.

Сначала обновите файл package.json, указав имя пакета, его имя, ключевые слова и ссылки на репозиторий GitHub:

{
  "name": "@oshliaer/building-typescript-cli-node-js-commander",
  "version": "1.0.0",
  "description": "Основано на статье [stanulilic](https://github.com/stanulilic) [Building a TypeScript CLI with Node.js and Commander](https://blog.logrocket.com/building-typescript-cli-node-js-commander)",
  "keywords": [
    "cli",
    "npmtool"
  ],
  "homepage": "https://github.com/oshliaer/building-typescript-cli-node-js-commander#readme",
  "bugs": {
    "url": "https://github.com/oshliaer/building-typescript-cli-node-js-commander/issues"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/oshliaer/building-typescript-cli-node-js-commander.git"
  },
  "license": "MIT",
  "author": "",
  "type": "commonjs",
  "main": "dist/index.js",
  "bin": {
    "dirmanager": "./dist/index.js"
  },
  "scripts": {
    "build": "npx tsc",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "commander": "^13.0.0",
    "figlet": "^1.8.0"
  },
  "devDependencies": {
    "@types/figlet": "^1.7.0",
    "@types/node": "^22.10.5",
    "typescript": "^5.7.2"
  }
}

В файле package.json мы обновляем свойство name новым именем, содержащим имя репозитория GitHub. Это необязательно, вы можете оставить имя без изменений, если хотите. Мы также обновили следующие свойства:

  • keywords: Содержит ключевые слова, которые помогают пользователям найти пакет.
  • author: Ваше полное имя
  • repository: Ссылка на ваш репозиторий GitHub
  • homepage: Ссылка на файл README.md вашего репозитория GitHub, который будет отображаться на странице npm пакета.

Далее, зайдите в свою учетную запись npm и щелкните свой аватар; это откроет выпадающее меню навигации. В навигации щелкните Access Tokens:

screenshot

На странице Access Tokens нажмите кнопку «Создать новую токен», затем нажмите кнопку «Классическая токен»:

screenshot

Далее на странице Новый токен доступа введите имя для вашего токена доступа, выберите опцию Опубликовать и нажмите Сгенерировать токен:

screenshot

Вы увидите, что был сгенерирован новый ключ. Скопируйте ключ в безопасное место, так как он больше не будет отображаться:

screenshot

Очень важно, что в настройка профиля npm не был установлен параметр Require two-factor authentication for write actions, иначе автоматическая публикация может требовать дополнительной аторизации (см. https://github.com/semantic-release/npm/issues/209#issuecomment-545112326 ).

Затем откройте новую вкладку и перейдите по адресу https://github.com//directory_manager/settings/secrets/actions , который находится на странице настроек репозитория, и нажмите «Новый секрет репозитория»:

screenshot

На странице «Новый секрет» введите NPM_AUTH_TOKEN в качестве имени ключа и вставьте токен доступа npm в поле «Секрет»:

screenshot

Вы видите, что секретный ключ создан:

screenshot

Далее мы настроим GitHub Actions для публикации пакета в npm.

Настройка действий GitHub для публикации пакета

  • В корневом каталоге создайте каталог .github/workflows:
$> mkdir -p .github/workflows

Теперь перейдите в каталог:

$> cd .github/workflows/

Далее создайте publish.yml со следующим кодом:

name: 'publish package to npm'

on:
  push:
    branches:
      - master

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: checkout
        uses: actions/checkout@v2
      - name: node
        uses: actions/setup-node@v2
        with:
          node-version: 22
          registry-url: https://registry.npmjs.org
      - name: publish
        run: npm publish --access public
        env:
          NODE_AUTH_TOKEN: ${{secrets.NPM_AUTH_TOKEN}}

Внедрите новые изменения:

$> git add .

Зафиксируйте новые изменения:

git commit -am "set up github actions"

Отправьте новые изменения, чтобы удалить репозиторий:

$> git push

Теперь перейдите по адресу https://github.com//directory_manager/actions :

screenshot

Если вы видите зеленую галочку, это означает, что действия GitHub завершены без каких-либо проблем — на это может потребоваться некоторое время. Если ничего не происходит, убедитесь, что вы делаете pull в ветку master.

Затем посетите https://www.npmjs.com/settings//packages , чтобы проверить, был ли опубликован пакет:

screenshot

Теперь перейдите по адресу https://www.npmjs.com/package/@/directory_manager , чтобы увидеть домашнюю страницу пакета:

screenshot

На этом вы успешно опубликовали пакет с помощью GitHub Actions.

Заключение

В этой статье мы рассмотрели, что такое CLI, а затем использовали Commander.js и TypeScript для создания CLI. Затем мы сделали CLI глобально доступным в любой точке системы и использовали GitHub Actions для публикации проекта на npm, чтобы другие могли его использовать. Теперь вы вооружены знаниями о том, как создавать CLI с помощью TypeScript.

Commander — мощная библиотека, и мы только слегка коснулись ее возможностей. Ниже приведены некоторые другие интересные функции:

Создание подкоманд, которые выполняют автономные скрипты : вы можете ознакомиться с примером pm в документации или прочитать это руководство .

Посетите документацию Commander, чтобы узнать больше о нем. Чтобы продолжить изучение TypeScript, посетите документацию TypeScript . Вы также можете ознакомиться с руководствами по TypeScript в этом блоге .

Оригинал статьи

Репозиторий GitHub , используемый для примера

Пакет npm , используемый для примера