diff --git a/packages/jsdoc-cli/lib/engine.js b/packages/jsdoc-cli/lib/engine.js index c375c639..bdfb2127 100644 --- a/packages/jsdoc-cli/lib/engine.js +++ b/packages/jsdoc-cli/lib/engine.js @@ -131,6 +131,7 @@ export default class Engine { this.api = opts.api ?? new Api({ emitter: opts.emitter }); this.emitter = this.api.emitter; + this.env = this.api.env; this.flags = []; this.log = opts.log ?? getLogFunctions(this.emitter); this.#logger = new Logger({ diff --git a/packages/jsdoc-core/lib/api.js b/packages/jsdoc-core/lib/api.js index e5441674..0939aa28 100644 --- a/packages/jsdoc-core/lib/api.js +++ b/packages/jsdoc-core/lib/api.js @@ -15,19 +15,86 @@ */ import EventEmitter from 'node:events'; +import path from 'node:path'; + +import { Env } from '@jsdoc/core'; +import glob from 'fast-glob'; /** * The API for programmatically generating documentation with JSDoc. + * + * @alias module:@jsdoc/core.Api */ export default class Api { /** * Creates an instance of the API. * * @param {?Object} opts - Options for the API instance. + * @param {?module:@jsdoc/core.Env} opts.env - The JSDoc environment and configuration settings to + * use. * @param {?node:events.EventEmitter} opts.eventEmitter - The event emitter to use as a message * bus for JSDoc. */ constructor(opts) { + /** + * An event emitter that acts as a message bus for all JSDoc modules. + */ this.emitter = opts?.emitter ?? new EventEmitter(); + /** + * The JSDoc environment, including configuration settings, for this API instance. + */ + this.env = opts?.env ?? new Env(); + } + + #buildSourceFileList() { + const { config, options } = this.env; + let sourceFiles = options._?.slice() ?? []; + + if (config.sourceFiles) { + sourceFiles = sourceFiles.concat(config.sourceFiles); + } + + return sourceFiles; + } + + /** + * Finds the absolute paths to source files that JSDoc should parse, based on an array of glob + * patterns, and adds the list of paths to the JSDoc environment. + * + * This method also resolves the paths in `this.env.options.package` and `this.env.options.readme` + * if they are present. + * + * There are a few ways to specify the array of glob patterns: + * + * + **Pass the array to this method.** Values in `this.env.options` are ignored. + * + **Assign the array to `this.env.options._` or `this.env.config.sourceFiles`,** then call this + * method. + * + * @instance + * @memberof module:@jsdoc/core.Api + * @param {Array} globPatterns - Glob patterns that match the source files to parse. You + * can use any glob syntax allowed by the + * [`fast-glob` package](https://www.npmjs.com/package/fast-glob). + * @returns {Array} The absolute paths to the source files. + */ + async findSourceFiles(globPatterns) { + const { options } = this.env; + + options._ = globPatterns?.slice() ?? this.#buildSourceFileList(); + if (options._.length) { + this.env.sourceFiles = await glob(options._, { + absolute: true, + onlyFiles: true, + }); + } + + if (options.package) { + options.package = path.resolve(options.package); + } + if (options.readme) { + options.readme = path.resolve(options.readme); + } + + return this.env.sourceFiles?.slice() ?? []; } } diff --git a/packages/jsdoc-core/test/fixtures/source-files/README.md b/packages/jsdoc-core/test/fixtures/source-files/README.md new file mode 100644 index 00000000..2b2c83b7 --- /dev/null +++ b/packages/jsdoc-core/test/fixtures/source-files/README.md @@ -0,0 +1,15 @@ + diff --git a/packages/jsdoc-core/test/fixtures/source-files/bar.js b/packages/jsdoc-core/test/fixtures/source-files/bar.js new file mode 100644 index 00000000..98e71229 --- /dev/null +++ b/packages/jsdoc-core/test/fixtures/source-files/bar.js @@ -0,0 +1,15 @@ +/* + 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. +*/ diff --git a/packages/jsdoc-core/test/fixtures/source-files/baz.cjs b/packages/jsdoc-core/test/fixtures/source-files/baz.cjs new file mode 100644 index 00000000..98e71229 --- /dev/null +++ b/packages/jsdoc-core/test/fixtures/source-files/baz.cjs @@ -0,0 +1,15 @@ +/* + 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. +*/ diff --git a/packages/jsdoc-core/test/fixtures/source-files/foo.js b/packages/jsdoc-core/test/fixtures/source-files/foo.js new file mode 100644 index 00000000..98e71229 --- /dev/null +++ b/packages/jsdoc-core/test/fixtures/source-files/foo.js @@ -0,0 +1,15 @@ +/* + 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. +*/ diff --git a/packages/jsdoc-core/test/fixtures/source-files/package.json b/packages/jsdoc-core/test/fixtures/source-files/package.json new file mode 100644 index 00000000..e69de29b diff --git a/packages/jsdoc-core/test/fixtures/source-files/subdir/qux.js b/packages/jsdoc-core/test/fixtures/source-files/subdir/qux.js new file mode 100644 index 00000000..98e71229 --- /dev/null +++ b/packages/jsdoc-core/test/fixtures/source-files/subdir/qux.js @@ -0,0 +1,15 @@ +/* + 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. +*/ diff --git a/packages/jsdoc-core/test/specs/lib/api.js b/packages/jsdoc-core/test/specs/lib/api.js index 6484bfb0..fe00eb78 100644 --- a/packages/jsdoc-core/test/specs/lib/api.js +++ b/packages/jsdoc-core/test/specs/lib/api.js @@ -14,9 +14,15 @@ limitations under the License. */ +/* global jsdoc */ + import EventEmitter from 'node:events'; +import path from 'node:path'; import Api from '../../../lib/api.js'; +import Env from '../../../lib/env.js'; + +const __dirname = jsdoc.dirname(import.meta.url); describe('Api', () => { let instance; @@ -33,15 +39,19 @@ describe('Api', () => { expect(factory).not.toThrow(); }); - it('has an `emitter` property', () => { - expect(instance.emitter).toBeObject(); + it('has an `emitter` property that contains an `EventEmitter` instance', () => { + expect(instance.emitter).toBeInstanceOf(EventEmitter); + }); + + it('has an `env` property that contains an `Env` instance', () => { + expect(instance.env).toBeInstanceOf(Env); + }); + + it('has a `findSourceFiles` method', () => { + expect(instance.findSourceFiles).toBeFunction(); }); describe('emitter', () => { - it('is an instance of `EventEmitter` by default', () => { - expect(instance.emitter).toBeInstanceOf(EventEmitter); - }); - it('lets you provide your own emitter', () => { const fakeEmitter = {}; @@ -50,4 +60,117 @@ describe('Api', () => { expect(instance.emitter).toBe(fakeEmitter); }); }); + + describe('env', () => { + it('lets you provide your own JSDoc environment', () => { + const fakeEnv = {}; + + instance = new Api({ env: fakeEnv }); + + expect(instance.env).toBe(fakeEnv); + }); + }); + + describe('findSourceFiles', () => { + it('returns an empty array if no patterns are specified', async () => { + const sourceFiles = await instance.findSourceFiles(); + + expect(sourceFiles).toBeEmptyArray(); + }); + + it('stores a copy of the return value in `env.sourceFiles`', async () => { + const actual = await instance.findSourceFiles([ + path.join(__dirname, '../../fixtures/source-files/**/*.cjs'), + ]); + const expected = [path.resolve(path.join(__dirname, '../../fixtures/source-files/baz.cjs'))]; + + expect(actual).toEqual(expected); + expect(instance.env.sourceFiles).toEqual(actual); + }); + + it('resolves the path in `env.options.package`', async () => { + const filepath = path.join(__dirname, '../../fixtures/source-files/package.json'); + const expected = path.resolve(filepath); + + instance.env.options.package = filepath; + await instance.findSourceFiles(); + + expect(instance.env.options.package).toEqual(expected); + }); + + it('resolves the path in `env.options.readme`', async () => { + const filepath = path.join(__dirname, '../../fixtures/source-files/README.md'); + const expected = path.resolve(filepath); + + instance.env.options.readme = filepath; + await instance.findSourceFiles(); + + expect(instance.env.options.readme).toEqual(expected); + }); + + describe('with `globPatterns` param', () => { + it('uses the glob patterns to return absolute paths to files', async () => { + const actual = await instance.findSourceFiles([ + path.join(__dirname, '../../fixtures/source-files/**/*.js'), + ]); + const expected = [ + path.resolve(path.join(__dirname, '../../fixtures/source-files/bar.js')), + path.resolve(path.join(__dirname, '../../fixtures/source-files/foo.js')), + path.resolve(path.join(__dirname, '../../fixtures/source-files/subdir/qux.js')), + ]; + + expect(actual).toEqual(expected); + }); + + it('ignores `env.options._` if glob patterns are passed in', async () => { + let actual; + const expected = [ + path.resolve(path.join(__dirname, '../../fixtures/source-files/baz.cjs')), + ]; + + instance.env.options._ = [path.join(__dirname, '../../fixtures/source-files/**/*.js')]; + + actual = await instance.findSourceFiles([ + path.join(__dirname, '../../fixtures/source-files/*.cjs'), + ]); + + expect(actual).toEqual(expected); + }); + }); + + describe('without `globPatterns` param', () => { + it('uses `env.options._` if glob patterns are not passed in', async () => { + let actual; + const expected = [ + path.resolve(path.join(__dirname, '../../fixtures/source-files/bar.js')), + path.resolve(path.join(__dirname, '../../fixtures/source-files/foo.js')), + path.resolve(path.join(__dirname, '../../fixtures/source-files/subdir/qux.js')), + ]; + + instance.env.options._ = [path.join(__dirname, '../../fixtures/source-files/**/*.js')]; + + actual = await instance.findSourceFiles(); + + expect(actual).toEqual(expected); + }); + + it('folds `env.options._` into `env.config.sourceFiles`', async () => { + let actual; + const expected = [ + path.resolve(path.join(__dirname, '../../fixtures/source-files/bar.js')), + path.resolve(path.join(__dirname, '../../fixtures/source-files/foo.js')), + path.resolve(path.join(__dirname, '../../fixtures/source-files/subdir/qux.js')), + ]; + + instance.env.config.sourceFiles = [ + path.join(__dirname, '../../fixtures/source-files/subdir/*.js'), + ]; + instance.env.options._ = [path.join(__dirname, '../../fixtures/source-files/*.js')]; + + actual = await instance.findSourceFiles(); + + expect(actual).toEqual(expected); + }); + }); + }); }); diff --git a/packages/jsdoc/cli.js b/packages/jsdoc/cli.js index 5d835d04..25d7cdcb 100644 --- a/packages/jsdoc/cli.js +++ b/packages/jsdoc/cli.js @@ -16,23 +16,20 @@ /* eslint-disable no-process-exit */ import fs from 'node:fs'; -import path from 'node:path'; +import { readFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import Engine from '@jsdoc/cli'; -import { config as jsdocConfig, Env, plugins } from '@jsdoc/core'; +import { config as jsdocConfig, plugins } from '@jsdoc/core'; import { augment, Package, resolveBorrows } from '@jsdoc/doclet'; import { createParser, handlers } from '@jsdoc/parse'; import { Dictionary } from '@jsdoc/tag'; -import fastGlob from 'fast-glob'; import _ from 'lodash'; import stripBom from 'strip-bom'; import stripJsonComments from 'strip-json-comments'; import test from './test/index.js'; -const { sync: glob } = fastGlob; - /** * Helper methods for running JSDoc on the command line. * @@ -48,9 +45,8 @@ export default (() => { }; const cli = {}; - const env = new Env(); - const { emitter, log } = env; - const engine = new Engine(env); + const engine = new Engine(); + const { api, emitter, env, log } = engine; const FATAL_ERROR_MESSAGE = 'Exiting JSDoc because an error occurred. See the previous log messages for details.'; const LOG_LEVELS = Engine.LOG_LEVELS; @@ -194,7 +190,7 @@ export default (() => { // TODO: docs cli.main = async () => { - cli.scanFiles(); + await api.findSourceFiles(); if (env.sourceFiles.length === 0) { console.log('There are no input files to process.'); @@ -202,21 +198,23 @@ export default (() => { return Promise.resolve(0); } else { await cli.createParser(); + await cli.parseFiles(); - return cli - .parseFiles() - .processParseResults() - .then(() => { - env.run.finish = new Date(); + return cli.processParseResults().then(() => { + env.run.finish = new Date(); - return 0; - }); + return 0; + }); } }; - function readPackageJson(filepath) { + async function readPackageJson(filepath) { + let data; + try { - return stripJsonComments(fs.readFileSync(filepath, 'utf8')); + data = await readFile(filepath, 'utf8'); + + return stripJsonComments(data); } catch (e) { log.error(`Unable to read the package file ${filepath}`); @@ -224,44 +222,6 @@ export default (() => { } } - function buildSourceList() { - const { config, options } = env; - let packageJson; - let sourceFiles = options._ ? options._.slice() : []; - - if (config.sourceFiles) { - sourceFiles = sourceFiles.concat(config.sourceFiles); - } - - // load the user-specified package file, if any - if (options.package) { - packageJson = readPackageJson(options.package); - props.packageJson = packageJson; - } - - // Resolve the path to the README. - if (options.readme) { - options.readme = path.resolve(options.readme); - } - - return sourceFiles; - } - - // TODO: docs - cli.scanFiles = () => { - const { options } = env; - - options._ = buildSourceList(); - if (options._.length) { - env.sourceFiles = glob(options._, { - absolute: true, - onlyFiles: true, - }); - } - - return cli; - }; - cli.createParser = async () => { const { config } = env; @@ -276,15 +236,18 @@ export default (() => { return cli; }; - cli.parseFiles = () => { + cli.parseFiles = async () => { const { options } = env; + let packageData = ''; let packageDocs; let docletStore; - docletStore = props.parser.parse(env.sourceFiles, options.encoding); + docletStore = props.docs = props.parser.parse(env.sourceFiles, options.encoding); - // If there is no package.json, just create an empty package - packageDocs = new Package(props.packageJson, env); + if (props.packageJson) { + packageData = await readPackageJson(props.packageJson); + } + packageDocs = new Package(packageData, env); packageDocs.files = env.sourceFiles || []; docletStore.add(packageDocs); @@ -293,9 +256,6 @@ export default (() => { log.debug('Adding borrowed doclets...'); resolveBorrows(docletStore); log.debug('Post-processing complete.'); - - props.docs = docletStore; - if (props.parser.listenerCount('processingComplete')) { props.parser.fireProcessingComplete(Array.from(docletStore.doclets)); }