Skip to content

Commit

Permalink
feature: adding first option require
Browse files Browse the repository at this point in the history
Allow requiring any module, e.g. to be able to require `.tsx?` files with ts-node.

+ `dependencies`:
- `commander` provides nice way of printing usage information from the configuration,
  and is actively maintained, well documented and has no dependencies

+ `devDependencies`
- `testdouble` good enough for what I need, much less dependencies then sinon
- `ts-node` (was already present via `tap`, but now it's explicit
  • Loading branch information
karfau committed Dec 25, 2019
1 parent 6db0454 commit d24bcd3
Show file tree
Hide file tree
Showing 5 changed files with 777 additions and 616 deletions.
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
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
}
}

0 comments on commit d24bcd3

Please sign in to comment.