Skip to content

Commit d24bcd3

Browse files
committed
feature: adding first option require
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
1 parent 6db0454 commit d24bcd3

File tree

5 files changed

+777
-616
lines changed

5 files changed

+777
-616
lines changed

README.md

+10-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,16 @@ including support for `async` functions/promises
3131

3232
## How to take advantage
3333

34-
You export a method with the name `run`. Your module is now "runnable": `npx runex script.js`.
34+
As soon as your module exports a method with the name `run`, it is "runnable":
35+
36+
```
37+
Usage: [npx] runex [options] runnable [args]
38+
39+
Options:
40+
-r, --require <module> 0..n modules for node to require (default: [])
41+
-h, --help output usage information
42+
```
43+
3544
- it receives (just the relevant) arguments (as strings)
3645
- it can be `async` / return a `Promise`
3746
- it can throw (rejected Promises will be treated the same way)

index.js

+91-30
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#! /usr/bin/env node
2-
const {join, resolve} = require('path');
2+
const {Command} = require('commander')
3+
const {join, resolve} = require('path')
34

45
const ExitCode = {
56
MissingArgument: 2,
@@ -26,14 +27,17 @@ const ExitCode = {
2627
const resolveRelativeAndRequirePaths = (moduleNameOrPath) => [
2728
resolve(moduleNameOrPath),
2829
...require.resolve.paths(moduleNameOrPath).map(dir => join(dir, moduleNameOrPath))
29-
];
30+
]
3031

3132
/**
3233
* Attempts to require the items in `possiblePaths` in order
3334
* and check for the presence of an exported `run` function.
3435
* The first module found is returned.
3536
*
3637
* @param {string[]} possiblePaths
38+
* @param {Options} opts the options from `parseArguments`
39+
* @param {NodeRequire} [_require] the require to use for --register option,
40+
* by default the regular `require` is used.
3741
* @returns {RunnableModule}
3842
*
3943
* @throws {
@@ -45,43 +49,91 @@ const resolveRelativeAndRequirePaths = (moduleNameOrPath) => [
4549
*
4650
* @see resolveRelativeAndRequirePaths
4751
*/
48-
const requireRunnable = (possiblePaths) => {
49-
const errors = [];
50-
let exitCode = ExitCode.ModuleNotFound;
52+
const requireRunnable = (
53+
possiblePaths, opts, _require = require
54+
) => {
55+
for (const hook of opts.require) {
56+
_require(hook)
57+
}
58+
59+
const errors = []
60+
let exitCode = ExitCode.ModuleNotFound
5161
for (const candidate of possiblePaths) {
5262
try {
53-
const required = require(candidate);
63+
const required = _require(candidate)
5464
if (typeof required.run !== 'function') {
55-
errors.push(`'${candidate}' is a module but has no export named 'run'`);
56-
exitCode = ExitCode.InvalidModuleExport;
57-
continue;
65+
errors.push(`'${candidate}' is a module but has no export named 'run'`)
66+
exitCode = ExitCode.InvalidModuleExport
67+
continue
5868
}
59-
return required;
69+
return required
6070
} catch (err) {
61-
errors.push(err.message);
71+
errors.push(err.message)
6272
}
6373
}
64-
console.error('No runnable module found:');
65-
errors.forEach(err => console.error(err));
66-
process.exit(exitCode);
67-
};
74+
console.error('No runnable module found:')
75+
errors.forEach(err => console.error(err))
76+
process.exit(exitCode)
77+
}
78+
79+
/**
80+
* Available CLI options for runex.
81+
*
82+
* Usage information: `npx runex -h|--help`
83+
*
84+
* @typedef {{
85+
* require: string[]
86+
* }} Options
87+
*/
88+
89+
/**
90+
* Collects all distinct values, order is not persisted
91+
*
92+
* @param {string} value
93+
* @param {string[]} prev
94+
* @returns {string[]}
95+
*/
96+
const collectDistinct = (value, prev) => [...new Set(prev).add(value).values()]
97+
98+
/**
99+
*
100+
* @param {Command} commander
101+
* @param {number} code
102+
* @returns {Function<never>}
103+
*/
104+
const exitWithUsage = (commander, code) => () => {
105+
commander.outputHelp()
106+
process.exit(code)
107+
}
68108

69109
/**
70110
* Parses a list of commend line arguments.
71111
*
72112
* If you are invoking it make sure to slice/remove anything that's not relevant for `runex`.
73113
*
74114
* @param {string[]} argv the relevant part of `process.argv`
75-
* @returns {{args: string[], moduleNameOrPath: string}}
115+
* @returns {{args: string[], moduleNameOrPath: string, opts: Options}}
76116
*
77117
* @throws {ExitCode.MissingArgument} (exits) in case missing argument for module
78118
*/
79-
const parseArguments = ([moduleNameOrPath, ...args]) => {
119+
const parseArguments = (argv) => {
120+
const commander = new Command('[npx] runex');
121+
const exitOnMissingArgument = exitWithUsage(commander, ExitCode.MissingArgument)
122+
commander.usage('[options] runnable [args]')
123+
.option(
124+
'-r, --require <module>', '0..n modules for node to require', collectDistinct, []
125+
)
126+
.exitOverride(exitOnMissingArgument)
127+
/** @see https://github.com/tj/commander.js/issues/512 */
128+
.parse([null, '', ...argv])
129+
const opts = commander.opts();
130+
const [moduleNameOrPath, ...args] = commander.args
131+
80132
if (moduleNameOrPath === undefined) {
81-
console.error('Missing argument: You need to specify the module to run');
82-
process.exit(ExitCode.MissingArgument);
133+
console.error('Missing argument: You need to specify the module to run.')
134+
exitOnMissingArgument();
83135
}
84-
return {moduleNameOrPath, args};
136+
return {args, moduleNameOrPath, opts}
85137
}
86138

87139
/**
@@ -91,31 +143,40 @@ const parseArguments = ([moduleNameOrPath, ...args]) => {
91143
* if you pass a your own value, you have to take care of it.
92144
*
93145
* @param {RunnableModule} runnable the module to "execute"
94-
* @param {{args: any[]}} [runArgs] the arguments to pass to `runnable.run`,
146+
* @param {{args: any[], opts: Options}} [runArgs] the arguments to pass to `runnable.run`,
95147
* by default they are parsed from `process.argv`
96148
*
97149
* @see parseArguments
98150
*/
99-
const run = (runnable, {args} = parseArguments(process.argv.slice(2))) => {
151+
const run = (
152+
runnable, {args} = parseArguments(process.argv.slice(2))
153+
) => {
100154
return new Promise(resolve => {
101155
resolve(runnable.run(...args))
102156
}).catch(err => {
103-
console.error(err);
104-
process.exit(ExitCode.ExportThrows);
105-
});
157+
console.error(err)
158+
process.exit(ExitCode.ExportThrows)
159+
})
106160
}
107161

108162
if (require.main === module) {
109-
const {moduleNameOrPath, args} = parseArguments(process.argv.slice(2));
110-
run(requireRunnable(resolveRelativeAndRequirePaths(moduleNameOrPath)), {args})
163+
const p = parseArguments(process.argv.slice(2))
164+
const runnable = requireRunnable(
165+
resolveRelativeAndRequirePaths(p.moduleNameOrPath),
166+
p.opts
167+
)
168+
run(runnable, p)
111169
.then(value => {
112-
if (value) console.log(value);
113-
});
170+
if (value !== undefined) console.log(value)
171+
})
114172
} else {
115173
module.exports = {
174+
collectDistinct,
116175
ExitCode,
176+
exitWithUsage,
117177
parseArguments,
118-
resolveModule: requireRunnable,
178+
requireRunnable,
179+
resolveRelativeAndRequirePaths,
119180
run
120181
}
121182
}

0 commit comments

Comments
 (0)