Skip to content

Commit 639904b

Browse files
committed
feat(cli): make configuration loader asynchronous
This bubbles up the call-chain of an asynchronous configuration loader in preparation for allowing dynamic imports of ESM module, which by design are handled asynchronously. No changes to the implementation logic have been made. The dynamic imports will return a promise and for us to be able to use the resolution, we need to bubble it up to the point where it actually is used. This (sadly) is the top-most level (bin/), therefore requiring the entrypoint to be asynchronous as well. I've avoided using the `(async () => {})()` shorthand, instead explicitly defining a main function, so that it is more clear on what's happening. Also, no top-level await as to not depend on ECMAScript 2022 support. https://nodejs.org/api/esm.html#top-level-await Overall it is advisable to refactor the entrypoint (bin/mocha.js) as to isolate the asynchronicity from the rest. main() is a little bit chunky right now... Implements: #5049
1 parent abf3dd9 commit 639904b

File tree

4 files changed

+113
-109
lines changed

4 files changed

+113
-109
lines changed

bin/mocha.js

Lines changed: 106 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -24,119 +24,123 @@ const mochaArgs = {};
2424
const nodeArgs = {};
2525
let hasInspect = false;
2626

27-
const opts = loadOptions(process.argv.slice(2));
28-
debug('loaded opts', opts);
29-
30-
/**
31-
* Given option/command `value`, disable timeouts if applicable
32-
* @param {string} [value] - Value to check
33-
* @ignore
34-
*/
35-
const disableTimeouts = value => {
36-
if (impliesNoTimeouts(value)) {
37-
debug('option %s disabled timeouts', value);
38-
mochaArgs.timeout = 0;
39-
}
40-
};
41-
42-
/**
43-
* If `value` begins with `v8-` and is not explicitly `v8-options`, remove prefix
44-
* @param {string} [value] - Value to check
45-
* @returns {string} `value` with prefix (maybe) removed
46-
* @ignore
47-
*/
48-
const trimV8Option = value =>
49-
value !== 'v8-options' && /^v8-/.test(value) ? value.slice(3) : value;
27+
async function main() {
28+
const opts = await loadOptions(process.argv.slice(2));
29+
debug('loaded opts', opts);
30+
31+
/**
32+
* Given option/command `value`, disable timeouts if applicable
33+
* @param {string} [value] - Value to check
34+
* @ignore
35+
*/
36+
const disableTimeouts = value => {
37+
if (impliesNoTimeouts(value)) {
38+
debug('option %s disabled timeouts', value);
39+
mochaArgs.timeout = 0;
40+
}
41+
};
42+
43+
/**
44+
* If `value` begins with `v8-` and is not explicitly `v8-options`, remove prefix
45+
* @param {string} [value] - Value to check
46+
* @returns {string} `value` with prefix (maybe) removed
47+
* @ignore
48+
*/
49+
const trimV8Option = value =>
50+
value !== 'v8-options' && /^v8-/.test(value) ? value.slice(3) : value;
51+
52+
// sort options into "node" and "mocha" buckets
53+
Object.keys(opts).forEach(opt => {
54+
if (isNodeFlag(opt)) {
55+
nodeArgs[trimV8Option(opt)] = opts[opt];
56+
} else {
57+
mochaArgs[opt] = opts[opt];
58+
}
59+
});
5060

51-
// sort options into "node" and "mocha" buckets
52-
Object.keys(opts).forEach(opt => {
53-
if (isNodeFlag(opt)) {
54-
nodeArgs[trimV8Option(opt)] = opts[opt];
55-
} else {
56-
mochaArgs[opt] = opts[opt];
57-
}
58-
});
59-
60-
// disable 'timeout' for debugFlags
61-
Object.keys(nodeArgs).forEach(opt => disableTimeouts(opt));
62-
mochaArgs['node-option'] &&
63-
mochaArgs['node-option'].forEach(opt => disableTimeouts(opt));
64-
65-
// Native debugger handling
66-
// see https://nodejs.org/api/debugger.html#debugger_debugger
67-
// look for 'inspect' that would launch this debugger,
68-
// remove it from Mocha's opts and prepend it to Node's opts.
69-
// A deprecation warning will be printed by node, if applicable.
70-
// (mochaArgs._ are "positional" arguments, not prefixed with - or --)
71-
if (mochaArgs._) {
72-
const i = mochaArgs._.findIndex(val => val === 'inspect');
73-
if (i > -1) {
74-
mochaArgs._.splice(i, 1);
75-
disableTimeouts('inspect');
76-
hasInspect = true;
61+
// disable 'timeout' for debugFlags
62+
Object.keys(nodeArgs).forEach(opt => disableTimeouts(opt));
63+
mochaArgs['node-option'] &&
64+
mochaArgs['node-option'].forEach(opt => disableTimeouts(opt));
65+
66+
// Native debugger handling
67+
// see https://nodejs.org/api/debugger.html#debugger_debugger
68+
// look for 'inspect' that would launch this debugger,
69+
// remove it from Mocha's opts and prepend it to Node's opts.
70+
// A deprecation warning will be printed by node, if applicable.
71+
// (mochaArgs._ are "positional" arguments, not prefixed with - or --)
72+
if (mochaArgs._) {
73+
const i = mochaArgs._.findIndex(val => val === 'inspect');
74+
if (i > -1) {
75+
mochaArgs._.splice(i, 1);
76+
disableTimeouts('inspect');
77+
hasInspect = true;
78+
}
7779
}
78-
}
7980

80-
if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) {
81-
const {spawn} = require('node:child_process');
82-
const mochaPath = require.resolve('../lib/cli/cli.js');
81+
if (mochaArgs['node-option'] || Object.keys(nodeArgs).length || hasInspect) {
82+
const {spawn} = require('node:child_process');
83+
const mochaPath = require.resolve('../lib/cli/cli.js');
8384

84-
const nodeArgv =
85-
(mochaArgs['node-option'] && mochaArgs['node-option'].map(v => '--' + v)) ||
86-
unparseNodeFlags(nodeArgs);
85+
const nodeArgv =
86+
(mochaArgs['node-option'] && mochaArgs['node-option'].map(v => '--' + v)) ||
87+
unparseNodeFlags(nodeArgs);
8788

88-
if (hasInspect) nodeArgv.unshift('inspect');
89-
delete mochaArgs['node-option'];
89+
if (hasInspect) nodeArgv.unshift('inspect');
90+
delete mochaArgs['node-option'];
9091

91-
debug('final node argv', nodeArgv);
92+
debug('final node argv', nodeArgv);
9293

93-
const args = [].concat(
94-
nodeArgv,
95-
mochaPath,
96-
unparse(mochaArgs, {alias: aliases})
97-
);
94+
const args = [].concat(
95+
nodeArgv,
96+
mochaPath,
97+
unparse(mochaArgs, {alias: aliases})
98+
);
9899

99-
debug(
100-
'forking child process via command: %s %s',
101-
process.execPath,
102-
args.join(' ')
103-
);
100+
debug(
101+
'forking child process via command: %s %s',
102+
process.execPath,
103+
args.join(' ')
104+
);
104105

105-
const proc = spawn(process.execPath, args, {
106-
stdio: 'inherit'
107-
});
106+
const proc = spawn(process.execPath, args, {
107+
stdio: 'inherit'
108+
});
108109

109-
proc.on('exit', (code, signal) => {
110-
process.on('exit', () => {
111-
if (signal) {
112-
process.kill(process.pid, signal);
113-
} else {
114-
process.exit(code);
115-
}
110+
proc.on('exit', (code, signal) => {
111+
process.on('exit', () => {
112+
if (signal) {
113+
process.kill(process.pid, signal);
114+
} else {
115+
process.exit(code);
116+
}
117+
});
116118
});
117-
});
118119

119-
// terminate children.
120-
process.on('SIGINT', () => {
121-
// XXX: a previous comment said this would abort the runner, but I can't see that it does
122-
// anything with the default runner.
123-
debug('main process caught SIGINT');
124-
proc.kill('SIGINT');
125-
// if running in parallel mode, we will have a proper SIGINT handler, so the below won't
126-
// be needed.
127-
if (!args.parallel || args.jobs < 2) {
128-
// win32 does not support SIGTERM, so use next best thing.
129-
if (require('node:os').platform() === 'win32') {
130-
proc.kill('SIGKILL');
131-
} else {
132-
// using SIGKILL won't cleanly close the output streams, which can result
133-
// in cut-off text or a befouled terminal.
134-
debug('sending SIGTERM to child process');
135-
proc.kill('SIGTERM');
120+
// terminate children.
121+
process.on('SIGINT', () => {
122+
// XXX: a previous comment said this would abort the runner, but I can't see that it does
123+
// anything with the default runner.
124+
debug('main process caught SIGINT');
125+
proc.kill('SIGINT');
126+
// if running in parallel mode, we will have a proper SIGINT handler, so the below won't
127+
// be needed.
128+
if (!args.parallel || args.jobs < 2) {
129+
// win32 does not support SIGTERM, so use next best thing.
130+
if (require('node:os').platform() === 'win32') {
131+
proc.kill('SIGKILL');
132+
} else {
133+
// using SIGKILL won't cleanly close the output streams, which can result
134+
// in cut-off text or a befouled terminal.
135+
debug('sending SIGTERM to child process');
136+
proc.kill('SIGTERM');
137+
}
136138
}
137-
}
138-
});
139-
} else {
140-
debug('running Mocha in-process');
141-
require('../lib/cli/cli').main([], mochaArgs);
139+
});
140+
} else {
141+
debug('running Mocha in-process');
142+
await require('../lib/cli/cli').main([], mochaArgs);
143+
}
142144
}
145+
146+
main()

lib/cli/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ const {cwd} = require('../utils');
3333
* @param {string[]} argv - Array of arguments to parse, or by default the lovely `process.argv.slice(2)`
3434
* @param {object} [mochaArgs] - Object of already parsed Mocha arguments (by bin/mocha)
3535
*/
36-
exports.main = (argv = process.argv.slice(2), mochaArgs) => {
36+
exports.main = async (argv = process.argv.slice(2), mochaArgs) => {
3737
debug('entered main with raw args', argv);
3838
// ensure we can require() from current working directory
3939
if (typeof module.paths !== 'undefined') {
@@ -46,7 +46,7 @@ exports.main = (argv = process.argv.slice(2), mochaArgs) => {
4646
debug('unable to set Error.stackTraceLimit = Infinity', err);
4747
}
4848

49-
var args = mochaArgs || loadOptions(argv);
49+
var args = mochaArgs || await loadOptions(argv);
5050

5151
yargs()
5252
.scriptName('mocha')

lib/cli/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ const parsers = (exports.parsers = {
6363
* @param {string} filepath - Config file path to load
6464
* @returns {Object} Parsed config object
6565
*/
66-
exports.loadConfig = filepath => {
66+
exports.loadConfig = async filepath => {
6767
let config = {};
6868
debug('loadConfig: trying to parse config at %s', filepath);
6969

lib/cli/options.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -209,10 +209,10 @@ const parse = (args = [], defaultValues = {}, ...configObjects) => {
209209
* @alias module:lib/cli.loadRc
210210
* @returns {external:yargsParser.Arguments|void} Parsed config, or nothing if `args.config` is `false`
211211
*/
212-
const loadRc = (args = {}) => {
212+
const loadRc = async (args = {}) => {
213213
if (args.config !== false) {
214214
const config = args.config || findConfig();
215-
return config ? loadConfig(config) : {};
215+
return config ? await loadConfig(config) : {};
216216
}
217217
};
218218

@@ -287,7 +287,7 @@ module.exports.loadPkgRc = loadPkgRc;
287287
* @alias module:lib/cli.loadOptions
288288
* @returns {external:yargsParser.Arguments} Parsed args from everything
289289
*/
290-
const loadOptions = (argv = []) => {
290+
const loadOptions = async (argv = []) => {
291291
let args = parse(argv);
292292
// short-circuit: look for a flag that would abort loading of options
293293
if (
@@ -300,7 +300,7 @@ const loadOptions = (argv = []) => {
300300
}
301301

302302
const envConfig = parse(process.env.MOCHA_OPTIONS || '');
303-
const rcConfig = loadRc(args);
303+
const rcConfig = await loadRc(args);
304304
const pkgConfig = loadPkgRc(args);
305305

306306
if (rcConfig) {

0 commit comments

Comments
 (0)