feat(jsdoc-core): move generateDocs() to public API

This commit is contained in:
Jeff Williams 2024-01-01 20:02:32 -08:00
parent 44671a57e6
commit 301a3a4e5d
No known key found for this signature in database
6 changed files with 207 additions and 30 deletions

View File

@ -20,6 +20,8 @@ import path from 'node:path';
import { Env } from '@jsdoc/core';
import glob from 'fast-glob';
const DEFAULT_TEMPLATE = '@jsdoc/template-legacy';
/**
* The API for programmatically generating documentation with JSDoc.
*
@ -38,10 +40,14 @@ export default class Api {
constructor(opts) {
/**
* An event emitter that acts as a message bus for all JSDoc modules.
*
* @type {node:events.EventEmitter}
*/
this.emitter = opts?.emitter ?? new EventEmitter();
/**
* The JSDoc environment, including configuration settings, for this API instance.
*
* @type {module:@jsdoc/core.Env}
*/
this.env = opts?.env ?? new Env();
}
@ -97,4 +103,52 @@ export default class Api {
return this.env.sourceFiles?.slice() ?? [];
}
/**
* Generates documentation with the template specified in the JSDoc environment, or with
* `@jsdoc/template-legacy` if no template is specified.
*
* The template must export a `publish` function that generates output when called and returns a
* `Promise`.
*
* @param {module:@jsdoc/doclet.DocletStore} docletStore - The doclet store obtained by parsing
* the source files.
* @returns {Promise<*>} A promise that is fulfilled after the template runs.
*/
async generateDocs(docletStore) {
let message;
const { log, options } = this.env;
let template;
options.template ??= DEFAULT_TEMPLATE;
try {
template = await import(options.template);
} catch (e) {
if (options.template === DEFAULT_TEMPLATE) {
message =
`Unable to load the default template, \`${DEFAULT_TEMPLATE}\`. You can install ` +
'the default template, or you can install a different template and configure JSDoc to ' +
'use that template.';
} else {
message = `Unable to load the template \`${options.template}\`: ${e.message ?? e}`;
}
log.fatal(message);
return Promise.reject(new Error(message));
}
// Templates must export a `publish` function.
if (template.publish && typeof template.publish === 'function') {
log.info('Generating output files...');
return template.publish(docletStore, this.env);
} else {
message = `\`${options.template}\` does not export a \`publish\` function.`;
log.fatal(message);
return Promise.reject(new Error(message));
}
}
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2024 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.
*/
export const publish = 'foo';

View File

@ -0,0 +1,22 @@
/*
Copyright 2024 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.
*/
export function publish(docletStore, env) {
env.docletStore = docletStore;
env.foo = 'bar';
return Promise.resolve();
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2024 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.
*/
export const foo = 'bar';

View File

@ -173,4 +173,100 @@ describe('Api', () => {
});
});
});
describe('generateDocs', () => {
let env;
let options;
beforeEach(() => {
env = instance.env;
options = env.options;
options.template = path.resolve(
path.join(__dirname, '../../fixtures/templates/fake-template.js')
);
});
it('calls the `publish` function from the specified template', async () => {
await instance.generateDocs();
expect(env.foo).toBe('bar');
});
it('passes the `DocletStore` and `Env` to the template', async () => {
const fakeDocletStore = {};
await instance.generateDocs(fakeDocletStore);
expect(env.docletStore).toBe(fakeDocletStore);
});
it('uses the legacy template by default', async () => {
options.template = null;
try {
await instance.generateDocs();
// We shouldn't get here.
expect(false).toBeTrue();
} catch (e) {
expect(e.message).toContain('default template');
expect(e.message).toContain('@jsdoc/template-legacy');
}
});
it('rejects the promise and logs a fatal error if the template cannot be found', async () => {
spyOn(env.log, 'fatal');
options.template = 'fleeble';
try {
await instance.generateDocs();
// We shouldn't get here.
expect(false).toBeTrue();
} catch (e) {
expect(e.message).toContain('fleeble');
}
expect(env.log.fatal).toHaveBeenCalled();
expect(env.log.fatal.calls.first().args[0]).toContain('fleeble');
});
it('rejects the promise and logs a fatal error if `publish` is undefined', async () => {
spyOn(env.log, 'fatal');
options.template = path.resolve(
path.join(__dirname, '../../fixtures/templates/no-publish-template.js')
);
try {
await instance.generateDocs();
// We shouldn't get here.
expect(false).toBeTrue();
} catch (e) {
expect(e.message).toContain('no-publish-template.js');
}
expect(env.log.fatal).toHaveBeenCalled();
expect(env.log.fatal.calls.first().args[0]).toContain('no-publish-template.js');
});
it('rejects the promise and logs a fatal error if `publish` is not a function', async () => {
spyOn(env.log, 'fatal');
options.template = path.resolve(
path.join(__dirname, '../../fixtures/templates/bad-publish-template.js')
);
try {
await instance.generateDocs();
// We shouldn't get here.
expect(false).toBeTrue();
} catch (e) {
expect(e.message).toContain('bad-publish-template.js');
}
expect(env.log.fatal).toHaveBeenCalled();
expect(env.log.fatal.calls.first().args[0]).toContain('bad-publish-template.js');
});
});
});

View File

@ -260,36 +260,7 @@ export default (() => {
return cli;
};
cli.generateDocs = async () => {
let message;
const { options } = env;
let template;
options.template ??= '@jsdoc/template-legacy';
try {
template = await import(options.template);
} catch (e) {
log.fatal(`Unable to load template: ${e.message ?? e}`);
}
// templates should export a "publish" function
if (template.publish && typeof template.publish === 'function') {
let publishPromise;
log.info('Generating output files...');
publishPromise = template.publish(props.docs, env);
return Promise.resolve(publishPromise);
} else {
message =
`${options.template} does not export a "publish" function. ` +
'Global "publish" functions are no longer supported.';
log.fatal(message);
return Promise.reject(new Error(message));
}
};
cli.generateDocs = () => api.generateDocs(props.docs);
return cli;
})();