diff --git a/packages/jsdoc-cli/lib/engine.js b/packages/jsdoc-cli/lib/engine.js index 90341237..c375c639 100644 --- a/packages/jsdoc-cli/lib/engine.js +++ b/packages/jsdoc-cli/lib/engine.js @@ -15,8 +15,7 @@ */ /* eslint-disable no-process-exit */ -import EventEmitter from 'node:events'; - +import { Api } from '@jsdoc/core'; import { getLogFunctions } from '@jsdoc/util'; import _ from 'lodash'; import ow from 'ow'; @@ -130,7 +129,8 @@ export default class Engine { ow(opts.revision, ow.optional.date); ow(opts.version, ow.any(ow.optional.string, ow.optional.object)); - this.emitter = opts.emitter ?? new EventEmitter(); + this.api = opts.api ?? new Api({ emitter: opts.emitter }); + this.emitter = this.api.emitter; this.flags = []; this.log = opts.log ?? getLogFunctions(this.emitter); this.#logger = new Logger({ diff --git a/packages/jsdoc-cli/test/specs/lib/engine.js b/packages/jsdoc-cli/test/specs/lib/engine.js index 9886b62f..303cc65e 100644 --- a/packages/jsdoc-cli/test/specs/lib/engine.js +++ b/packages/jsdoc-cli/test/specs/lib/engine.js @@ -14,6 +14,8 @@ limitations under the License. */ +import EventEmitter from 'node:events'; + import Engine from '../../../lib/engine.js'; import flags from '../../../lib/flags.js'; import { LEVELS } from '../../../lib/logger.js'; @@ -33,6 +35,14 @@ describe('@jsdoc/cli/lib/engine', () => { expect(Engine.LOG_LEVELS).toBeObject(); }); + it('has an `api` property that contains the API instance', () => { + expect(new Engine().api).toBeObject(); + }); + + it('has an `emitter` property that contains a shared event emitter', () => { + expect(new Engine().emitter).toBeObject(); + }); + it('has an empty array of flags by default', () => { expect(new Engine().flags).toBeEmptyArray(); }); @@ -97,6 +107,49 @@ describe('@jsdoc/cli/lib/engine', () => { expect(() => new Engine({ version: 1 })).toThrow(); }); + describe('emitter', () => { + it('creates an `EventEmitter` instance by default', () => { + expect(new Engine().emitter).toBeInstanceOf(EventEmitter); + }); + + it('lets you provide the emitter', () => { + const fakeEmitter = { + off: () => null, + on: () => null, + once: () => null, + }; + const instance = new Engine({ emitter: fakeEmitter }); + + expect(instance.emitter).toBe(fakeEmitter); + }); + + it('shares one emitter between the `Api` instance and the `Engine` instance', () => { + const instance = new Engine(); + + expect(instance.emitter).toBe(instance.api.emitter); + }); + }); + + 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('help', () => { const instance = new Engine(); @@ -132,26 +185,6 @@ 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(); diff --git a/packages/jsdoc-core/index.js b/packages/jsdoc-core/index.js index 8d26c959..cc330ee7 100644 --- a/packages/jsdoc-core/index.js +++ b/packages/jsdoc-core/index.js @@ -19,10 +19,11 @@ * * @module @jsdoc/core */ +import Api from './lib/api.js'; import * as config from './lib/config.js'; import Env from './lib/env.js'; import * as name from './lib/name.js'; import * as plugins from './lib/plugins.js'; -export { config, Env, name, plugins }; -export default { config, Env, name, plugins }; +export { Api, config, Env, name, plugins }; +export default { Api, config, Env, name, plugins }; diff --git a/packages/jsdoc-core/lib/api.js b/packages/jsdoc-core/lib/api.js new file mode 100644 index 00000000..e5441674 --- /dev/null +++ b/packages/jsdoc-core/lib/api.js @@ -0,0 +1,33 @@ +/* + Copyright 2023 the JSDoc Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import EventEmitter from 'node:events'; + +/** + * The API for programmatically generating documentation with JSDoc. + */ +export default class Api { + /** + * Creates an instance of the API. + * + * @param {?Object} opts - Options for the API instance. + * @param {?node:events.EventEmitter} opts.eventEmitter - The event emitter to use as a message + * bus for JSDoc. + */ + constructor(opts) { + this.emitter = opts?.emitter ?? new EventEmitter(); + } +} diff --git a/packages/jsdoc-core/test/specs/index.js b/packages/jsdoc-core/test/specs/index.js index 1b7a5d4f..ee428794 100644 --- a/packages/jsdoc-core/test/specs/index.js +++ b/packages/jsdoc-core/test/specs/index.js @@ -15,12 +15,19 @@ */ import core from '../../index.js'; +import Api from '../../lib/api.js'; import * as config from '../../lib/config.js'; import Env from '../../lib/env.js'; import * as name from '../../lib/name.js'; import * as plugins from '../../lib/plugins.js'; describe('@jsdoc/core', () => { + describe('Api', () => { + it('is lib/api', () => { + expect(core.Api).toEqual(Api); + }); + }); + describe('config', () => { it('is lib/config', () => { expect(core.config).toEqual(config); diff --git a/packages/jsdoc-core/test/specs/lib/api.js b/packages/jsdoc-core/test/specs/lib/api.js new file mode 100644 index 00000000..6484bfb0 --- /dev/null +++ b/packages/jsdoc-core/test/specs/lib/api.js @@ -0,0 +1,53 @@ +/* + Copyright 2023 the JSDoc Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +import EventEmitter from 'node:events'; + +import Api from '../../../lib/api.js'; + +describe('Api', () => { + let instance; + + beforeEach(() => { + instance = new Api(); + }); + + it('is a constructor', () => { + function factory() { + return new Api(); + } + + expect(factory).not.toThrow(); + }); + + it('has an `emitter` property', () => { + expect(instance.emitter).toBeObject(); + }); + + describe('emitter', () => { + it('is an instance of `EventEmitter` by default', () => { + expect(instance.emitter).toBeInstanceOf(EventEmitter); + }); + + it('lets you provide your own emitter', () => { + const fakeEmitter = {}; + + instance = new Api({ emitter: fakeEmitter }); + + expect(instance.emitter).toBe(fakeEmitter); + }); + }); +});