Сниппет, расширение для 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/


Интересные статьи

Интересные статьи

Ночь. Стук в дверь. Открыть. Стоят двое. "Верите ли вы в Event loop, нашу главную браузерную цепочку?" Вздохнуть. Закрыть дверь. Лечь досыпать. До начала рабочего дня еще 4 ...
В такое мрачное время откопал один из своих ранних мрачных коротких рассказов. Придумано около 1990 года, записано чуть позже. После 91 пошла оптимистичная движуха, не в пример тому, что сейча...
C++ сложный и интересный язык, совершенствоваться в нем можно чуть ли не всю жизнь. В какой-то момент мне захотелось изучать его следующим образом: взять какой-то аспект языка, возможно довольно ...
1. Вступление. Здравствуйте. Меня зовут Илья. Я из Санкт-Петербурга. Мне 31 год. С давних пор у меня основные увлечения — аккумуляторы, электротранспорт и авиация. В 2010 году я узнал про самый...
В первой части я немного рассказал о приложении RF & Microwave Toolbox. Во второй части расскажу о не менее интересном приложении, которое позволяет проектировать СВЧ устройства (преимуществе...