Cтворення консольного додатку на JavaScript

22 хв. читання

node

Багато хто вважає, що Node.js тільки для серверних додатків, але це не так. Мікросервіси, REST API, утиліти, IoT та десктопні додатки — все це сфери, де використовують Node.

Сьогодні ми напишемо консольний додаток за допомогою Node.js. Ми будемо використовувати вже готові модулі для роботи з різними частинами ОС і створимо дійсно корисний додаток.

Наш додаток буде ініціалізувати git репозитарій, але не просто виконувати git init, а ще й дозволить відразу створити віддалений репозитарій на GitHub, інтерактивно створювати .gitignore, сам зробить початковий комміт та синхронізує рипозитарії.

Весь код з цього уроку можна знайти тут.

Але чому Node.js?

Перед самою розробкою слід пояснити чому ми вибрали саме Node. Ну, якщо ви це читаєте, то, скоріше за все, знайомі з JavaScript і для розробки утиліт не потрібно вчити нову мову. Другою причиною є екосистема Node: для нього написані тисячі і тисячі пакетів для різних потреб, серед яких є і ті, що розроблені спеціально для розробки CLI (Command-line interface) додатків. І останньою причиною буде npm, завдяки якому маніпуляція пакетами стає дуже простою і вам не потрібно хвилюватися про якісь специфічні пакети для кожної ОС.

Початок

Ginit

Ми будемо писати додаток під назвою Ginit. Це як git init, але набагато краще.

Окреслимо алгоритм нашого додатку детальніше. Як ви знаєте, git init створює репозитарій в поточному каталозі. Зазвичай це лише один з кроків реального створення репозитарію. Як правило, це проходить так:

  1. Створення локального репозитарію за допомогою git init
  2. Створення віддаленого репозитарію на Github чи Bitbucket (зазвичай приходиться перемикатися на браузер)
  3. Додання віддаленого репозитарію до локального
  4. Створення файлу .gitignore
  5. Додання файлів проекту
  6. Початковий комміт
  7. Завантаження файлів на сервер

Іноді можуть знадобитися й інші дії, але зазвичай перед ними завжи виконують дії зі списку. І повторюються вони доволі часто. Чому б нам їх не оптимізувати?

Встановлення залежностей

Звісно, в терміналі не зробити якийсь дуже об'ємний інтерфейс, але це не означає, що наш додаток буде звичайним монохромним текстом. Ви будете вражені як легко робити консольний інтерфейс на Node.js, при цьому не втрачаючи функціональності. Ми будемо використовувати chalk для кольорового тексту, clui, щоб додати деякі візуальні компоненти, figlet, щоб додати прикольний ASCII-банер та clear, щоб очищати термінал.

Для введення даних користувачем можна використати стандартний модуль Readline, якого зазвичай достатньо, але ми використаємо більш функціональний модуль Inquirer. Окрім звичайного запиту на введення тексту, він дозволяє створювати прості віджети: щось схоже на radio button чи checkbox, але в терміналі.

Також ми використаємо minimist для парсингу аргументів командної стрічки.

Крім цього, ми також використаємо такі бібліотеки:

  • github для взаємодії з API гітхабу
  • loadsh для деяких JavaScript-утиліт
  • simple-git для запуску git-команд прямо з Node
  • touch — реалізація *nix команди touch

Початок розробки

Створимо директорію для нашого проекту:

mkdir ginit
cd ginit

Та створимо файл package.json:

npm init

npm задасть вам декілька питань про проект і на основі цих даних створить файл. Тепер слід встановити залежності:

npm install chalk clear clui figlet inquirer minimist preferences github lodash simple-git touch --save

Або ж ви можете просто скопіювати файл package.json з репозитарію цього уроку.

{
  "name": "ginit",
  "version": "1.0.0",
  "description": "\"git init\" on steroids",
  "main": "index.js",
  "keywords": [
    "Git",
    "CLI"
  ],
  "author": "Lukas White <hello@lukaswhite.com>",
  "license": "ISC",
  "dependencies": {
    "chalk": "^1.1.3",
    "clear": "0.0.1",
    "clui": "^0.3.1",
    "figlet": "^1.1.2",
    "github": "^2.1.0",
    "inquirer": "^1.1.0",
    "lodash": "^4.13.1",
    "minimist": "^1.2.0",
    "preferences": "^0.2.1",
    "simple-git": "^1.40.0",
    "touch": "^1.0.0"
  }
}

Тепер створимо файл index.js і імпортуємо всі залежності.

var chalk       = require('chalk');
var clear       = require('clear');
var CLI         = require('clui');
var figlet      = require('figlet');
var inquirer    = require('inquirer');
var Preferences = require('preferences');
var Spinner     = CLI.Spinner;
var GitHubApi   = require('github');
var _           = require('lodash');
var git         = require('simple-git')();
var touch       = require('touch');
var fs          = require('fs');

Зауважте, що модуль simple-git потрібно не просто імпортувати, а й викликати.

Створення допоміжних функцій

Для комфортної роботи нам потрібно написати дві допоміжні функції, а саме:

  • Отримання назви поточної директорії (для імені репозитарію по замовчуванню)
  • Перевірка чи існує певна директорія в поточній (для перевірки наявності директорії .git, що свідчить про те, що репозитарій вже створено)

Спочатку можна піддатися спокусі і використати модуль fs і функцію realpathSync, щоб отримати поточну директорію:

path.basename(path.dirname(fs.realpathSync(__filename)));

Це спрацює лише якщо викликати утиліту з тієї ж директорії (наприклад, ось так node index.js), але ж ми хочемо зробити її доступною глобально, тому краще використати process.cwd. А для перевірки наявності директорії можна використати функції fs.stat/fs.statSync. Але вони викликають помилку, якщо файл відсутній, тому ми використаємо конструкцію try..catch.

Також слід окремо відзначити, що при написанні CLI-додатку краще використовувати синхронні варіанти функцій.

Тепер давайте зберемо це все разом в файлі lib/files.js:

var fs = require('fs');
var path = require('path');

module.exports = {
  getCurrentDirectoryBase : function() {
    return path.basename(process.cwd());
  },

  directoryExists : function(filePath) {
    try {
      return fs.statSync(filePath).isDirectory();
    } catch (err) {
      return false;
    }
  }
};

А зараз повернемось до файлу index.js та імпортуємо наш новий файл:

var files = require('./lib/files');

Приготування завершені і тепер настав час писати сам додаток.

Ініціалізація Node CLI

Для початку давайте очистимо термінал та виведемо банер. Ми встановили декілька бібліотек для цього, давайте ними скористаємось.

clear();
console.log(
  chalk.yellow(
    figlet.textSync('Ginit', { horizontalLayout: 'full' })
  )
);

А ось так воно буде виглядати:

ginit

Тепер час написати перевірку на наявність репозитарію в поточній директорії:

if (files.directoryExists('.git')) {
  console.log(chalk.red('Already a git repository!'));
  process.exit();
}

Тут ми використовуємо модуль chalk щоб вивести текст червоного кольору.

Введення даних користувачем

Наступним кроком буде написання функції, що запитує у користувача його Github-дані. Для цього ми будемо використовувати Inquirer. Модуль надає декілька типів вводу, вони схожі на типи форм в HTML. Для нікнейму та паролю добре підійдуть типи input та password.

function getGithubCredentials(callback) {
  var questions = [
    {
      name: 'username',
      type: 'input',
      message: 'Enter your Github username or e-mail address:',
      validate: function( value ) {
        if (value.length) {
          return true;
        } else {
          return 'Please enter your username or e-mail address';
        }
      }
    },
    {
      name: 'password',
      type: 'password',
      message: 'Enter your password:',
      validate: function(value) {
        if (value.length) {
          return true;
        } else {
          return 'Please enter your password';
        }
      }
    }
  ];

  inquirer.prompt(questions).then(callback);
}

Як ви бачите inquirer.prompt() задає користувачу серію питань, що міститься в першому аргументі. Кожне питання це об'єкт з полями name, type, message (повідомлення для користувача) та validate (функція валідації).

Ви можете протестувати нашу функцію так:

getGithubCredentials(function(){
  console.log(arguments);
});

А тепер запустимо додаток:

node index.js

ginit

Авторизація для Github API

Тепер нам потрібно написати функцію, що за логіном та паролем буде отримувати OAuth-токен для наступних запитів.

Звісно, нам не хочеться кожен раз запитувати у користувача його дані. Тому ми будемо довгостроково зберігати токен. Саме тут ми використаємо модуль preferences.

Збереження налаштувань

Збереження налаштувань здається досить простою задачею: ви можете просто писати та читати їх з json файлу, використовуючи стандартні засоби Node. Але модуль preferences представляє й інші потрібні функції:

  1. Він сам вирішить де краще зберігати налаштування, базуючись на поточному користувачі та ОС.
  2. Не потрібно працювати з функціями читання чи запису, ви просто модифікуєте об'єкт, решту модуль бере на себе.
  3. Налаштування шифруються. В нашому випадку це важливо, адже ми зберігаємо цінну інформацію.

Для початку використання слід просто створити екземпляр класу Preferences.

var prefs = new Preferences('ginit');

Якщо файл з налаштуваннями відсутній, то вам буде повернуто пустий об'єкт, а файл створено. А якщо він є, то його зміст буде декодовано в JSON і передано в ваш додаток. Тепер ви можете використовувати prefs як звичайний об'єкт, присвоюючи та отримуючи властивості, а решту за вас зробить Preferences.

На Mac OSX/Linux файл лежить тут: /Users/[YOUR-USERNME]/.config/preferences/ginit.pref.

Взаємодія з Github API

Давайте створимо екземпляр GitHubApi. Ми будемо використовувати його в декількох місцях, тому зробимо його доступним глобально.

var github = new GitHubApi({
  version: '3.0.0'
});

А тепер функція, що перевіряє чи не маємо ми токен в налаштуваннях

function getGithubToken(callback) {
  var prefs = new Preferences('ginit');

  if (prefs.github && prefs.github.token) {
    return callback(null, prefs.github.token);
  }

  // Fetch token
  getGithubCredentials(function(credentials) {
    ...
  });
}

Якщо токен вже отримано, то викликаємо функцію-калбек, якщо ні — отримуємо його. Зауважте, що отримання токену це мережевна операція, тобто займе деякий час. Давайте покажемо користувачу, що додаток не завис. Для цього ми використаємо clui, а точніше спінер з цього модулю.

Створимо його:

var status = new Spinner('Authenticating you, please wait...');
status.start();

Зупинити його можна так:

status.stop();

Також ви можете динамічно змінювати його заголовок за допомогою методу update.

А ось і сам код авторизації:

getGithubCredentials(function(credentials) {
  var status = new Spinner('Authenticating you, please wait...');
  status.start();

  github.authenticate(
    _.extend(
      {
        type: 'basic',
      },
      credentials
    )
  );

  github.authorization.create({
    scopes: ['user', 'public_repo', 'repo', 'repo:status'],
    note: 'ginit, the command-line tool for initalizing Git repos'
  }, function(err, res) {
    status.stop();
    if ( err ) {
      return callback( err );
    }
    if (res.token) {
      prefs.github = {
        token : res.token
      };
      return callback(null, res.token);
    }
    return callback();
  });
});

Покроково розберемо цей код:

  1. Ми просимо користувача ввести його дані за допомогою функції getGithubCredentials.
  2. Використовуємо базову аутентифікацію перед тим, як отримати токен.
  3. Намагаємось отримати токен для нашого додатку.
  4. Записуємо токен у налаштування.
  5. Повертаємо токен.

Після створення токену, без різниці, через API чи через веб-інтерфейс, він буде доступний тут.

Якщо у вас ввімкнена двофакторна авторизація, то все буде складніше. Вам треба буде запитати у користувача код та відправити його, використовуючи заголовок X-Github-OTP. Детальніше тут.

Створення репозитарію

Після того, як ми отримали токен, ми можемо створити віддалений репозитарій на Github.

Ми знову використаємо Inquirer щоб задати серію запитань користувачеві. Нам потрібне ім'я репозитарію, його тип (публічний чи приватний) та опціональний опис.

Ми використаємо minimist щоб отримати значення для назви та опису за замочуванням. Наприклад, так:

ginit my-repo "just a test repository"

Це встановить іменем за замовчуванням "my-repo", а описом "just a test repository".

А ось так ми можемо отримати їх в коді:

var argv = require('minimist')(process.argv.slice(2));
// { _: [ 'my-repo', 'just a test repository' ] }

Насправді, це тільки невеличка частина функціоналу модулю minimist. Також він підтримує прапорці, перемикачі, пари ключ-значення. Почитайте документацію, якщо бажаєте.

Ось повний код для парсингу аргументів та серії запитань:

function createRepo(callback) {
  var argv = require('minimist')(process.argv.slice(2));

  var questions = [
    {
      type: 'input',
      name: 'name',
      message: 'Enter a name for the repository:',
      default: argv._[0] || files.getCurrentDirectoryBase(),
      validate: function( value ) {
        if (value.length) {
          return true;
        } else {
          return 'Please enter a name for the repository';
        }
      }
    },
    {
      type: 'input',
      name: 'description',
      default: argv._[1] || null,
      message: 'Optionally enter a description of the repository:'
    },
    {
      type: 'list',
      name: 'visibility',
      message: 'Public or private:',
      choices: [ 'public', 'private' ],
      default: 'public'
    }
  ];

  inquirer.prompt(questions).then(function(answers) {
    var status = new Spinner('Creating repository...');
    status.start();

    var data = {
      name : answers.name,
      description : answers.description,
      private : (answers.visibility === 'private')
    };

    github.repos.create(
      data,
      function(err, res) {
        status.stop();
        if (err) {
          return callback(err);
        }
        return callback(null, res.ssh_url);
      }
    );
  });
}

Тепер, коли ми маємо необхідну інформацію, ми можемо викликати API створення репозитарію, а у відповідь отримати його URL. Але спочатку нам треба створити .gitignore

Створення .gitignore

Зараз ми напишемо невеличкий консольний помічник у створенні файлу .gitignore. Якщо користувач запустить утиліту в директорії існуючого проекта, то ми виведемо список папок та файлів та дозволимо відмітити ті, що не варто додати в репозитарій. Пакет Inquirer як раз надає потрібний компонент — checkbox.

checkbox

Перше, що слід зробити — це просканувати поточну директорію та виключити папку .git та файл .gitignore, якщо вони присутні.

function createGitignore(callback) {
  var filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');
  ...
}

Якщо ж файлів немає, то просто створимо пустий .gitignore.

function createGitignore(callback) {
  var filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');

  if (filelist.length) {
    ...
  } else {
    touch( '.gitignore' );
    return callback();
  }
}

Ну а тепер створимо віджети. Коли користувач підтвердить відповідь, ми сформуємо на її основі файл .gitignore, розділивши файли і папки знаком переносу каретки ("\
").

function createGitignore(callback) {
  var filelist = _.without(fs.readdirSync('.'), '.git', '.gitignore');

  if (filelist.length) {
    inquirer.prompt(
      [
        {
          type: 'checkbox',
          name: 'ignore',
          message: 'Select the files and/or folders you wish to ignore:',
          choices: filelist,
          default: ['node_modules', 'bower_components']
        }
      ]
    ).then(function( answers ) {
        if (answers.ignore.length) {
          fs.writeFileSync( '.gitignore', answers.ignore.join( '\
' ) );
        } else {
          touch( '.gitignore' );
        }
        return callback();
      }
    );
  } else {
    touch('.gitignore');
    return callback();
  }
}

Зауважте, що ми передаємо також значення за замовчуванням: папки node_modules та bower_components, якщо вони присутні.

Взаємодіємо з git прямо з додатку

Є декілька способів взаємодіяти з git, але ми виберемо найпростіший — модуль simple-git, що представляє зручне і просте API.

Ось ці дії ми автоматизуємо:

  1. Запуск git init
  2. Додання файлу .gitignore
  3. Додання файлів проекту (окрім тих, що в .gitignore)
  4. Початковий комміт
  5. Додання новоствореного віддаленого репозитарію
  6. Завантаження на нього файлів

А ось сам код:

function setupRepo( url, callback ) {
  var status = new Spinner('Setting up the repository...');
  status.start();

  git
    .init()
    .add('.gitignore')
    .add('./*')
    .commit('Initial commit')
    .addRemote('origin', url)
    .push('origin', 'master')
    .then(function(){
      status.stop();
      return callback();
    });
}

Збірка всього разом

Ну і під кінець потрібно все це зібрати в єдину програму. Для початку напишемо функцію для отримання токену та аутентифікації користувача:

function githubAuth(callback) {
  getGithubToken(function(err, token) {
    if (err) {
      return callback(err);
    }
    github.authenticate({
      type : 'oauth',
      token : token
    });
    return callback(null, token);
  });
}

А тепер викличемо функцію, що виконує головну логіку утиліти:

githubAuth(function(err, authed) {
  if (err) {
    switch (err.code) {
      case 401:
        console.log(chalk.red('Couldn\\'t log you in. Please try again.'));
        break;
      case 422:
        console.log(chalk.red('You already have an access token.'));
        break;
    }
  }
  if (authed) {
    console.log(chalk.green('Sucessfully authenticated!'));
    createRepo(function(err, url){
      if (err) {
        console.log('An error has occured');
      }
      if (url) {
        createGitignore(function() {
          setupRepo(url, function(err) {
            if (!err) {
              console.log(chalk.green('All done!'));
            }
          });
        });
      }
    });
  }
});

Як бачите, вона перевіряє чи користувач авторизований перед послідовним викликом решти функцій (createRepo, createGitignore, setupRepo). Також воно відловлює помилки, що покращує взаємодію з користувачем.

Кінцевий варіант файлу index.js можна побачити тут.

Встановлення утиліти глобально

Ще однією важливою задачею є встановлення нашої утиліти глобально, щоб вона була доступна з будь якої папки. Щоб це все виглядало красиво та зручно, спершу слід додати на самий початок файлу шебанг, що вкаже нашій командній оболонці який інтерпретатор використовувати.

#!/usr/bin/env node

Після цього слід додати властивість bin до файлу package.json, це зв'яже команду ginit з файлом index.js.

"bin": {
  "ginit": "./index.js"
}

Після цього виконайте команду, щоб встановити наш додаток:

npm install -g

До речі, це також спрацює і на Windows, про це подбав npm. Більше інформації тут.

Післямова

Сьогодні ми написали досить простий додаток на Node.js, але ви можете зробити більше.

Якщо ви користувач BitBucket, ви можете використати обгортку API для Node (а ще краще офіційний bitbucketjs — пер.), щоб адаптувати утиліту до роботи з Bitbucket. А ще краще було б дати користувачу на вибір яку платформу використовувати.

Також можна зробити альтернативне створення .gitignore, де користувачу треба буде лише ввести мову, на якій написаний проект, а утиліта сама завантажить потрібний файл з .gitignore.io.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 629
Приєднався: 1 рік тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід