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

Feature: first option require #5

Merged
merged 4 commits into from
Dec 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,16 @@ including support for `async` functions/promises

## How to take advantage

You export a method with the name `run`. Your module is now "runnable": `npx runex script.js`.
As soon as your module exports a method with the name `run`, it is "runnable":

```
Usage: [npx] runex [options] runnable [args]

Options:
-r, --require <module> 0..n modules for node to require (default: [])
-h, --help output usage information
```

- it receives (just the relevant) arguments (as strings)
- it can be `async` / return a `Promise`
- it can throw (rejected Promises will be treated the same way)
Expand Down
10 changes: 10 additions & 0 deletions examples/sum.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Requires ts-node for being runnable which can be added by calling
*
* `npx runex -r ts-node/register examples/sum.ts` => 0
* `npx runex -r ts-node/register examples/sum.ts 1 2.5` => 3.5
*
*/
export const run = (...args: string[]) => {
return args.map(parseFloat).reduce((sum, cur) => sum + cur, 0);
};
121 changes: 91 additions & 30 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#! /usr/bin/env node
const {join, resolve} = require('path');
const {Command} = require('commander')
const {join, resolve} = require('path')

const ExitCode = {
MissingArgument: 2,
Expand All @@ -26,14 +27,17 @@ const ExitCode = {
const resolveRelativeAndRequirePaths = (moduleNameOrPath) => [
resolve(moduleNameOrPath),
...require.resolve.paths(moduleNameOrPath).map(dir => join(dir, moduleNameOrPath))
];
]

/**
* Attempts to require the items in `possiblePaths` in order
* and check for the presence of an exported `run` function.
* The first module found is returned.
*
* @param {string[]} possiblePaths
* @param {Options} opts the options from `parseArguments`
* @param {NodeRequire} [_require] the require to use for --register option,
* by default the regular `require` is used.
* @returns {RunnableModule}
*
* @throws {
Expand All @@ -45,43 +49,91 @@ const resolveRelativeAndRequirePaths = (moduleNameOrPath) => [
*
* @see resolveRelativeAndRequirePaths
*/
const requireRunnable = (possiblePaths) => {
const errors = [];
let exitCode = ExitCode.ModuleNotFound;
const requireRunnable = (
possiblePaths, opts, _require = require
) => {
for (const hook of opts.require) {
_require(hook)
}

const errors = []
let exitCode = ExitCode.ModuleNotFound
for (const candidate of possiblePaths) {
try {
const required = require(candidate);
const required = _require(candidate)
if (typeof required.run !== 'function') {
errors.push(`'${candidate}' is a module but has no export named 'run'`);
exitCode = ExitCode.InvalidModuleExport;
continue;
errors.push(`'${candidate}' is a module but has no export named 'run'`)
exitCode = ExitCode.InvalidModuleExport
continue
}
return required;
return required
} catch (err) {
errors.push(err.message);
errors.push(err.message)
}
}
console.error('No runnable module found:');
errors.forEach(err => console.error(err));
process.exit(exitCode);
};
console.error('No runnable module found:')
errors.forEach(err => console.error(err))
process.exit(exitCode)
}

/**
* Available CLI options for runex.
*
* Usage information: `npx runex -h|--help`
*
* @typedef {{
* require: string[]
* }} Options
*/

/**
* Collects all distinct values, order is not persisted
*
* @param {string} value
* @param {string[]} prev
* @returns {string[]}
*/
const collectDistinct = (value, prev) => [...new Set(prev).add(value).values()]

/**
*
* @param {Command} commander
* @param {number} code
* @returns {Function<never>}
*/
const exitWithUsage = (commander, code) => () => {
commander.outputHelp()
process.exit(code)
}

/**
* Parses a list of commend line arguments.
*
* If you are invoking it make sure to slice/remove anything that's not relevant for `runex`.
*
* @param {string[]} argv the relevant part of `process.argv`
* @returns {{args: string[], moduleNameOrPath: string}}
* @returns {{args: string[], moduleNameOrPath: string, opts: Options}}
*
* @throws {ExitCode.MissingArgument} (exits) in case missing argument for module
*/
const parseArguments = ([moduleNameOrPath, ...args]) => {
const parseArguments = (argv) => {
const commander = new Command('[npx] runex');
const exitOnMissingArgument = exitWithUsage(commander, ExitCode.MissingArgument)
commander.usage('[options] runnable [args]')
.option(
'-r, --require <module>', '0..n modules for node to require', collectDistinct, []
)
.exitOverride(exitOnMissingArgument)
/** @see https://github.com/tj/commander.js/issues/512 */
.parse([null, '', ...argv])
const opts = commander.opts();
const [moduleNameOrPath, ...args] = commander.args

if (moduleNameOrPath === undefined) {
console.error('Missing argument: You need to specify the module to run');
process.exit(ExitCode.MissingArgument);
console.error('Missing argument: You need to specify the module to run.')
exitOnMissingArgument();
}
return {moduleNameOrPath, args};
return {args, moduleNameOrPath, opts}
}

/**
Expand All @@ -91,31 +143,40 @@ const parseArguments = ([moduleNameOrPath, ...args]) => {
* if you pass a your own value, you have to take care of it.
*
* @param {RunnableModule} runnable the module to "execute"
* @param {{args: any[]}} [runArgs] the arguments to pass to `runnable.run`,
* @param {{args: any[], opts: Options}} [runArgs] the arguments to pass to `runnable.run`,
* by default they are parsed from `process.argv`
*
* @see parseArguments
*/
const run = (runnable, {args} = parseArguments(process.argv.slice(2))) => {
const run = (
runnable, {args} = parseArguments(process.argv.slice(2))
) => {
return new Promise(resolve => {
resolve(runnable.run(...args))
}).catch(err => {
console.error(err);
process.exit(ExitCode.ExportThrows);
});
console.error(err)
process.exit(ExitCode.ExportThrows)
})
}

if (require.main === module) {
const {moduleNameOrPath, args} = parseArguments(process.argv.slice(2));
run(requireRunnable(resolveRelativeAndRequirePaths(moduleNameOrPath)), {args})
const p = parseArguments(process.argv.slice(2))
const runnable = requireRunnable(
resolveRelativeAndRequirePaths(p.moduleNameOrPath),
p.opts
)
run(runnable, p)
.then(value => {
if (value) console.log(value);
});
if (value !== undefined) console.log(value)
})
} else {
module.exports = {
collectDistinct,
ExitCode,
exitWithUsage,
parseArguments,
resolveModule: requireRunnable,
requireRunnable,
resolveRelativeAndRequirePaths,
run
}
}
Loading