mirror of
https://github.com/jsdoc/jsdoc.git
synced 2025-12-08 19:46:11 +00:00
add @jsdoc/task-runner package
This commit is contained in:
parent
5fc4c2c13c
commit
b4c6c0fb1c
2
packages/jsdoc-task-runner/.gitignore
vendored
Normal file
2
packages/jsdoc-task-runner/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Dotfile detritus
|
||||
.DS_Store
|
||||
14
packages/jsdoc-task-runner/.npmignore
Normal file
14
packages/jsdoc-task-runner/.npmignore
Normal file
@ -0,0 +1,14 @@
|
||||
.editorconfig
|
||||
.eslintignore
|
||||
.eslintrc.js
|
||||
.gitignore
|
||||
.github/
|
||||
.renovaterc.json
|
||||
.travis.yml
|
||||
CHANGES.md
|
||||
CODE_OF_CONDUCT.md
|
||||
CONTRIBUTING.md
|
||||
gulpfile.js
|
||||
lerna.json
|
||||
packages/
|
||||
test/
|
||||
202
packages/jsdoc-task-runner/LICENSE
Normal file
202
packages/jsdoc-task-runner/LICENSE
Normal file
@ -0,0 +1,202 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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
|
||||
|
||||
http://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.
|
||||
3
packages/jsdoc-task-runner/README.md
Normal file
3
packages/jsdoc-task-runner/README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# `@jsdoc/task-runner`
|
||||
|
||||
Task runner for JSDoc templates.
|
||||
7
packages/jsdoc-task-runner/index.js
Normal file
7
packages/jsdoc-task-runner/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
const Task = require('./lib/task');
|
||||
const TaskRunner = require('./lib/task-runner');
|
||||
|
||||
module.exports = {
|
||||
Task,
|
||||
TaskRunner
|
||||
};
|
||||
333
packages/jsdoc-task-runner/lib/task-runner.js
Normal file
333
packages/jsdoc-task-runner/lib/task-runner.js
Normal file
@ -0,0 +1,333 @@
|
||||
const _ = require('lodash');
|
||||
const { DepGraph } = require('dependency-graph');
|
||||
const Emittery = require('emittery');
|
||||
const ow = require('ow');
|
||||
const Queue = require('p-queue').default;
|
||||
const v = require('./validators');
|
||||
|
||||
// Work around an annoying typo in a method name.
|
||||
DepGraph.prototype.dependentsOf = DepGraph.prototype.dependantsOf;
|
||||
|
||||
module.exports = class TaskRunner extends Emittery {
|
||||
constructor(context) {
|
||||
super();
|
||||
|
||||
ow(context, ow.optional.object);
|
||||
|
||||
this._init(context);
|
||||
}
|
||||
|
||||
_addOrRemoveTasks(tasks, func, action) {
|
||||
func = _.bind(func, this);
|
||||
|
||||
if (Array.isArray(tasks)) {
|
||||
tasks.forEach((task, i) => {
|
||||
try {
|
||||
func(task);
|
||||
} catch (e) {
|
||||
e.message = `Can't ${action} task ${i}: ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
} else if (tasks !== null && typeof tasks === 'object') {
|
||||
for (const task of Object.keys(tasks)) {
|
||||
try {
|
||||
func(tasks[task]);
|
||||
} catch (e) {
|
||||
e.message = `Can't ${action} task "${task}": ${e.message}`;
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_addTaskEmitters(task) {
|
||||
const u = {};
|
||||
|
||||
u.start = task.on('start', t => this.emit('taskStart', t));
|
||||
u.end = task.on('end', t => this.emit('taskEnd', t));
|
||||
u.error = task.on('error', (e => {
|
||||
this.emit('taskError', {
|
||||
task: e.task,
|
||||
error: e.error
|
||||
});
|
||||
|
||||
if (!this._error) {
|
||||
this._error = e.error;
|
||||
}
|
||||
}));
|
||||
|
||||
this._unsubscribers.set(task.name, u);
|
||||
}
|
||||
|
||||
_addTaskSequenceToQueue(tasks) {
|
||||
let firstTask;
|
||||
let promise;
|
||||
|
||||
if (!tasks.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
firstTask = this._nameToTask.get(tasks[0]);
|
||||
// We don't want to run the first task yet, so we wrap it in another promise.
|
||||
promise = new Promise((resolve, reject) => {
|
||||
this._bindTaskFunc(firstTask)().then(resolve, reject);
|
||||
});
|
||||
|
||||
promise = tasks.reduce((p, _taskName, i) => {
|
||||
const nextTask = this._nameToTask.get(tasks[i + 1]);
|
||||
|
||||
if (!nextTask) {
|
||||
return p;
|
||||
} else {
|
||||
return p.then(this._bindTaskFunc(nextTask));
|
||||
}
|
||||
}, promise);
|
||||
|
||||
this._queue.add(() => promise);
|
||||
}
|
||||
|
||||
_addTaskToQueue(task) {
|
||||
this._queue.add(this._bindTaskFunc(task));
|
||||
}
|
||||
|
||||
_bindTaskFunc(task) {
|
||||
return _.bind(task.run, task, this.context);
|
||||
}
|
||||
|
||||
_init(context) {
|
||||
this._deps = new Map();
|
||||
this._error = null;
|
||||
this._queue = new Queue();
|
||||
this._taskToName = new WeakMap();
|
||||
this._nameToTask = new Map();
|
||||
this._running = false;
|
||||
this._unsubscribers = new Map();
|
||||
|
||||
this.context = context || {};
|
||||
}
|
||||
|
||||
_newCyclicalDependencyError(cyclePath) {
|
||||
return new v.CyclicalDependencyError(
|
||||
`Tasks have circular dependencies: ${cyclePath.join(' > ')}`,
|
||||
cyclePath
|
||||
);
|
||||
}
|
||||
|
||||
_newStateError() {
|
||||
return new v.StateError('The task runner is already running.');
|
||||
}
|
||||
|
||||
_newUnknownDepsError(dependent, unknownDeps) {
|
||||
let errorText;
|
||||
|
||||
if (unknownDeps.length === 1) {
|
||||
errorText = 'an unknown task';
|
||||
} else {
|
||||
errorText = 'unknown tasks';
|
||||
}
|
||||
|
||||
return new v.UnknownDependencyError(`The task ${dependent} depends on ${errorText}: ` +
|
||||
`${unknownDeps.join(', ')}`);
|
||||
}
|
||||
|
||||
_orderTasks() {
|
||||
let error;
|
||||
const graph = new DepGraph();
|
||||
let parallel;
|
||||
let sequential;
|
||||
|
||||
for (const [task] of this._nameToTask) {
|
||||
graph.addNode(task);
|
||||
}
|
||||
|
||||
for (const [dependent] of this._deps) {
|
||||
const unknownDeps = [];
|
||||
|
||||
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);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
try {
|
||||
// Get standalone tasks with no dependencies and no dependents.
|
||||
parallel = graph.overallOrder(true)
|
||||
.filter(task => !(graph.dependentsOf(task).length));
|
||||
// Get tasks with dependencies, in a correctly ordered list.
|
||||
sequential = graph.overallOrder().filter(task => !parallel.includes(task));
|
||||
} catch (e) {
|
||||
error = this._newCyclicalDependencyError(e.cyclePath);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
error,
|
||||
parallel,
|
||||
sequential
|
||||
};
|
||||
}
|
||||
|
||||
_rejectIfRunning() {
|
||||
if (this.running) {
|
||||
return Promise.reject(this._newStateError());
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
_throwIfRunning() {
|
||||
if (this.running) {
|
||||
throw this._newStateError();
|
||||
}
|
||||
}
|
||||
|
||||
_throwIfUnknownDeps(dependent, unknownDeps) {
|
||||
if (!unknownDeps.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw this._newUnknownDepsError(dependent, unknownDeps);
|
||||
}
|
||||
|
||||
addTask(task) {
|
||||
ow(task, v.checkTaskOrString);
|
||||
|
||||
this._throwIfRunning();
|
||||
|
||||
this._nameToTask.set(task.name, task);
|
||||
if (task.dependsOn) {
|
||||
this._deps.set(task.name, task.dependsOn);
|
||||
}
|
||||
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');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
end() {
|
||||
this.emit('end', {
|
||||
error: this._error
|
||||
});
|
||||
this._queue.clear();
|
||||
this._init();
|
||||
}
|
||||
|
||||
removeTask(task) {
|
||||
let unsubscribers;
|
||||
|
||||
ow(task, v.checkTaskOrString);
|
||||
this._throwIfRunning();
|
||||
|
||||
if (typeof task === 'string') {
|
||||
task = this._nameToTask.get(task);
|
||||
|
||||
if (!task) {
|
||||
throw new v.UnknownTaskError(`Unknown task: ${task}`);
|
||||
}
|
||||
} else if (typeof task === 'object') {
|
||||
if (!this._taskToName.has(task)) {
|
||||
throw new v.UnknownTaskError(`Unknown task: ${task}`);
|
||||
}
|
||||
}
|
||||
|
||||
this._nameToTask.delete(task.name);
|
||||
this._taskToName.delete(task);
|
||||
this._deps.delete(task.name);
|
||||
|
||||
unsubscribers = this._unsubscribers.get(task.name);
|
||||
for (const u of Object.keys(unsubscribers)) {
|
||||
unsubscribers[u]();
|
||||
}
|
||||
this._unsubscribers.delete(task.name);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
removeTasks(tasks) {
|
||||
ow(tasks, ow.any(ow.array, ow.object));
|
||||
this._addOrRemoveTasks(tasks, this.removeTask, 'remove');
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
run() {
|
||||
let endPromise;
|
||||
let { error, parallel, sequential } = this._orderTasks();
|
||||
let runningPromise;
|
||||
|
||||
// First, fail based on the runner's state.
|
||||
runningPromise = this._rejectIfRunning();
|
||||
if (runningPromise) {
|
||||
return runningPromise;
|
||||
}
|
||||
|
||||
// Then fail if the tasks couldn't be ordered.
|
||||
if (error) {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
this._queue.pause();
|
||||
|
||||
for (const taskName of parallel) {
|
||||
this._addTaskToQueue(this._nameToTask.get(taskName));
|
||||
}
|
||||
this._addTaskSequenceToQueue(sequential);
|
||||
|
||||
endPromise = this._queue.onIdle().then(() => {
|
||||
let p;
|
||||
|
||||
if (this._error) {
|
||||
p = Promise.reject(this._error);
|
||||
} else {
|
||||
p = Promise.resolve();
|
||||
}
|
||||
this.end();
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
this.emit('start');
|
||||
this._running = true;
|
||||
try {
|
||||
this._queue.start();
|
||||
|
||||
return endPromise;
|
||||
} catch (e) {
|
||||
this._error = e;
|
||||
this.end();
|
||||
|
||||
return Promise.reject(e);
|
||||
}
|
||||
}
|
||||
|
||||
get running() {
|
||||
return this._running;
|
||||
}
|
||||
|
||||
get tasks() {
|
||||
const entries = [];
|
||||
|
||||
for (const entry of this._nameToTask.entries()) {
|
||||
entries.push(entry);
|
||||
}
|
||||
|
||||
return _.fromPairs(entries);
|
||||
}
|
||||
};
|
||||
48
packages/jsdoc-task-runner/lib/task.js
Normal file
48
packages/jsdoc-task-runner/lib/task.js
Normal file
@ -0,0 +1,48 @@
|
||||
const Emittery = require('emittery');
|
||||
const ow = require('ow');
|
||||
|
||||
module.exports = class Task extends Emittery {
|
||||
constructor(opts = {}) {
|
||||
let deps;
|
||||
|
||||
super();
|
||||
|
||||
ow(opts.name, ow.optional.string);
|
||||
ow(opts.func, ow.optional.function);
|
||||
ow(opts.dependsOn, ow.any(
|
||||
ow.optional.string,
|
||||
ow.optional.array.ofType(ow.string)
|
||||
));
|
||||
|
||||
if (typeof opts.dependsOn === 'string') {
|
||||
deps = [opts.dependsOn];
|
||||
} else if (Array.isArray(opts.dependsOn)) {
|
||||
deps = opts.dependsOn.slice(0);
|
||||
}
|
||||
|
||||
this.name = opts.name || null;
|
||||
this.func = opts.func || null;
|
||||
this.dependsOn = deps || [];
|
||||
}
|
||||
|
||||
run(context) {
|
||||
ow(this.name, ow.string);
|
||||
ow(this.func, ow.function);
|
||||
ow(this.dependsOn, ow.array.ofType(ow.string));
|
||||
|
||||
this.emit('start', this);
|
||||
|
||||
return this.func(context).then(
|
||||
() => {
|
||||
this.emit('end', this);
|
||||
},
|
||||
error => {
|
||||
this.emit('error', {
|
||||
task: this,
|
||||
error
|
||||
});
|
||||
this.emit('end', this);
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
47
packages/jsdoc-task-runner/lib/validators.js
Normal file
47
packages/jsdoc-task-runner/lib/validators.js
Normal file
@ -0,0 +1,47 @@
|
||||
const ow = require('ow');
|
||||
const Task = require('./task');
|
||||
|
||||
function checkTask(t) {
|
||||
return {
|
||||
validator: t instanceof Task,
|
||||
message: `Expected ${t} to be a Task object`
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkTaskOrString: ow.any(ow.object.validate(checkTask), ow.string),
|
||||
CyclicalDependencyError: class CyclicalDependencyError extends Error {
|
||||
constructor(message, cyclePath) {
|
||||
ow(message, ow.string);
|
||||
ow(cyclePath, ow.array.ofType(ow.string));
|
||||
super(message);
|
||||
|
||||
this.cyclePath = cyclePath;
|
||||
this.name = 'CyclicalDependencyError';
|
||||
}
|
||||
},
|
||||
StateError: class StateError extends Error {
|
||||
constructor(message) {
|
||||
ow(message, ow.string);
|
||||
super(message);
|
||||
|
||||
this.name = 'StateError';
|
||||
}
|
||||
},
|
||||
UnknownDependencyError: class UnknownDependencyError extends Error {
|
||||
constructor(message) {
|
||||
ow(message, ow.string);
|
||||
super(message);
|
||||
|
||||
this.name = 'UnknownDependencyError';
|
||||
}
|
||||
},
|
||||
UnknownTaskError: class UnknownTaskError extends Error {
|
||||
constructor(message) {
|
||||
ow(message, ow.string);
|
||||
super(message);
|
||||
|
||||
this.name = 'UnknownTaskError';
|
||||
}
|
||||
}
|
||||
};
|
||||
58
packages/jsdoc-task-runner/package-lock.json
generated
Normal file
58
packages/jsdoc-task-runner/package-lock.json
generated
Normal file
@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@jsdoc/task-runner",
|
||||
"version": "0.0.1",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"dependency-graph": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.8.0.tgz",
|
||||
"integrity": "sha512-DCvzSq2UiMsuLnj/9AL484ummEgLtZIcRS7YvtO38QnpX3vqh9nJ8P+zhu8Ja+SmLrBHO2iDbva20jq38qvBkQ=="
|
||||
},
|
||||
"emittery": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/emittery/-/emittery-0.5.0.tgz",
|
||||
"integrity": "sha512-NTvZ9CPTp4ehb0MSWHGuvMUDEDSRXBuuA9vt8pUfJ4afU5EqakJsuGPwnhbw/smOqmFdtgFcziIeYZ9uQleSOA=="
|
||||
},
|
||||
"eventemitter3": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz",
|
||||
"integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg=="
|
||||
},
|
||||
"ow": {
|
||||
"version": "0.13.2",
|
||||
"resolved": "https://registry.npmjs.org/ow/-/ow-0.13.2.tgz",
|
||||
"integrity": "sha512-9wvr+q+ZTDRvXDjL6eDOdFe5WUl/wa5sntf9kAolxqSpkBqaIObwLgFCGXSJASFw+YciXnOVtDWpxXa9cqV94A==",
|
||||
"requires": {
|
||||
"type-fest": "^0.5.1"
|
||||
}
|
||||
},
|
||||
"p-finally": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
|
||||
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4="
|
||||
},
|
||||
"p-queue": {
|
||||
"version": "6.1.1",
|
||||
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.1.1.tgz",
|
||||
"integrity": "sha512-R9gq36Th88xZ+rWAptN5IXLwqkwA1gagCQhT6ZXQ6RxEfmjb9ZW+UBzRVqv9sm5TQmbbI/TsKgGLbOaA61xR5w==",
|
||||
"requires": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"p-timeout": "^3.1.0"
|
||||
}
|
||||
},
|
||||
"p-timeout": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz",
|
||||
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
|
||||
"requires": {
|
||||
"p-finally": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.5.2.tgz",
|
||||
"integrity": "sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw=="
|
||||
}
|
||||
}
|
||||
}
|
||||
31
packages/jsdoc-task-runner/package.json
Normal file
31
packages/jsdoc-task-runner/package.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "@jsdoc/task-runner",
|
||||
"version": "0.0.1",
|
||||
"description": "Task runner for JSDoc templates.",
|
||||
"keywords": [
|
||||
"jsdoc",
|
||||
"task"
|
||||
],
|
||||
"author": "Jeff Williams <jeffrey.l.williams@gmail.com>",
|
||||
"homepage": "https://jsdoc.app/",
|
||||
"license": "Apache-2.0",
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/jsdoc/jsdoc.git"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: run tests from root\" && exit 1"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/jsdoc/jsdoc/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"dependency-graph": "^0.8.0",
|
||||
"emittery": "^0.5.0",
|
||||
"ow": "^0.13.2",
|
||||
"p-queue": "^6.1.1"
|
||||
}
|
||||
}
|
||||
23
packages/jsdoc-task-runner/test/specs/index.js
Normal file
23
packages/jsdoc-task-runner/test/specs/index.js
Normal file
@ -0,0 +1,23 @@
|
||||
const taskRunner = require('../../index');
|
||||
|
||||
describe('@jsdoc/task-runner', () => {
|
||||
it('is an object', () => {
|
||||
expect(taskRunner).toBeObject();
|
||||
});
|
||||
|
||||
describe('Task', () => {
|
||||
it('is lib/task', () => {
|
||||
const Task = require('../../lib/task');
|
||||
|
||||
expect(taskRunner.Task).toBe(Task);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskRunner', () => {
|
||||
it('is lib/task-runner', () => {
|
||||
const TaskRunner = require('../../lib/task-runner');
|
||||
|
||||
expect(taskRunner.TaskRunner).toBe(TaskRunner);
|
||||
});
|
||||
});
|
||||
});
|
||||
782
packages/jsdoc-task-runner/test/specs/lib/task-runner.js
Normal file
782
packages/jsdoc-task-runner/test/specs/lib/task-runner.js
Normal file
@ -0,0 +1,782 @@
|
||||
const Emittery = require('emittery');
|
||||
const Task = require('../../../lib/task');
|
||||
const TaskRunner = require('../../../lib/task-runner');
|
||||
|
||||
const ARGUMENT_ERROR = 'ArgumentError';
|
||||
const CYCLICAL_DEPENDENCY_ERROR = 'CyclicalDependencyError';
|
||||
const STATE_ERROR = 'StateError';
|
||||
const UNKNOWN_DEPENDENCY_ERROR = 'UnknownDependencyError';
|
||||
const UNKNOWN_TASK_ERROR = 'UnknownTaskError';
|
||||
|
||||
function rethrower(e) {
|
||||
return () => {
|
||||
throw e;
|
||||
};
|
||||
}
|
||||
|
||||
describe('@jsdoc/task-runner/lib/task-runner', () => {
|
||||
let badTask;
|
||||
let bar;
|
||||
let barResult;
|
||||
const fakeTask = {
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve()
|
||||
};
|
||||
let foo;
|
||||
let fooResult;
|
||||
let runner;
|
||||
|
||||
beforeEach(() => {
|
||||
runner = new TaskRunner();
|
||||
foo = new Task({
|
||||
name: 'foo',
|
||||
func: () => new Promise(resolve => {
|
||||
fooResult = true;
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
fooResult = null;
|
||||
bar = new Task({
|
||||
name: 'bar',
|
||||
func: () => new Promise(resolve => {
|
||||
barResult = true;
|
||||
resolve();
|
||||
})
|
||||
});
|
||||
barResult = null;
|
||||
badTask = new Task({
|
||||
name: 'badTask',
|
||||
func: () => Promise.reject(new Error())
|
||||
});
|
||||
});
|
||||
|
||||
it('is a function', () => {
|
||||
expect(TaskRunner).toBeFunction();
|
||||
});
|
||||
|
||||
it('inherits from emittery', () => {
|
||||
expect(runner instanceof Emittery).toBeTrue();
|
||||
});
|
||||
|
||||
it('has no required parameters', () => {
|
||||
function factory() {
|
||||
return new TaskRunner();
|
||||
}
|
||||
|
||||
expect(factory).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts a context object', () => {
|
||||
const context = {};
|
||||
|
||||
runner = new TaskRunner(context);
|
||||
|
||||
expect(runner.context).toBe(context);
|
||||
});
|
||||
|
||||
it('does not accept a non-object context', () => {
|
||||
function factory() {
|
||||
return new TaskRunner(7);
|
||||
}
|
||||
|
||||
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
describe('addTask', () => {
|
||||
it('adds `Task` objects', () => {
|
||||
function add() {
|
||||
runner.addTask(foo);
|
||||
}
|
||||
|
||||
expect(add).not.toThrow();
|
||||
});
|
||||
|
||||
it('fails with non-`Task` objects', () => {
|
||||
function add() {
|
||||
runner.addTask(fakeTask);
|
||||
}
|
||||
|
||||
expect(add).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('fails if the task runner is already running', () => {
|
||||
function addWhileRunning() {
|
||||
runner.run();
|
||||
runner.addTask(foo);
|
||||
}
|
||||
|
||||
expect(addWhileRunning).toThrowErrorOfType(STATE_ERROR);
|
||||
});
|
||||
|
||||
// We run the task, rather than just checking the value of `runner.tasks`, to make sure
|
||||
// _all_ the internal state was set correctly.
|
||||
it('correctly adds the task', async () => {
|
||||
runner.addTask(foo);
|
||||
await runner.run();
|
||||
|
||||
expect(fooResult).toBeTrue();
|
||||
});
|
||||
|
||||
it('causes the task to be emitted when the task starts', async () => {
|
||||
let promise;
|
||||
|
||||
runner.addTask(foo);
|
||||
promise = runner.once('taskStart');
|
||||
|
||||
foo.run();
|
||||
await promise.then(event => {
|
||||
expect(event).toBe(foo);
|
||||
});
|
||||
});
|
||||
|
||||
it('causes the task to be emitted when the task ends', async () => {
|
||||
let event;
|
||||
let promise;
|
||||
|
||||
runner.addTask(foo);
|
||||
promise = runner.once('taskEnd');
|
||||
|
||||
foo.run();
|
||||
event = await promise;
|
||||
|
||||
expect(event).toBe(foo);
|
||||
});
|
||||
|
||||
it('causes an error to be emitted if the task errors', async () => {
|
||||
let error;
|
||||
let event;
|
||||
let promise;
|
||||
|
||||
runner.addTask(badTask);
|
||||
promise = runner.once('taskError');
|
||||
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
event = await promise;
|
||||
|
||||
expect(event).toBeObject();
|
||||
expect(rethrower(event.error)).toThrowError();
|
||||
expect(event.error).toBe(error);
|
||||
expect(event.task).toBe(badTask);
|
||||
});
|
||||
});
|
||||
|
||||
describe('addTasks', () => {
|
||||
it('accepts an object whose values are tasks', () => {
|
||||
function addTasks() {
|
||||
runner.addTasks({ foo });
|
||||
}
|
||||
|
||||
expect(addTasks).not.toThrow();
|
||||
});
|
||||
|
||||
it('fails with an object whose values are not tasks', () => {
|
||||
function addTasks() {
|
||||
runner.addTasks({ foo: fakeTask });
|
||||
}
|
||||
|
||||
expect(addTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('accepts an array of tasks', () => {
|
||||
function addTasks() {
|
||||
runner.addTasks([foo]);
|
||||
}
|
||||
|
||||
expect(addTasks).not.toThrow();
|
||||
});
|
||||
|
||||
it('fails with an array of non-tasks', () => {
|
||||
function addTasks() {
|
||||
runner.addTasks([fakeTask]);
|
||||
}
|
||||
|
||||
expect(addTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('fails with non-object, non-array input', () => {
|
||||
function addTasks() {
|
||||
runner.addTasks(7);
|
||||
}
|
||||
|
||||
expect(addTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('adds all the tasks in an object', () => {
|
||||
let tasks;
|
||||
|
||||
runner.addTasks({
|
||||
foo,
|
||||
bar
|
||||
});
|
||||
tasks = runner.tasks;
|
||||
|
||||
expect(tasks.foo).toBe(foo);
|
||||
expect(tasks.bar).toBe(bar);
|
||||
});
|
||||
|
||||
it('adds all the tasks in an array', () => {
|
||||
let tasks;
|
||||
|
||||
runner.addTasks([
|
||||
foo,
|
||||
bar
|
||||
]);
|
||||
tasks = runner.tasks;
|
||||
|
||||
expect(tasks.foo).toBe(foo);
|
||||
expect(tasks.bar).toBe(bar);
|
||||
});
|
||||
|
||||
it('returns `this`', () => {
|
||||
const result = runner.addTasks({
|
||||
foo,
|
||||
bar
|
||||
});
|
||||
|
||||
expect(result).toBe(runner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('context', () => {
|
||||
it('is the provided context', () => {
|
||||
const context = {};
|
||||
const r = new TaskRunner(context);
|
||||
|
||||
expect(r.context).toBe(context);
|
||||
});
|
||||
});
|
||||
|
||||
describe('end', () => {
|
||||
it('stops the task runner', () => {
|
||||
function addAfterEnding() {
|
||||
runner.addTask(foo);
|
||||
runner.run();
|
||||
runner.end();
|
||||
runner.addTask(bar);
|
||||
}
|
||||
|
||||
expect(addAfterEnding).not.toThrow();
|
||||
});
|
||||
|
||||
it('resets the tasks', async () => {
|
||||
runner.addTask(foo);
|
||||
runner.run();
|
||||
await runner.end();
|
||||
|
||||
expect(runner.tasks).toBeEmptyObject();
|
||||
});
|
||||
|
||||
it('resets the context', async () => {
|
||||
runner.addTask(foo);
|
||||
runner.run();
|
||||
await runner.end();
|
||||
|
||||
expect(runner.context).toBeEmptyObject();
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTask', () => {
|
||||
it('removes `Task` objects', () => {
|
||||
runner.addTask(foo);
|
||||
runner.removeTask(foo);
|
||||
|
||||
expect(runner.tasks.foo).toBeUndefined();
|
||||
});
|
||||
|
||||
it('removes tasks by name', () => {
|
||||
runner.addTask(foo);
|
||||
runner.removeTask('foo');
|
||||
|
||||
expect(runner.tasks.foo).toBeUndefined();
|
||||
});
|
||||
|
||||
it('fails on invalid input types', () => {
|
||||
function addRemove() {
|
||||
runner.addTask(foo);
|
||||
runner.removeTask(7);
|
||||
}
|
||||
|
||||
expect(addRemove).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('fails on unknown tasks', () => {
|
||||
function addRemove() {
|
||||
runner.addTask(foo);
|
||||
runner.removeTask(bar);
|
||||
}
|
||||
|
||||
expect(addRemove).toThrowErrorOfType(UNKNOWN_TASK_ERROR);
|
||||
});
|
||||
|
||||
it('fails if the task runner is already running', () => {
|
||||
function removeWhileRunning() {
|
||||
runner.addTasks([foo, bar]);
|
||||
runner.run();
|
||||
runner.removeTask(foo);
|
||||
}
|
||||
|
||||
expect(removeWhileRunning).toThrowErrorOfType(STATE_ERROR);
|
||||
});
|
||||
|
||||
it('correctly removes the task', async () => {
|
||||
let error;
|
||||
|
||||
runner.addTasks([foo, bar]);
|
||||
runner.removeTask(foo);
|
||||
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeUndefined();
|
||||
expect(fooResult).toBeNull();
|
||||
expect(barResult).toBeTrue();
|
||||
});
|
||||
|
||||
it('prevents `taskStart`/`taskEnd` events for the task', async () => {
|
||||
let startEvent;
|
||||
let endEvent;
|
||||
|
||||
runner.on('taskStart', e => {
|
||||
startEvent = e;
|
||||
});
|
||||
runner.on('taskEnd', e => {
|
||||
endEvent = e;
|
||||
});
|
||||
runner.addTask(foo);
|
||||
runner.removeTask(foo);
|
||||
await foo.run();
|
||||
|
||||
expect(startEvent).toBeUndefined();
|
||||
expect(endEvent).toBeUndefined();
|
||||
});
|
||||
|
||||
it('prevents `taskError` events for the task', async () => {
|
||||
let errorEvent;
|
||||
let taskErrorEvent;
|
||||
|
||||
runner.addTask(badTask);
|
||||
runner.removeTask(badTask);
|
||||
|
||||
badTask.on('error', e => {
|
||||
errorEvent = e;
|
||||
});
|
||||
runner.on('taskError', e => {
|
||||
taskErrorEvent = e;
|
||||
});
|
||||
|
||||
try {
|
||||
await badTask.run();
|
||||
} catch (e) {
|
||||
// Expected behavior.
|
||||
}
|
||||
|
||||
expect(errorEvent).toBeObject();
|
||||
expect(errorEvent.task).toBe(badTask);
|
||||
expect(taskErrorEvent).toBeUndefined();
|
||||
});
|
||||
|
||||
it('returns `this`', () => {
|
||||
let result;
|
||||
|
||||
runner.addTask(foo);
|
||||
result = runner.removeTask(foo);
|
||||
|
||||
expect(result).toBe(runner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeTasks', () => {
|
||||
it('accepts an object whose values are tasks', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks({ foo });
|
||||
}
|
||||
|
||||
runner.addTask(foo);
|
||||
|
||||
expect(removeTasks).not.toThrow();
|
||||
});
|
||||
|
||||
it('fails with an object whose values are not tasks', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks({ foo: 7 });
|
||||
}
|
||||
|
||||
runner.addTask(foo);
|
||||
|
||||
expect(removeTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('accepts an array of tasks', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks([foo]);
|
||||
}
|
||||
|
||||
runner.addTask(foo);
|
||||
|
||||
expect(removeTasks).not.toThrow();
|
||||
});
|
||||
|
||||
it('accepts an array of strings', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks(['foo']);
|
||||
}
|
||||
|
||||
runner.addTask(foo);
|
||||
|
||||
expect(removeTasks).not.toThrow();
|
||||
});
|
||||
|
||||
it('fails with an array whose values are not tasks or strings', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks([fakeTask]);
|
||||
}
|
||||
|
||||
expect(removeTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('fails with non-object, non-array input', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks(7);
|
||||
}
|
||||
|
||||
expect(removeTasks).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('fails with unknown tasks', () => {
|
||||
function removeTasks() {
|
||||
runner.removeTasks([bar]);
|
||||
}
|
||||
|
||||
expect(removeTasks).toThrowErrorOfType(UNKNOWN_TASK_ERROR);
|
||||
});
|
||||
|
||||
it('removes all the tasks in an object', () => {
|
||||
const tasks = {
|
||||
foo,
|
||||
bar
|
||||
};
|
||||
|
||||
runner.addTasks(tasks);
|
||||
runner.removeTasks(tasks);
|
||||
|
||||
expect(runner.tasks).toBeEmptyObject();
|
||||
});
|
||||
|
||||
it('removes all the tasks in an array', () => {
|
||||
const tasks = [
|
||||
foo,
|
||||
bar
|
||||
];
|
||||
|
||||
runner.addTasks(tasks);
|
||||
runner.removeTasks(tasks);
|
||||
|
||||
expect(runner.tasks).toBeEmptyObject();
|
||||
});
|
||||
|
||||
it('returns `this`', () => {
|
||||
runner.addTask(foo);
|
||||
|
||||
expect(runner.removeTask(foo)).toBe(runner);
|
||||
});
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
let a;
|
||||
let b;
|
||||
let c;
|
||||
let taskA;
|
||||
let taskB;
|
||||
let taskC;
|
||||
|
||||
beforeEach(() => {
|
||||
a = null;
|
||||
b = null;
|
||||
c = null;
|
||||
|
||||
taskA = new Task({
|
||||
name: 'a',
|
||||
func: () => {
|
||||
a = 5;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
taskB = new Task({
|
||||
name: 'b',
|
||||
func: () => {
|
||||
b = a * 3;
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
dependsOn: ['a']
|
||||
});
|
||||
taskC = new Task({
|
||||
name: 'c',
|
||||
func: () => {
|
||||
c = a + b;
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
dependsOn: ['b']
|
||||
});
|
||||
});
|
||||
|
||||
it('runs every task', async () => {
|
||||
runner.addTasks([taskA, taskB, taskC]);
|
||||
await runner.run();
|
||||
|
||||
expect(a).toBe(5);
|
||||
expect(b).toBe(15);
|
||||
expect(c).toBe(20);
|
||||
});
|
||||
|
||||
it('runs tasks with dependencies in the correct order', async () => {
|
||||
const results = [];
|
||||
|
||||
taskA.on('end', () => {
|
||||
results.push(a);
|
||||
});
|
||||
taskB.on('end', () => {
|
||||
results.push(b);
|
||||
});
|
||||
taskC.on('end', () => {
|
||||
results.push(c);
|
||||
});
|
||||
|
||||
runner.addTasks([taskA, taskB, taskC]);
|
||||
await runner.run();
|
||||
|
||||
expect(results).toEqual([5, 15, 20]);
|
||||
});
|
||||
|
||||
it('fails if the task runner is already running', async () => {
|
||||
let error;
|
||||
|
||||
runner.addTask(taskA);
|
||||
runner.run();
|
||||
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(rethrower(error)).toThrowErrorOfType(STATE_ERROR);
|
||||
});
|
||||
|
||||
it('clears all of its state after it runs', async () => {
|
||||
runner.addTask(taskA);
|
||||
await runner.run();
|
||||
|
||||
expect(runner.context).toBeEmptyObject();
|
||||
expect(runner.tasks).toBeEmptyObject();
|
||||
});
|
||||
|
||||
it('fails if a task errors', async () => {
|
||||
let error;
|
||||
|
||||
runner.addTask(badTask);
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(rethrower(error)).toThrowError();
|
||||
});
|
||||
|
||||
describe('context', () => {
|
||||
it('passes the context to tasks with no dependencies', async () => {
|
||||
const context = {};
|
||||
const r = new TaskRunner(context);
|
||||
|
||||
r.addTask(new Task({
|
||||
name: 'usesContext',
|
||||
func: ctx => {
|
||||
ctx.foo = 'bar';
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
|
||||
await r.run();
|
||||
expect(context.foo).toBe('bar');
|
||||
});
|
||||
|
||||
it('passes the context to tasks with dependencies', async () => {
|
||||
const context = {};
|
||||
const r = new TaskRunner(context);
|
||||
|
||||
r.addTasks([
|
||||
new Task({
|
||||
name: 'one',
|
||||
func: ctx => {
|
||||
ctx.foo = 'bar';
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}),
|
||||
new Task({
|
||||
name: 'two',
|
||||
func: ctx => {
|
||||
ctx.bar = ctx.foo + ' baz';
|
||||
|
||||
return Promise.resolve();
|
||||
},
|
||||
dependsOn: ['one']
|
||||
})
|
||||
]);
|
||||
|
||||
await r.run();
|
||||
expect(context.bar).toBe('bar baz');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dependencies', () => {
|
||||
it('errors if a task depends on an unknown task', async () => {
|
||||
let error;
|
||||
|
||||
runner.addTask(new Task({
|
||||
name: 'badDependsOn',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn: ['mysteryTask']
|
||||
}));
|
||||
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(rethrower(error)).toThrowErrorOfType(UNKNOWN_DEPENDENCY_ERROR);
|
||||
});
|
||||
|
||||
it('errors if there are circular dependencies', async () => {
|
||||
let error;
|
||||
|
||||
runner.addTasks([
|
||||
new Task({
|
||||
name: 'one',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn: ['two']
|
||||
}),
|
||||
new Task({
|
||||
name: 'two',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn: ['one']
|
||||
})
|
||||
]);
|
||||
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(rethrower(error)).toThrowErrorOfType(CYCLICAL_DEPENDENCY_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('emits a `start` event', async () => {
|
||||
let emitted;
|
||||
|
||||
runner.addTask(foo);
|
||||
runner.on('start', () => {
|
||||
emitted = true;
|
||||
});
|
||||
await runner.run();
|
||||
|
||||
expect(emitted).toBeTrue();
|
||||
});
|
||||
|
||||
it('emits an `end` event', async () => {
|
||||
let emitted;
|
||||
|
||||
runner.addTask(foo);
|
||||
runner.on('end', e => {
|
||||
emitted = e;
|
||||
});
|
||||
await runner.run();
|
||||
|
||||
expect(emitted).toBeObject();
|
||||
expect(emitted.error).toBeNull();
|
||||
});
|
||||
|
||||
it('fails and emits an error in the `end` event if necessary', async () => {
|
||||
let endError;
|
||||
let error;
|
||||
|
||||
runner.addTask(badTask);
|
||||
runner.on('end', e => {
|
||||
endError = e.error;
|
||||
});
|
||||
|
||||
try {
|
||||
await runner.run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(rethrower(endError)).toThrowError();
|
||||
expect(endError).toBe(error);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('running', () => {
|
||||
it('is true when the task runner is running', async () => {
|
||||
let running;
|
||||
|
||||
runner.addTask(new Task({
|
||||
name: 'checkRunning',
|
||||
func: () => {
|
||||
running = runner.running;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
}));
|
||||
await runner.run();
|
||||
|
||||
expect(running).toBeTrue();
|
||||
});
|
||||
|
||||
it('is false when the task runner has not started', () => {
|
||||
runner.addTask(foo);
|
||||
|
||||
expect(runner.running).toBeFalse();
|
||||
});
|
||||
|
||||
it('is false after the task runner has finished', async () => {
|
||||
runner.addTask(foo);
|
||||
await runner.run();
|
||||
|
||||
expect(runner.running).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
describe('tasks', () => {
|
||||
it('is an object in which keys are task names and values are tasks', () => {
|
||||
let tasks;
|
||||
|
||||
runner.addTask(foo);
|
||||
tasks = runner.tasks;
|
||||
|
||||
expect(tasks).toBeObject();
|
||||
expect(tasks.foo).toBe(foo);
|
||||
});
|
||||
|
||||
it('is an empty object if no tasks have been added', () => {
|
||||
expect(runner.tasks).toBeEmptyObject();
|
||||
});
|
||||
});
|
||||
});
|
||||
204
packages/jsdoc-task-runner/test/specs/lib/task.js
Normal file
204
packages/jsdoc-task-runner/test/specs/lib/task.js
Normal file
@ -0,0 +1,204 @@
|
||||
const Emittery = require('emittery');
|
||||
const Task = require('../../../lib/task');
|
||||
|
||||
const ARGUMENT_ERROR = 'ArgumentError';
|
||||
|
||||
describe('@jsdoc/task-runner/lib/task', () => {
|
||||
it('is a function', () => {
|
||||
expect(Task).toBeFunction();
|
||||
});
|
||||
|
||||
it('inherits from emittery', () => {
|
||||
expect(new Task() instanceof Emittery).toBeTrue();
|
||||
});
|
||||
|
||||
it('can be constructed with no arguments', () => {
|
||||
function factory() {
|
||||
return new Task();
|
||||
}
|
||||
|
||||
expect(factory).not.toThrow();
|
||||
});
|
||||
|
||||
it('uses the provided name', () => {
|
||||
const task = new Task({ name: 'foo' });
|
||||
|
||||
expect(task.name).toBe('foo');
|
||||
});
|
||||
|
||||
it('uses the provided function', () => {
|
||||
const func = () => Promise.resolve();
|
||||
const task = new Task({ func });
|
||||
|
||||
expect(task.func).toBe(func);
|
||||
});
|
||||
|
||||
describe('dependsOn', () => {
|
||||
it('accepts an array of task names as dependencies', () => {
|
||||
const dependsOn = ['bar', 'baz'];
|
||||
const task = new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn
|
||||
});
|
||||
|
||||
expect(task.dependsOn).toEqual(dependsOn);
|
||||
});
|
||||
|
||||
it('accepts a single task name as a dependency', () => {
|
||||
const task = new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn: 'bar'
|
||||
});
|
||||
|
||||
expect(task.dependsOn).toEqual(['bar']);
|
||||
});
|
||||
|
||||
it('fails with non-string, non-array dependencies', () => {
|
||||
function factory() {
|
||||
return new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn: 7
|
||||
});
|
||||
}
|
||||
|
||||
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
|
||||
it('fails with non-string arrays of dependencies', () => {
|
||||
function factory() {
|
||||
return new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve(),
|
||||
dependsOn: [7]
|
||||
});
|
||||
}
|
||||
|
||||
expect(factory).toThrowErrorOfType(ARGUMENT_ERROR);
|
||||
});
|
||||
});
|
||||
|
||||
it('uses the provided dependencies', () => {
|
||||
const dependsOn = ['foo', 'bar'];
|
||||
const task = new Task({ dependsOn });
|
||||
|
||||
expect(task.dependsOn).toEqual(dependsOn);
|
||||
});
|
||||
|
||||
describe('run', () => {
|
||||
it('requires a name', async () => {
|
||||
let error;
|
||||
|
||||
async function start() {
|
||||
const task = new Task({
|
||||
func: () => Promise.resolve()
|
||||
});
|
||||
|
||||
await task.run();
|
||||
}
|
||||
|
||||
try {
|
||||
await start();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('requires a function', async () => {
|
||||
let error;
|
||||
|
||||
async function run() {
|
||||
const task = new Task({
|
||||
name: 'foo'
|
||||
});
|
||||
|
||||
await task.run();
|
||||
}
|
||||
|
||||
try {
|
||||
await run();
|
||||
} catch (e) {
|
||||
error = e;
|
||||
}
|
||||
|
||||
expect(error).toBeDefined();
|
||||
});
|
||||
|
||||
it('accepts a context object', async () => {
|
||||
const context = {};
|
||||
const task = new Task({
|
||||
name: 'foo',
|
||||
func: c => {
|
||||
c.foo = 'bar';
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
await task.run(context);
|
||||
|
||||
expect(context.foo).toBe('bar');
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('emits a `start` event', async () => {
|
||||
let event;
|
||||
const task = new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve()
|
||||
});
|
||||
|
||||
task.on('start', e => {
|
||||
event = e;
|
||||
});
|
||||
|
||||
await task.run();
|
||||
|
||||
expect(event).toBe(task);
|
||||
});
|
||||
|
||||
it('emits an `end` event', async () => {
|
||||
let event;
|
||||
const task = new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.resolve()
|
||||
});
|
||||
|
||||
task.on('end', e => {
|
||||
event = e;
|
||||
});
|
||||
|
||||
await task.run();
|
||||
|
||||
expect(event).toBe(task);
|
||||
});
|
||||
|
||||
it('emits an `error` event if necessary', async () => {
|
||||
let error = new Error('oh no!');
|
||||
let event;
|
||||
const task = new Task({
|
||||
name: 'foo',
|
||||
func: () => Promise.reject(error)
|
||||
});
|
||||
|
||||
task.on('error', e => {
|
||||
event = e;
|
||||
});
|
||||
|
||||
try {
|
||||
await task.run();
|
||||
} catch (e) {
|
||||
// Expected behavior.
|
||||
}
|
||||
|
||||
|
||||
expect(event.error).toBe(error);
|
||||
expect(event.task).toBe(task);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user