Compare commits

...

154 Commits

Author SHA1 Message Date
Jeff Williams
49dbb37f74
chore: remove Slack link 2025-12-08 09:06:46 -08:00
renovate[bot]
ea821957e1
chore(deps): update dependency knip to ^5.72.0 2025-12-08 10:12:11 +00:00
renovate[bot]
4120dae2de
chore(deps): update dependency prettier to ^3.7.4 2025-12-03 05:52:17 +00:00
renovate[bot]
a836bb623d
chore(deps): update dependency jasmine to ^5.13.0 2025-12-02 01:56:13 +00:00
renovate[bot]
46678337d8
chore(deps): update dependency knip to ^5.71.0 2025-12-01 09:56:12 +00:00
renovate[bot]
7d8f083f6e
chore(deps): update dependency execa to ^9.6.1 2025-11-29 22:54:37 +00:00
renovate[bot]
e668135e77
chore(deps): update dependency prettier to ^3.7.3 2025-11-29 18:33:01 +00:00
renovate[bot]
3cebdc8c90
chore(deps): update dependency prettier to ^3.7.2 2025-11-29 02:42:16 +00:00
Jeff Williams
ace19d938f
docs(jsdoc-name): improve API docs 2025-11-28 15:17:24 -08:00
renovate[bot]
77e39cf892
chore(deps): update dependency prettier to ^3.7.1 2025-11-27 15:55:45 +00:00
renovate[bot]
2ea0705c55
chore(deps): update dependency knip to ^5.70.2 2025-11-24 21:45:08 +00:00
Jeff Williams
a14e7c6b7a
chore(deps): update lockfile 2025-11-23 15:56:11 -08:00
renovate[bot]
892ff706c7
chore(deps): update actions/checkout action to v6 2025-11-20 18:26:17 +00:00
renovate[bot]
acb9ced243
chore(deps): update dependency knip to ^5.70.1 2025-11-19 21:46:44 +00:00
renovate[bot]
cd24b82d29
chore(deps): update dependency knip to ^5.70.0 2025-11-19 05:30:45 +00:00
renovate[bot]
ea8b08db71
fix(deps): update dependency p-queue to ^9.0.1 2025-11-19 00:35:54 +00:00
Jeff Williams
ee72a9d3e8
chore(deps): update lockfile 2025-11-15 15:32:52 -08:00
renovate[bot]
9938011597
chore(deps): update dependency knip to ^5.69.1 2025-11-12 21:13:22 +00:00
renovate[bot]
5ced5a3d45
chore(deps): update dependency knip to ^5.69.0 2025-11-11 07:08:59 +00:00
renovate[bot]
7a30116c1b
chore(deps): update dependency knip to ^5.68.0 2025-11-06 22:41:30 +00:00
renovate[bot]
2979bf9559
chore(deps): update dependency eslint to ^9.39.1 2025-11-03 22:26:36 +00:00
renovate[bot]
279529a97f
chore(deps): update dependency knip to ^5.67.1 2025-11-03 11:54:38 +00:00
renovate[bot]
0f899c01a9
chore(deps): update dependency knip to ^5.67.0 2025-11-02 20:41:13 +00:00
renovate[bot]
c804735739
fix(deps): update dependency globals to ^16.5.0 2025-11-01 10:45:56 +00:00
renovate[bot]
4b5d017dd2
chore(deps): update dependency eslint to ^9.39.0 2025-10-31 22:54:55 +00:00
renovate[bot]
59fcecdd04
chore(deps): update dependency knip to ^5.66.4 2025-10-28 16:06:17 +00:00
renovate[bot]
083286c59f
fix(deps): update dependency on-change to ^6.0.1 2025-10-27 05:54:13 +00:00
renovate[bot]
78eada9c47
chore(deps): update dependency knip to ^5.66.3 2025-10-25 10:15:10 +00:00
renovate[bot]
9495d511e2
fix(deps): update babel monorepo to ^7.28.5 2025-10-23 17:01:42 +00:00
renovate[bot]
9ff5121bf8
chore(deps): update dependency knip to ^5.66.2 2025-10-20 17:00:36 +00:00
renovate[bot]
d93065705c
chore(deps): update dependency knip to ^5.66.1 2025-10-19 21:09:10 +00:00
renovate[bot]
cdc9fe4eba
chore(deps): update dependency eslint to ^9.38.0 2025-10-18 03:46:10 +00:00
renovate[bot]
73fa170135
chore(deps): update dependency knip to ^5.66.0 2025-10-17 02:39:56 +00:00
renovate[bot]
291434048c
fix(deps): update dependency ow to ^3.1.1 2025-10-16 22:29:43 +00:00
renovate[bot]
aac6f335cb
chore(deps): update actions/setup-node action to v6 2025-10-14 04:55:16 +00:00
renovate[bot]
1c2aea88ba
chore(deps): update dependency hereby to ^1.11.1 2025-10-13 18:06:47 +00:00
renovate[bot]
83eba455e1
chore(deps): update dependency knip to ^5.65.0 2025-10-13 14:43:52 +00:00
renovate[bot]
9260ddd6a4
chore(deps): update dependency knip to ^5.64.3 2025-10-10 09:02:20 +00:00
renovate[bot]
beb6639522
fix(deps): update dependency ow to ^3.1.0 2025-10-10 05:30:22 +00:00
mrdoob
de12f99a42
Replaced expensive database queries in getAncestors() with O(1) Map lookups (#2150) 2025-10-07 19:46:14 -07:00
renovate[bot]
6fb7ec895e
chore(deps): update dependency knip to ^5.64.2 2025-10-06 09:35:00 +00:00
renovate[bot]
c099ed70e5
chore(deps): update dependency jasmine to ^5.12.0 2025-10-05 20:44:11 +00:00
Jeff Williams
053ace645b
fix(jsdoc-name): don't mangle names like prototypeMethod 2025-10-05 10:03:42 -07:00
Jeff Williams
13cdfd18b7
fix: use the correct name and longname for an exported symbol with a @memberof tag 2025-10-04 17:40:13 -07:00
renovate[bot]
4555ff0b68
chore(deps): update dependency eslint to ^9.37.0 2025-10-03 21:44:28 +00:00
renovate[bot]
74d3b73eb5
fix(deps): update dependency p-queue to v9 2025-10-01 21:30:51 +00:00
Jeff Williams
2a2d950b56
chore(deps): clean up dependencies
* Remove unused dependencies
* Add missing dependencies
* Add `dependency-cleanup` script to check dependencies with [Knip](https://knip.dev/)
2025-09-30 20:48:29 -07:00
Jeff Williams
c076b5d8f1
docs: add and improve JSDoc comments
Plus some minor refactoring.
2025-09-28 12:53:50 -07:00
renovate[bot]
f1e9667815
chore(deps): update dependency jasmine to ^5.11.0 2025-09-27 02:09:16 +00:00
renovate[bot]
141e27df09
fix(deps): update dependency on-change to v6 2025-09-23 18:39:16 +00:00
renovate[bot]
e8e7350d93
chore(deps): update dependency eslint to ^9.36.0 2025-09-19 17:09:41 +00:00
renovate[bot]
e97ca7592b
fix(deps): update dependency @fontsource-variable/open-sans to ^5.2.7 2025-09-17 10:04:50 +00:00
renovate[bot]
efc8dac1a9
fix(deps): update dependency ow to v3 2025-09-10 02:26:45 +00:00
renovate[bot]
a6307578e5
fix(deps): update dependency globals to ^16.4.0 2025-09-09 20:40:45 +00:00
renovate[bot]
9705c7295e
fix(deps): update dependency p-queue to ^8.1.1 2025-09-07 16:51:55 +00:00
renovate[bot]
c51b539b57
chore(deps): update dependency eslint to ^9.35.0 2025-09-05 21:51:55 +00:00
renovate[bot]
5ec5199263
fix(deps): update babel monorepo to ^7.28.4 2025-09-05 18:02:54 +00:00
renovate[bot]
9f1a442581
chore(deps): update actions/setup-node action to v5 2025-09-04 05:41:55 +00:00
renovate[bot]
944ad72964
chore(deps): update dependency jasmine to ^5.10.0 2025-08-31 09:25:11 +00:00
renovate[bot]
ee198d85ca
chore(deps): update dependency eslint to ^9.34.0 2025-08-22 23:11:03 +00:00
renovate[bot]
cde67f6f58
fix(deps): update dependency @babel/parser to ^7.28.3 2025-08-14 13:28:00 +00:00
renovate[bot]
65faaba82c
chore(deps): update actions/checkout action to v5 2025-08-11 14:41:56 +00:00
renovate[bot]
5f51d8623d
chore(deps): update dependency eslint to ^9.33.0 2025-08-08 21:13:20 +00:00
renovate[bot]
8d9fa98561
fix(deps): update dependency strip-json-comments to ^5.0.3 2025-08-08 17:05:30 +00:00
renovate[bot]
1c057e63a0
fix(deps): update dependency memize to ^2.1.1 2025-08-07 00:49:54 +00:00
renovate[bot]
544db1ec8e
fix(deps): update dependency eslint-plugin-prettier to ^5.5.4 2025-08-06 13:37:39 +00:00
renovate[bot]
ab1fb91431
chore(deps): update dependency eslint to ^9.32.0 2025-07-25 20:44:21 +00:00
renovate[bot]
0621d6391a
chore(deps): update dependency jasmine to ^5.9.0 2025-07-19 17:06:52 +00:00
renovate[bot]
2de6b34ab0
fix(deps): update dependency eslint-plugin-prettier to ^5.5.3 2025-07-19 02:00:39 +00:00
renovate[bot]
6555d1e454
fix(deps): update dependency eslint-config-prettier to ^10.1.8 2025-07-18 21:33:10 +00:00
Jeff Williams
bb6c83e93d
chore(deps): update lockfile 2025-07-13 14:36:45 -07:00
renovate[bot]
1955041df7
chore(deps): update dependency eslint to ^9.31.0 2025-07-11 23:03:43 +00:00
Jeff Williams
11d92f85d1
refactor(jsdoc-util): remove unused EventBus class 2025-07-06 17:31:09 -07:00
Jeff Williams
8a0b40502e
refactor(jsdoc-doclet): make combineDoclets a static method on Doclet
This change puts `combineDoclets` in the same place as methods like `clone` and `emptyDoclet`.
2025-07-05 09:49:44 -07:00
Jeff Williams
4c4a58260d
docs(jsdoc-doclet): improve JSDoc comments 2025-07-05 09:37:16 -07:00
renovate[bot]
3ca5db9db5
fix(deps): update babel monorepo to ^7.28.0 2025-07-02 10:59:37 +00:00
renovate[bot]
3c21fd21de
chore(deps): update dependency eslint to ^9.30.1 2025-07-01 22:00:01 +00:00
renovate[bot]
b4570c6c71
fix(deps): update dependency globals to v16 2025-07-01 12:44:52 +00:00
renovate[bot]
7a6b3afc57
chore(deps): update dependency eslint to ^9.30.0 2025-06-28 00:00:25 +00:00
renovate[bot]
62c9924dca
chore(deps): update dependency prettier to ^3.6.2 2025-06-27 05:49:12 +00:00
renovate[bot]
72153299ef
fix(deps): update dependency @babel/parser to ^7.27.7 2025-06-26 16:31:02 +00:00
renovate[bot]
72c1e79892
fix(deps): update dependency eslint-plugin-prettier to ^5.5.1 2025-06-25 17:35:54 +00:00
renovate[bot]
68bffc795b
chore(deps): update dependency prettier to ^3.6.1 2025-06-25 10:59:43 +00:00
renovate[bot]
7772195487
chore(deps): update dependency prettier to ^3.6.0 2025-06-23 03:09:49 +00:00
renovate[bot]
2892faaf4e
fix(deps): update dependency emittery to ^1.2.0 2025-06-20 20:04:02 +00:00
renovate[bot]
8c2d6acca8
fix(deps): update dependency eslint-plugin-prettier to ^5.5.0 2025-06-17 14:11:33 +00:00
renovate[bot]
d4a2b0fba6
chore(deps): update dependency eslint to ^9.29.0 2025-06-13 19:30:51 +00:00
renovate[bot]
56c2fc8e09
fix(deps): update dependency @fontsource-variable/open-sans to ^5.2.6 2025-06-08 02:15:18 +00:00
renovate[bot]
2cbfdd9ad6
chore(deps): update dependency jasmine to ^5.8.0 2025-06-07 02:36:40 +00:00
renovate[bot]
71ccf46189
fix(deps): update babel monorepo to ^7.27.5 2025-06-03 14:46:48 +00:00
Jeff Williams
ee53851057
chore(deps): update supported engines; upgrade yargs-parser
Node.js 18 is EOL, and yargs-parser requires recent versions of Node.js 20 and 22.
2025-05-30 21:51:32 -07:00
renovate[bot]
68d5ee6a96
chore(deps): update dependency eslint to ^9.28.0 2025-05-30 22:58:38 +00:00
renovate[bot]
013fa9370b
fix(deps): update dependency @babel/parser to ^7.27.4 2025-05-30 18:20:34 +00:00
renovate[bot]
89014bbb79
fix(deps): update dependency eslint-plugin-prettier to ^5.4.1 2025-05-30 07:15:38 +00:00
renovate[bot]
9cb4b74727
fix(deps): update dependency @babel/parser to ^7.27.3 2025-05-27 11:41:57 +00:00
renovate[bot]
43151bc46b
chore(deps): update dependency execa to ^9.6.0 2025-05-26 22:47:20 +00:00
renovate[bot]
30d87034e4
fix(deps): update dependency globals to v16 2025-05-25 15:06:45 +00:00
renovate[bot]
13c52db01e
fix(deps): update dependency strip-json-comments to ^5.0.2 2025-05-16 22:05:00 +00:00
renovate[bot]
e96de763bb
chore(deps): update dependency eslint to ^9.27.0 2025-05-16 19:08:35 +00:00
Jeff Williams
7a2e561e11
fix(jsdoc-parse): use the correct scopes when exported objects have properties
Previously, JSDoc parsed the test code and found the namepath `module:icecream.FLAVORS` (correct), but also `module:icecream~FLAVORS.VANILLA` (wrong, because `FLAVORS` is a static member of the module, not an inner member).

With this change, JSDoc should consistently identify `FLAVORS` as a static member of `module:icecream`.
2025-05-15 20:40:45 -07:00
renovate[bot]
30df56712e
chore(deps): update dependency hereby to ^1.11.0 2025-05-09 19:46:17 +00:00
renovate[bot]
69fc7ae586
fix(deps): update dependency eslint-config-prettier to ^10.1.5 2025-05-09 07:57:11 +00:00
renovate[bot]
7898aa800a
chore(deps): update dependency execa to ^9.5.3 2025-05-08 22:55:21 +00:00
renovate[bot]
7f4704f766
fix(deps): update dependency globals to v16 2025-05-07 20:01:54 +00:00
renovate[bot]
55325dc623
fix(deps): update dependency eslint-config-prettier to ^10.1.3 2025-05-07 13:47:22 +00:00
Jeff Williams
7af5d1d55a
fix(jsdoc-parse): remove duplicative code 2025-05-06 20:52:34 -07:00
Jeff Williams
1983cda4e3
refactor(jsdoc-parse): start decomposing astnodeToMemberof 2025-05-06 20:49:44 -07:00
renovate[bot]
dfe25ba285
fix(deps): update dependency @babel/parser to ^7.27.2 2025-05-06 19:01:52 +00:00
renovate[bot]
bd52c21788
fix(deps): update dependency eslint-plugin-prettier to ^5.4.0 2025-05-05 11:37:41 +00:00
renovate[bot]
76dae361bb
fix(deps): update dependency eslint-plugin-prettier to ^5.3.1 2025-05-04 09:54:52 +00:00
renovate[bot]
8ca09d5845
chore(deps): update dependency eslint to ^9.26.0 2025-05-03 02:36:23 +00:00
Jeff Williams
5d918e0ace
chore(deps): update lockfile 2025-05-02 09:11:51 -07:00
renovate[bot]
54ff30e6b2
chore(deps): update dependency jasmine to ^5.7.1 2025-05-02 05:48:22 +00:00
renovate[bot]
087eaf5da0
fix(deps): update babel monorepo to ^7.27.1 2025-04-30 19:28:21 +00:00
renovate[bot]
4f14e9df3a
chore(deps): update dependency jasmine to ^5.7.0 2025-04-26 18:43:34 +00:00
renovate[bot]
6f032cc874
chore(deps): update dependency eslint to ^9.25.1 2025-04-21 19:10:49 +00:00
renovate[bot]
acaeaaeada
chore(deps): update dependency eslint to ^9.25.0 2025-04-18 22:42:09 +00:00
renovate[bot]
58b1279d3e
fix(deps): update dependency eslint-config-prettier to ^10.1.2 2025-04-10 10:54:49 +00:00
renovate[bot]
a40ed69834
chore(deps): update dependency eslint to ^9.24.0 2025-04-04 23:01:58 +00:00
renovate[bot]
94a8b8c369
fix(deps): update dependency eslint-plugin-prettier to ^5.2.6 2025-04-02 18:20:02 +00:00
renovate[bot]
3c6a04d0a3
fix(deps): update dependency eslint-plugin-prettier to ^5.2.5 2025-03-25 12:39:55 +00:00
renovate[bot]
ffae2ff857
fix(deps): update babel monorepo to ^7.27.0 2025-03-24 23:52:12 +00:00
renovate[bot]
dd364023f8
fix(deps): update dependency eslint-plugin-prettier to ^5.2.4 2025-03-24 03:40:34 +00:00
renovate[bot]
5e21e9925b
chore(deps): update dependency eslint to ^9.23.0 2025-03-21 21:56:42 +00:00
renovate[bot]
9fc769ecdc
chore(deps): update dependency klaw-sync to v7 2025-03-19 19:24:25 +00:00
renovate[bot]
64ca8f1ddf
fix(deps): update babel monorepo to ^7.26.10 2025-03-11 19:26:14 +00:00
renovate[bot]
41aa13b7e3
chore(deps): update dependency eslint to ^9.22.0 2025-03-07 21:37:25 +00:00
renovate[bot]
1864c7d238
fix(deps): update dependency eslint-config-prettier to ^10.1.1 2025-03-07 11:07:21 +00:00
renovate[bot]
f556f5e615
chore(deps): update dependency prettier to ^3.5.3 2025-03-03 03:18:46 +00:00
renovate[bot]
325d4f4c4e
fix(deps): update dependency @fontsource-variable/open-sans to ^5.2.5 2025-03-02 10:31:26 +00:00
renovate[bot]
d7f17e2f65
fix(deps): update dependency @fontsource-variable/open-sans to ^5.2.0 2025-03-01 18:50:08 +00:00
renovate[bot]
5f2e6f459d
fix(deps): update dependency eslint-config-prettier to ^10.0.2 2025-02-26 14:36:32 +00:00
renovate[bot]
f09568673d
chore(deps): update dependency prettier to ^3.5.2 2025-02-22 06:45:39 +00:00
renovate[bot]
cc4a661106
chore(deps): update dependency eslint to ^9.21.0 2025-02-21 22:53:09 +00:00
renovate[bot]
e54340c961
fix(deps): update dependency globals to v16 2025-02-20 14:30:02 +00:00
Jeff Williams
7b8c3c750f
fix(ci): address zizmor findings
https://github.com/woodruffw/zizmor
2025-02-18 14:55:58 -08:00
Jeff Williams
2d33171cda
chore(deps): remove lerna from devDependencies
It's no longer required (although I still use it to publish to npm); it's triggering security warnings; and removing it gets rid of 499 packages (!).
2025-02-18 13:13:12 -08:00
renovate[bot]
e5ba496e8f
fix(deps): update dependency @babel/parser to ^7.26.9 2025-02-14 15:11:19 +00:00
renovate[bot]
394481ebaa
chore(deps): update dependency prettier to ^3.5.1 2025-02-13 15:51:26 +00:00
renovate[bot]
84522cc085
fix(deps): update dependency globals to v15 2025-02-12 18:58:26 +00:00
Jeff Williams
153b899c2e
refactor(jsdoc-core): remove obsolete getter and method 2025-02-11 21:22:49 -08:00
Jeff Williams
457c38fd4d
refactor(jsdoc-task-runner): use class-private members 2025-02-11 21:17:36 -08:00
renovate[bot]
360abb78b6
chore(deps): update dependency eslint to ^9.20.1 2025-02-11 19:35:06 +00:00
renovate[bot]
943faa7953
chore(deps): update dependency prettier to ^3.5.0 2025-02-09 13:33:31 +00:00
renovate[bot]
8f408f73c2
chore(deps): update dependency jasmine to ^5.6.0 2025-02-08 21:29:12 +00:00
renovate[bot]
787059b7b8
fix(deps): update babel monorepo to ^7.26.8 2025-02-08 13:27:36 +00:00
renovate[bot]
23dfa8e1f9
chore(deps): update dependency eslint to ^9.20.0 2025-02-08 00:52:32 +00:00
renovate[bot]
09a5646241
chore(deps): update dependency mock-fs to ^5.5.0 2025-02-06 18:36:21 +00:00
renovate[bot]
857801651a
fix(deps): update dependency ast-module-types to ^6.0.1 2025-02-01 21:08:18 +00:00
renovate[bot]
c605b99303
fix(deps): update dependency emittery to ^1.1.0 2025-01-27 16:30:56 +00:00
renovate[bot]
edd911322f
chore(deps): update dependency eslint to ^9.19.0 2025-01-26 12:40:35 +00:00
renovate[bot]
b209812374
fix(deps): update dependency @babel/parser to ^7.26.7 2025-01-26 09:45:11 +00:00
renovate[bot]
1faff383a4
fix(deps): update dependency p-queue to ^8.1.0 2025-01-22 09:01:59 +00:00
renovate[bot]
a0d8bddb9a
fix(deps): update dependency eslint-plugin-prettier to ^5.2.3 2025-01-19 08:11:16 +00:00
54 changed files with 1884 additions and 7457 deletions

View File

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

View File

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

View File

@ -43,6 +43,16 @@ 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 () => {
@ -91,7 +101,7 @@ export const dependencyLicenses = task({
export const dependencies = task({
name: 'dependencies',
dependencies: [dependencyEngines, dependencyLicenses],
dependencies: [dependencyCleanup, dependencyEngines, dependencyLicenses],
});
export const format = task({

View File

@ -80,7 +80,6 @@ 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

61
knip.json Normal file
View File

@ -0,0 +1,61 @@
{
"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"]
}
}
}

7666
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,44 +3,35 @@
"private": true,
"license": "Apache-2.0",
"devDependencies": {
"@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",
"@jsdoc/core": "^0.5.10",
"@jsdoc/eslint-config": "^2.0.2",
"@jsdoc/tag": "^0.2.13",
"@jsdoc/util": "^0.3.4",
"ajv": "^8.17.1",
"c8": "^10.1.3",
"eslint": "^9.18.0",
"execa": "^9.5.2",
"hereby": "^1.10.0",
"eslint": "^9.39.1",
"execa": "^9.6.1",
"hereby": "^1.11.1",
"husky": "^9.1.7",
"installed-check": "^9.3.0",
"jasmine": "^5.5.0",
"jasmine": "^5.13.0",
"jasmine-console-reporter": "^3.1.0",
"js-green-licenses": "^4.0.0",
"klaw-sync": "^6.0.0",
"lerna": "^8.1.9",
"knip": "^5.72.0",
"license-check-and-add": "^4.0.5",
"lodash": "^4.17.21",
"mock-fs": "^5.4.1",
"prettier": "^3.4.2"
"mock-fs": "^5.5.0",
"prettier": "^3.7.4"
},
"type": "module",
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.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 = `${LONGNAMES.MODULE_EXPORT}.${nodeToValue(node.declaration.declarations[0])}`;
str = `${nodeToValue(node.declaration.declarations[0])}`;
} else {
str = `${LONGNAMES.MODULE_EXPORT}.${nodeToValue(node.declaration)}`;
str = `${nodeToValue(node.declaration)}`;
}
}

View File

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

View File

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

View File

@ -17,7 +17,8 @@
/**
* Manages configuration settings for JSDoc.
*
* @alias module:@jsdoc/core.config
* @namespace config
* @memberof module:@jsdoc/core
*/
import { cosmiconfig, defaultLoaders } from 'cosmiconfig';
import _ from 'lodash';
@ -26,67 +27,124 @@ 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(hegemonic): Integrate CLI options with other options.
// 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
*/
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 JSDoc plugins to load.
* 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
*/
plugins: [],
/**
* Settings for loading and parsing source files.
* The source files to parse.
*
* @type {Array<string>}
* @memberof module:@jsdoc/core.config.defaultConfig
*/
sourceFiles: [],
/**
* 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`.
* 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
*/
sourceType: 'module',
/**
* Settings for interpreting JSDoc tags.
*
* @namespace tags
* @memberof module:@jsdoc/core.config.defaultConfig
*/
tags: {
/**
* Set to `true` to allow tags that JSDoc does not recognize.
* Whether to allow tags that JSDoc does not recognize. The default value is `true`.
*
* @type {boolean}
* @memberof module:@jsdoc/core.config.defaultConfig.tags
*/
allowUnknownTags: true,
// TODO(hegemonic): Use module paths, not magic strings.
// TODO: 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: {
/**
* Set to `true` to use a monospaced font for links to other code symbols, but not links to
* websites.
* 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
*/
cleverLinks: false,
/**
* Set to `true` to use a monospaced font for all links.
* Whether to use a monospaced font for all links. The default value is `false`.
*
* @type {boolean}
* @memberof module:@jsdoc/core.config.defaultConfig.templates
*/
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)));
}
@ -114,6 +172,43 @@ 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;
@ -123,5 +218,8 @@ export async function load(filepath) {
loaded = (await explorer.search()) ?? {};
}
return new Config(loaded.filepath, _.defaultsDeep({}, loaded.config, defaultConfig));
return {
config: _.defaultsDeep({}, loaded.config, defaultConfig),
filepath: loaded.filepath,
};
}

View File

@ -43,22 +43,21 @@ export default class Env {
/**
* The event emitter shared across JSDoc.
*
* @type {Object}
* @type {node:events}
*/
this.emitter = new EventEmitter();
/**
* Logging functions shared across JSDoc.
*
* @type {Object<string, function>}
* @type {module:@jsdoc/util~logFunctions}
*/
this.log = getLogFunctions(this.emitter);
/**
* The command-line arguments, parsed into a key/value hash.
* The command-line arguments, expressed as key-value pairs.
*
* @type {Object}
* @example if (global.env.opts.help) { console.log('Helpful message.'); }
* @type {Object<string, *>}
*/
this.opts = {};
@ -67,7 +66,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(),
@ -101,20 +100,20 @@ export default class Env {
};
}
/**
* The data parsed from JSDoc's configuration file.
*
* @type Object<string, *>
*/
get config() {
return this.conf;
}
// TODO: Remove.
get env() {
return this;
}
// TODO: Remove.
get(key) {
return this[key];
}
/**
* The command-line arguments, expressed as key-value pairs.
*
* @type {Object<string, *>}
*/
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.1"
"strip-json-comments": "^5.0.3"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
},
"gitHead": "a595f7f2a525f32da9a0498a027667af3d938158"
}

View File

@ -46,14 +46,6 @@ 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 { combineDoclets, Doclet } from './lib/doclet.js';
import { 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, combineDoclets, Doclet, DocletStore, Package, resolveBorrows, schema };
export default { augment, combineDoclets, Doclet, DocletStore, Package, resolveBorrows, schema };
export { augment, Doclet, DocletStore, Package, resolveBorrows, schema };
export default { augment, Doclet, DocletStore, Package, resolveBorrows, schema };

View File

@ -16,11 +16,14 @@
/**
* 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 { combineDoclets, Doclet } from './doclet.js';
import { Doclet } from './doclet.js';
const DEPENDENCY_KINDS = ['class', 'external', 'interface', 'mixin'];
@ -133,7 +136,7 @@ function staticToInstance(doclet) {
}
/**
* Update the list of doclets to be added to another symbol.
* Updates 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.
@ -222,7 +225,7 @@ function getInheritedAdditions(depDoclets, docletStore) {
childDoclet = {};
}
member = combineDoclets(childDoclet, parentDoclet);
member = Doclet.combineDoclets(childDoclet, parentDoclet);
if (!member.inherited) {
member.inherits = member.longname;
@ -408,7 +411,7 @@ function getImplementedAdditions(implDoclets, docletStore) {
childDoclet = {};
}
implementationDoclet = combineDoclets(childDoclet, parentDoclet);
implementationDoclet = Doclet.combineDoclets(childDoclet, parentDoclet);
reparentDoclet(doclet, implementationDoclet);
updateImplements(implementationDoclet, parentDoclet.longname);
@ -472,19 +475,20 @@ function augment(docletStore, propertyName, docletFinder, env) {
}
/**
* Add doclets to reflect class inheritance.
* Adds 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`.
*
* @return {void}
* @alias module:@jsdoc/doclet.augment.addInherited
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
*/
export function addInherited(docletStore) {
augment(docletStore, 'augments', getInheritedAdditions);
}
/**
* Add doclets to reflect mixins. When a symbol is mixed into a class, the class' version of the
* Adds 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:
@ -494,16 +498,15 @@ 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`.
*
* @param {!Array.<module:@jsdoc/doclet.Doclet>} doclets - The doclets generated by JSDoc.
* @param {!Object} doclets.index - The doclet index.
* @return {void}
* @alias module:@jsdoc/doclet.augment.addMixedIn
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
*/
export function addMixedIn(doclets) {
augment(doclets, 'mixes', getMixedInAdditions);
export function addMixedIn(docletStore) {
augment(docletStore, 'mixes', getMixedInAdditions);
}
/**
* Add and update doclets to reflect implementations of interfaces.
* Adds and updates 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:
@ -515,14 +518,15 @@ export function addMixedIn(doclets) {
* 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`.
*
* @return {void}
* @alias module:@jsdoc/doclet.augment.addImplemented
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
*/
export function addImplemented(doclets) {
augment(doclets, 'implements', getImplementedAdditions);
export function addImplemented(docletStore) {
augment(docletStore, 'implements', getImplementedAdditions);
}
/**
* Add and update doclets to reflect all of the following:
* Adds and updates doclets to reflect all of the following:
*
* + Inherited classes
* + Mixins
@ -530,7 +534,8 @@ export function addImplemented(doclets) {
*
* Calling this method is equivalent to calling all other methods exported by this module.
*
* @return {void}
* @alias module:@jsdoc/doclet.augment.augmentAll
* @param {!module:@jsdoc/doclet.DocletStore} docletStore - The doclet store to update.
*/
export function augmentAll(docletStore) {
addMixedIn(docletStore);

View File

@ -20,7 +20,7 @@
import { SCOPE } from '@jsdoc/name';
import { combineDoclets, Doclet } from './doclet.js';
import { 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 = combineDoclets(borrowedDoclet, Doclet.emptyDoclet(borrowedDoclet.env));
const clone = Doclet.clone(borrowedDoclet);
// TODO: this will fail on longnames like '"Foo#bar".baz'
parts = borrowedAs.split(SCOPE.PUNC.INSTANCE);
@ -55,10 +55,15 @@ function cloneBorrowedDoclets({ borrowed, longname }, docletStore) {
}
/**
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.
* 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.
*/
export function resolveBorrows(docletStore) {
for (const doclet of docletStore.docletsWithBorrowed) {

View File

@ -56,17 +56,25 @@ function removeFromSet(targetMap, key, value) {
/**
* Stores and classifies the doclets that JSDoc creates as it parses your source files.
*
* The doclet store categorizes doclets based on their properties, so that the JSDoc template can
* A 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.
*
* @alias @jsdoc/doclet.DocletStore
* 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
*/
export class DocletStore {
#commonPathPrefix;
@ -91,9 +99,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 tracks it automatically.
* doclet store adds it automatically and tracks updates to the doclet.
*
* @param {@jsdoc/core.Env} env - The JSDoc environment to use.
* @param {module:@jsdoc/core.Env} env - The JSDoc environment to use.
*/
constructor(env) {
this.#commonPathPrefix = null;
@ -101,28 +109,85 @@ export class DocletStore {
this.#isListening = false;
this.#sourcePaths = new Map();
// TODO: Add descriptions and types for public properties.
/** @type Map<string, Set<Doclet>> */
/**
* 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>>
*/
this.allDocletsByLongname = new Map();
/** Doclets that are used to generate output. */
/**
* All visible doclets.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.doclets = new Set();
/** @type Map<string, Set<Doclet>> */
/**
* Map from a doclet kind to a `Set` of doclets with that kind.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
this.docletsByKind = new Map();
/** @type Map<string, Set<Doclet>> */
/**
* Map from a doclet longname to a `Set` of doclets with that longname.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
this.docletsByLongname = new Map();
/** @type Map<string, Set<Doclet>> */
/**
* Map from a doclet `memberof` value to a `Set` of doclets with that `memberof`.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
this.docletsByMemberof = new Map();
/** @type Map<string, Set<Doclet>> */
/**
* 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>>
*/
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();
/** @type Map<string, Set<Doclet>> */
/**
* Map from an event's longname to a `Set` of doclets that listen to that event.
*
* @type Map<string, Set<module:@jsdoc/doclet.Doclet>>
*/
this.listenersByListensTo = new Map();
/** Doclets that aren't used to generate output. */
/**
* Doclets that are hidden and shouldn't be used to generate output.
*
* @type Set<module:@jsdoc/doclet.Doclet>
*/
this.unusedDoclets = new Set();
this.#docletChangedHandler = (e) => this.#handleDocletChanged(e, {});
@ -318,7 +383,15 @@ 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.
/**
* 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.
*/
add(doclet) {
let doclets;
let nodeId;
@ -337,10 +410,25 @@ 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;
@ -365,14 +453,30 @@ 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);
@ -382,6 +486,12 @@ 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,30 +410,7 @@ function copyPropsWithIncludelist(primary, secondary, target, include) {
}
/**
* 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.
* Information about a single JSDoc comment, or a single symbol in a source file.
*
* @alias module:@jsdoc/doclet.Doclet
*/
@ -441,7 +418,7 @@ Doclet = class {
#dictionary;
/**
* Create a doclet.
* Creates a doclet.
*
* @param {string} docletSrc - The raw source code of the jsdoc comment.
* @param {object} meta - Properties describing the code related to this comment.
@ -468,7 +445,11 @@ Doclet = class {
});
WATCHABLE_PROPS.forEach((prop) => this.#defineWatchableProp(prop));
/** The original text of the comment from the source code. */
/**
* The text of the comment from the source code.
*
* @type {string}
*/
this.comment = docletSrc;
meta ??= {};
this.setMeta(meta);
@ -492,10 +473,45 @@ 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 combineDoclets(doclet, Doclet.emptyDoclet(doclet.env));
return Doclet.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);
}
@ -534,7 +550,7 @@ Doclet = class {
}
/**
* Add a tag to the doclet.
* Adds 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.
@ -554,7 +570,7 @@ Doclet = class {
}
/**
* Check whether the doclet represents a globally available symbol.
* Checks whether the doclet represents a globally available symbol.
*
* @returns {boolean} `true` if the doclet represents a global; `false` otherwise.
*/
@ -563,7 +579,7 @@ Doclet = class {
}
/**
* Check whether the doclet should be used to generate output.
* Checks whether the doclet should be used to generate output.
*
* @returns {boolean} `true` if the doclet should be used to generate output; `false` otherwise.
*/
@ -647,20 +663,21 @@ Doclet = class {
}
/**
* Set the doclet's `longname` property.
* Sets 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;
}
@ -672,6 +689,7 @@ Doclet = class {
setMemberof(sid) {
/**
* The longname of the symbol that contains this one, if any.
*
* @type {string}
*/
this.memberof = removeGlobal(sid)
@ -680,7 +698,7 @@ Doclet = class {
}
/**
* Set the doclet's `scope` property. Must correspond to a scope name that is defined in
* Sets 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.
@ -707,10 +725,10 @@ Doclet = class {
}
/**
* Add a symbol to this doclet's `borrowed` array.
* Adds a symbol to the doclet's `borrowed` array.
*
* @param {string} source - The longname of the symbol that is the source.
* @param {string} target - The name the symbol is being assigned to.
* @param {string} source - The longname of the symbol that is borrowed.
* @param {string} target - The name that the borrowed symbol is assigned to.
*/
borrow(source, target) {
const about = { from: source };
@ -728,6 +746,11 @@ 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.
@ -739,7 +762,7 @@ Doclet = class {
}
/**
* Add a symbol to the doclet's `augments` array.
* Adds a symbol to the doclet's `augments` array.
*
* @param {string} base - The longname of the base symbol.
*/
@ -753,10 +776,12 @@ Doclet = class {
this.augments.push(base);
}
// TODO: Add typedef for `meta`.
/**
* Set the `meta` property of this doclet.
* Sets the `meta` property of the doclet, which contains metadata about the source code that the
* doclet corresponds to.
*
* @param {object} meta
* @param {object} meta - The data to add to the doclet.
*/
setMeta(meta) {
let pathname;

View File

@ -16,12 +16,6 @@
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() : [];
@ -75,9 +69,13 @@ 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.
*/
@ -142,7 +140,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;
}
@ -195,7 +193,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 = [];
@ -212,7 +210,7 @@ export class Package {
/**
* Keywords to help users find the package.
*
* @type {Array.<string>}
* @type {Array<string>}
*/
this.keywords = packageInfo.keywords;
}
@ -222,7 +220,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",
"@jsdoc/util": "^0.3.4",
"common-path-prefix": "^3.0.0",
"lodash": "^4.17.21",
"on-change": "^5.0.1",
"on-change": "^6.0.1",
"strip-bom": "^5.0.0"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.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 { combineDoclets, Doclet } from '../../lib/doclet.js';
import { 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,12 +33,6 @@ 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.get('emitter').emit('newDoclet', { doclet });
env.emitter.emit('newDoclet', { doclet });
}
return doclet;

View File

@ -29,10 +29,6 @@ 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();
});
@ -41,74 +37,6 @@ 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*/`;
@ -305,6 +233,86 @@ 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.26.5",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"@babel/eslint-parser": "^7.28.5",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-simple-import-sort": "^12.1.1",
"globals": "^15.14.0"
"globals": "^16.5.0"
},
"peerDependencies": {
"eslint": "^9.18.0",
"prettier": "^3.4.2"
"eslint": "^9.39.1",
"prettier": "^3.7.4"
},
"publishConfig": {
"access": "public"
@ -41,7 +41,7 @@
}
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
},
"gitHead": "81a824eb0968851f836fc6069376d0337775f9ba"
}

View File

@ -14,10 +14,4 @@
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.
*
* @alias module:@jsdoc/name
* @module @jsdoc/name
*/
import escape from 'escape-string-regexp';
@ -27,7 +27,6 @@ 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. */
@ -47,30 +46,89 @@ 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: '.',
},
};
// Keys must be lowercase.
/**
* Doclet scope identifiers mapped to the equivalent punctuation.
*
* @enum {string}
*/
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,
};
export const PUNC_TO_SCOPE = _.invert(SCOPE_TO_PUNC);
/**
* 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',
};
const SCOPE_PUNC = _.values(SCOPE.PUNC);
const SCOPE_PUNC_STRING = `[${SCOPE_PUNC.join()}]`;
@ -82,7 +140,7 @@ const REGEXP_DESCRIPTION = new RegExp(DESCRIPTION);
const REGEXP_NAME_DESCRIPTION = new RegExp(`^(\\[[^\\]]+\\]|\\S+)${DESCRIPTION}`);
/**
* Check whether a name appears to represent a complete longname that is a member of the specified
* Checks whether a name appears to represent a complete longname that is a member of the specified
* parent.
*
* @example
@ -101,7 +159,7 @@ export function nameIsLongname(name, memberof) {
}
/**
* For names that identify a property of a prototype, replace the `prototype` portion of the name
* For names that identify a property of a prototype, replaces the `prototype` portion of the name
* with `#`, which indicates instance-level scope. For example, `Foo.prototype.bar` becomes
* `Foo#bar`.
*
@ -114,7 +172,8 @@ export function prototypeToPunc(name) {
return name;
}
return name.replace(/(?:^|\.)prototype\.?/g, SCOPE.PUNC.INSTANCE);
// If there's a trailing open bracket ([), as in `Foo.prototype['bar']`, keep it.
return name.replace(/(?:^|\.)prototype(?:$|\.|(\[))/g, `${SCOPE.PUNC.INSTANCE}$1`);
}
/**
@ -123,11 +182,11 @@ export function prototypeToPunc(name) {
* @param {string} name - The name to check.
* @returns {?string} The leading scope character, if one is present.
*/
export const getLeadingScope = (name) => {
export function getLeadingScope(name) {
const match = name.match(REGEXP_LEADING_SCOPE);
return match?.[1];
};
}
/**
* Gets the trailing scope character, if any, from a name.
@ -135,14 +194,14 @@ export const getLeadingScope = (name) => {
* @param {string} name - The name to check.
* @returns {?string} The trailing scope character, if one is present.
*/
export const getTrailingScope = (name) => {
export function getTrailingScope(name) {
const match = name.match(REGEXP_TRAILING_SCOPE);
return match?.[1];
};
}
/**
* Get a symbol's basename, which is the first part of its full name before any scope punctuation.
* Gets 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`
@ -164,7 +223,9 @@ export function getBasename(name) {
}
// TODO: docs
export const stripNamespace = (longname) => longname.replace(/^[a-zA-Z]+:/, '');
export function stripNamespace(longname) {
return longname.replace(/^[a-zA-Z]+:/, '');
}
// TODO: docs
function slice(longname, sliceChars, forcedMemberof) {
@ -252,8 +313,9 @@ function slice(longname, sliceChars, forcedMemberof) {
};
}
// TODO: Document this with a typedef.
/**
* Given a longname like `a.b#c(2)`, split it into the following parts:
* Given a longname like `a.b#c(2)`, this method splits it into the following parts:
*
* + `longname`
* + `memberof`
@ -265,12 +327,17 @@ function slice(longname, sliceChars, forcedMemberof) {
* @param {string} forcedMemberof
* @returns {object} Representing the properties of the given name.
*/
export const toParts = (longname, forcedMemberof) => slice(longname, null, forcedMemberof);
export function toParts(longname, forcedMemberof) {
return slice(longname, null, forcedMemberof);
}
// TODO: docs
/**
* @param {string} longname The full longname of the symbol.
* @param {string} ns The namespace to be applied.
* 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.
* @returns {string} The longname with the namespace applied.
*/
export function applyNamespace(longname, ns) {
@ -287,11 +354,11 @@ export function applyNamespace(longname, ns) {
}
/**
* Check whether a parent longname is an ancestor of a child longname.
* Checks whether a parent longname is an ancestor of a child longname.
*
* @param {string} parent - The parent longname.
* @param {string} child - The child longname.
* @return {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
* @returns {boolean} `true` if the parent is an ancestor of the child; otherwise, `false`.
*/
export function hasAncestor(parent, child) {
let parentIsAncestor = false;
@ -360,19 +427,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.
/**
* Convert an array of doclet longnames into a tree structure, optionally attaching doclets to the
* Converts 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:
@ -439,7 +506,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.
* @return {Object} A tree with information about each longname in the format shown above.
* @returns {Object} A tree with information about each longname in the format shown above.
*/
export function longnamesToTree(longnames, doclets) {
const splitOptions = { includeVariation: false };
@ -481,13 +548,14 @@ export function longnamesToTree(longnames, doclets) {
return tree;
}
// TODO: Document this with a typedef.
/**
* Split a string that starts with a name and ends with a description into its parts. Allows the
* Splits 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
* @returns {?Object} Hash with "name" and "description" properties.
* @param {string} nameDesc - The combined name and description.
* @returns {?Object} An object with `name` and `description` properties.
*/
function splitNameMatchingBrackets(nameDesc) {
const buffer = [];
@ -529,10 +597,12 @@ function splitNameMatchingBrackets(nameDesc) {
};
}
// TODO: Document this with a typedef.
/**
* 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.
* 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.
*/
export function splitNameAndDescription(str) {
// Like: `name`, `[name]`, `name text`, `[name] text`, `name - text`, or `[name] - text`.
@ -540,7 +610,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": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
},
"gitHead": "a595f7f2a525f32da9a0498a027667af3d938158"
}

View File

@ -115,37 +115,63 @@ 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 CommonJS module symbols that are _not_ assigned to `module.exports`.
// Handle module symbols, excluding CommonJS `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 (doclet.meta?.code?.node?.type === Syntax.MethodDefinition) {
parentDoclet = parser._getDocletById(doclet.meta.code.node.parent.parent.nodeId);
if (node?.type === Syntax.MethodDefinition) {
parentDoclet = parser._getDocletById(node.parent.parent.nodeId);
// special case for constructors of classes that have @alias tags
if (doclet.meta.code.node.kind === 'constructor' && parentDoclet?.alias) {
if (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(doclet.meta.code.node.static ? 'static' : 'instance');
doclet.addTag(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 (doclet.meta?.code?.node?.parent?.type === Syntax.ExportNamedDeclaration) {
// Is this something that the module exports? if so, it's a static member.
else if (
node?.type === Syntax.ExportNamedDeclaration ||
findAncestorWithType(node, Syntax.ExportNamedDeclaration)
) {
doclet.addTag('static');
}
// otherwise, it must be an inner member
// Otherwise, it must be an inner member.
else {
doclet.addTag('inner');
}
@ -153,7 +179,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 (!doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL && !skipMemberof) {
if (!skipMemberof && !doclet.memberof && doclet.scope !== SCOPE.NAMES.GLOBAL) {
doclet.addTag('memberof', moduleInfo.longname);
}
}
@ -180,12 +206,12 @@ function addDoclet(parser, newDoclet) {
}
}
function processAlias(parser, doclet, astNode) {
function processAlias(parser, doclet, node) {
let match;
let memberofName;
if (doclet.alias === '{@thisClass}') {
memberofName = parser.resolveThis(astNode);
memberofName = parser.resolveThis(node);
// "class" refers to the owner of the prototype, not the prototype itself
match = memberofName.match(PROTOTYPE_OWNER_REGEXP);
@ -204,7 +230,7 @@ function isModuleObject(doclet) {
}
// TODO: separate code that resolves `this` from code that resolves the module object
function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPunc) {
function findSymbolMemberof(parser, doclet, node, nameStartsWith, trailingPunc) {
const docletIsModuleObject = isModuleObject(doclet);
let memberof = '';
let nameAndPunc;
@ -237,7 +263,7 @@ function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPun
doclet.addTag('name', currentModule.longname);
doclet.postProcess();
} else {
memberof = parser.resolveThis(astNode);
memberof = parser.resolveThis(node);
// like the following at the top level of a module:
// this.foo = 1;
@ -255,7 +281,7 @@ function findSymbolMemberof(parser, doclet, astNode, nameStartsWith, trailingPun
};
}
function addSymbolMemberof(parser, doclet, astNode) {
function addSymbolMemberof(parser, doclet, node) {
let basename;
let memberof;
let memberofInfo;
@ -264,7 +290,7 @@ function addSymbolMemberof(parser, doclet, astNode) {
let scopePunc;
let unresolved;
if (!astNode) {
if (!node) {
return;
}
@ -279,7 +305,7 @@ function addSymbolMemberof(parser, doclet, astNode) {
unresolved = resolveTargetRegExp.exec(doclet.name);
if (unresolved) {
memberofInfo = findSymbolMemberof(parser, doclet, astNode, unresolved[1], unresolved[2]);
memberofInfo = findSymbolMemberof(parser, doclet, node, unresolved[1], unresolved[2]);
memberof = memberofInfo.memberof;
scopePunc = memberofInfo.scopePunc;
@ -287,7 +313,7 @@ function addSymbolMemberof(parser, doclet, astNode) {
doclet.name = doclet.name ? memberof + scopePunc + doclet.name : memberof;
}
} else {
memberofInfo = parser.astnodeToMemberof(astNode);
memberofInfo = parser.astnodeToMemberof(node);
basename = memberofInfo.basename;
memberof = memberofInfo.memberof;
}

View File

@ -60,6 +60,30 @@ 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
@ -324,18 +348,14 @@ export class Parser extends EventEmitter {
astnodeToMemberof(node) {
let basename;
let doclet;
let scope;
let memberof;
const result = {};
const type = node.type;
const result = {
basename: null,
memberof: null,
};
if (
(type === Syntax.FunctionDeclaration ||
type === Syntax.FunctionExpression ||
type === Syntax.ArrowFunctionExpression ||
type === Syntax.VariableDeclarator) &&
node.enclosingScope
) {
if (isFunctionOrVariableDeclarator(node) && node.enclosingScope) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (!doclet) {
@ -343,7 +363,7 @@ export class Parser extends EventEmitter {
} else {
result.memberof = doclet.longname + SCOPE.PUNC.INNER;
}
} else if (type === Syntax.ClassPrivateProperty || type === Syntax.ClassProperty) {
} else if (isClassProperty(node)) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (!doclet) {
@ -351,21 +371,17 @@ export class Parser extends EventEmitter {
} else {
result.memberof = doclet.longname + SCOPE.PUNC.INSTANCE;
}
} else if (type === Syntax.MethodDefinition && node.kind === 'constructor') {
} else if (isConstructor(node)) {
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 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
) {
// 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)) {
doclet = this._getDocletById(node.enclosingScope.nodeId);
if (doclet) {
@ -373,35 +389,25 @@ 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));
// 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;
}
memberof = this._getMemberofFromScopes(node, basename);
if (memberof) {
result.basename = basename;
result.memberof = memberof;
}
// 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.memberof = doclet.meta.vars[basename];
result.basename = basename;
result.memberof = doclet.meta.vars[basename];
} else {
doclet = this._getDocletById(node.parent.nodeId);
// set the result if we found a doclet. (if we didn't, the AST node may describe a
// Set the result if we found a doclet. (If we didn't, then the AST node might represent a
// global symbol.)
if (doclet) {
result.memberof = doclet.longname || doclet.name;
result.memberof = doclet.longname ?? doclet.name;
}
}
}
@ -453,6 +459,26 @@ 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 { combineDoclets } from '@jsdoc/doclet';
import { Doclet } 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 = combineDoclets(parentDoclet, eventDoclet, parser.env);
combined = Doclet.combineDoclets(parentDoclet, eventDoclet, parser.env);
parser.addResult(combined);
parentDoclet.undocumented = eventDoclet.undocumented = true;

View File

@ -16,11 +16,10 @@
"@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": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.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 { combineDoclets, Doclet } from '@jsdoc/doclet';
import { 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 = combineDoclets(e.doclet, new Doclet('', {}, jsdoc.env));
e.doclet = Doclet.combineDoclets(e.doclet, new Doclet('', {}, jsdoc.env));
e.doclet.foo = 'bar';
}

View File

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

View File

@ -11,9 +11,9 @@
"license": "Apache-2.0",
"main": "index.js",
"peerDependencies": {
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.2",
"prettier": "^3.4.2"
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.5.4",
"prettier": "^3.7.4"
},
"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.0"
"memize": "^2.1.1"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
}
}

View File

@ -13,6 +13,7 @@
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';
@ -23,22 +24,28 @@ import v from './validators.js';
const { DepGraph } = dependencyGraph;
// Work around an annoying typo in a method name.
DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
export class TaskRunner extends Emittery {
#context;
#deps;
#error;
#nameToTask;
#queue;
#running;
#taskToName;
#unsubscribers;
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 (Array.isArray(tasks)) {
if (_.isArray(tasks)) {
tasks.forEach((task, i) => {
try {
func(task);
@ -47,7 +54,7 @@ export class TaskRunner extends Emittery {
throw e;
}
});
} else if (tasks !== null && typeof tasks === 'object') {
} else if (_.isObject(tasks)) {
for (const task of Object.keys(tasks)) {
try {
func(tasks[task]);
@ -59,7 +66,7 @@ export class TaskRunner extends Emittery {
}
}
_addTaskEmitters(task) {
#addTaskEmitters(task) {
const u = {};
u.start = task.on('start', (t) => this.emit('taskStart', t));
@ -70,56 +77,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._queue = new Queue();
this._taskToName = new WeakMap();
this._nameToTask = new Map();
this._running = false;
this._unsubscribers = new Map();
#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();
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) {
@ -133,28 +140,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;
}
}
@ -167,7 +174,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);
}
}
@ -178,92 +185,84 @@ 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 (typeof task === 'string') {
task = this._nameToTask.get(task);
if (_.isString(task)) {
task = this.#nameToTask.get(task);
if (!task) {
throw new v.UnknownTaskError(`Unknown task: ${task}`);
}
} else if (typeof task === 'object') {
if (!this._taskToName.has(task)) {
} else if (_.isObject(task)) {
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;
}
@ -272,13 +271,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;
}
@ -288,23 +287,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();
}
@ -317,13 +316,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);
@ -331,13 +330,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.0.3",
"ow": "^2.0.0",
"p-queue": "^8.0.1"
"emittery": "^1.2.0",
"ow": "^3.1.1",
"p-queue": "^9.0.1"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
}
}

View File

@ -720,6 +720,26 @@ 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.
*
@ -733,9 +753,13 @@ export function getAncestors(data, doclet) {
let doc = doclet;
let previousDoc;
// Build index once for all lookups
const index = buildMemberofIndex(data);
while (doc) {
previousDoc = doc;
doc = find(data, { longname: doc.memberof })[0];
// Use index instead of database query
doc = index.get(doc.memberof);
// prevent infinite loop that can be caused by duplicated module definitions
if (previousDoc === doc) {

View File

@ -8,16 +8,15 @@
"author": "Jeff Williams <jeffrey.l.williams@gmail.com>",
"homepage": "https://github.com/jsdoc/jsdoc",
"license": "Apache-2.0",
"main": "index.js",
"main": "publish.js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@fontsource-variable/open-sans": "^5.1.1",
"@fontsource-variable/open-sans": "^5.2.7",
"@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",
@ -28,7 +27,7 @@
"markdown-it-anchor": "^9.2.0"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
},
"repository": {
"type": "git",

View File

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

View File

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

View File

@ -1,65 +0,0 @@
/*
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,6 +16,33 @@
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,12 +30,8 @@
"import": "./lib/*"
}
},
"dependencies": {
"lodash": "^4.17.21",
"ow": "^2.0.0"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.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('EventBus', () => {
it('is lib/bus', () => {
expect(util.EventBus).toEqual(bus);
describe('getLogFunctions', () => {
it('is lib/log', () => {
expect(util.getLogFunctions).toEqual(log);
});
});
});

View File

@ -1,83 +0,0 @@
/*
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": "1667509813080",
"revision": "1748665787180",
"description": "An API documentation generator for JavaScript.",
"keywords": [
"documentation",
@ -15,18 +15,10 @@
},
"dependencies": {
"@jsdoc/cli": "^0.3.12",
"@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"
"strip-bom": "^5.0.0"
},
"engines": {
"node": ">=v18.12.0"
"node": "^20.19.0 || ^22.12.0 || >=23.0.0"
},
"type": "module",
"bin": {

View File

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

View File

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

View File

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

View File

@ -0,0 +1,27 @@
/*
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

@ -0,0 +1,26 @@
/*
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,6 +20,7 @@ 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();
@ -40,4 +41,8 @@ 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();
});
});