Compare commits

..

No commits in common. "main" and "@jsdoc/prettier-config@0.2.10" have entirely different histories.

54 changed files with 7467 additions and 1894 deletions

View File

@ -14,7 +14,6 @@
name: build
on: [push, pull_request]
permissions: {}
env:
# Don't install Git hooks.
HUSKY: 0
@ -24,12 +23,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node: ['20']
node: [20]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
@ -42,12 +39,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node: ['20']
node: [20]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'
@ -60,12 +55,10 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
node: ['20.19.0', '22.12.0', '24']
node: [18, 20]
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
cache: 'npm'

View File

@ -13,7 +13,6 @@ CODE_OF_CONDUCT.md
CONTRIBUTING.md
Herebyfile*
js-green-licenses.json
knip.json
lerna.json
packages/
test/

View File

@ -43,16 +43,6 @@ export const coverage = task({
},
});
export const dependencyCleanup = task({
name: 'dependency-cleanup',
run: async () => {
await execa(bin('knip'), [], {
stdout: 'inherit',
stderr: 'inherit',
});
},
});
export const dependencyEngines = task({
name: 'dependency-engines',
run: async () => {
@ -101,7 +91,7 @@ export const dependencyLicenses = task({
export const dependencies = task({
name: 'dependencies',
dependencies: [dependencyCleanup, dependencyEngines, dependencyLicenses],
dependencies: [dependencyEngines, dependencyLicenses],
});
export const format = task({

View File

@ -80,6 +80,7 @@ and customize your documentation. Here are a few of them:
- Documentation is available at [jsdoc.app](https://jsdoc.app/).
- Contribute to the docs at
[jsdoc/jsdoc.github.io](https://github.com/jsdoc/jsdoc.github.io).
- [Join JSDoc's Slack channel](https://jsdoc-slack.appspot.com/).
- Ask for help on the
[JSDoc Users mailing list](http://groups.google.com/group/jsdoc-users).
- Post questions tagged `jsdoc` to

View File

@ -1,61 +0,0 @@
{
"workspaces": {
".": {
"entry": ["Herebyfile.js"],
"ignoreDependencies": ["ajv", "c8", "installed-check", "license-check-and-add", "mock-fs"]
},
"packages/jsdoc": {
"project": ["*.js", "test/index.js"]
},
"packages/jsdoc-ast": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-cli": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-core": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-doclet": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-eslint-config": {
"project": ["*.js"]
},
"packages/jsdoc-name": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-parse": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-plugins": {
"ignore": ["*.js", "test/**/*.js"]
},
"packages/jsdoc-prettier-config": {
"project": ["*.js"]
},
"packages/jsdoc-salty": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-tag": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-task-runner": {
"project": ["lib/**/*.js"]
},
"packages/jsdoc-template-legacy": {
"project": ["*.js", "lib/**/*.js"],
"ignoreDependencies": [
"@fontsource-variable/open-sans",
"code-prettify",
"color-themes-for-google-code-prettify"
]
},
"packages/jsdoc-test-matchers": {
"project": ["*.js"]
},
"packages/jsdoc-util": {
"project": ["lib/**/*.js"]
}
}
}

7686
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,35 +3,44 @@
"private": true,
"license": "Apache-2.0",
"devDependencies": {
"@jsdoc/core": "^0.5.10",
"@jsdoc/eslint-config": "^2.0.2",
"@jsdoc/tag": "^0.2.13",
"@jsdoc/util": "^0.3.4",
"@babel/eslint-parser": "7.26.5",
"@jsdoc/ast": "^0.2.5",
"@jsdoc/cli": "^0.3.5",
"@jsdoc/core": "^0.5.4",
"@jsdoc/doclet": "^0.2.5",
"@jsdoc/eslint-config": "^2.0.0",
"@jsdoc/parse": "^0.3.5",
"@jsdoc/prettier-config": "^0.2.3",
"@jsdoc/salty": "^0.2.5",
"@jsdoc/tag": "^0.2.5",
"@jsdoc/task-runner": "^0.2.2",
"@jsdoc/test-matchers": "^0.2.4",
"@jsdoc/util": "^0.3.0",
"ajv": "^8.17.1",
"c8": "^10.1.3",
"eslint": "^9.39.1",
"execa": "^9.6.1",
"hereby": "^1.11.1",
"eslint": "^9.18.0",
"execa": "^9.5.2",
"hereby": "^1.10.0",
"husky": "^9.1.7",
"installed-check": "^9.3.0",
"jasmine": "^5.13.0",
"jasmine": "^5.5.0",
"jasmine-console-reporter": "^3.1.0",
"js-green-licenses": "^4.0.0",
"knip": "^5.72.0",
"klaw-sync": "^6.0.0",
"lerna": "^8.1.9",
"license-check-and-add": "^4.0.5",
"lodash": "^4.17.21",
"mock-fs": "^5.5.0",
"prettier": "^3.7.4"
"mock-fs": "^5.4.1",
"prettier": "^3.4.2"
},
"type": "module",
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"scripts": {
"coverage": "node_modules/.bin/hereby coverage",
"default": "node_modules/.bin/hereby",
"dependencies": "node_modules/.bin/hereby dependencies",
"dependency-cleanup": "node_modules/.bin/hereby dependency-cleanup",
"dependency-engines": "node_modules/.bin/hereby dependency-engines",
"dependency-licenses": "node_modules/.bin/hereby dependency-licenses",
"format": "node_modules/.bin/hereby format",

View File

@ -173,9 +173,9 @@ export function nodeToValue(node) {
// Like the declaration in: `export const foo = 'bar';`
// We need a single value, so we use the first variable name.
if (node.declaration.declarations) {
str = `${nodeToValue(node.declaration.declarations[0])}`;
str = `${LONGNAMES.MODULE_EXPORT}.${nodeToValue(node.declaration.declarations[0])}`;
} else {
str = `${nodeToValue(node.declaration)}`;
str = `${LONGNAMES.MODULE_EXPORT}.${nodeToValue(node.declaration)}`;
}
}

View File

@ -31,13 +31,13 @@
}
},
"dependencies": {
"@babel/parser": "^7.28.5",
"@babel/parser": "^7.26.5",
"@jsdoc/name": "^0.1.1",
"@jsdoc/util": "^0.3.4",
"ast-module-types": "^6.0.1",
"ast-module-types": "^6.0.0",
"lodash": "^4.17.21"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
}
}

View File

@ -34,11 +34,12 @@
"@jsdoc/core": "^0.5.10",
"@jsdoc/util": "^0.3.4",
"lodash": "^4.17.21",
"ow": "^3.1.1",
"yargs-parser": "^22.0.0"
"ow": "^2.0.0",
"strip-bom": "^5.0.0",
"yargs-parser": "^21.1.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"gitHead": "a595f7f2a525f32da9a0498a027667af3d938158"
}

View File

@ -17,8 +17,7 @@
/**
* Manages configuration settings for JSDoc.
*
* @namespace config
* @memberof module:@jsdoc/core
* @alias module:@jsdoc/core.config
*/
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
import _ from 'lodash';
@ -27,124 +26,67 @@ import stripJsonComments from 'strip-json-comments';
const MODULE_NAME = 'jsdoc';
/**
* The default configuration settings for JSDoc.
*
* @namespace defaultConfig
* @memberof module:@jsdoc/core.config
*/
export const defaultConfig = {
// TODO: Integrate CLI options with other options.
/**
* JSDoc options that can be specified on the command line.
*
* @namespace opts
* @memberof module:@jsdoc/core.config.defaultConfig
*/
// TODO(hegemonic): Integrate CLI options with other options.
opts: {
/**
* The output directory for the generated documentation. If this directory does not exist, then
* JSDoc creates it. The default value is `./out`.
*
* @type {string}
* @memberof module:@jsdoc/core.config.defaultConfig.opts
*/
destination: './out',
/**
* The character encoding to use. Use an
* [encoding supported by Node.js](https://nodejs.org/api/buffer.html#buffers-and-character-encodings).
* The default value is `utf8`.
*
* @type {string}
* @memberof module:@jsdoc/core.config.defaultConfig.opts
*/
encoding: 'utf8',
},
/**
* The paths to the JSDoc plugins to load. Use the same paths that you would use to import each
* plugin as a module.
*
* @type {Array<string>}
* @memberof module:@jsdoc/core.config.defaultConfig
* The JSDoc plugins to load.
*/
plugins: [],
/**
* The source files to parse.
*
* @type {Array<string>}
* @memberof module:@jsdoc/core.config.defaultConfig
* Settings for loading and parsing source files.
*/
sourceFiles: [],
/**
* The type of source file. In most cases, you should use the value `module`, which is the default
* value.
*
* If none of your source files use syntax from ECMAScript 2015 or later, then use the value
* `script`.
*
* @type {string}
* @memberof module:@jsdoc/core.config.defaultConfig
* The type of source file. In general, you should use the value `module`. If none of your
* source files use ECMAScript >=2015 syntax, you can use the value `script`.
*/
sourceType: 'module',
/**
* Settings for interpreting JSDoc tags.
*
* @namespace tags
* @memberof module:@jsdoc/core.config.defaultConfig
*/
tags: {
/**
* Whether to allow tags that JSDoc does not recognize. The default value is `true`.
*
* @type {boolean}
* @memberof module:@jsdoc/core.config.defaultConfig.tags
* Set to `true` to allow tags that JSDoc does not recognize.
*/
allowUnknownTags: true,
// TODO: Use module paths, not magic strings.
// TODO(hegemonic): Use module paths, not magic strings.
/**
* The JSDoc tag dictionaries to load.
*
* If you specify two or more tag dictionaries, and a tag is defined in multiple
* dictionaries, JSDoc uses the definition from the first dictionary that includes that tag.
*
* @type {Array<string>}
* @memberof module:@jsdoc/core.config.defaultConfig.tags
*/
dictionaries: ['jsdoc', 'closure'],
},
/**
* Settings for generating output with JSDoc templates. Some JSDoc templates might ignore these
* settings.
*
* @namespace
* @memberof module:@jsdoc/core.config.defaultConfig
*/
templates: {
/**
* Whether to use a monospaced font for links to other code symbols, but not links to websites.
* The default value is `false`.
*
* @type {boolean}
* @memberof module:@jsdoc/core.config.defaultConfig.templates
* Set to `true` to use a monospaced font for links to other code symbols, but not links to
* websites.
*/
cleverLinks: false,
/**
* Whether to use a monospaced font for all links. The default value is `false`.
*
* @type {boolean}
* @memberof module:@jsdoc/core.config.defaultConfig.templates
* Set to `true` to use a monospaced font for all links.
*/
monospaceLinks: false,
},
};
// TODO: Consider exporting this class.
class Config {
constructor(filepath, config) {
this.config = config;
this.filepath = filepath;
}
}
function loadJson(filepath, content) {
return defaultLoaders['.json'](filepath, stripBom(stripJsonComments(content)));
}
@ -172,43 +114,6 @@ const explorer = cosmiconfig(MODULE_NAME, {
],
});
/**
* Information about a JSDoc configuration file.
*
* @typedef {Object<string, string>} module:@jsdoc/core.config~ConfigInfo
* @property {Object<string, *>} config - The configuration settings.
* @property {string} filepath - The path to the configuration file that was loaded.
*/
/**
* Loads JSDoc configuration settings from the specified filepath.
*
* You can provide configuration settings in the following formats:
*
* + A CommonJS or ES2015 module that exports the configuration settings. For CommonJS modules, use
* the extension `.cjs`.
* + A JSON file that contains the configuration settings.
* + A YAML file that contains the configuration settings.
* + A `jsdoc` property in a `package.json` file that contains the configuration settings.
*
* If `filepath` is a configuration file, then JSDoc loads that configuration file.
*
* If `filepath` is a directory, then JSDoc looks for the following files in that directory and
* loads the first one it finds:
*
* 1. `package.json` (`jsdoc` property)
* 2. `.jsdocrc` (JSON or YAML)
* 3. `.jsdocrc.json`
* 4. `.jsdocrc.yaml`
* 5. `.jsdocrc.yml`
* 6. `.jsdocrc.js` (ES2015 module)
* 7. `jsdoc.config.js` (ES2015 module)
*
* @alias module:@jsdoc/core.config.load
* @param {string} filepath - The path to the configuration file, or a directory that contains the
* configuration file.
* @returns {module:@jsdoc/core.config~ConfigInfo} The configuration settings.
*/
export async function load(filepath) {
let loaded;
@ -218,8 +123,5 @@ export async function load(filepath) {
loaded = (await explorer.search()) ?? {};
}
return {
config: _.defaultsDeep({}, loaded.config, defaultConfig),
filepath: loaded.filepath,
};
return new Config(loaded.filepath, _.defaultsDeep({}, loaded.config, defaultConfig));
}

View File

@ -43,21 +43,22 @@ export default class Env {
/**
* The event emitter shared across JSDoc.
*
* @type {node:events}
* @type {Object}
*/
this.emitter = new EventEmitter();
/**
* Logging functions shared across JSDoc.
*
* @type {module:@jsdoc/util~logFunctions}
* @type {Object<string, function>}
*/
this.log = getLogFunctions(this.emitter);
/**
* The command-line arguments, expressed as key-value pairs.
* The command-line arguments, parsed into a key/value hash.
*
* @type {Object<string, *>}
* @type {Object}
* @example if (global.env.opts.help) { console.log('Helpful message.'); }
*/
this.opts = {};
@ -66,7 +67,7 @@ export default class Env {
*
* @type {Object}
* @property {Date} start - The time at which JSDoc started running.
* @property {?Date} finish - The time at which JSDoc finished running.
* @property {Date} finish - The time at which JSDoc finished running.
*/
this.run = {
start: new Date(),
@ -100,20 +101,20 @@ export default class Env {
};
}
/**
* The data parsed from JSDoc's configuration file.
*
* @type Object<string, *>
*/
get config() {
return this.conf;
}
/**
* The command-line arguments, expressed as key-value pairs.
*
* @type {Object<string, *>}
*/
// TODO: Remove.
get env() {
return this;
}
// TODO: Remove.
get(key) {
return this[key];
}
get options() {
return this.opts;
}

View File

@ -38,10 +38,10 @@
"fast-glob": "^3.3.3",
"lodash": "^4.17.21",
"strip-bom": "^5.0.0",
"strip-json-comments": "^5.0.3"
"strip-json-comments": "^5.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"gitHead": "a595f7f2a525f32da9a0498a027667af3d938158"
}

View File

@ -46,6 +46,14 @@ describe('@jsdoc/core.Env', () => {
expect(env.emitter).toBeInstanceOf(EventEmitter);
});
it('has an `env` getter', () => {
expect(env.env).toBe(env);
});
it('has a `get` method that returns the value of the specified property', () => {
expect(env.get('sourceFiles')).toBe(env.sourceFiles);
});
it('has a `log` property that contains logging functions', () => {
expect(env.log).toBeObject();

View File

@ -22,10 +22,10 @@
*/
import * as augment from './lib/augment.js';
import { resolveBorrows } from './lib/borrow.js';
import { Doclet } from './lib/doclet.js';
import { combineDoclets, Doclet } from './lib/doclet.js';
import { DocletStore } from './lib/doclet-store.js';
import { Package } from './lib/package.js';
import * as schema from './lib/schema.js';
export { augment, Doclet, DocletStore, Package, resolveBorrows, schema };
export default { augment, Doclet, DocletStore, Package, resolveBorrows, schema };
export { augment, combineDoclets, Doclet, DocletStore, Package, resolveBorrows, schema };
export default { augment, combineDoclets, Doclet, DocletStore, Package, resolveBorrows, schema };

View File

@ -16,14 +16,11 @@
/**
* Provides methods for augmenting the parse results based on their content.
*
* @namespace augment
* @memberof module:@jsdoc/doclet
*/
import { fromParts, SCOPE, toParts } from '@jsdoc/name';
import { Doclet } from './doclet.js';
import { combineDoclets, Doclet } from './doclet.js';
const DEPENDENCY_KINDS = ['class', 'external', 'interface', 'mixin'];
@ -136,7 +133,7 @@ function staticToInstance(doclet) {
}
/**
* Updates the list of doclets to be added to another symbol.
* Update the list of doclets to be added to another symbol.
*
* We add only one doclet per longname. For example: If `ClassA` inherits from two classes that both
* use the same method name, `ClassA` gets docs for one method rather than two.
@ -225,7 +222,7 @@ function getInheritedAdditions(depDoclets, docletStore) {
childDoclet = {};
}
member = Doclet.combineDoclets(childDoclet, parentDoclet);
member = combineDoclets(childDoclet, parentDoclet);
if (!member.inherited) {
member.inherits = member.longname;
@ -411,7 +408,7 @@ function getImplementedAdditions(implDoclets, docletStore) {
childDoclet = {};
}
implementationDoclet = Doclet.combineDoclets(childDoclet, parentDoclet);
implementationDoclet = combineDoclets(childDoclet, parentDoclet);
reparentDoclet(doclet, implementationDoclet);
updateImplements(implementationDoclet, parentDoclet.longname);
@ -475,20 +472,19 @@ function augment(docletStore, propertyName, docletFinder, env) {
}
/**
* Adds doclets to reflect class inheritance.
* Add doclets to reflect class inheritance.
*
* For example, if `ClassA` has the instance method `myMethod`, and `ClassB` inherits from `ClassA`,
* calling this method creates a new doclet for `ClassB#myMethod`.
*
* @alias module:@jsdoc/doclet.augment.addInherited
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
* @return {void}
*/
export function addInherited(docletStore) {
augment(docletStore, 'augments', getInheritedAdditions);
}
/**
* Adds doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the
* Add doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the
* mixed-in symbol is treated as an instance member.
*
* For example:
@ -498,15 +494,16 @@ export function addInherited(docletStore) {
* + If `MixinA` has the static method `myMethod`, and `ClassA` mixes `MixinA`, calling this method
* creates a new doclet for the instance method `ClassA#myMethod`.
*
* @alias module:@jsdoc/doclet.augment.addMixedIn
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
* @param {!Array.<module:@jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index.
* @return {void}
*/
export function addMixedIn(docletStore) {
augment(docletStore, 'mixes', getMixedInAdditions);
export function addMixedIn(doclets) {
augment(doclets, 'mixes', getMixedInAdditions);
}
/**
* Adds and updates doclets to reflect implementations of interfaces.
* Add and update doclets to reflect implementations of interfaces.
*
* For example, if `InterfaceA` has the instance method `myMethod`, and `ClassA` implements
* `InterfaceA`, calling this method does the following:
@ -518,15 +515,14 @@ export function addMixedIn(docletStore) {
* If `ClassA#myMethod` used the `@override` or `@inheritdoc` tag, calling this method would also
* generate a new doclet that reflects the interface's documentation for `InterfaceA#myMethod`.
*
* @alias module:@jsdoc/doclet.augment.addImplemented
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
* @return {void}
*/
export function addImplemented(docletStore) {
augment(docletStore, 'implements', getImplementedAdditions);
export function addImplemented(doclets) {
augment(doclets, 'implements', getImplementedAdditions);
}
/**
* Adds and updates doclets to reflect all of the following:
* Add and update doclets to reflect all of the following:
*
* + Inherited classes
* + Mixins
@ -534,8 +530,7 @@ export function addImplemented(docletStore) {
*
* Calling this method is equivalent to calling all other methods exported by this module.
*
* @alias module:@jsdoc/doclet.augment.augmentAll
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
* @return {void}
*/
export function augmentAll(docletStore) {
addMixedIn(docletStore);

View File

@ -20,7 +20,7 @@
import { SCOPE } from '@jsdoc/name';
import { Doclet } from './doclet.js';
import { combineDoclets, Doclet } from './doclet.js';
function cloneBorrowedDoclets({ borrowed, longname }, docletStore) {
borrowed?.forEach(({ from, as }) => {
@ -32,7 +32,7 @@ function cloneBorrowedDoclets({ borrowed, longname }, docletStore) {
if (borrowedDoclets) {
borrowedAs = borrowedAs.replace(/^prototype\./, SCOPE.PUNC.INSTANCE);
borrowedDoclets.forEach((borrowedDoclet) => {
const clone = Doclet.clone(borrowedDoclet);
const clone = combineDoclets(borrowedDoclet, Doclet.emptyDoclet(borrowedDoclet.env));
// TODO: this will fail on longnames like '"Foo#bar".baz'
parts = borrowedAs.split(SCOPE.PUNC.INSTANCE);
@ -55,15 +55,10 @@ function cloneBorrowedDoclets({ borrowed, longname }, docletStore) {
}
/**
* Creates doclets for borrowed symbols, and adds them to the doclet store.
*
* The `name`, `memberof`, and `longname` properties for the new doclets are rewritten to show that
* they belong to the borrowing symbol.
*
* This method also removes the `borrowed` property from each borrowed doclet.
*
* @alias module:@jsdoc/doclet.resolveBorrows
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
Take a copy of the docs for borrowed symbols and attach them to the
docs for the borrowing symbol. This process changes the symbols involved,
moving docs from the "borrowed" array and into the general docs, then
deleting the "borrowed" array.
*/
export function resolveBorrows(docletStore) {
for (const doclet of docletStore.docletsWithBorrowed) {

View File

@ -56,25 +56,17 @@ function removeFromSet(targetMap, key, value) {
/**
* Stores and classifies the doclets that JSDoc creates as it parses your source files.
*
* A doclet store categorizes doclets based on their properties, so that the JSDoc template can
* The doclet store categorizes doclets based on their properties, so that the JSDoc template can
* efficiently retrieve the doclets that it needs. For example, when the template generates
* documentation for a class, it can retrieve all of the doclets that represent members of that
* class.
*
* To retrieve the doclets that you need, use the doclet store's instance properties. For example,
* {@link module:@jsdoc/doclet.DocletStore#docletsByLongname} maps longnames to the doclets with
* that longname.
*
* After you add a doclet to the store, the store automatically tracks changes to a doclet's
* properties and recategorizes the doclet as needed. For example, if a doclet's `kind` property
* changes from `class` to `interface`, then the doclet store automatically recategorizes the doclet
* as an interface.
*
* Doclets can be _visible_, meaning that they should be used to generate output, or _hidden_,
* meaning that they're ignored when generating output. Except as noted, the doclet store exposes
* only visible doclets.
*
* @alias module:@jsdoc/doclet.DocletStore
* @alias @jsdoc/doclet.DocletStore
*/
export class DocletStore {
#commonPathPrefix;
@ -99,9 +91,9 @@ export class DocletStore {
*
* When you create a doclet store, you provide a JSDoc environment object. The doclet store
* listens for new doclets that are created in that environment. When a new doclet is created, the
* doclet store adds it automatically and tracks updates to the doclet.
* doclet store tracks it automatically.
*
* @param {module:@jsdoc/core.Env} env - The JSDoc environment to use.
* @param {@jsdoc/core.Env} env - The JSDoc environment to use.
*/
constructor(env) {
this.#commonPathPrefix = null;
@ -109,85 +101,28 @@ export class DocletStore {
this.#isListening = false;
this.#sourcePaths = new Map();
/**
* Map of all doclet longnames to a `Set` of all doclets with that longname. Includes both
* visible and hidden doclets.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
// TODO: Add descriptions and types for public properties.
/** @type Map<string, Set<Doclet>> */
this.allDocletsByLongname = new Map();
/**
* All visible doclets.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
/** Doclets that are used to generate output. */
this.doclets = new Set();
/**
* Map from a doclet kind to a `Set` of doclets with that kind.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
/** @type Map<string, Set<Doclet>> */
this.docletsByKind = new Map();
/**
* Map from a doclet longname to a `Set` of doclets with that longname.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
/** @type Map<string, Set<Doclet>> */
this.docletsByLongname = new Map();
/**
* Map from a doclet `memberof` value to a `Set` of doclets with that `memberof`.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
/** @type Map<string, Set<Doclet>> */
this.docletsByMemberof = new Map();
/**
* Map from an AST node ID, generated during parsing, to a `Set` of doclets for that node ID.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
/** @type Map<string, Set<Doclet>> */
this.docletsByNodeId = new Map();
/**
* Doclets that have an `augments` property.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.docletsWithAugments = new Set();
/**
* Doclets that have a `borrowed` property.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.docletsWithBorrowed = new Set();
/**
* Doclets that have an `implements` property.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.docletsWithImplements = new Set();
/**
* Doclets that have a `mixes` property.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.docletsWithMixes = new Set();
/**
* Doclets that belong to the global scope.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.globals = new Set();
/**
* Map from an event's longname to a `Set` of doclets that listen to that event.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
/** @type Map<string, Set<Doclet>> */
this.listenersByListensTo = new Map();
/**
* Doclets that are hidden and shouldn't be used to generate output.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
/** Doclets that aren't used to generate output. */
this.unusedDoclets = new Set();
this.#docletChangedHandler = (e) => this.#handleDocletChanged(e, {});
@ -383,15 +318,7 @@ export class DocletStore {
// `undocumented` only affects visibility, which is handled above, so we ignore it here.
}
/**
* Adds a doclet to the store directly, rather than by listening to events from the JSDoc
* environment.
*
* Use this method if you need to track a doclet that's generated outside of JSDoc's parsing
* process.
*
* @param {module:@jsdoc/doclet.Doclet} doclet - The doclet to add.
*/
// Adds a doclet to the store directly, rather than by listening to events.
add(doclet) {
let doclets;
let nodeId;
@ -410,25 +337,10 @@ export class DocletStore {
}
}
/**
* All known doclets, including both visible and hidden doclets.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
get allDoclets() {
return new Set([...this.doclets, ...this.unusedDoclets]);
}
/**
* The longest filepath prefix that's shared by the source files that were parsed.
*
* + If there's only one source file, then the prefix is the source file's directory name.
* + If the source files don't have a common prefix, then the prefix is an empty string.
*
* If a doclet is hidden, then its source filepath is ignored when determining the prefix.
*
* @type {string}
*/
get commonPathPrefix() {
let commonPrefix;
let sourcePaths;
@ -453,30 +365,14 @@ export class DocletStore {
return commonPrefix ?? '';
}
/**
* The longnames of all visible doclets.
*
* @type Array<string>
*/
get longnames() {
return Array.from(this.docletsByLongname.keys());
}
/**
* The source paths associated with all visible doclets.
*
* @type Array<string>
*/
get sourcePaths() {
return Array.from(this.#sourcePaths.values());
}
/**
* Start listening to events from the JSDoc environment.
*
* In general, you don't need to call this method. A `DocletStore` always listens for events by
* default.
*/
startListening() {
if (!this.#isListening) {
this.#emitter.on('docletChanged', this.#docletChangedHandler);
@ -486,12 +382,6 @@ export class DocletStore {
}
}
/**
* Stop listening to events from the JSDoc environment.
*
* Call this method if you're done using a `DocletStore`, and you don't want it to listen to
* future events.
*/
stopListening() {
if (this.#isListening) {
this.#emitter.removeListener('docletChanged', this.#docletChangedHandler);

View File

@ -410,7 +410,30 @@ function copyPropsWithIncludelist(primary, secondary, target, include) {
}
/**
* Information about a single JSDoc comment, or a single symbol in a source file.
* Combine two doclets into a new doclet.
*
* @param {module:@jsdoc/doclet.Doclet} primary - The doclet whose properties will be used.
* @param {module:@jsdoc/doclet.Doclet} secondary - The doclet to use as a fallback for properties
* that the primary doclet does not have.
* @returns {module:@jsdoc/doclet.Doclet} A new doclet that combines the primary and secondary
* doclets.
*/
export function combineDoclets(primary, secondary) {
const excludelist = ['env', 'params', 'properties', 'undocumented'];
const includelist = ['params', 'properties'];
const target = Doclet.emptyDoclet(secondary.env);
// First, copy most properties to the target doclet.
copyPropsWithExcludelist(primary, secondary, target, excludelist);
// Then copy a few specific properties to the target doclet, as long as they're not falsy and
// have a length greater than 0.
copyPropsWithIncludelist(primary, secondary, target, includelist);
return target;
}
/**
* Represents a single JSDoc comment.
*
* @alias module:@jsdoc/doclet.Doclet
*/
@ -418,7 +441,7 @@ Doclet = class {
#dictionary;
/**
* Creates a doclet.
* Create a doclet.
*
* @param {string} docletSrc - The raw source code of the jsdoc comment.
* @param {object} meta - Properties describing the code related to this comment.
@ -445,11 +468,7 @@ Doclet = class {
});
WATCHABLE_PROPS.forEach((prop) => this.#defineWatchableProp(prop));
/**
* The text of the comment from the source code.
*
* @type {string}
*/
/** The original text of the comment from the source code. */
this.comment = docletSrc;
meta ??= {};
this.setMeta(meta);
@ -473,45 +492,10 @@ Doclet = class {
}
}
/**
* Creates a copy of an existing doclet.
*
* @param {module:@jsdoc/doclet.Doclet} doclet - The doclet to copy.
* @returns {module:@jsdoc/doclet.Doclet} A copy of the doclet.
*/
static clone(doclet) {
return Doclet.combineDoclets(doclet, Doclet.emptyDoclet(doclet.env));
return combineDoclets(doclet, Doclet.emptyDoclet(doclet.env));
}
/**
* Combines two doclets into a new doclet.
*
* @param {module:@jsdoc/doclet.Doclet} primary - The doclet whose properties will be used.
* @param {module:@jsdoc/doclet.Doclet} secondary - The doclet to use as a fallback for properties
* that the primary doclet does not have.
* @returns {module:@jsdoc/doclet.Doclet} A new doclet that combines the primary and secondary
* doclets.
*/
static combineDoclets(primary, secondary) {
const excludelist = ['env', 'params', 'properties', 'undocumented'];
const includelist = ['params', 'properties'];
const target = Doclet.emptyDoclet(secondary.env);
// First, copy most properties to the target doclet.
copyPropsWithExcludelist(primary, secondary, target, excludelist);
// Then copy a few specific properties to the target doclet, as long as they're not falsy and
// have a length greater than 0.
copyPropsWithIncludelist(primary, secondary, target, includelist);
return target;
}
/**
* Creates an empty doclet.
*
* @param {module:@jsdoc/core.Env} env - The JSDoc environment to use.
* @returns {module:@jsdoc/doclet.Doclet} An empty doclet.
*/
static emptyDoclet(env) {
return new Doclet('', {}, env);
}
@ -550,7 +534,7 @@ Doclet = class {
}
/**
* Adds a tag to the doclet.
* Add a tag to the doclet.
*
* @param {string} title - The title of the tag being added.
* @param {string} [text] - The text of the tag being added.
@ -570,7 +554,7 @@ Doclet = class {
}
/**
* Checks whether the doclet represents a globally available symbol.
* Check whether the doclet represents a globally available symbol.
*
* @returns {boolean} `true` if the doclet represents a global; `false` otherwise.
*/
@ -579,7 +563,7 @@ Doclet = class {
}
/**
* Checks whether the doclet should be used to generate output.
* Check whether the doclet should be used to generate output.
*
* @returns {boolean} `true` if the doclet should be used to generate output; `false` otherwise.
*/
@ -663,21 +647,20 @@ Doclet = class {
}
/**
* Sets the doclet's `longname` property.
* Set the doclet's `longname` property.
*
* @param {string} longname - The longname for the doclet.
*/
setLongname(longname) {
/**
* The fully resolved symbol name.
* @type {string}
*/
longname = removeGlobal(longname);
if (this.#dictionary.isNamespace(this.kind)) {
longname = applyNamespace(longname, this.kind);
}
/**
* The fully resolved symbol name.
*
* @type {string}
*/
this.longname = longname;
}
@ -689,7 +672,6 @@ Doclet = class {
setMemberof(sid) {
/**
* The longname of the symbol that contains this one, if any.
*
* @type {string}
*/
this.memberof = removeGlobal(sid)
@ -698,7 +680,7 @@ Doclet = class {
}
/**
* Sets the doclet's `scope` property. Must correspond to a scope name that is defined in
* Set the doclet's `scope` property. Must correspond to a scope name that is defined in
* {@link module:@jsdoc/name.SCOPE.NAMES}.
*
* @param {string} scope - The scope for the doclet relative to the symbol's parent.
@ -725,10 +707,10 @@ Doclet = class {
}
/**
* Adds a symbol to the doclet's `borrowed` array.
* Add a symbol to this doclet's `borrowed` array.
*
* @param {string} source - The longname of the symbol that is borrowed.
* @param {string} target - The name that the borrowed symbol is assigned to.
* @param {string} source - The longname of the symbol that is the source.
* @param {string} target - The name the symbol is being assigned to.
*/
borrow(source, target) {
const about = { from: source };
@ -746,11 +728,6 @@ Doclet = class {
this.borrowed.push(about);
}
/**
* Adds a symbol to the doclet's `mixes` array.
*
* @param {string} source - The longname of the symbol that is mixed in.
*/
mix(source) {
/**
* A list of symbols that are mixed into this one, if any.
@ -762,7 +739,7 @@ Doclet = class {
}
/**
* Adds a symbol to the doclet's `augments` array.
* Add a symbol to the doclet's `augments` array.
*
* @param {string} base - The longname of the base symbol.
*/
@ -776,12 +753,10 @@ Doclet = class {
this.augments.push(base);
}
// TODO: Add typedef for `meta`.
/**
* Sets the `meta` property of the doclet, which contains metadata about the source code that the
* doclet corresponds to.
* Set the `meta` property of this doclet.
*
* @param {object} meta - The data to add to the doclet.
* @param {object} meta
*/
setMeta(meta) {
let pathname;

View File

@ -16,6 +16,12 @@
import stripBom from 'strip-bom';
/**
* Provides access to information about a JavaScript package.
*
* @see https://www.npmjs.org/doc/files/package.json.html
*/
// Collect all of the license information from a `package.json` file.
function getLicenses(packageInfo) {
const licenses = packageInfo.licenses ? packageInfo.licenses.slice() : [];
@ -69,13 +75,9 @@ function getLicenses(packageInfo) {
* **Note**: JSDoc does not validate or normalize the contents of `package.json` files. If your
* `package.json` file does not follow the npm specification, some properties of the `Package`
* object may not use the format documented here.
*
* @alias module:@jsdoc/doclet.Package
*/
export class Package {
/**
* Creates an object that represents a package.
*
* @param {string} json - The contents of the `package.json` file.
* @param {Object} env - The JSDoc environment.
*/
@ -140,7 +142,7 @@ export class Package {
/**
* The contributors to this package.
*
* @type {Array<(module:@jsdoc/doclet.Package~PersonInfo|string)>}
* @type {Array.<(module:@jsdoc/doclet.Package~PersonInfo|string)>}
*/
this.contributors = packageInfo.contributors;
}
@ -193,7 +195,7 @@ export class Package {
* After JSDoc parses your input files, it sets this property to a list of paths to your input
* files.
*
* @type {Array<string>}
* @type {Array.<string>}
*/
this.files = [];
@ -210,7 +212,7 @@ export class Package {
/**
* Keywords to help users find the package.
*
* @type {Array<string>}
* @type {Array.<string>}
*/
this.keywords = packageInfo.keywords;
}
@ -220,7 +222,7 @@ export class Package {
* The licenses used by this package. Combines information from the `package.json` file's
* `license` property and the deprecated `licenses` property.
*
* @type {Array<module:@jsdoc/doclet.Package~LicenseInfo>}
* @type {Array.<module:@jsdoc/doclet.Package~LicenseInfo>}
*/
this.licenses = getLicenses(packageInfo);
}

View File

@ -34,12 +34,12 @@
"@jsdoc/ast": "^0.2.13",
"@jsdoc/name": "^0.1.1",
"@jsdoc/tag": "^0.2.13",
"common-path-prefix": "^3.0.0",
"@jsdoc/util": "^0.3.4",
"lodash": "^4.17.21",
"on-change": "^6.0.1",
"on-change": "^5.0.1",
"strip-bom": "^5.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
}
}

View File

@ -17,7 +17,7 @@
import doclet from '../../index.js';
import * as augment from '../../lib/augment.js';
import { resolveBorrows } from '../../lib/borrow.js';
import { Doclet } from '../../lib/doclet.js';
import { combineDoclets, Doclet } from '../../lib/doclet.js';
import { DocletStore } from '../../lib/doclet-store.js';
import { Package } from '../../lib/package.js';
import * as schema from '../../lib/schema.js';
@ -33,6 +33,12 @@ describe('@jsdoc/doclet', () => {
});
});
describe('combineDoclets', () => {
it('is lib/doclet.combineDoclets', () => {
expect(doclet.combineDoclets).toEqual(combineDoclets);
});
});
describe('Doclet', () => {
it('is lib/doclet.Doclet', () => {
expect(doclet.Doclet).toEqual(Doclet);

View File

@ -29,7 +29,7 @@ function makeDoclet(comment, meta, env) {
env ??= jsdoc.env;
doclet = new Doclet(`/**\n${comment.join('\n')}\n*/`, meta, env);
if (meta?._emitEvent !== false) {
env.emitter.emit('newDoclet', { doclet });
env.get('emitter').emit('newDoclet', { doclet });
}
return doclet;

View File

@ -29,6 +29,10 @@ describe('@jsdoc/doclet/lib/doclet', () => {
expect(doclet).toBeObject();
});
it('has a combineDoclets method', () => {
expect(doclet.combineDoclets).toBeFunction();
});
it('has a Doclet class', () => {
expect(doclet.Doclet).toBeFunction();
});
@ -37,6 +41,74 @@ describe('@jsdoc/doclet/lib/doclet', () => {
expect(doclet.WATCHABLE_PROPS).toBeArrayOfStrings();
});
describe('combineDoclets', () => {
it('overrides most properties of the secondary doclet', () => {
let descriptors;
const primaryDoclet = new Doclet('/** New and improved!\n@version 2.0.0 */', null, jsdoc.env);
const secondaryDoclet = new Doclet('/** Hello!\n@version 1.0.0 */', null, jsdoc.env);
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
descriptors = Object.getOwnPropertyDescriptors(newDoclet);
Object.keys(descriptors).forEach((property) => {
if (!descriptors[property].enumerable) {
return;
}
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
});
});
it('adds properties from the secondary doclet that are missing', () => {
const primaryDoclet = new Doclet('/** Hello!\n@version 2.0.0 */', null, jsdoc.env);
const secondaryDoclet = new Doclet('/** Hello! */', null, jsdoc.env);
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
expect(newDoclet.version).toBe('2.0.0');
});
describe('params and properties', () => {
const properties = ['params', 'properties'];
it('uses params and properties from the secondary doclet if the primary lacks them', () => {
const primaryDoclet = new Doclet('/** Hello! */', null, jsdoc.env);
const secondaryComment = [
'/**',
' * @param {string} foo - The foo.',
' * @property {number} bar - The bar.',
' */',
].join('\n');
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.env);
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
properties.forEach((property) => {
expect(newDoclet[property]).toEqual(secondaryDoclet[property]);
});
});
it('uses params and properties from the primary doclet, if present', () => {
const primaryComment = [
'/**',
' * @param {number} baz - The baz.',
' * @property {string} qux - The qux.',
' */',
].join('\n');
const primaryDoclet = new Doclet(primaryComment, null, jsdoc.env);
const secondaryComment = [
'/**',
' * @param {string} foo - The foo.',
' * @property {number} bar - The bar.',
' */',
].join('\n');
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.env);
const newDoclet = doclet.combineDoclets(primaryDoclet, secondaryDoclet);
properties.forEach((property) => {
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
});
});
});
});
describe('Doclet', () => {
function makeDoclet(tagStrings, env) {
const comment = `/**\n${tagStrings.join('\n')}\n*/`;
@ -233,86 +305,6 @@ describe('@jsdoc/doclet/lib/doclet', () => {
xit('TODO: write tests');
});
xdescribe('clone', () => {
xit('TODO: write tests');
});
describe('combineDoclets', () => {
it('overrides most properties of the secondary doclet', () => {
let descriptors;
const primaryDoclet = new Doclet(
'/** New and improved!\n@version 2.0.0 */',
null,
jsdoc.env
);
const secondaryDoclet = new Doclet('/** Hello!\n@version 1.0.0 */', null, jsdoc.env);
const newDoclet = Doclet.combineDoclets(primaryDoclet, secondaryDoclet);
descriptors = Object.getOwnPropertyDescriptors(newDoclet);
Object.keys(descriptors).forEach((property) => {
if (!descriptors[property].enumerable) {
return;
}
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
});
});
it('adds properties from the secondary doclet that are missing', () => {
const primaryDoclet = new Doclet('/** Hello!\n@version 2.0.0 */', null, jsdoc.env);
const secondaryDoclet = new Doclet('/** Hello! */', null, jsdoc.env);
const newDoclet = Doclet.combineDoclets(primaryDoclet, secondaryDoclet);
expect(newDoclet.version).toBe('2.0.0');
});
describe('params and properties', () => {
const properties = ['params', 'properties'];
it('uses params and properties from the secondary doclet if the primary lacks them', () => {
const primaryDoclet = new Doclet('/** Hello! */', null, jsdoc.env);
const secondaryComment = [
'/**',
' * @param {string} foo - The foo.',
' * @property {number} bar - The bar.',
' */',
].join('\n');
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.env);
const newDoclet = Doclet.combineDoclets(primaryDoclet, secondaryDoclet);
properties.forEach((property) => {
expect(newDoclet[property]).toEqual(secondaryDoclet[property]);
});
});
it('uses params and properties from the primary doclet, if present', () => {
const primaryComment = [
'/**',
' * @param {number} baz - The baz.',
' * @property {string} qux - The qux.',
' */',
].join('\n');
const primaryDoclet = new Doclet(primaryComment, null, jsdoc.env);
const secondaryComment = [
'/**',
' * @param {string} foo - The foo.',
' * @property {number} bar - The bar.',
' */',
].join('\n');
const secondaryDoclet = new Doclet(secondaryComment, null, jsdoc.env);
const newDoclet = Doclet.combineDoclets(primaryDoclet, secondaryDoclet);
properties.forEach((property) => {
expect(newDoclet[property]).toEqual(primaryDoclet[property]);
});
});
});
});
xdescribe('emptyDoclet', () => {
xit('TODO: write tests');
});
describe('isGlobal', () => {
it('identifies global constants', () => {
const newDoclet = makeDoclet(['@constant', '@global', '@name foo']);

View File

@ -11,15 +11,15 @@
"license": "Apache-2.0",
"main": "index.js",
"dependencies": {
"@babel/eslint-parser": "^7.28.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"@babel/eslint-parser": "^7.26.5",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^16.5.0"
"globals": "^15.14.0"
},
"peerDependencies": {
"eslint": "^9.39.1",
"prettier": "^3.7.4"
"eslint": "^9.18.0",
"prettier": "^3.4.2"
},
"publishConfig": {
"access": "public"
@ -41,7 +41,7 @@
}
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"gitHead": "81a824eb0968851f836fc6069376d0337775f9ba"
}

View File

@ -14,4 +14,10 @@
limitations under the License.
*/
/**
* Methods for working with namepaths in JSDoc.
*
* @module @jsdoc/name
*/
export * from './lib/name.js';

View File

@ -17,7 +17,7 @@
/**
* Methods for working with namepaths in JSDoc.
*
* @module @jsdoc/name
* @alias module:@jsdoc/name
*/
import escape from 'escape-string-regexp';
@ -27,6 +27,7 @@ import _ from 'lodash';
* Longnames that have a special meaning in JSDoc.
*
* @enum {string}
* @static
*/
export const LONGNAMES = {
/** Longname used for doclets that do not have a longname, such as anonymous functions. */
@ -46,89 +47,30 @@ export const MODULE_NAMESPACE = 'module:';
* Names and punctuation marks that identify doclet scopes.
*
* @enum {string}
* @static
*/
export const SCOPE = {
/**
* Scope names.
*/
NAMES: {
/**
* Global scope. The symbol is available globally.
*/
GLOBAL: 'global',
/**
* Inner scope. The symbol is available only within the enclosing scope.
*/
INNER: 'inner',
/**
* Instance scope. The symbol is available on instances of its parent.
*/
INSTANCE: 'instance',
/**
* Static scope. The symbol is a static property of its parent.
*/
STATIC: 'static',
},
/**
* Punctuation used in JSDoc namepaths to identify a symbol's scope.
*
* Global scope does not have a punctuation equivalent.
*/
PUNC: {
/**
* The abbreviation for inner scope: `~`
*/
INNER: '~',
/**
* The abbreviation for instance scope: `#`
*/
INSTANCE: '#',
/**
* The abbreviation for static scope: `.`
*/
STATIC: '.',
},
};
/**
* Doclet scope identifiers mapped to the equivalent punctuation.
*
* @enum {string}
*/
// Keys must be lowercase.
export const SCOPE_TO_PUNC = {
/**
* The abbreviation for inner scope.
*/
inner: SCOPE.PUNC.INNER,
/**
* The abbreviation for instance scope.
*/
instance: SCOPE.PUNC.INSTANCE,
/**
* The abbreviation for static scope.
*/
static: SCOPE.PUNC.STATIC,
};
/**
* Doclet scope punctuation mapped to the equivalent identifiers.
*
* @enum {string}
*/
export const PUNC_TO_SCOPE = {
/**
* The identifier for inner scope.
*/
'~': 'inner',
/**
* The identifier for instance scope.
*/
'#': 'instance',
/**
* The identifier for static scope.
*/
'.': 'static',
};
export const PUNC_TO_SCOPE = _.invert(SCOPE_TO_PUNC);
const SCOPE_PUNC = _.values(SCOPE.PUNC);
const SCOPE_PUNC_STRING = `[${SCOPE_PUNC.join()}]`;
@ -140,7 +82,7 @@ const REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`);
/**
* Checks whether a name appears to represent a complete longname that is a member of the specified
* Check whether a name appears to represent a complete longname that is a member of the specified
* parent.
*
* @example
@ -159,7 +101,7 @@ export function nameIsLongname(name, memberof) {
}
/**
* For names that identify a property of a prototype, replaces the `prototype` portion of the name
* For names that identify a property of a prototype, replace the `prototype` portion of the name
* with `#`, which indicates instance-level scope. For example, `Foo.prototype.bar` becomes
* `Foo#bar`.
*
@ -172,8 +114,7 @@ export function prototypeToPunc(name) {
return name;
}
// If there's a trailing open bracket ([), as in `Foo.prototype['bar']`, keep it.
return name.replace(/(?:^|\.)prototype(?:$|\.|(\[))/g, `${SCOPE.PUNC.INSTANCE}$1`);
return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
}
/**
@ -182,11 +123,11 @@ export function prototypeToPunc(name) {
* @param {string} name - The name to check.
* @returns {?string} The leading scope character, if one is present.
*/
export function getLeadingScope(name) {
export const getLeadingScope = (name) => {
const match = name.match(REGEXP_LEADING_SCOPE);
return match?.[1];
}
};
/**
* Gets the trailing scope character, if any, from a name.
@ -194,14 +135,14 @@ export function getLeadingScope(name) {
* @param {string} name - The name to check.
* @returns {?string} The trailing scope character, if one is present.
*/
export function getTrailingScope(name) {
export const getTrailingScope = (name) => {
const match = name.match(REGEXP_TRAILING_SCOPE);
return match?.[1];
}
};
/**
* Gets a symbol's basename, which is the first part of its full name before any scope punctuation.
* Get a symbol's basename, which is the first part of its full name before any scope punctuation.
* For example, all of the following names have the basename `Foo`:
*
* + `Foo`
@ -223,9 +164,7 @@ export function getBasename(name) {
}
// TODO: docs
export function stripNamespace(longname) {
return longname.replace(/^[a-zA-Z]+:/, '');
}
export const stripNamespace = (longname) => longname.replace(/^[a-zA-Z]+:/, '');
// TODO: docs
function slice(longname, sliceChars, forcedMemberof) {
@ -313,9 +252,8 @@ function slice(longname, sliceChars, forcedMemberof) {
};
}
// TODO: Document this with a typedef.
/**
* Given a longname like `a.b#c(2)`, this method splits it into the following parts:
* Given a longname like `a.b#c(2)`, split it into the following parts:
*
* + `longname`
* + `memberof`
@ -327,17 +265,12 @@ function slice(longname, sliceChars, forcedMemberof) {
* @param {string} forcedMemberof
* @returns {object} Representing the properties of the given name.
*/
export function toParts(longname, forcedMemberof) {
return slice(longname, null, forcedMemberof);
}
export const toParts = (longname, forcedMemberof) => slice(longname, null, forcedMemberof);
// TODO: docs
/**
* Applies a namespace to a longname.
*
* If the longname already has a namespace, then the namespace is not applied.
*
* @param {string} longname - The longname of the symbol.
* @param {string} ns - The namespace to be applied.
* @param {string} longname The full longname of the symbol.
* @param {string} ns The namespace to be applied.
* @returns {string} The longname with the namespace applied.
*/
export function applyNamespace(longname, ns) {
@ -354,11 +287,11 @@ export function applyNamespace(longname, ns) {
}
/**
* Checks whether a parent longname is an ancestor of a child longname.
* Check whether a parent longname is an ancestor of a child longname.
*
* @param {string} parent - The parent longname.
* @param {string} child - The child longname.
* @returns {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
* @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
*/
export function hasAncestor(parent, child) {
let parentIsAncestor = false;
@ -427,19 +360,19 @@ function splitLongname(longname, options) {
// TODO: Document this with a typedef.
// TODO: Add at least one or two basic tests, so we know if this completely breaks.
/**
* Converts an array of doclet longnames into a tree structure, optionally attaching doclets to the
* Convert an array of doclet longnames into a tree structure, optionally attaching doclets to the
* tree.
*
* Each level of the tree is an object with the following properties:
*
* + `longname {string}`: The longname.
* + `memberof {?string}`: The memberof.
* + `scope {?string}`: The longname's scope, represented as a punctuation mark (for example, `#`
* + `memberof {string?}`: The memberof.
* + `scope {string?}`: The longname's scope, represented as a punctuation mark (for example, `#`
* for instance and `.` for static).
* + `name {string}`: The short name.
* + `doclet {?Object}`: The doclet associated with the longname, or `null` if the doclet was not
* + `doclet {Object?}`: The doclet associated with the longname, or `null` if the doclet was not
* provided.
* + `children {?Object}`: The children of the current longname. Not present if there are no
* + `children {Object?}`: The children of the current longname. Not present if there are no
* children.
*
* For example, suppose you have the following array of doclet longnames:
@ -506,7 +439,7 @@ function splitLongname(longname, options) {
* @param {Object<string, module:@jsdoc/doclet.Doclet>} doclets - The doclets to attach to a tree.
* Each property should be the longname of a doclet, and each value should be the doclet for that
* longname.
* @returns {Object} A tree with information about each longname in the format shown above.
* @return {Object} A tree with information about each longname in the format shown above.
*/
export function longnamesToTree(longnames, doclets) {
const splitOptions = { includeVariation: false };
@ -548,14 +481,13 @@ export function longnamesToTree(longnames, doclets) {
return tree;
}
// TODO: Document this with a typedef.
/**
* Splits a string that starts with a name and ends with a description into its parts. Allows the
* Split a string that starts with a name and ends with a description into its parts. Allows the
* default value (if present) to contain brackets. Returns `null` if the name contains mismatched
* brackets.
*
* @param {string} nameDesc - The combined name and description.
* @returns {?Object} An object with `name` and `description` properties.
* @param {string} nameDesc
* @returns {?Object} Hash with "name" and "description" properties.
*/
function splitNameMatchingBrackets(nameDesc) {
const buffer = [];
@ -597,12 +529,10 @@ function splitNameMatchingBrackets(nameDesc) {
};
}
// TODO: Document this with a typedef.
/**
* Splits a string that starts with a name and ends with a description into separate parts.
*
* @param {string} str - The combined name and description.
* @returns {Object} An object with `name` and `description` properties.
* Split a string that starts with a name and ends with a description into separate parts.
* @param {string} str - The string that contains the name and description.
* @returns {object} An object with `name` and `description` properties.
*/
export function splitNameAndDescription(str) {
// Like: `name`, `[name]`, `name text`, `[name] text`, `name - text`, or `[name] - text`.
@ -610,7 +540,7 @@ export function splitNameAndDescription(str) {
// must be on the same line as the name.
let match;
// Optional values get special treatment.
// Optional values get special treatment,
let result = null;
if (str[0] === '[') {

View File

@ -35,7 +35,7 @@
"lodash": "^4.17.21"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"gitHead": "a595f7f2a525f32da9a0498a027667af3d938158"
}

View File

@ -115,63 +115,37 @@ function isModuleExports(module, doclet) {
return module.longname === doclet.name;
}
/**
* Finds an AST node's closest ancestor with the specified type.
*
* @private
* @param {Object} node - The AST node.
* @param {(module:@jsdoc/ast.Syntax|string)} ancestorType - The type of ancestor node to find.
* @return {?Object} The closest ancestor with the specified type.
*/
function findAncestorWithType(node, ancestorType) {
let parent = node?.parent;
while (parent) {
if (parent.type === ancestorType) {
return parent;
}
parent = parent.parent;
}
return null;
}
function setModuleScopeMemberOf(parser, doclet) {
const moduleInfo = getModule();
const node = doclet.meta?.code?.node;
let parentDoclet;
let skipMemberof;
// Handle module symbols, excluding CommonJS `module.exports`.
// Handle CommonJS module symbols that are _not_ assigned to `module.exports`.
if (moduleInfo && !isModuleExports(moduleInfo, doclet)) {
if (!doclet.scope) {
// is this a method definition? if so, we usually get the scope from the node directly
if (node?.type === Syntax.MethodDefinition) {
parentDoclet = parser._getDocletById(node.parent.parent.nodeId);
if (doclet.meta?.code?.node?.type === Syntax.MethodDefinition) {
parentDoclet = parser._getDocletById(doclet.meta.code.node.parent.parent.nodeId);
// special case for constructors of classes that have @alias tags
if (node.kind === 'constructor' && parentDoclet?.alias) {
if (doclet.meta.code.node.kind === 'constructor' && parentDoclet?.alias) {
// the constructor should use the same name as the class
doclet.addTag('alias', parentDoclet.alias);
doclet.addTag('name', parentDoclet.alias);
// and we shouldn't try to set a memberof value
skipMemberof = true;
} else {
doclet.addTag(node.static ? 'static' : 'instance');
doclet.addTag(doclet.meta.code.node.static ? 'static' : 'instance');
// The doclet should be a member of the parent doclet's alias.
if (parentDoclet?.alias) {
doclet.memberof = parentDoclet.alias;
}
}
}
// Is this something that the module exports? if so, it's a static member.
else if (
node?.type === Syntax.ExportNamedDeclaration ||
findAncestorWithType(node, Syntax.ExportNamedDeclaration)
) {
// is this something that the module exports? if so, it's a static member
else if (doclet.meta?.code?.node?.parent?.type === Syntax.ExportNamedDeclaration) {
doclet.addTag('static');
}
// Otherwise, it must be an inner member.
// otherwise, it must be an inner member
else {
doclet.addTag('inner');
}
@ -179,7 +153,7 @@ function setModuleScopeMemberOf(parser, doclet) {
// if the doclet isn't a memberof anything yet, and it's not a global, it must be a memberof
// the current module (unless we were told to skip adding memberof)
if (!skipMemberof && !doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL) {
if (!doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL && !skipMemberof) {
doclet.addTag('memberof', moduleInfo.longname);
}
}
@ -206,12 +180,12 @@ function addDoclet(parser, newDoclet) {
}
}
function processAlias(parser, doclet, node) {
function processAlias(parser, doclet, astNode) {
let match;
let memberofName;
if (doclet.alias === '{@thisClass}') {
memberofName = parser.resolveThis(node);
memberofName = parser.resolveThis(astNode);
// "class" refers to the owner of the prototype, not the prototype itself
match = memberofName.match(PROTOTYPE_OWNER_REGEXP);
@ -230,7 +204,7 @@ function isModuleObject(doclet) {
}
// TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, node, nameStartsWith, trailingPunc) {
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
const docletIsModuleObject = isModuleObject(doclet);
let memberof = '';
let nameAndPunc;
@ -263,7 +237,7 @@ function findSymbolMemberof(parser, doclet, node, nameStartsWith, trailingPunc)
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
} else {
memberof = parser.resolveThis(node);
memberof = parser.resolveThis(astNode);
// like the following at the top level of a module:
// this.foo = 1;
@ -281,7 +255,7 @@ function findSymbolMemberof(parser, doclet, node, nameStartsWith, trailingPunc)
};
}
function addSymbolMemberof(parser, doclet, node) {
function addSymbolMemberof(parser, doclet, astNode) {
let basename;
let memberof;
let memberofInfo;
@ -290,7 +264,7 @@ function addSymbolMemberof(parser, doclet, node) {
let scopePunc;
let unresolved;
if (!node) {
if (!astNode) {
return;
}
@ -305,7 +279,7 @@ function addSymbolMemberof(parser, doclet, node) {
unresolved = resolveTargetRegExp.exec(doclet.name);
if (unresolved) {
memberofInfo = findSymbolMemberof(parser, doclet, node, unresolved[1], unresolved[2]);
memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]);
memberof = memberofInfo.memberof;
scopePunc = memberofInfo.scopePunc;
@ -313,7 +287,7 @@ function addSymbolMemberof(parser, doclet, node) {
doclet.name = doclet.name ? memberof + scopePunc + doclet.name : memberof;
}
} else {
memberofInfo = parser.astnodeToMemberof(node);
memberofInfo = parser.astnodeToMemberof(astNode);
basename = memberofInfo.basename;
memberof = memberofInfo.memberof;
}

View File

@ -60,30 +60,6 @@ function getLastValue(set) {
return value;
}
function isClassMethodFromArrowFunction(node) {
return (
node.type === Syntax.MethodDefinition &&
node.parent.parent.parent?.type === Syntax.ArrowFunctionExpression
);
}
function isClassProperty(node) {
return node.type === Syntax.ClassPrivateProperty || node.type === Syntax.ClassProperty;
}
function isConstructor(node) {
return node.type === Syntax.MethodDefinition && node.kind === 'constructor';
}
function isFunctionOrVariableDeclarator({ type }) {
return (
type === Syntax.FunctionDeclaration ||
type === Syntax.FunctionExpression ||
type === Syntax.ArrowFunctionExpression ||
type === Syntax.VariableDeclarator
);
}
// TODO: docs
/**
* @alias module:jsdoc/src/parser.Parser
@ -348,14 +324,18 @@ export class Parser extends EventEmitter {
astnodeToMemberof(node) {
let basename;
let doclet;
let memberof;
let scope;
const result = {
basename: null,
memberof: null,
};
const result = {};
const type = node.type;
if (isFunctionOrVariableDeclarator(node) && node.enclosingScope) {
if (
(type === Syntax.FunctionDeclaration ||
type === Syntax.FunctionExpression ||
type === Syntax.ArrowFunctionExpression ||
type === Syntax.VariableDeclarator) &&
node.enclosingScope
) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (!doclet) {
@ -363,7 +343,7 @@ export class Parser extends EventEmitter {
} else {
result.memberof = doclet.longname + SCOPE.PUNC.INNER;
}
} else if (isClassProperty(node)) {
} else if (type === Syntax.ClassPrivateProperty || type === Syntax.ClassProperty) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (!doclet) {
@ -371,17 +351,21 @@ export class Parser extends EventEmitter {
} else {
result.memberof = doclet.longname + SCOPE.PUNC.INSTANCE;
}
} else if (isConstructor(node)) {
} else if (type === Syntax.MethodDefinition && node.kind === 'constructor') {
doclet = this._getDocletById(node.enclosingScope.nodeId);
// Global classes aren't a member of anything.
// global classes aren't a member of anything
if (doclet.memberof) {
result.memberof = doclet.memberof + SCOPE.PUNC.INNER;
}
}
// Special case for methods in classes that are returned by arrow function expressions. For
// other method declarations, we get the memberof from the node name elsewhere.
else if (isClassMethodFromArrowFunction(node)) {
// special case for methods in classes that are returned by arrow function expressions; for
// other method definitions, we get the memberof from the node name elsewhere. yes, this is
// confusing...
else if (
type === Syntax.MethodDefinition &&
node.parent.parent.parent?.type === Syntax.ArrowFunctionExpression
) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (doclet) {
@ -389,25 +373,35 @@ export class Parser extends EventEmitter {
doclet.longname + (node.static === true ? SCOPE.PUNC.STATIC : SCOPE.PUNC.INSTANCE);
}
} else {
// check local references for aliases
scope = node;
basename = getBasename(astNode.nodeToValue(node));
memberof = this._getMemberofFromScopes(node, basename);
if (memberof) {
result.basename = basename;
result.memberof = memberof;
// walk up the scope chain until we find the scope in which the node is defined
while (scope.enclosingScope) {
doclet = this._getDocletById(scope.enclosingScope.nodeId);
if (doclet && definedInScope(doclet, basename)) {
result.memberof = doclet.meta.vars[basename];
result.basename = basename;
break;
} else {
// move up
scope = scope.enclosingScope;
}
}
// Do we know that it's a global?
// do we know that it's a global?
doclet = this._getDocletByLongname(LONGNAMES.GLOBAL);
if (doclet && definedInScope(doclet, basename)) {
result.basename = basename;
result.memberof = doclet.meta.vars[basename];
result.basename = basename;
} else {
doclet = this._getDocletById(node.parent.nodeId);
// Set the result if we found a doclet. (If we didn't, then the AST node might represent a
// set the result if we found a doclet. (if we didn't, the AST node may describe a
// global symbol.)
if (doclet) {
result.memberof = doclet.longname ?? doclet.name;
result.memberof = doclet.longname || doclet.name;
}
}
}
@ -459,26 +453,6 @@ export class Parser extends EventEmitter {
return isClass(doclet) ? doclet : null;
}
_getMemberofFromScopes(node, basename) {
let doclet;
let memberof;
let scope = node;
// Walk up the scope chain until we find the scope in which the node is declared.
while (scope.enclosingScope) {
doclet = this._getDocletById(scope.enclosingScope.nodeId);
if (doclet && definedInScope(doclet, basename)) {
memberof = doclet.meta.vars[basename];
break;
} else {
// Move up.
scope = scope.enclosingScope;
}
}
return memberof;
}
// TODO: docs
/**
* Resolve what "this" refers to relative to a node.

View File

@ -17,7 +17,7 @@
import path from 'node:path';
import { astNode, Syntax } from '@jsdoc/ast';
import { Doclet } from '@jsdoc/doclet';
import { combineDoclets } from '@jsdoc/doclet';
import * as name from '@jsdoc/name';
const { getBasename, LONGNAMES } = name;
@ -333,7 +333,7 @@ function makeConstructorFinisher(parser) {
// We prefer the parent doclet because it has the correct kind, longname, and memberof.
// The child doclet might or might not have the correct kind, longname, and memberof.
combined = Doclet.combineDoclets(parentDoclet, eventDoclet, parser.env);
combined = combineDoclets(parentDoclet, eventDoclet, parser.env);
parser.addResult(combined);
parentDoclet.undocumented = eventDoclet.undocumented = true;

View File

@ -16,10 +16,11 @@
"@jsdoc/ast": "^0.2.13",
"@jsdoc/doclet": "^0.2.13",
"@jsdoc/name": "^0.1.1",
"@jsdoc/util": "^0.3.4",
"escape-string-regexp": "^5.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"repository": {
"type": "git",

View File

@ -20,7 +20,7 @@ import fs from 'node:fs';
import path from 'node:path';
import { Syntax, Walker } from '@jsdoc/ast';
import { Doclet } from '@jsdoc/doclet';
import { combineDoclets, Doclet } from '@jsdoc/doclet';
import { attachTo } from '../../../lib/handlers.js';
import * as jsdocParser from '../../../lib/parser.js';
@ -156,7 +156,7 @@ describe('@jsdoc/parse/lib/parser', () => {
const sourceCode = 'javascript:/** @class */function Foo() {}';
function handler(e) {
e.doclet = Doclet.combineDoclets(e.doclet, new Doclet('', {}, jsdoc.env));
e.doclet = combineDoclets(e.doclet, new Doclet('', {}, jsdoc.env));
e.doclet.foo = 'bar';
}

View File

@ -28,6 +28,13 @@
}
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"devDependencies": {
"@jsdoc/core": "^0.5.10",
"@jsdoc/parse": "^0.3.13"
},
"dependencies": {
"@jsdoc/util": "^0.3.4"
}
}

View File

@ -11,9 +11,9 @@
"license": "Apache-2.0",
"main": "index.js",
"peerDependencies": {
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"prettier": "^3.7.4"
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"prettier": "^3.4.2"
},
"publishConfig": {
"access": "public"

View File

@ -40,9 +40,9 @@
"catharsis": "^0.11.0",
"common-path-prefix": "^3.0.0",
"lodash": "^4.17.21",
"memize": "^2.1.1"
"memize": "^2.1.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
}
}

View File

@ -13,7 +13,6 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
import dependencyGraph from 'dependency-graph';
import Emittery from 'emittery';
import _ from 'lodash';
@ -24,28 +23,22 @@ import v from './validators.js';
const { DepGraph } = dependencyGraph;
export class TaskRunner extends Emittery {
#context;
#deps;
#error;
#nameToTask;
#queue;
#running;
#taskToName;
#unsubscribers;
// Work around an annoying typo in a method name.
DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
export class TaskRunner extends Emittery {
constructor(context) {
super();
ow(context, ow.optional.object);
this.#init(context);
this._init(context);
}
#addOrRemoveTasks(tasks, func, action) {
_addOrRemoveTasks(tasks, func, action) {
func = _.bind(func, this);
if (_.isArray(tasks)) {
if (Array.isArray(tasks)) {
tasks.forEach((task, i) => {
try {
func(task);
@ -54,7 +47,7 @@ export class TaskRunner extends Emittery {
throw e;
}
});
} else if (_.isObject(tasks)) {
} else if (tasks !== null && typeof tasks === 'object') {
for (const task of Object.keys(tasks)) {
try {
func(tasks[task]);
@ -66,7 +59,7 @@ export class TaskRunner extends Emittery {
}
}
#addTaskEmitters(task) {
_addTaskEmitters(task) {
const u = {};
u.start = task.on('start', (t) => this.emit('taskStart', t));
@ -77,56 +70,56 @@ export class TaskRunner extends Emittery {
error: e.error,
});
if (!this.#error) {
this.#error = e.error;
if (!this._error) {
this._error = e.error;
}
});
this.#unsubscribers.set(task.name, u);
this._unsubscribers.set(task.name, u);
}
#bindTaskFunc(task) {
return _.bind(task.run, task, this.#context);
_bindTaskFunc(task) {
return _.bind(task.run, task, this._context);
}
#createTaskSequence(tasks) {
_createTaskSequence(tasks) {
if (!tasks.length) {
return null;
}
return () =>
tasks.reduce((p, taskName) => {
const task = this.#nameToTask.get(taskName);
const task = this._nameToTask.get(taskName);
return p.then(this.#bindTaskFunc(task), (e) => Promise.reject(e));
return p.then(this._bindTaskFunc(task), (e) => Promise.reject(e));
}, Promise.resolve());
}
#init(context) {
this.#context = context;
this.#deps = new Map();
this.#error = null;
this.#nameToTask = new Map();
this.#queue = new Queue();
this.#running = false;
this.#taskToName = new WeakMap();
this.#unsubscribers = new Map();
_init(context) {
this._context = context;
this._deps = new Map();
this._error = null;
this._queue = new Queue();
this._taskToName = new WeakMap();
this._nameToTask = new Map();
this._running = false;
this._unsubscribers = new Map();
this.#queue.pause();
this._queue.pause();
}
#newDependencyCycleError(cyclePath) {
_newDependencyCycleError(cyclePath) {
return new v.DependencyCycleError(
`Tasks have circular dependencies: ${cyclePath.join(' > ')}`,
cyclePath
);
}
#newStateError() {
_newStateError() {
return new v.StateError('The task runner is already running.');
}
#newUnknownDepsError(dependent, unknownDeps) {
_newUnknownDepsError(dependent, unknownDeps) {
let errorText;
if (unknownDeps.length === 1) {
@ -140,28 +133,28 @@ export class TaskRunner extends Emittery {
);
}
#orderTasks() {
_orderTasks() {
let error;
const graph = new DepGraph();
let parallel;
let sequential;
for (const [task] of this.#nameToTask) {
for (const [task] of this._nameToTask) {
graph.addNode(task);
}
for (const [dependent] of this.#deps) {
for (const [dependent] of this._deps) {
const unknownDeps = [];
for (const dependency of this.#deps.get(dependent)) {
if (!this.#nameToTask.has(dependency)) {
for (const dependency of this._deps.get(dependent)) {
if (!this._nameToTask.has(dependency)) {
unknownDeps.push(dependency);
} else {
graph.addDependency(dependent, dependency);
}
if (unknownDeps.length) {
error = this.#newUnknownDepsError(dependency, unknownDeps);
error = this._newUnknownDepsError(dependency, unknownDeps);
break;
}
}
@ -174,7 +167,7 @@ export class TaskRunner extends Emittery {
// Get tasks with dependencies, in a correctly ordered list.
sequential = graph.overallOrder().filter((task) => !parallel.includes(task));
} catch (e) {
error = this.#newDependencyCycleError(e.cyclePath);
error = this._newDependencyCycleError(e.cyclePath);
}
}
@ -185,84 +178,92 @@ export class TaskRunner extends Emittery {
};
}
#rejectIfRunning() {
_rejectIfRunning() {
if (this.running) {
return Promise.reject(this.#newStateError());
return Promise.reject(this._newStateError());
}
return null;
}
#throwIfRunning() {
_throwIfRunning() {
if (this.running) {
throw this.#newStateError();
throw this._newStateError();
}
}
_throwIfUnknownDeps(dependent, unknownDeps) {
if (!unknownDeps.length) {
return;
}
throw this._newUnknownDepsError(dependent, unknownDeps);
}
addTask(task) {
ow(task, v.checkTaskOrString);
this.#throwIfRunning();
this._throwIfRunning();
this.#nameToTask.set(task.name, task);
this._nameToTask.set(task.name, task);
if (task.dependsOn) {
this.#deps.set(task.name, task.dependsOn);
this._deps.set(task.name, task.dependsOn);
}
this.#taskToName.set(task, task.name);
this.#addTaskEmitters(task);
this._taskToName.set(task, task.name);
this._addTaskEmitters(task);
return this;
}
addTasks(tasks) {
ow(tasks, ow.any(ow.array, ow.object));
this.#addOrRemoveTasks(tasks, this.addTask, 'add');
this._addOrRemoveTasks(tasks, this.addTask, 'add');
return this;
}
end() {
this.emit('end', {
error: this.#error,
error: this._error,
});
this.#queue.clear();
this.#init();
this._queue.clear();
this._init();
}
removeTask(task) {
let unsubscribers;
ow(task, v.checkTaskOrString);
this.#throwIfRunning();
this._throwIfRunning();
if (_.isString(task)) {
task = this.#nameToTask.get(task);
if (typeof task === 'string') {
task = this._nameToTask.get(task);
if (!task) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
} else if (_.isObject(task)) {
if (!this.#taskToName.has(task)) {
} else if (typeof task === 'object') {
if (!this._taskToName.has(task)) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
}
this.#nameToTask.delete(task.name);
this.#taskToName.delete(task);
this.#deps.delete(task.name);
this._nameToTask.delete(task.name);
this._taskToName.delete(task);
this._deps.delete(task.name);
unsubscribers = this.#unsubscribers.get(task.name);
unsubscribers = this._unsubscribers.get(task.name);
for (const u of Object.keys(unsubscribers)) {
unsubscribers[u]();
}
this.#unsubscribers.delete(task.name);
this._unsubscribers.delete(task.name);
return this;
}
removeTasks(tasks) {
ow(tasks, ow.any(ow.array, ow.object));
this.#addOrRemoveTasks(tasks, this.removeTask, 'remove');
this._addOrRemoveTasks(tasks, this.removeTask, 'remove');
return this;
}
@ -271,13 +272,13 @@ export class TaskRunner extends Emittery {
ow(context, ow.optional.object);
let endPromise;
const { error, parallel, sequential } = this.#orderTasks();
const { error, parallel, sequential } = this._orderTasks();
let runningPromise;
let taskFuncs = [];
let taskSequence;
// First, fail if the runner is already running.
runningPromise = this.#rejectIfRunning();
runningPromise = this._rejectIfRunning();
if (runningPromise) {
return runningPromise;
}
@ -287,23 +288,23 @@ export class TaskRunner extends Emittery {
return Promise.reject(error);
}
this.#context = context || this.#context;
this._context = context || this._context;
for (const taskName of parallel) {
taskFuncs.push(this.#bindTaskFunc(this.#nameToTask.get(taskName)));
taskFuncs.push(this._bindTaskFunc(this._nameToTask.get(taskName)));
}
taskSequence = this.#createTaskSequence(sequential);
taskSequence = this._createTaskSequence(sequential);
if (taskSequence) {
taskFuncs.push(taskSequence);
}
endPromise = this.#queue.addAll(taskFuncs).then(
endPromise = this._queue.addAll(taskFuncs).then(
() => {
this.end();
if (this.#error) {
return Promise.reject(this.#error);
if (this._error) {
return Promise.reject(this._error);
} else {
return Promise.resolve();
}
@ -316,13 +317,13 @@ export class TaskRunner extends Emittery {
);
this.emit('start');
this.#running = true;
this._running = true;
try {
this.#queue.start();
this._queue.start();
return endPromise;
} catch (e) {
this.#error = e;
this._error = e;
this.end();
return Promise.reject(e);
@ -330,13 +331,13 @@ export class TaskRunner extends Emittery {
}
get running() {
return this.#running;
return this._running;
}
get tasks() {
const entries = [];
for (const entry of this.#nameToTask.entries()) {
for (const entry of this._nameToTask.entries()) {
entries.push(entry);
}

View File

@ -33,11 +33,11 @@
},
"dependencies": {
"dependency-graph": "^1.0.0",
"emittery": "^1.2.0",
"ow": "^3.1.1",
"p-queue": "^9.0.1"
"emittery": "^1.0.3",
"ow": "^2.0.0",
"p-queue": "^8.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
}
}

View File

@ -720,26 +720,6 @@ export function getSignatureReturns({ yields, returns }, cssClass) {
return returnTypes;
}
// Cache for memberof index to speed up ancestor lookups
let memberofIndex = null;
function buildMemberofIndex(data) {
if (memberofIndex) {
return memberofIndex;
}
memberofIndex = new Map();
const allDoclets = data().get();
for (const doclet of allDoclets) {
if (doclet.longname) {
memberofIndex.set(doclet.longname, doclet);
}
}
return memberofIndex;
}
/**
* Retrieve an ordered list of doclets for a symbol's ancestors.
*
@ -753,13 +733,9 @@ export function getAncestors(data, doclet) {
let doc = doclet;
let previousDoc;
// Build index once for all lookups
const index = buildMemberofIndex(data);
while (doc) {
previousDoc = doc;
// Use index instead of database query
doc = index.get(doc.memberof);
doc = find(data, { longname: doc.memberof })[0];
// prevent infinite loop that can be caused by duplicated module definitions
if (previousDoc === doc) {

View File

@ -8,15 +8,16 @@
"author": "Jeff Williams <jeffrey.l.williams@gmail.com>",
"homepage": "https://github.com/jsdoc/jsdoc",
"license": "Apache-2.0",
"main": "publish.js",
"main": "index.js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fontsource-variable/open-sans": "^5.2.7",
"@fontsource-variable/open-sans": "^5.1.1",
"@jsdoc/name": "^0.1.1",
"@jsdoc/salty": "^0.2.9",
"@jsdoc/tag": "^0.2.13",
"@jsdoc/util": "^0.3.4",
"catharsis": "^0.11.0",
"code-prettify": "^0.1.0",
"color-themes-for-google-code-prettify": "^2.0.4",
@ -27,7 +28,7 @@
"markdown-it-anchor": "^9.2.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"repository": {
"type": "git",

View File

@ -29,6 +29,6 @@
"lodash": "^4.17.21"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
}
}

View File

@ -19,8 +19,9 @@
*
* @module @jsdoc/util
*/
import EventBus from './lib/bus.js';
import cast from './lib/cast.js';
import getLogFunctions from './lib/log.js';
export { cast, getLogFunctions };
export default { cast, getLogFunctions };
export { cast, EventBus, getLogFunctions };
export default { cast, EventBus, getLogFunctions };

View File

@ -0,0 +1,65 @@
/*
Copyright 2019 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 _ from 'lodash';
import ow from 'ow';
let cache = {};
/**
* An event bus that works the same way as a standard Node.js event emitter, with a few key
* differences:
*
* + When you create an event bus, you must specify its name. Consider using your package's name.
* + Event buses are cached and shared by default. If module A and module B both create an event bus
* called `foo`, both modules get the same event bus. This behavior makes it easier to share one
* event bus among all of the modules in your package.
*
* To prevent a new event bus from being cached and shared, set the `opts.cache` property to
* `false` when you create the event bus. Setting this property to `false` also forces a new
* event bus to be created, even if there's a cached event bus with the same name.
*
* @alias module:@jsdoc/util.EventBus
* @extends module:events.EventEmitter
*/
export default class EventBus extends EventEmitter {
/**
* Create a new event bus, or retrieve the cached event bus for the ID you specify.
*
* @param {(string|Symbol)} id - The ID for the event bus.
* @param {Object} opts - Options for the event bus.
* @param {boolean} [opts.cache=true] - Set to `false` to prevent the event bus from being
* cached, and to return a new event bus even if there is already an event bus with the same ID.
*/
constructor(id, opts = {}) {
super();
ow(id, ow.any(ow.string, ow.symbol));
const shouldCache = _.isBoolean(opts.cache) ? opts.cache : true;
if (Object.hasOwn(cache, id) && shouldCache) {
return cache[id];
}
this._id = id;
if (shouldCache) {
cache[id] = this;
}
}
}

View File

@ -16,33 +16,6 @@
export const LOG_TYPES = ['debug', 'error', 'info', 'fatal', 'verbose', 'warn'];
/**
* Logging functions for JSDoc.
*
* @typedef {Object} module:@jsdoc/util~logFunctions
* @property {function(...*)} debug - The `debug` logging function.
* @property {function(...*)} error - The `error` logging function.
* @property {function(...*)} info - The `info` logging function.
* @property {function(...*)} fatal - The `fatal` logging function.
* @property {function(...*)} verbose - The `verbose` logging function.
* @property {function(...*)} warn - The `warn` logging function.
*/
/**
* Creates shared logging functions for JSDoc.
*
* Calling a logging function has the following effects:
*
* + The specified `emitter` emits an event with the name `logger:LOG_TYPE`, where `LOG_TYPE` is a
* value like `debug` or `verbose`.
* + If JSDoc's CLI is running, and if the user asked to see log messages of the specified type,
* then the message is written to the console.
*
* @alias module:@jsdoc/util.getLogFunctions
* @param {node:events} emitter - The event emitter to use. In general, you should use the emitter
* stored in {@link module:@jsdoc/core.Env#emitter Env#emitter}.
* @returns {module:@jsdoc/util~logFunctions} The logging functions.
*/
export default function getLogFunctions(emitter) {
const logFunctions = {};

View File

@ -30,8 +30,12 @@
"import": "./lib/*"
}
},
"dependencies": {
"lodash": "^4.17.21",
"ow": "^2.0.0"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"gitHead": "a595f7f2a525f32da9a0498a027667af3d938158"
}

View File

@ -15,8 +15,8 @@
*/
import util from '../../index.js';
import bus from '../../lib/bus.js';
import cast from '../../lib/cast.js';
import log from '../../lib/log.js';
describe('@jsdoc/util', () => {
it('is an object', () => {
@ -29,9 +29,9 @@ describe('@jsdoc/util', () => {
});
});
describe('getLogFunctions', () => {
it('is lib/log', () => {
expect(util.getLogFunctions).toEqual(log);
describe('EventBus', () => {
it('is lib/bus', () => {
expect(util.EventBus).toEqual(bus);
});
});
});

View File

@ -0,0 +1,83 @@
/*
Copyright 2019 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 EventBus from '../../../lib/bus.js';
describe('@jsdoc/util/lib/bus', () => {
const ignoreCache = { cache: false };
it('inherits from EventEmitter', () => {
expect(new EventBus('foo', ignoreCache) instanceof EventEmitter).toBeTrue();
});
it('accepts a string for the ID', () => {
function makeBus() {
return new EventBus('foo', ignoreCache);
}
expect(makeBus).not.toThrow();
});
it('accepts a Symbol for the ID', () => {
function makeBus() {
return new EventBus(Symbol('foo'), ignoreCache);
}
expect(makeBus).not.toThrow();
});
it('throws on bad IDs', () => {
function crashBus() {
return new EventBus(true, ignoreCache);
}
expect(crashBus).toThrowError();
});
it('uses a cache by default', () => {
let fired = false;
const id = Symbol('cache-test');
const bus1 = new EventBus(id);
const bus2 = new EventBus(id);
bus1.once('foo', () => {
fired = true;
});
bus2.emit('foo');
expect(bus1).toBe(bus2);
expect(fired).toBeTrue();
});
it('ignores the cache when asked', () => {
let fired = false;
const id = Symbol('cache-test');
const bus1 = new EventBus(id, ignoreCache);
const bus2 = new EventBus(id, ignoreCache);
bus1.once('foo', () => {
fired = true;
});
bus2.emit('foo');
expect(bus1).not.toBe(bus2);
expect(fired).toBeFalse();
});
});

View File

@ -2,7 +2,7 @@
"name": "jsdoc",
"private": true,
"version": "5.0.0-dev.19",
"revision": "1748665787180",
"revision": "1667509813080",
"description": "An API documentation generator for JavaScript.",
"keywords": [
"documentation",
@ -15,10 +15,18 @@
},
"dependencies": {
"@jsdoc/cli": "^0.3.12",
"strip-bom": "^5.0.0"
"@jsdoc/core": "^0.5.10",
"@jsdoc/doclet": "^0.2.13",
"@jsdoc/parse": "^0.3.13",
"@jsdoc/tag": "^0.2.13",
"@jsdoc/template-legacy": "^0.1.13",
"@jsdoc/util": "^0.3.4",
"lodash": "^4.17.21",
"strip-bom": "^5.0.0",
"strip-json-comments": "^5.0.1"
},
"engines": {
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
"node": ">=v18.12.0"
},
"type": "module",
"bin": {

View File

@ -1,13 +0,0 @@
/** @module icecream */
/**
* Ice cream flavors.
*
* @enum
*/
export const FLAVORS = {
/** Vanilla. */
VANILLA: 0,
/** Chocolate. */
CHOCOLATE: 1,
};

View File

@ -1,7 +0,0 @@
/**
* The bar namespace.
*
* @namespace
* @memberof module:foo
*/
export const bar = {};

View File

@ -12,6 +12,3 @@ var prototype = {
/** document me */
var hasOwnProperty = Object.prototype.hasOwnProperty;
/** document me */
function prototypeMethod() {}

View File

@ -1,27 +0,0 @@
/*
Copyright 2025 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.
*/
describe('symbols exported by an ES2015 module', () => {
const docSet = jsdoc.getDocSetFromFile('test/fixtures/moduleexport.js');
it('uses the correct scopes when exported objects have properties', () => {
const chocolate = docSet.getByLongname('module:icecream.FLAVORS.CHOCOLATE')[0];
const vanilla = docSet.getByLongname('module:icecream.FLAVORS.VANILLA')[0];
expect(chocolate).toBeObject();
expect(vanilla).toBeObject();
});
});

View File

@ -1,26 +0,0 @@
/*
Copyright 2025 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.
*/
describe('memberof a module', () => {
const docSet = jsdoc.getDocSetFromFile('test/fixtures/modulememberof.js');
it('uses the correct name and longname for an exported symbol with a @memberof tag', () => {
const bar = docSet.getByLongname('module:foo.bar').filter((d) => !d.undocumented)[0];
expect(bar).toBeObject();
expect(bar.name).toBe('bar');
});
});

View File

@ -20,7 +20,6 @@ describe('documenting symbols with special names', () => {
const hasOwnProp = docSet.getByLongname('hasOwnProperty')[0];
const proto = docSet.getByLongname('prototype')[0];
const protoValueOf = docSet.getByLongname('prototype.valueOf')[0];
const protoMethod = docSet.getByLongname('prototypeMethod')[0];
it('When a symbol is named "constructor", the symbol should appear in the docs.', () => {
expect(construct).toBeObject();
@ -41,8 +40,4 @@ describe('documenting symbols with special names', () => {
it('When a symbol is named "prototype", its members are resolved correctly.', () => {
expect(protoValueOf).toBeObject();
});
it('preserves names that start with `prototype`', () => {
expect(protoMethod).toBeObject();
});
});