mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
feat(jsdoc-doclet): emit events when certain doclet properties change
This commit is contained in:
parent
6ececc4680
commit
954d17f87c
@ -17,9 +17,8 @@
|
|||||||
* Provides methods for augmenting the parse results based on their content.
|
* Provides methods for augmenting the parse results based on their content.
|
||||||
*/
|
*/
|
||||||
import { name } from '@jsdoc/core';
|
import { name } from '@jsdoc/core';
|
||||||
import _ from 'lodash';
|
|
||||||
|
|
||||||
import { combineDoclets } from './doclet.js';
|
import { combineDoclets, Doclet } from './doclet.js';
|
||||||
|
|
||||||
const { fromParts, SCOPE, toParts } = name;
|
const { fromParts, SCOPE, toParts } = name;
|
||||||
|
|
||||||
@ -37,7 +36,7 @@ function mapDependencies(index, propertyName) {
|
|||||||
if (kinds.includes(doc.kind)) {
|
if (kinds.includes(doc.kind)) {
|
||||||
dependencies[indexName] = {};
|
dependencies[indexName] = {};
|
||||||
if (Object.hasOwn(doc, propertyName)) {
|
if (Object.hasOwn(doc, propertyName)) {
|
||||||
len = doc[propertyName].length;
|
len = doc[propertyName]?.length;
|
||||||
for (let j = 0; j < len; j++) {
|
for (let j = 0; j < len; j++) {
|
||||||
dependencies[indexName][doc[propertyName][j]] = true;
|
dependencies[indexName][doc[propertyName][j]] = true;
|
||||||
}
|
}
|
||||||
@ -382,7 +381,8 @@ function getMixedInAdditions(mixinDoclets, allDoclets, { documented, memberof })
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
mixedDoclet = _.cloneDeep(mixedDoclets[k]);
|
mixedDoclet = new Doclet('', null, mixedDoclets[k].dependencies);
|
||||||
|
mixedDoclet = combineDoclets(mixedDoclets[k], mixedDoclet);
|
||||||
|
|
||||||
updateMixes(mixedDoclet, mixedDoclet.longname);
|
updateMixes(mixedDoclet, mixedDoclet.longname);
|
||||||
mixedDoclet.mixed = true;
|
mixedDoclet.mixed = true;
|
||||||
@ -411,9 +411,7 @@ function updateImplements(implDoclets, implementedLongname) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
implDoclets.forEach((implDoclet) => {
|
implDoclets.forEach((implDoclet) => {
|
||||||
if (!Object.hasOwn(implDoclet, 'implements')) {
|
implDoclet.implements ??= [];
|
||||||
implDoclet.implements = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!implDoclet.implements.includes(implementedLongname)) {
|
if (!implDoclet.implements.includes(implementedLongname)) {
|
||||||
implDoclet.implements.push(implementedLongname);
|
implDoclet.implements.push(implementedLongname);
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import _ from 'lodash';
|
|||||||
const { SCOPE } = name;
|
const { SCOPE } = name;
|
||||||
|
|
||||||
function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
|
function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
|
||||||
borrowed.forEach(({ from, as }) => {
|
borrowed?.forEach(({ from, as }) => {
|
||||||
const borrowedDoclets = doclets.index.longname[from];
|
const borrowedDoclets = doclets.index.longname[from];
|
||||||
let borrowedAs = as || from;
|
let borrowedAs = as || from;
|
||||||
let parts;
|
let parts;
|
||||||
@ -60,7 +60,7 @@ function cloneBorrowedDoclets({ borrowed, longname }, doclets) {
|
|||||||
export function resolveBorrows(doclets) {
|
export function resolveBorrows(doclets) {
|
||||||
for (let doclet of doclets.index.borrowed) {
|
for (let doclet of doclets.index.borrowed) {
|
||||||
cloneBorrowedDoclets(doclet, doclets);
|
cloneBorrowedDoclets(doclet, doclets);
|
||||||
delete doclet.borrowed;
|
doclet.borrowed = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
doclets.index.borrowed = [];
|
doclets.index.borrowed = [];
|
||||||
|
|||||||
@ -40,6 +40,21 @@ const DEFAULT_SCOPE = SCOPE.NAMES.STATIC;
|
|||||||
// TODO: `class` should be on this list, right? What are the implications of adding it?
|
// TODO: `class` should be on this list, right? What are the implications of adding it?
|
||||||
const GLOBAL_KINDS = ['constant', 'function', 'member', 'typedef'];
|
const GLOBAL_KINDS = ['constant', 'function', 'member', 'typedef'];
|
||||||
|
|
||||||
|
export const WATCHABLE_PROPS = [
|
||||||
|
'access',
|
||||||
|
'augments',
|
||||||
|
'borrowed',
|
||||||
|
'ignore',
|
||||||
|
'implements',
|
||||||
|
'kind',
|
||||||
|
'listens',
|
||||||
|
'longname',
|
||||||
|
'memberof',
|
||||||
|
'mixes',
|
||||||
|
'scope',
|
||||||
|
'undocumented',
|
||||||
|
];
|
||||||
|
|
||||||
function fakeMeta(node) {
|
function fakeMeta(node) {
|
||||||
return {
|
return {
|
||||||
type: node ? node.type : null,
|
type: node ? node.type : null,
|
||||||
@ -242,7 +257,7 @@ function resolve(doclet) {
|
|||||||
if (doclet.scope === SCOPE.NAMES.GLOBAL) {
|
if (doclet.scope === SCOPE.NAMES.GLOBAL) {
|
||||||
// via @global tag?
|
// via @global tag?
|
||||||
doclet.setLongname(doclet.name);
|
doclet.setLongname(doclet.name);
|
||||||
delete doclet.memberof;
|
doclet.memberof = undefined;
|
||||||
} else if (about.scope) {
|
} else if (about.scope) {
|
||||||
if (about.memberof === LONGNAMES.GLOBAL) {
|
if (about.memberof === LONGNAMES.GLOBAL) {
|
||||||
// via @memberof <global> ?
|
// via @memberof <global> ?
|
||||||
@ -375,6 +390,7 @@ function copySpecificProperties(primary, secondary, target, include) {
|
|||||||
export class Doclet {
|
export class Doclet {
|
||||||
#accessConfig;
|
#accessConfig;
|
||||||
#dictionary;
|
#dictionary;
|
||||||
|
#eventBus;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a doclet.
|
* Create a doclet.
|
||||||
@ -389,12 +405,31 @@ export class Doclet {
|
|||||||
meta = meta || {};
|
meta = meta || {};
|
||||||
this.#accessConfig = dependencies.get('config')?.opts?.access ?? [];
|
this.#accessConfig = dependencies.get('config')?.opts?.access ?? [];
|
||||||
this.#dictionary = dependencies.get('tags');
|
this.#dictionary = dependencies.get('tags');
|
||||||
/** The original text of the comment from the source code. */
|
this.#eventBus = dependencies.get('eventBus');
|
||||||
this.comment = docletSrc;
|
|
||||||
Object.defineProperty(this, 'dependencies', {
|
Object.defineProperty(this, 'dependencies', {
|
||||||
enumerable: false,
|
enumerable: false,
|
||||||
value: dependencies,
|
value: dependencies,
|
||||||
});
|
});
|
||||||
|
Object.defineProperty(this, 'watchableProps', {
|
||||||
|
enumerable: false,
|
||||||
|
value: {},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
for (const prop of WATCHABLE_PROPS) {
|
||||||
|
Object.defineProperty(this, prop, {
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
return this.watchableProps[prop];
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
this.#setWatchableProperty(prop, newValue);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The original text of the comment from the source code. */
|
||||||
|
this.comment = docletSrc;
|
||||||
this.setMeta(meta);
|
this.setMeta(meta);
|
||||||
|
|
||||||
docletSrc = unwrap(docletSrc);
|
docletSrc = unwrap(docletSrc);
|
||||||
@ -409,6 +444,15 @@ export class Doclet {
|
|||||||
this.postProcess();
|
this.postProcess();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#setWatchableProperty(name, newValue) {
|
||||||
|
const oldValue = this.watchableProps[name];
|
||||||
|
|
||||||
|
if (newValue !== oldValue) {
|
||||||
|
this.watchableProps[name] = newValue;
|
||||||
|
this.#eventBus.emit('docletChanged', { doclet: this, property: name, oldValue, newValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: We call this method in the constructor _and_ in `jsdoc/src/handlers`. It appears that
|
// TODO: We call this method in the constructor _and_ in `jsdoc/src/handlers`. It appears that
|
||||||
// if we don't call the method twice, various doclet properties can be incorrect, including name
|
// if we don't call the method twice, various doclet properties can be incorrect, including name
|
||||||
// and memberof.
|
// and memberof.
|
||||||
@ -421,7 +465,7 @@ export class Doclet {
|
|||||||
this.setLongname(this.name);
|
this.setLongname(this.name);
|
||||||
}
|
}
|
||||||
if (this.memberof === '') {
|
if (this.memberof === '') {
|
||||||
delete this.memberof;
|
this.memberof = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.kind && this.meta && this.meta.code) {
|
if (!this.kind && this.meta && this.meta.code) {
|
||||||
|
|||||||
@ -38,6 +38,10 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
|||||||
expect(doclet.Doclet).toBeFunction();
|
expect(doclet.Doclet).toBeFunction();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('has a WATCHABLE_PROPS array', () => {
|
||||||
|
expect(doclet.WATCHABLE_PROPS).toBeArrayOfStrings();
|
||||||
|
});
|
||||||
|
|
||||||
describe('combineDoclets', () => {
|
describe('combineDoclets', () => {
|
||||||
it('overrides most properties of the secondary doclet', () => {
|
it('overrides most properties of the secondary doclet', () => {
|
||||||
const primaryDoclet = new Doclet(
|
const primaryDoclet = new Doclet(
|
||||||
@ -347,6 +351,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
|||||||
config.opts.access = access.slice();
|
config.opts.access = access.slice();
|
||||||
}
|
}
|
||||||
map.set('config', config);
|
map.set('config', config);
|
||||||
|
map.set('eventBus', jsdoc.deps.get('eventBus'));
|
||||||
map.set('tags', jsdoc.deps.get('tags'));
|
map.set('tags', jsdoc.deps.get('tags'));
|
||||||
|
|
||||||
return map;
|
return map;
|
||||||
@ -385,7 +390,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
|||||||
const newDoclet = makeDoclet(['@name foo', '@function']);
|
const newDoclet = makeDoclet(['@name foo', '@function']);
|
||||||
|
|
||||||
// Just to be sure.
|
// Just to be sure.
|
||||||
delete newDoclet.access;
|
newDoclet.access = undefined;
|
||||||
|
|
||||||
expect(newDoclet.isVisible()).toBeTrue();
|
expect(newDoclet.isVisible()).toBeTrue();
|
||||||
});
|
});
|
||||||
@ -402,7 +407,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
|||||||
newDoclet = makeDoclet(tags, fakeDeps);
|
newDoclet = makeDoclet(tags, fakeDeps);
|
||||||
// Just to be sure.
|
// Just to be sure.
|
||||||
if (!value) {
|
if (!value) {
|
||||||
delete newDoclet.access;
|
newDoclet.access = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return newDoclet;
|
return newDoclet;
|
||||||
@ -439,7 +444,7 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
|||||||
const newDoclet = makeDoclet(['@function', '@name foo'], fakeDeps);
|
const newDoclet = makeDoclet(['@function', '@name foo'], fakeDeps);
|
||||||
|
|
||||||
// Just to be sure.
|
// Just to be sure.
|
||||||
delete newDoclet.access;
|
newDoclet.access = undefined;
|
||||||
|
|
||||||
expect(newDoclet.isVisible()).toBeFalse();
|
expect(newDoclet.isVisible()).toBeFalse();
|
||||||
});
|
});
|
||||||
@ -488,6 +493,76 @@ describe('@jsdoc/doclet/lib/doclet', () => {
|
|||||||
expect(setScope).toThrow();
|
expect(setScope).toThrow();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('watchable properties', () => {
|
||||||
|
const eventBus = jsdoc.deps.get('eventBus');
|
||||||
|
let events;
|
||||||
|
|
||||||
|
function listener(e) {
|
||||||
|
events.push(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
eventBus.on('docletChanged', listener);
|
||||||
|
events = [];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
eventBus.removeListener('docletChanged', listener);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('sends events to the event bus when watchable properties change', () => {
|
||||||
|
const propValues = {
|
||||||
|
access: 'private',
|
||||||
|
augments: 'Foo',
|
||||||
|
borrowed: true,
|
||||||
|
ignore: true,
|
||||||
|
implements: 'Foo',
|
||||||
|
kind: 'class',
|
||||||
|
listens: 'event:foo',
|
||||||
|
longname: 'foo',
|
||||||
|
memberof: 'foo',
|
||||||
|
mixes: 'foo',
|
||||||
|
scope: 'static',
|
||||||
|
undocumented: true,
|
||||||
|
};
|
||||||
|
const keys = Object.keys(propValues);
|
||||||
|
|
||||||
|
// Make sure this test covers all watchable properties.
|
||||||
|
expect(keys).toEqual(doclet.WATCHABLE_PROPS);
|
||||||
|
|
||||||
|
keys.forEach((key) => {
|
||||||
|
const newDoclet = new Doclet('/** Huzzah, a doclet! */', null, jsdoc.deps);
|
||||||
|
|
||||||
|
events = [];
|
||||||
|
|
||||||
|
// Generates first event.
|
||||||
|
newDoclet[key] = propValues[key];
|
||||||
|
// Generates second event.
|
||||||
|
newDoclet[key] = undefined;
|
||||||
|
|
||||||
|
expect(events.length).toBe(2);
|
||||||
|
|
||||||
|
expect(events[0]).toBeObject();
|
||||||
|
expect(events[0].doclet).toBe(newDoclet);
|
||||||
|
expect(events[0].property).toBe(key);
|
||||||
|
if (key === 'kind') {
|
||||||
|
expect(events[0].oldValue).toBe('member');
|
||||||
|
} else if (key === 'longname') {
|
||||||
|
expect(events[0].oldValue).toBeEmptyString();
|
||||||
|
} else {
|
||||||
|
expect(events[0].oldValue).toBeUndefined();
|
||||||
|
}
|
||||||
|
expect(events[0].newValue).toBe(propValues[key]);
|
||||||
|
|
||||||
|
expect(events[1]).toBeObject();
|
||||||
|
expect(events[1].doclet).toBe(newDoclet);
|
||||||
|
expect(events[1].property).toBe(key);
|
||||||
|
expect(events[1].oldValue).toBe(propValues[key]);
|
||||||
|
expect(events[1].newValue).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -631,7 +631,7 @@ export class Parser extends EventEmitter {
|
|||||||
e.doclet.type = _.cloneDeep(doclet.type);
|
e.doclet.type = _.cloneDeep(doclet.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
delete e.doclet.undocumented;
|
e.doclet.undocumented = undefined;
|
||||||
e.doclet.defaultvalue = e.doclet.meta.code.value;
|
e.doclet.defaultvalue = e.doclet.meta.code.value;
|
||||||
|
|
||||||
// add the doclet to the parent's properties
|
// add the doclet to the parent's properties
|
||||||
|
|||||||
@ -44,7 +44,7 @@ export const tags = {
|
|||||||
if (/^(package|private|protected|public)$/i.test(value)) {
|
if (/^(package|private|protected|public)$/i.test(value)) {
|
||||||
doclet.access = value.toLowerCase();
|
doclet.access = value.toLowerCase();
|
||||||
} else {
|
} else {
|
||||||
delete doclet.access;
|
doclet.access = undefined;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -263,7 +263,7 @@ export const tags = {
|
|||||||
mustNotHaveValue: true,
|
mustNotHaveValue: true,
|
||||||
onTagged(doclet) {
|
onTagged(doclet) {
|
||||||
doclet.scope = SCOPE.NAMES.GLOBAL;
|
doclet.scope = SCOPE.NAMES.GLOBAL;
|
||||||
delete doclet.memberof;
|
doclet.memberof = undefined;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
hideconstructor: {
|
hideconstructor: {
|
||||||
@ -349,7 +349,7 @@ export const tags = {
|
|||||||
doclet.forceMemberof = true;
|
doclet.forceMemberof = true;
|
||||||
if (tag.value === LONGNAMES.GLOBAL) {
|
if (tag.value === LONGNAMES.GLOBAL) {
|
||||||
doclet.addTag('global');
|
doclet.addTag('global');
|
||||||
delete doclet.memberof;
|
doclet.memberof = undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
util.setDocletMemberof(doclet, tag);
|
util.setDocletMemberof(doclet, tag);
|
||||||
|
|||||||
@ -179,6 +179,8 @@ export default (() => {
|
|||||||
let cmd;
|
let cmd;
|
||||||
const options = dependencies.get('options');
|
const options = dependencies.get('options');
|
||||||
|
|
||||||
|
dependencies.registerValue('eventBus', bus);
|
||||||
|
|
||||||
// If we already need to exit with an error, don't do any more work.
|
// If we already need to exit with an error, don't do any more work.
|
||||||
if (props.shouldExitWithError) {
|
if (props.shouldExitWithError) {
|
||||||
cmd = () => Promise.resolve(0);
|
cmd = () => Promise.resolve(0);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user