Is there a better way to run CLI commands with Node.js?

对着背影说爱祢 提交于 2019-12-14 02:17:36

问题


I just wrote a script to release a build of one of the products I'm working on. The script does the job, but I don't really like the code itself, looks like spaghetti code and callback hell combined.

Is there a cleaner way to do this? I'd like to be able to run commands in series, log the outputs (stdout.on('data')) and when the task is finished. (easier for further debug and when waiting for the task to be done, reassuring to know what's happening on the background)

Maybe using Promises would help clean the mess a bit, but still, I feel like there should be a cleaner way to deal with multiple commands.


Some explanation about what the code does:

  1. Create a tag with the commit you want and the tag version you want, i.e: git tag 1.2.5.
  2. Build the release file with gulp build.
  3. Create a folder doc/<tag>.
  4. Convert doc/doc_reader.odt to doc/<tag>/documentation.pdf. (Open it and export as PDF)
  5. Copy build/reader.js and doc/changelog.txt in the created folder.
  6. Zip the 3 files.
  7. Commit everything with commit message: Release 1.2.11 (for example)
  8. Push.
  9. Create a new release on GitHub using the commit you just pushed and the same tag.

Here is the code, as an example. (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's 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:

Here is the implementation using async/await (node 7.8.0) I used special mkdirp and exec modules, that allow usage with await. But I couldn't find an equivalent for 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's 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:


There is an mz module that can be very helpful here. See:

  • https://www.npmjs.com/package/mz

This, combined with async/await will allow you to write code like this:

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);
  // ...
})();

This is a simple example but you can use all functions from the child_process, fs and many other modules that way.

What's important here is that this code is still asynchronous and non-blocking.

Note that you can only use await inside of a function created with the async keyword. For more info, see:

  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
  • https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await

For support in browsers, see:

  • http://caniuse.com/async-functions

For support in Node, see:

  • http://node.green/#ES2017-features-async-functions

In places where you don't have native support for async and await you can use Babel:

  • https://babeljs.io/docs/plugins/transform-async-to-generator/

or with a slightly different syntax a generator based approach like in co or Bluebird coroutines:

  • https://www.npmjs.com/package/co
  • http://bluebirdjs.com/docs/api/promise.coroutine.html


来源:https://stackoverflow.com/questions/43187952/is-there-a-better-way-to-run-cli-commands-with-node-js

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!