Есть ли лучший способ запуска команд CLI с помощью Node.js?

Я только что написал script, чтобы выпустить сборку одного из продуктов, над которыми я работаю. script выполняет эту работу, но мне не очень нравится сам код, выглядит как код спагетти и комбинированный аддон.

Есть ли более чистый способ сделать это? Я хотел бы иметь возможность запускать команды последовательно, регистрировать выходы (stdout.on('data')) и когда задача завершена. (проще для дальнейшего отладки и ожидая выполнения задачи, успокаивая, чтобы узнать, что происходит на заднем плане)

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


Некоторое объяснение того, что делает код:

  • Создайте тег с требуемым фиксацией и нужную версию тега, т.е. git tag 1.2.5.
  • Создайте файл выпуска с gulp build.
  • Создайте папку doc/<tag>.
  • Преобразуйте doc/doc_reader.odt в doc/<tag>/documentation.pdf. (Откройте его и экспортируйте в формате PDF)
  • Скопируйте build/reader.js и doc/changelog.txt в созданную папку.
  • Замените 3 файла.
  • Зафиксировать все с сообщением фиксации: Release 1.2.11 (например)
  • Нажмите.
  • Создайте новую версию на GitHub, используя комманду, которую вы только что нажали, и те же теги.

Вот пример кода. (ES5, Node 4.6.0 +)

var mkdirp = require('mkdirp');
var fs = require('fs-extra');
var path = require('path');
var spawn = require('child_process').spawn;
var zip = new require('node-zip')();

var package = require('../package.json');
var version = package.version;
var releaseDirectory = 'doc'
var gitTagProcess = spawn('git', ['tag', version]);
var gulpBuildProcess = spawn('gulp', ['build']);

console.log(`Running "git tag ${version}"...`);
gitTagProcess.stdout.on('data', function (chunk) {
  console.log(chunk.toString('utf8'));
});

gitTagProcess.on('close', function () {
  console.log('Tag created.')

  console.log('Running "gulp build"...');
  gulpBuildProcess.stdout.on('data', function (chunk) {
    console.log(chunk.toString('utf8'));
  });

  gulpBuildProcess.on('close', function () {
    console.log('"gulp build" done.')

    console.log(`Creating "${releaseDirectory}/${version}" directory.`)
    mkdirp(`${releaseDirectory}/${version}`, function () {
      console.log('Directory created.');
      var docFile = `${releaseDirectory}/doc_reader.md`;
      console.log(`Converting ${docFile} to pdf ...`);
      var docBuildProcess = spawn('npm', ['run', 'build:doc']);

      docBuildProcess.stdout.on('data', function (chunk) {
        console.log(chunk.toString('utf8'));
      });

      docBuildProcess.on('close', function () {
        console.log('Doc created.');

        console.log('Copying changelog.txt ...');
        fs.copySync('doc/changelog.txt', `doc/${version}/changelog.txt`);
        console.log('changelog.txt copied.');

        console.log(`Copying "build/reader.js" to "doc/reader-${version}.js" and "doc/reader.js" ...`);
        fs.copySync('build/reader.js', `doc/${version}/reader.js`);
        fs.copySync('build/reader.js', `doc/${version}/reader-${version}.js`);
        console.log('reader.js copied.');

        console.log('Zipping all files ...');
        zip.file('changelog.txt', fs.readFileSync(`doc/${version}/changelog.txt`));
        zip.file('doc_reader.pdf', fs.readFileSync(`doc/${version}/doc_reader.pdf`));
        zip.file('reader.js', fs.readFileSync(`doc/${version}/reader.js`));
        zip.file(`reader-${version}.js`, fs.readFileSync(`doc/${version}/reader-${version}.js`));

        var data = zip.generate({ base64: false, compression: 'DEFLATE' });
        var zipFilename = `doc/${version}/HTML5Reader_${version}.zip`;
        fs.writeFileSync(zipFilename, data, 'binary'); // it important to use *binary* encode
        console.log(`${zipFilename} created.`);

        console.log(`\nRelease ${version} done. Please add generated files and commit using:`);
        console.log(`\n\tgit add * && git commit -m "Release ${version}"`);
        console.log(`\n\nDon't forget to push and create a new release on GitHub at https://github.com/$domain/$product/releases/new?tag=${version}`);
      });
    });
  });
});

Edit:

Вот реализация с использованием async/await (node 7.8.0) Я использовал специальные модули mkdirp и exec, которые позволяют использовать с await. Но я не мог найти эквивалент для spawn.

const mkdirp = require('async-mkdirp');
const fs = require('fs-extra');
const spawn = require('child-process-promise').spawn;
const exec = require('mz/child_process').exec;
const zip = new require('node-zip')();
const c = require('chalk');

const error = c.bold.red;
const warn = c.yellow;
const info = c.cyan;
const info2 = c.magenta;

const version = require('../package.json').version;
const releaseDirectory = 'doc'

async function git_tag() {
  async function exec_git_tag() {
    return await exec(`git tag ${version}`);
  }

  console.log(info(`Creating git tag ${version}`));
  return exec_git_tag()
    .then(() => {
      console.log(info(`Git tag created for ${version}`))
    })
    .catch((err) => {
      console.log(warn('warn', err));
    })
    // Finally
    .then(() => {
      console.log(info(`"git tag ${version}" - Completed`))
    });
};

async function gulp_build() {
  async function exec_gulp_build() {
    const promise = spawn('gulp', ['build'])
    const childProcess = promise.childProcess;

    childProcess.stdout.on('data', (data) => {
      console.log(info2(data.toString()));
    });
    childProcess.stderr.on('data', (data) => {
      console.log(error(data.toString()));
    });

    return promise
      .catch((err) => {
        console.error(error(err));
      })
      // Finally
      .then(() => {
        console.log(info('"gulp build" - Completed'))
      });
  }

  console.log(info('Running "gulp build"...'))
  return exec_gulp_build()
}

async function create_dir() {
  const dirPath = `${releaseDirectory}/${version}`;
  console.log(info(`Creating "${dirPath}" directory.`))
  await mkdirp(`${dirPath}`);
  console.log(info(`Directory ${dirPath} created.`));
}

async function build_doc() {
  const docFile = `${releaseDirectory}/doc_reader.md`;
  console.log(info(`Converting ${docFile} to pdf ...`));

  async function exec_build_doc() {
    return await exec(`npm run build:doc`);
  }

  return exec_build_doc()
    .catch((err) => {
      console.error(error(err));
    })
    .then(() => {
      console.log(info(`Doc "${docFile}" created.`));
    })
}

function copy_files() {
  console.log(info('Copying changelog.txt ...'));
  fs.copySync('doc/changelog.txt', `doc/${version}/changelog.txt`);
  console.log(info('changelog.txt copied.'));

  console.log(info(`Copying "build/reader.js" to "doc/reader-${version}.js" and "doc/reader.js" ...`));
  fs.copySync('build/reader.js', `doc/${version}/reader.js`);
  fs.copySync('build/reader.js', `doc/${version}/reader-${version}.js`);
  console.log(info('reader.js copied.'));
}

function zip_files() {
  console.log(info('Zipping all files ...'));
  zip.file('changelog.txt', fs.readFileSync(`doc/${version}/changelog.txt`));
  zip.file('doc_reader.pdf', fs.readFileSync(`doc/${version}/doc_reader.pdf`));
  zip.file('reader.js', fs.readFileSync(`doc/${version}/reader.js`));
  zip.file(`reader-${version}.js`, fs.readFileSync(`doc/${version}/reader-${version}.js`));

  const data = zip.generate({ base64: false, compression: 'DEFLATE' });
  const zipFilename = `doc/${version}/HTML5Reader_${version}.zip`;
  fs.writeFileSync(zipFilename, data, 'binary'); // it important to use *binary* encode
  console.log(info(`${zipFilename} created.`));
}

async function release() {
  await git_tag();
  await gulp_build();
  await create_dir();
  await build_doc();
  copy_files();
  zip_files();

  console.log(`\nRelease ${version} done. Please add generated files and commit using:`);
  console.log(`\n\tgit add . && git commit -m "Release ${version}"`);
}

release();

Ответ 1

Существует модуль mz, который может быть очень полезен здесь. См:

Это, в сочетании с async/await, позволит вам написать такой код:

let exec = require('mz/child_process').exec;

(async () => {
  let version = await exec('node --version');
  console.log(version);
  let result = await exec('some other command');
  console.log(result);
  // ...
})();

Это простой пример, но вы можете использовать все функции из child_process, fs и многих других модулей таким образом.

Важно то, что этот код все еще асинхронный и неблокирующий.

Обратите внимание, что вы можете использовать await только внутри функции, созданной с помощью ключевого слова async. Для получения дополнительной информации см.:

Для поддержки в браузерах см.

Для поддержки в Node см.

В местах, где у вас нет встроенной поддержки для async и await, вы можете использовать Babel:

или со слегка отличающимся синтаксисом, основанным на генераторе, как в co или сопрограмме Bluebird: