-
Notifications
You must be signed in to change notification settings - Fork 1
/
index.js
executable file
·195 lines (182 loc) · 5.78 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
#! /usr/bin/env node
const {Command} = require('commander')
const {join, resolve} = require('path')
/**
* The possible exit codes runex will use to distinguish different causes.
* Exit code 1 is not used because it can also come from many other sources.
*/
const ExitCode = {
/** A required argument is missing or parsing options failed. */
MissingArgument: 2,
/** The module to run could not be resolved. */
ModuleNotFound: 4,
/** The module to run could be resolved, but it doesn't export `run`. */
InvalidModuleExport: 8,
/** Executing `runnable.run` threw an exception of the returned Promise was rejected. */
ExportThrows: 16
}
/**
* List of path to check for `require`-able modules. Relative path comes first.
*
* @param {string} moduleNameOrPath a relative path or node module name
* @returns {string[]} the absolute paths of module locations
*
* @see path.resolve
* @see require.resolve.paths
*/
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 {
* ExitCode.ModuleNotFound
* } (exits) when no item in `possiblePaths` points to a module
* @throws {
* ExitCode.InvalidModuleExport
* } (exits) when no required module in `possiblePaths` provides `run` export
*
* @see resolveRelativeAndRequirePaths
*/
const requireRunnable = (
possiblePaths, opts, _require = require
) => {
for (const hook of opts.require) {
_require(_require.resolve(hook, {paths: ['.']}));
}
const errors = []
let exitCode = ExitCode.ModuleNotFound
for (const candidate of possiblePaths) {
try {
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
}
return required
} catch (err) {
errors.push(err.message === `Cannot find module '${candidate}'` ? err.message : err)
}
}
console.error('No runnable module found:')
errors.forEach(err => console.error(err))
process.exit(exitCode)
}
/**
* 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()]
/**
* Returns a function that, when called (without any arguments)
* will print the usage/help and call `process.exit(code)`.
*
* @param {Command} commander the commander instance to call `outputHelp` on.
* @param {number} code the exit code to use
* @return {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, opts: Options}}
*
* @throws {ExitCode.MissingArgument} (exits) in case missing argument for module
*/
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(['', '', ...argv])
const opts = /** @type {Options} (type cast for tsc aka `as`) */ (commander.opts());
const [moduleNameOrPath, ...args] = commander.args
if (moduleNameOrPath === undefined) {
console.error('Missing argument: You need to specify the module to run.')
exitOnMissingArgument();
}
return {args, moduleNameOrPath, opts}
}
/**
* The proper way to execute any `run` method including async & error handling.
*
* The defaults of argument parsing takes care of slicing `process.argv`,
* if you pass a your own value, you have to take care of it.
*
* @param {RunnableModule} runnable the module to "execute"
* @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))
) => {
return new Promise(resolve => {
resolve(runnable.run(...args))
}).catch(err => {
console.error(err)
process.exit(ExitCode.ExportThrows)
})
}
if (module === /** @type {NodeModule | typeof module} */(require.main)) {
const p = parseArguments(process.argv.slice(2))
const runnable = requireRunnable(
resolveRelativeAndRequirePaths(p.moduleNameOrPath),
p.opts
)
run(runnable, p)
.then(value => {
if (value !== undefined) console.log(
typeof value === 'object' ? JSON.stringify(value) : value
)
})
} else {
module.exports = {
collectDistinct,
ExitCode,
exitWithUsage,
parseArguments,
requireRunnable,
resolveRelativeAndRequirePaths,
run
}
}
/**
* A Module that exports a method named `run`.
*
* @typedef {NodeModule & {run: Function}} RunnableModule
*/
/**
* Available CLI options for runex.
*
* Usage information: `npx runex -h|--help`
*
* @typedef {{
* require: string[]
* }} Options
*/