Сниппет, расширение для VSCode и CLI. Часть 2

от автора

Доброго времени суток, друзья!

В процессе разработки Современного стартового HTML-шаблона я задумался о расширении возможностей его использования. На тот момент варианты его применения ограничивались клонированием репозитория и скачиванием архива. Так появились HTML-сниппет и расширение для Microsoft Visual Studio Code — HTML Template, а также интерфейс командной строки — create-modern-template. Конечно, указанные инструменты далеки от совершенства и я буду их дорабатывать по мере сил и возможностей. Однако, в процессе их создания я узнал несколько интересных вещей, которыми и хочу с вами поделиться.

Сниппет и расширение были рассмотрены в первой части. В этой части мы рассмотрим CLI.

Если вас интересует лишь исходный код, вот ссылка на репозиторий.

Oclif

Oclif — это фреймворк от Heroku для создания интерфейсов командной строки.

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

Исходный код проекта находится здесь. Там же находится CLI для проверки работоспособности сайта по URL.

Устанавливаем oclif глобально:

npm i -g oclif / yarn global add oclif 

Oclif предоставляет возможность создавать как одно-, так и мультикомандные CLI. Нам нужен второй вариант.

Создаем проект:

oclif multi todocli 

  • аргумент multi указывает oclif создать мультикомандный интерфейс
  • todocli — название проекта

Добавляем необходимые команды:

oclif command add oclif command update oclif command remove oclif command show 

Файл src/commands/hello.js можно удалить.

В качестве локальной базы данных мы будем использовать lowdb. Также для кастомизации сообщений, выводимых в терминал, мы будем использовать chalk. Устанавливаем эти библиотеки:

npm i chalk lowdb / yarn add chalk lowdb 

Создаем в корневой директории пустой файл db.json. Это будет нашим хранилищем задач.

В директории src создаем файл db.js следующего содержания:

const low = require('lowdb') const FileSync = require('lowdb/adapters/FileSync') const adapter = new FileSync('db.json') const db = low(adapter)  // добавляем свойство todos с пустым массивом в качестве значения в db.json db.defaults({ todos: [] }).write()  // функция получения всех задач const Todo = db.get('todos')  module.exports = Todo 

Редактируем файл src/commands/add.js:

// импорт необходимого функционала const { Command } = require('@oclif/command') const Todo = require('../db') const chalk = require('chalk')  class AddCommand extends Command {   async run() {     // получаем аргументы из командной строки     const { argv } = this.parse(AddCommand)     try {       // записываем новую задачу в список       await Todo.push({         id: Todo.value().length,         // текст задачи может состоять из нескольких слов,         // разделенных пробелом         task: argv.join(' '),         done: false       }).write()       // сообщаем об успехе операции       this.log(chalk.green('New todo created.'))     } catch {       // сообщаем о провале операции       this.log(chalk.red('Operation failed.'))     }   } }  // описание команды AddCommand.description = `Adds a new todo`  // возможность указывать несколько аргументов после команды AddCommand.strict = false  // экспорт команды module.exports = AddCommand 

Редактируем файл src/commands/update.js:

const { Command } = require('@oclif/command') const Todo = require('../db') const chalk = require('chalk')  class UpdateCommand extends Command {   async run() {     // получаем идентификатор задачи     const { id } = this.parse(UpdateCommand).args     try {       // находим задачу по id и обновляем ее       await Todo.find({ id: parseInt(id, 10) })         .assign({ done: true })         .write()       this.log(chalk.green('Todo updated.'))     } catch {       this.log('Operation failed.')     }   } }  UpdateCommand.description = `Marks a task as done by id`  // название и описание передаваемого аргумента UpdateCommand.args = [   {     name: 'id',     description: 'todo id',     required: true   } ]  module.exports = UpdateCommand 

Файл src/commands/remove.js выглядит похожим образом:

const { Command } = require('@oclif/command') const Todo = require('../db') const chalk = require('chalk')  class RemoveCommand extends Command {   async run() {     const { id } = this.parse(RemoveCommand).args     try {       await Todo.remove({ id: parseInt(id, 10) }).write()       this.log(chalk.green('Todo removed.'))     } catch {       this.log(chalk.red('Operation failed.'))     }   } }  RemoveCommand.description = `Removes a task by id`  RemoveCommand.args = [   {     name: 'id',     description: 'todo id',     required: true   } ]  module.exports = RemoveCommand 

Наконец, редактируем файл src/commands/show.js:

const { Command } = require('@oclif/command') const Todo = require('../db') const chalk = require('chalk')  class ShowCommand extends Command {   async run() {     // находим все задачи и сортируем их по id     const res = await Todo.sortBy('id').value()     // если в списке имеется хотя бы одна задача     // выводим список в терминал     if (res.length) {       res.forEach(({ id, task, done }) => {         this.log(           `[${             done ? chalk.green('DONE') : chalk.red('NOT DONE')           }] id: ${chalk.yellowBright(id)}, task: ${chalk.yellowBright(task)}`         )       })     // иначе сообщаем об отсутствии задач     } else {       this.log('There are no todos.')     }   } }  ShowCommand.description = `Shows existing tasks`  module.exports = ShowCommand 

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

npm link / yarn link 

Далее выполняем несколько операций.

Отлично. Все работает, как ожидается. Осталось отредактировать package.json и README.md, и можно публиковать пакет в реестре npm.

CLI своими руками

Наш CLI по своему функционалу будет напоминать create-react-app или vue-cli. По команде create он будет создавать в целевой директории проект, содержащий все необходимые для работы приложения файлы. Кроме того, в нем будет предусмотрена возможность опциональной инициализации git и установки зависимостей.

Исходный код проекта находится здесь.

Создаем директорию и инициализируем проект:

mkdir create-modern-template cd create-modern-template npm init -y / yarn init -y 

Устанавливаем необходимые библиотеки:

yarn add arg chalk clear esm execa figlet inquirer listr ncp pkg-install 

  • arg — инструмент для разбора аргументов командной строки
  • clear — инструмент для очистки терминала
  • esm — инструмент, обеспечивающий поддержку ES6-модулей в Node.js
  • execa — инструмент для автоматического выполнения некоторых распространенных операций (мы будем использовать его для инициализации git)
  • figlet — инструмент для вывода в терминал кастомизированного текста
  • inquirer — инструмент для работы с командной строкой, в частности, позволяет задавать вопросы пользователю и разбирать его ответы
  • listr — инструмент для создания списка задач и визуализации их выполнения в терминале
  • ncp — инструмент для копирования файлов и директорий
  • pkg-install — инструмент для программной установки зависимостей проекта

В корневой директории создаем файл bin/create (без расширения) следующего содержания:

#!/usr/bin/env node  require = require('esm')(module)  require('../src/cli').cli(process.argv) 

Редактируем package.json:

"main": "src/main.js", "bin": "bin/create" 

Команда create зарегистрирована.

Создаем директорию src/template и помещаем туда файлы проекта, которые будут копироваться в целевую директорию.

Создаем файл src/cli.js следующего содержания:

// импорт необходимого функционала import arg from 'arg' import inquirer from 'inquirer' import { createProject } from './main'  // разбор аргументов командной строки // --yes или -y означает пропуск инициализации git и установки зависимостей // --git или -g означает инициализацию git // --install или -i означает установку зависимостей const parseArgumentsIntoOptions = (rawArgs) => {   const args = arg(     {       '--yes': Boolean,       '--git': Boolean,       '--install': Boolean,       '-y': '--yes',       '-g': '--git',       '-i': '--install'     },     {       argv: rawArgs.slice(2)     }   )    // возвращаем объект с настройками   return {     template: 'template',     skipPrompts: args['--yes'] || false,     git: args['--git'] || false,     install: args['--install'] || false   } }  // запрос недостающих аргументов const promptForMissingOptions = async (options) => {   // если пользователь указал флаг --yes или -y   if (options.skipPrompts) {     return {       ...options,       git: false,       install: false     }   }    // вопросы   const questions = []    // если отсутствует аргумент для инициализации git   if (!options.git) {     questions.push({       type: 'confirm',       name: 'git',       message: 'Would you like to initialize git?',       default: false     })   }    // если отсутствует аргумент для установки зависимостей   if (!options.install) {     questions.push({       type: 'confirm',       name: 'install',       message: 'Would you like to install dependencies?',       default: false     })   }    // получаем ответы пользователя   const answers = await inquirer.prompt(questions)    // возвращаем объект с настройками   return {     ...options,     git: options.git || answers.git,     install: options.install || answers.install   } }  // функция разбора и запроса недостающих аргументов командной строки export async function cli(args) {   let options = parseArgumentsIntoOptions(args)    options = await promptForMissingOptions(options)    await createProject(options) } 

Файл src/main.js выглядит так:

// импорт необходимого функционала import path from 'path' import chalk from 'chalk' import execa from 'execa' import fs from 'fs' import Listr from 'listr' import ncp from 'ncp' import { projectInstall } from 'pkg-install' import { promisify } from 'util' import clear from 'clear' import figlet from 'figlet'  // промисификация получения доступа к файлу и копирования файлов const access = promisify(fs.access) const copy = promisify(ncp)  // очищаем терминал clear()  // отображаем в терминале текст HTML ярко-желтого цвета console.log(   chalk.yellowBright(figlet.textSync('HTML', { horizontalLayout: 'full' })) )  // функция копирования файлов const copyFiles = async (options) => {   try {     // templateDirectory - директория с файлами проекта,     // targetDirectory - целевая директория     await copy(options.templateDirectory, options.targetDirectory)   } catch {     // сообщаем о провале операции     console.error('%s Failed to copy files', chalk.red.bold('ERROR'))     process.exit(1)   } }  // функция инициализации git const initGit = async (options) => {   try {     await execa('git', ['init'], {       cwd: options.targetDirectory,     })   } catch {     // сообщаем о провале операции     console.error('%s Failed to initialize git', chalk.red.bold('ERROR'))     process.exit(1)   } }  // функция создания проекта export const createProject = async (options) => {   // определяем путь к целевой директории   options.targetDirectory = process.cwd()    // полный путь к данному файлу   const fullPath = path.resolve(__filename)    // определяем путь к директории с файлами проекта   const templateDir = fullPath.replace('main.js', `${options.template}`)    options.templateDirectory = templateDir    try {     // получаем доступ к целевой директории     // флаг R_OK - разрешение на чтение файла     await access(options.templateDirectory, fs.constants.R_OK)   } catch {     // сообщаем о провале операции     console.error('%s Invalid template name', chalk.red.bold('ERROR'))     process.exit(1)   }    // создаем список задач   const tasks = new Listr(     [       {         title: 'Copy project files',         task: () => copyFiles(options),       },       {         title: 'Initialize git',         task: () => initGit(options),         enabled: () => options.git,       },       {         title: 'Install dependencies',         task: () =>           projectInstall({             cwd: options.targetDirectory,           }),         enabled: () => options.install,       },     ],     {       exitOnError: false,     }   )    // запускаем задачи   await tasks.run()    // сообщаем об успехе операции   console.log('%s Project ready', chalk.green.bold('DONE'))    return true } 

Подключаем CLI (находясь в корневой директории):

yarn link 

Создаем целевую директорию и проект:

mkdir test-dir cd test-dir create-modern-template && code . 

Прекрасно. CLI готов к публикации.

Публикация пакета в реестре npm

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

Затем нужно авторизоваться, выполнив команду npm login и указав email и пароль.

После этого редактируем package.json и создаем файлы .gitignore, .npmignore, LICENSE и README.md (см. репозиторий проекта).

Упаковываем файлы проекта с помощью команды npm package. Получаем файл create-modern-template.tgz. Публикуем данный файл, выполняя команду npm publish create-modern-template.tgz.

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

После публикации пакета, его можно устанавливать как любой другой пакет с помощью npm i / yarn add.

Как видите, в создании CLI и публикации пакета в реестре npm нет ничего сложного.

Надеюсь, вы нашли для себя что-нибудь интересное. Благодарю за внимание.

ссылка на оригинал статьи https://habr.com/ru/post/532302/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *