@vvscode

CLI для кодогенерации своими руками

PHP

Работал руками

Что-то не так ?

PHP base cli

Yii / Symfony

  • Создание новых структур
  • Работа с базой и миграциями
  • Работа с переводами
  • Работа с assets
  • Запуск сервера
  • ...

Frontend

Работать руками

Ember

ember-cli

  • build / dev server
  • Генерация моделей с полями и отношениями
  • Генерация компонентов
  • Генерация роутов с параметрами
  • Генерация тестов
  • Собственные blueprints

Иметь cli "модно"

  • упрощает разработку
  • снижает порог вхождения

 

  • работает с четкой структурой

Что нужно от генератора кода?

 

Генерировать код

Сколько вешать ?

  • Проект
     
  • Функция/класс
     
  • Компонент

cli генератор кода

  • консольный интерфейс
  • принимает параметры
  • генерирует код

     
  • умеет шаблоны
  • умеет справку
  • что душе угодно

Консольный интерфейс

{
  "name": "acli",
  "version": "0.0.1",
  "bin": {
    "acli": "./scripts/acli.js"
  }
}
#!/usr/bin/env node
chmod +x scripts/acli.js

package.json:

добавить shebang в scripts/acli.js

сделать файл исполняемым

На что стоит обратить внимание

// package.json
{
  // ...
  "engines": {
    "node": ">=9.11.1 <10.0",
    "yarn": ">=1.6.0"
  },
  // ...
}
# during development
npm link

Ограничение версий

Упрощение разработки

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

scripts/acli.js

#!/usr/bin/env node
console.log(process.argv);
acli --xxx -yyy 123

Run with params:

[ 
    '/usr/local/bin/node',
    '/usr/local/bin/acli',
    '--xxx',
    '-yyy',
    '123'
]

Помощь по использованию

~ $ ls --help
ls: illegal option -- -
usage: ls [-ABCFGHLOPRSTUWabcdefghiklmnopqrstuwx1] [file ...]
~ $ acli
  Usage: acli [options] [command]

  Awesome cli tool 0.0.28

  Options:

    -V, --version         output the version number
    -h, --help            output usage information

  Commands:

    init|i [options]      Initialize new project based on predefined template at Gitlabs's /templates subgroup
    generate|g [options]  Generate code from template
    config|c [options]    Configures cli tool

И дальше вот это все

  • os / path / fs
  • util
  • readline

По-старинке

Валидация параметров

# Ковыряния в работе терминала
console.log('\x1b[36m%s\x1b[0m', 'I am cyan');
console.log('\x1b[33m%s\x1b[0m', 'I am yellow');

Вывод справки и сообщений об ошибках

Это не наш метод

Подбираем пакеты

Парсинг параметров

#!/usr/bin/env node
const program = require('commander');

const projectPackage = require('../package.json');

program
    .version(`${projectPackage.version}`)
    .description(`Awesome cli tool ${projectPackage.version}`);

require('./commands/init')(program);
require('./commands/generate')(program);
require('./commands/config')(program);

program
    .allowUnknownOption()
    .parse(process.argv);

// show help by default
if (!process.argv.slice(2).length) {
  program.outputHelp();
}
scripts/acli.js

command

module.exports = program =>
  program
    .command('generate')
    .alias('g')
    .description('Generate code from template')
    .option(
        '-t, --template [template]', 
        'A template to generate new code'
    )
    .action(generateByTemplate);

Взаимодействие с пользователем

  • Гибкая конфигурация
  • Поддержка типов запросов
    - текст
    - пароль
    - список
    - чекбоксы
    - подтверждения
    - автоподсказка при вводе
  • Значения по-умолчанию
  • асинхронные хуки
const generateByTemplate = options => {
  // first we get list of available templates
  // which stored in scripts/generator-templates
  getTemplatesList()
    // after that we ask user about target template
    .then(templateNamesList => {
      if (!templateNamesList.includes(options.template)) {
        return prompt([
          {
            type: 'list',
            name: 'templateName',
            choices: templateNamesList,
            default: options.template,
            message: 'Choose template for a new code',
          },
        ]).then(i => i.templateName);
      }
      return options.template;
    })
    .then(() => {
        // ...
    })
}

Украшения

Chalk

Chalk

Multispinner

Генерация кода

любой шаблонизатор

import React, { Component } from 'react';

export class ${camelizedComponentName} extends Component {
  render() {
    return <div className="${dasherizedComponentName}">
             ${camelizedComponentName}
           </div>;
  }
}

export default ${camelizedComponentName};

/*
File was generated with tamm-cli from next settings:
${JSON.stringify(_answers, null, 2)}
*/
console.log(
    require('es6-template-strings')(
        'Hello ${place.toUpperCase()}!',
        { place: "World" }
    )
);

По-мелочи

  • find-root поиск ближайшего package.json
     
  • fs-extra  более удобная работа с fs

Чего хотелось от шаблонизации?

  • гибкость и простота
     
  • настройка под себя
     
  • многофайловость
     

3 уровня шаблонов

  1. На уровне cli
    Шаблоны в комплекте с генератором
     
  2. На уровне проекта
    Шаблоны из `.acli` директории проекта
     
  3. На уровне пользователя
    Шаблоны из `~/.acli`

Структура шаблона

.
├── _blank-template
│   ├── _template-definition.js
│   └── index.js
├── client-component
│   ├── _template-definition.js
│   ├── component.spec.js
│   ├── index.connected.js
│   ├── index.js
│   ├── statefull-component.js
│   ├── stateless-component.js
│   └── styles.less
├── client-route
│   ├── Route.component.js
│   ├── Route.container.js
│   ├── Route.css
│   ├── Route.spec.js
│   ├── _template-definition.js
│   ├── index.js
│   └── index.spec.js
_template-definition.js
module.exports = {
  prompts: (/* answers */) => [
    {
      type: 'input',
      name: 'path',
      message: 'Enter component path (name)',
    },
  ],

  // extendAnswers: answers => ({
  //   ...answers,
  //   random: Math.random(),
  // }),

  files: (/* answers */) => {
    const files = {
      'index.js': fs.readFileSync(`${__dirname}/index.js`),
    };
    return files;
  },
  
  // postGenerateMessage: (/* answers */) => `Well done`,

  // pinToDirectory: (/* answers */) => 'src/client/redux',
};
files:
  files(answers) {
    let { camelizedComponentName, statefull, connectedToRedux } = answers;
    const withRedux = connectedToRedux === 'Yes';
    statefull = statefull === 'Yes';

    const files = {
      [`${camelizedComponentName}.js`]: readFile(
        `${__dirname}/${statefull ? 'statefull' : 'stateless'}-component.js`,
      ),
      'index.js': readFile(
        `${__dirname}/index${withRedux ? '.connected' : ''}.js`,
      ),
    };
    answers.addStyles && files[`${camelizedComponentName}.less`] = readFile(
      `${__dirname}/styles.less`,
    );
    return files;
  },

Решили писать cli?

  • подумайте про пользователя
     
  • продумайте интерфейс
     
  • напишите документацию
     
  • обработайте ошибки
     
  • сохраняйте данные
     
  • подберите удобные инструменты
process.exit();

CLI для кодогенерации своими руками

By Vasiliy Vanchuk

CLI для кодогенерации своими руками

Паттерны написания cli приложений на примере реализации cli для нужд проекта.

  • 1,418