diff --git a/packages/jsdoc-cli/lib/engine.js b/packages/jsdoc-cli/lib/engine.js index c3748461..90341237 100644 --- a/packages/jsdoc-cli/lib/engine.js +++ b/packages/jsdoc-cli/lib/engine.js @@ -14,6 +14,7 @@ limitations under the License. */ +/* eslint-disable no-process-exit */ import EventEmitter from 'node:events'; import { getLogFunctions } from '@jsdoc/util'; @@ -136,6 +137,9 @@ export default class Engine { emitter: this.emitter, level: opts.logLevel, }); + // TODO: Make these private when `cli.js` no longer needs them. + this.shouldExitWithError = false; + this.shouldPrintHelp = false; // Support the format used by `Env`. // TODO: Make the formats consistent. if (_.isObject(opts.version)) { @@ -147,6 +151,28 @@ export default class Engine { } } + exit(exitCode, message) { + ow(exitCode, ow.number); + ow(message, ow.optional.string); + + if (exitCode > 0) { + this.shouldExitWithError = true; + + process.on('exit', () => { + if (message) { + console.error(message); + } + }); + } + + process.on('exit', () => { + if (this.shouldPrintHelp) { + this.printHelp(); + } + process.exit(exitCode); + }); + } + /** * Get help text for JSDoc. * @@ -241,6 +267,19 @@ export default class Engine { return this.flags; } + printHelp() { + this.printVersion(); + console.log(this.help({ maxLength: process.stdout.columns })); + + return Promise.resolve(0); + } + + printVersion() { + console.log(this.versionDetails); + + return Promise.resolve(0); + } + /** * A string that describes the current JSDoc version. */ diff --git a/packages/jsdoc-cli/test/specs/lib/engine.js b/packages/jsdoc-cli/test/specs/lib/engine.js index d58b249e..9886b62f 100644 --- a/packages/jsdoc-cli/test/specs/lib/engine.js +++ b/packages/jsdoc-cli/test/specs/lib/engine.js @@ -45,6 +45,10 @@ describe('@jsdoc/cli/lib/engine', () => { expect(new Engine().logLevel).toBe(LEVELS.WARN); }); + it('has a shouldExitWithError property that defaults to false', () => { + expect(new Engine().shouldExitWithError).toBeFalse(); + }); + it('has an undefined revision property by default', () => { expect(new Engine().revision).toBeUndefined(); }); @@ -128,6 +132,26 @@ describe('@jsdoc/cli/lib/engine', () => { }); }); + describe('exit', () => { + // TODO: This is testing implementation details, not behavior. Refactor the method, then rewrite + // this test to test the behavior. + it('adds one listener for `exit` events if the exit code is 0', () => { + spyOn(process, 'on'); + + new Engine().exit(0); + + expect(process.on).toHaveBeenCalledTimes(1); + }); + + it('adds two listeners for `exit` events if the exit code is >0', () => { + spyOn(process, 'on'); + + new Engine().exit(1); + + expect(process.on).toHaveBeenCalledTimes(2); + }); + }); + describe('parseFlags', () => { it('throws with no input', () => { expect(() => new Engine().parseFlags()).toThrow(); @@ -205,6 +229,59 @@ describe('@jsdoc/cli/lib/engine', () => { }); }); + describe('printHelp', () => { + beforeEach(() => { + spyOn(console, 'log'); + }); + + it('returns a promise that resolves to 0', async () => { + const instance = new Engine({ version: '1.2.3' }); + const returnValue = await instance.printHelp(); + + expect(returnValue).toBe(0); + }); + + it('prints the version number, then the help text', () => { + const instance = new Engine({ version: '1.2.3' }); + + instance.printHelp(); + + expect(console.log.calls.argsFor(0)[0]).toContain('JSDoc 1.2.3'); + expect(console.log.calls.argsFor(1)[0]).not.toContain('JSDoc 1.2.3'); + expect(console.log.calls.argsFor(1)[0]).toContain('-v, --version'); + }); + }); + + describe('printVersion', () => { + beforeEach(() => { + spyOn(console, 'log'); + }); + + it('returns a promise that resolves to 0', async () => { + const instance = new Engine({ version: '1.2.3' }); + const returnValue = await instance.printVersion(); + + expect(returnValue).toBe(0); + }); + + it('prints the version number', () => { + const instance = new Engine({ version: '1.2.3' }); + + instance.printVersion(); + + expect(console.log).toHaveBeenCalledOnceWith('JSDoc 1.2.3'); + }); + + it('prints the revision if present', () => { + const date = new Date(1700000000000); + const instance = new Engine({ version: '1.2.3', revision: date }); + + instance.printVersion(); + + expect(console.log).toHaveBeenCalledOnceWith('JSDoc 1.2.3 (Tue, 14 Nov 2023 22:13:20 GMT)'); + }); + }); + describe('versionDetails', () => { it('works with a version but no revision', () => { const instance = new Engine({ version: '1.2.3' }); diff --git a/packages/jsdoc/cli.js b/packages/jsdoc/cli.js index 10a29576..5d835d04 100644 --- a/packages/jsdoc/cli.js +++ b/packages/jsdoc/cli.js @@ -78,8 +78,8 @@ export default (() => { try { env.opts = engine.parseFlags(env.args); } catch (e) { - props.shouldPrintHelp = true; - cli.exit(1, `${e.message}\n`); + engine.shouldPrintHelp = true; + engine.exit(1, `${e.message}\n`); return cli; } @@ -88,7 +88,7 @@ export default (() => { // eslint-disable-next-line require-atomic-updates env.conf = (await jsdocConfig.load(env.opts.configure)).config; } catch (e) { - cli.exit(1, `Cannot parse the config file: ${e}\n${FATAL_ERROR_MESSAGE}`); + engine.exit(1, `Cannot parse the config file: ${e}\n${FATAL_ERROR_MESSAGE}`); return cli; } @@ -105,11 +105,11 @@ export default (() => { const { options } = env; function recoverableError() { - props.shouldExitWithError = true; + engine.shouldExitWithError = true; } function fatalError() { - cli.exit(1); + engine.exit(1); } if (options.test) { @@ -163,36 +163,28 @@ export default (() => { const { options } = env; // If we already need to exit with an error, don't do any more work. - if (props.shouldExitWithError) { + if (engine.shouldExitWithError) { cmd = () => Promise.resolve(0); } else if (options.help) { - cmd = cli.printHelp; + cmd = () => engine.printHelp(); } else if (options.test) { cmd = cli.runTests; } else if (options.version) { - cmd = cli.printVersion; + cmd = () => engine.printVersion(); } else { cmd = cli.main; } return cmd().then((errorCode) => { - if (!errorCode && props.shouldExitWithError) { + if (!errorCode && engine.shouldExitWithError) { errorCode = 1; } cli.logFinish(); - cli.exit(errorCode || 0); + engine.exit(errorCode || 0); }); }; - // TODO: docs - cli.printHelp = () => { - cli.printVersion(); - console.log(engine.help({ maxLength: process.stdout.columns })); - - return Promise.resolve(0); - }; - // TODO: docs cli.runTests = async () => { const result = await test(env); @@ -200,13 +192,6 @@ export default (() => { return result.overallStatus === 'failed' ? 1 : 0; }; - // TODO: docs - cli.printVersion = () => { - console.log(engine.versionDetails); - - return Promise.resolve(0); - }; - // TODO: docs cli.main = async () => { cli.scanFiles(); @@ -365,24 +350,5 @@ export default (() => { } }; - // TODO: docs - cli.exit = (exitCode, message) => { - if (exitCode > 0) { - props.shouldExitWithError = true; - - if (message) { - console.error(message); - } - } - - process.on('exit', () => { - if (props.shouldPrintHelp) { - cli.printHelp(); - } - - process.exit(exitCode); - }); - }; - return cli; })();