feat(jsdoc-core): move source-file scanning to public API

This commit is contained in:
Jeff Williams 2023-12-29 11:51:34 -08:00
parent 865b9faa1e
commit 638a89a204
No known key found for this signature in database
10 changed files with 295 additions and 69 deletions

View File

@ -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({

View File

@ -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<string>} 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<string>} 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() ?? [];
}
}

View File

@ -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.
-->

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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.
*/

View File

@ -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);
});
});
});
});

View File

@ -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));
}