Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3 major release #95

Closed
tunnckoCore opened this issue Mar 4, 2018 · 1 comment · Fixed by #109
Closed

v3 major release #95

tunnckoCore opened this issue Mar 4, 2018 · 1 comment · Fixed by #109

Comments

@tunnckoCore
Copy link
Owner

tunnckoCore commented Mar 4, 2018

  • asynchronous preset/config loading
    • through resolve-plugins and plugins-resolver
  • integrated with Sade

Working implementation, and illustrative command/task config definition

hela v3:

Latest update. Couple of fixes and rethinkings.

  • release as monorepo

@hela/core

import process from 'process';
import execa from 'execa-pro';
import sade from 'sade';

// the `dargs` from latest `gitcommit`
// externalize to `darks/darcks` - does opposite of `mri` parser
import dargs from './dargs'; // eslint-disable-line import/extensions, import/no-unresolved

const defaultOptions = {
  stdio: 'inherit',
  env: process.env,
};

/**
 * Executes improved version of [execa][] `.shell` method.
 *
 * @param {string|string[]} cmd
 * @param {object} [opts]
 * @public
 */
export function shell(cmd, opts) {
  const options = Object.assign({}, defaultOptions, opts);
  return execa.shell(cmd, options);
}

/**
 * Executes improved version of [execa][] `.exec` method.
 *
 * @param {string|string[]} cmd
 * @param {object} [opts]
 * @public
 */
export function exec(cmd, opts) {
  const options = Object.assign({}, defaultOptions, opts);
  return execa.exec(cmd, options);
}

/**
 *
 * @param {object} [options]
 * @public
 */
export function hela(options) {
  const prog = sade('hela').version('3.0.0');
  const opts = Object.assign(defaultOptions, options, { lazy: true });

  return Object.assign(prog, {
    /**
     * Define some function that will be called,
     * when no commands are given.
     * Allows bypassing the `No command specified.` error,
     * instead for example show the help output.
     * https://github.com/lukeed/sade/blob/987ffa974626e281de7ff0b9eaa63acadb2a134e/lib/index.js#L128-L130
     *
     * @param {Function} fn
     */
    commandless(fn) {
      const k = '__default__';
      const KEY = '__DEF__';
      prog.default = prog.curr = KEY; // eslint-disable-line no-multi-assign
      prog.tree[KEY] = Object.assign({}, prog.tree[k]);
      prog.tree[KEY].usage = '';
      prog.tree[KEY].handler = async () => fn();

      return prog;
    },

    /**
     * Action that will be done when command is called.
     *
     * @param {Function} fn
     * @public
     */
    action(fn) {
      const name = prog.curr || '__default__';
      const task = prog.tree[name];

      const stringActionWrapper = (cmd) => (...args) => {
        const argv = args[args.length - 1];
        const dargsOptions = Object.assign({ allowExtraFlags: true }, task);
        const flags = `${dargs(argv, dargsOptions).join(' ')}`;

        return shell(`${cmd} ${flags}`, opts);
      };

      if (typeof fn === 'function') {
        task.handler = async function fnc(...args) {
          const result = fn(...args);

          if (typeof result === 'string') {
            return stringActionWrapper(result)(...args);
          }

          // Specific & intentional case.
          // 1. Allows directly calling execa.shell
          // without passing the flags passed to hela task.
          // 2. Runs the commands in series.
          if (Array.isArray(result)) {
            return shell(result, opts);
          }

          return result;
        };
      }
      if (typeof fn === 'string') {
        task.handler = stringActionWrapper(fn);
      }
      if (Array.isArray(fn)) {
        fn.forEach((func) => {
          prog.action(func);
        });
      }

      // Friendlier error message.
      task.handler.command = () => {
        throw new Error('You cannot chain more after the `.action` call');
      };

      // Metadata about that specific task.
      task.handler.getMeta = () => task;
      return task.handler;
    },

    /**
     * Start the magic. Parse input commands and flags,
     * give them the corresponding command and its action function.
     *
     * @returns {Promise}
     * @public
     * @async
     */
    async listen() {
      const result = prog.parse(process.argv, opts);

      const { args, name, handler } = result;

      try {
        return handler(...args);
      } catch (err) {
        err.commandArgv = args[args.length - 1];
        err.commandArgs = args;
        err.commandName = name;
        throw err;
      }
    },
  });
}

@hela/cli

/* eslint-disable import/extensions, import/no-unresolved */
import { hela } from './index';

const cli = hela();

/**
 * Hela's CLI options and commands
 */

cli.commandless(() => cli.help());

cli.option('--show-stack', 'Show error stack trace when command fail', false);

/**
 * TODO: loading of tasks/presets/config files
 *
 * @returns {Promise}
 */
async function main() {
  const configModule = await import('./echo-preset');
  const preset = Object.assign({}, configModule);

  if (preset.default && typeof preset.default === 'function') {
    const meta = preset.default.getMeta();
    const taskName = meta.usage.split(' ')[0];
    preset[taskName] = preset.default;
    delete preset.default;
  }

  const tasks = Object.keys(preset).reduce((acc, name) => {
    acc[name] = preset[name].getMeta();
    return acc;
  }, {});

  cli.tree = Object.assign({}, cli.tree, tasks);

  return cli.listen();
}

main()
  .then(() => {
    // This is a CLI file, so please.
    // eslint-disable-next-line unicorn/no-process-exit
    process.exit(0);
  })
  .catch((err) => {
    console.error('Error task:', err.commandName);

    if (err.commandArgv && !err.commandArgv['show-stack']) {
      console.error('Error message:', err.name, err.message);
    } else {
      console.error('Error stack:', err.stack);
    }

    // This is a CLI file, so please.
    // eslint-disable-next-line unicorn/no-process-exit
    process.exit(1);
  });
@tunnckoCore
Copy link
Owner Author

tunnckoCore commented Jun 6, 2018

example shareable config of commands/tasks

// the v3 hela
import { hela, shell, exec } from 'hela';

// returns instance of `sade`
const cli = hela()

export const commit = cli
  .command('commit')
  .describe('All original `git commit` flags are available, plus 3 more - scope, body & footer')
  .option('--gpg-sign, -S', 'GPG-sign commits', true)
  .option('--signoff, -s', 'Add Signed-off-by line by the committer at the end', true)
  .option('--scope, -x', 'Prompt a question for commit scope', false)
  .option('--body, -y', 'Prompt a question for commit body', false)
  .option('--footer, -w', 'Prompt a question for commit footer', false)

  // allows AsyncFunction, Function, String and Array to be passed
  // if function, it also can return promise, string or array.
  .action('gitcommit');

  /**
   *
   */
export const test = cli
  .command('test')
  .describe('Run all tests from test directory')
  .option('--coverage, --cov', 'Run with coverage', true)
  .option('--check', 'Run checking coverage threshold', true)
  .option('--build, -b', 'Run with build step', true)
  .action((argv) => shell([ 'echo "start testing.."',  'node test' ]));

  /**
   *
   */
export const lint = cli
  .command('lint <src>')
  .describe('Lint and prettify the src and test files')
  .option('--fix', 'Auto fixing', true)
  .action((src, argv) => {
    console.log('lint task', src, argv)
    return 'echo "lint task is done"';
  })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

1 participant