hereby 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +72 -0
- package/dist/cli/formatTasks.js +28 -0
- package/dist/cli/index.js +74 -0
- package/dist/cli/loadHerebyfile.js +74 -0
- package/dist/cli/parseArgs.js +70 -0
- package/dist/cli/reexec.js +31 -0
- package/dist/cli/runner.js +42 -0
- package/dist/cli/utils.js +50 -0
- package/dist/cli.js +5 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.js +28 -0
- package/dist/runner.js +40 -0
- package/dist/utils.js +13 -0
- package/package.json +117 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 Jake Bailey
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# hereby
|
|
2
|
+
|
|
3
|
+
`hereby` is a simple task runner.
|
|
4
|
+
|
|
5
|
+
# Herebyfile.mjs
|
|
6
|
+
|
|
7
|
+
Tasks are defined in `Herebyfile.mjs`, like:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import { execa } from "execa";
|
|
11
|
+
import { task } from "hereby";
|
|
12
|
+
|
|
13
|
+
export const build = task({
|
|
14
|
+
name: "build",
|
|
15
|
+
run: async () => {
|
|
16
|
+
await execa("tsc", ["-b", "./src"]);
|
|
17
|
+
},
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const test = task({
|
|
21
|
+
name: "test",
|
|
22
|
+
dependencies: [build],
|
|
23
|
+
run: async () => {
|
|
24
|
+
await execa("node", ["./out/test.js"]);
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export const lint = task({
|
|
29
|
+
name: "lint",
|
|
30
|
+
run: async () => {
|
|
31
|
+
await runLinter(...);
|
|
32
|
+
},
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
export const testAndLint = task({
|
|
36
|
+
name: "testAndLint",
|
|
37
|
+
dependencies: [test, lint],
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export default testAndLint;
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
# Running tasks
|
|
44
|
+
|
|
45
|
+
Given the above Herebyfile:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
$ hereby build # Run only build
|
|
49
|
+
$ hereby test # Run test, which depends on build.
|
|
50
|
+
$ hereby # Run the default exported task.
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
# Flags
|
|
54
|
+
|
|
55
|
+
`hereby` also supports a handful of flags:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
-h, --help Display this usage guide.
|
|
59
|
+
--herebyfile path A path to a Herebyfile. Optional.
|
|
60
|
+
-T, --tasks Print a listing of the available tasks.
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
# ESM
|
|
64
|
+
|
|
65
|
+
`hereby` is implemented in ES modules. But, don't fret! This does not mean
|
|
66
|
+
that your project must be ESM-only, only that your `Herebyfile` must be ESM
|
|
67
|
+
module so that `hereby`'s `task` function can be imported. It's recommended
|
|
68
|
+
to use the filename `Herebyfile.mjs` to ensure that it is treated as ESM. This
|
|
69
|
+
will work in a CommonJS project; ES modules can import CommonJS modules.
|
|
70
|
+
|
|
71
|
+
If your package already sets `"type": "module"`, `Herebyfile.js` will work
|
|
72
|
+
as well.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import commandLineUsage from "command-line-usage";
|
|
3
|
+
import { stringSorter, taskSorter } from "./utils.js";
|
|
4
|
+
export function formatTasks(tasks, defaultTask) {
|
|
5
|
+
tasks = tasks.slice(0).sort(taskSorter);
|
|
6
|
+
const sections = [];
|
|
7
|
+
sections.push({
|
|
8
|
+
header: "Available tasks",
|
|
9
|
+
content: tasks.map((task) => {
|
|
10
|
+
const blueName = chalk.blue(task.options.name);
|
|
11
|
+
const name = task !== defaultTask ? blueName : `${blueName} (default)`;
|
|
12
|
+
const descriptionParts = [];
|
|
13
|
+
if (task.options.description) {
|
|
14
|
+
descriptionParts.push(task.options.description);
|
|
15
|
+
}
|
|
16
|
+
if (task.options.dependencies && task.options.dependencies.length > 0) {
|
|
17
|
+
const deps = task.options.dependencies
|
|
18
|
+
.map((task) => task.options.name)
|
|
19
|
+
.sort(stringSorter)
|
|
20
|
+
.map((v) => chalk.blue(v));
|
|
21
|
+
descriptionParts.push(`Depends on: ${deps.join(", ")}`);
|
|
22
|
+
}
|
|
23
|
+
return { name, description: descriptionParts.join("\n") };
|
|
24
|
+
}),
|
|
25
|
+
});
|
|
26
|
+
return commandLineUsage(sections);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=formatTasks.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { formatTasks } from "./formatTasks.js";
|
|
4
|
+
import { findHerebyfile, loadHerebyfile } from "./loadHerebyfile.js";
|
|
5
|
+
import { getUsage, parseArgs } from "./parseArgs.js";
|
|
6
|
+
import { reexec } from "./reexec.js";
|
|
7
|
+
import { runTasksWithCLIRunner } from "./runner.js";
|
|
8
|
+
import { ExitCodeError, simplifyPath, UserError } from "./utils.js";
|
|
9
|
+
export async function main(system) {
|
|
10
|
+
try {
|
|
11
|
+
await mainWorker(system);
|
|
12
|
+
}
|
|
13
|
+
catch (e) {
|
|
14
|
+
if (e instanceof ExitCodeError) {
|
|
15
|
+
system.process.exitCode = e.exitCode;
|
|
16
|
+
}
|
|
17
|
+
else if (e instanceof UserError) {
|
|
18
|
+
system.error(`${chalk.red("Error")}: ${e.message}`);
|
|
19
|
+
system.process.exitCode = 1;
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
throw e;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async function mainWorker(system) {
|
|
27
|
+
const args = parseArgs(system.process.argv.slice(2));
|
|
28
|
+
if (args.help) {
|
|
29
|
+
system.log(getUsage());
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
let herebyfilePath = args.herebyfile ?? (await findHerebyfile(system.process.cwd()));
|
|
33
|
+
herebyfilePath = path.resolve(system.process.cwd(), herebyfilePath);
|
|
34
|
+
if (await reexec(system, herebyfilePath)) {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
system.process.chdir(path.dirname(herebyfilePath));
|
|
38
|
+
const herebyfile = await loadHerebyfile(herebyfilePath);
|
|
39
|
+
if (args.printTasks) {
|
|
40
|
+
system.log(formatTasks(herebyfile.tasks, herebyfile.defaultTask));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const allTasks = new Map();
|
|
44
|
+
for (const task of herebyfile.tasks) {
|
|
45
|
+
allTasks.set(task.options.name, task);
|
|
46
|
+
}
|
|
47
|
+
let tasks;
|
|
48
|
+
if (args.run && args.run.length > 0) {
|
|
49
|
+
tasks = args.run.map((name) => {
|
|
50
|
+
const task = allTasks.get(name);
|
|
51
|
+
if (!task) {
|
|
52
|
+
throw new UserError(`Task ${name} does not exist or is not exported in the Herebyfile.`);
|
|
53
|
+
}
|
|
54
|
+
return task;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (!herebyfile.defaultTask) {
|
|
59
|
+
throw new UserError("No default task defined; please specify a task name.");
|
|
60
|
+
}
|
|
61
|
+
tasks = [herebyfile.defaultTask];
|
|
62
|
+
}
|
|
63
|
+
system.log(`Using ${simplifyPath(herebyfilePath)}`);
|
|
64
|
+
try {
|
|
65
|
+
await runTasksWithCLIRunner(system, ...tasks);
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// We will have already printed some message here.
|
|
69
|
+
// Set the error code and let the process run to completion,
|
|
70
|
+
// so we don't end up with an unflushed output.
|
|
71
|
+
throw new ExitCodeError(1);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import fs from "fs/promises";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { Task } from "../index.js";
|
|
5
|
+
import { forEachTask } from "../utils.js";
|
|
6
|
+
import { UserError } from "./utils.js";
|
|
7
|
+
const filenames = ["Herebyfile", "herebyfile"];
|
|
8
|
+
const extensions = ["mjs", "js"];
|
|
9
|
+
const allFilenames = new Set(extensions.map((e) => filenames.map((f) => `${f}.${e}`)).flat());
|
|
10
|
+
export async function findHerebyfile(dir) {
|
|
11
|
+
const root = path.parse(dir).root;
|
|
12
|
+
for (; dir !== root; dir = path.dirname(dir)) {
|
|
13
|
+
const entries = await fs.readdir(dir);
|
|
14
|
+
const matching = entries.filter((e) => allFilenames.has(e));
|
|
15
|
+
if (matching.length > 1) {
|
|
16
|
+
throw new UserError(`Found more than one Herebyfile: ${matching.join(", ")}`);
|
|
17
|
+
}
|
|
18
|
+
if (matching.length === 1) {
|
|
19
|
+
const candidate = path.join(dir, matching[0]);
|
|
20
|
+
const stat = await fs.stat(candidate);
|
|
21
|
+
if (!stat.isFile()) {
|
|
22
|
+
throw new UserError(`${matching[0]} is not a file.`);
|
|
23
|
+
}
|
|
24
|
+
return candidate;
|
|
25
|
+
}
|
|
26
|
+
if (entries.includes("package.json")) {
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
throw new UserError("Unable to find Herebyfile.");
|
|
31
|
+
}
|
|
32
|
+
export async function loadHerebyfile(herebyfilePath) {
|
|
33
|
+
const herebyfile = await import(herebyfilePath);
|
|
34
|
+
const exportedTasks = new Set();
|
|
35
|
+
let defaultTask;
|
|
36
|
+
for (const [key, value] of Object.entries(herebyfile)) {
|
|
37
|
+
if (value instanceof Task) {
|
|
38
|
+
if (key === "default") {
|
|
39
|
+
defaultTask = value;
|
|
40
|
+
}
|
|
41
|
+
else if (exportedTasks.has(value)) {
|
|
42
|
+
throw new UserError(`Task "${chalk.blue(value.options.name)}" has been exported twice.`);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
exportedTasks.add(value);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (defaultTask) {
|
|
50
|
+
exportedTasks.add(defaultTask);
|
|
51
|
+
}
|
|
52
|
+
if (exportedTasks.size === 0) {
|
|
53
|
+
throw new UserError("No tasks found. Did you forget to export your tasks?");
|
|
54
|
+
}
|
|
55
|
+
const tasks = Array.from(exportedTasks.values());
|
|
56
|
+
// We check this here by walking the DAG, as some dependencies may not be
|
|
57
|
+
// exported and therefore would not be seen by the above loop.
|
|
58
|
+
assertUniqueNames(tasks);
|
|
59
|
+
return {
|
|
60
|
+
tasks,
|
|
61
|
+
defaultTask,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function assertUniqueNames(tasks) {
|
|
65
|
+
const names = new Set();
|
|
66
|
+
forEachTask(tasks, (task) => {
|
|
67
|
+
const name = task.options.name;
|
|
68
|
+
if (names.has(name)) {
|
|
69
|
+
throw new UserError(`Task "${chalk.blue(name)}" was declared twice.`);
|
|
70
|
+
}
|
|
71
|
+
names.add(name);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=loadHerebyfile.js.map
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import commandLineArgs from "command-line-args";
|
|
2
|
+
import commandLineUsage from "command-line-usage";
|
|
3
|
+
export function parseArgs(argv) {
|
|
4
|
+
const idx = argv.indexOf("--");
|
|
5
|
+
if (idx !== -1) {
|
|
6
|
+
argv = argv.slice(0, idx);
|
|
7
|
+
}
|
|
8
|
+
const options = commandLineArgs([
|
|
9
|
+
{ name: "run", multiple: true, defaultOption: true, type: String },
|
|
10
|
+
{ name: "herebyfile", type: String },
|
|
11
|
+
{ name: "tasks", alias: "T", type: Boolean },
|
|
12
|
+
{ name: "help", alias: "h", type: Boolean },
|
|
13
|
+
], {
|
|
14
|
+
argv,
|
|
15
|
+
stopAtFirstUnknown: true,
|
|
16
|
+
});
|
|
17
|
+
return {
|
|
18
|
+
help: options["help"],
|
|
19
|
+
run: options["run"],
|
|
20
|
+
herebyfile: options["herebyfile"],
|
|
21
|
+
printTasks: options["tasks"],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function getUsage() {
|
|
25
|
+
const usage = commandLineUsage([
|
|
26
|
+
{
|
|
27
|
+
header: "hereby",
|
|
28
|
+
content: "A simple task runner.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
header: "Synopsis",
|
|
32
|
+
content: "$ hereby <task>",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
header: "Options",
|
|
36
|
+
optionList: [
|
|
37
|
+
{
|
|
38
|
+
name: "help",
|
|
39
|
+
description: "Display this usage guide.",
|
|
40
|
+
alias: "h",
|
|
41
|
+
type: Boolean,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
name: "herebyfile",
|
|
45
|
+
description: "A path to a Herebyfile. Optional.",
|
|
46
|
+
type: String,
|
|
47
|
+
defaultOption: true,
|
|
48
|
+
typeLabel: "{underline path}",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "tasks",
|
|
52
|
+
description: "Print a listing of the available tasks.",
|
|
53
|
+
alias: "T",
|
|
54
|
+
type: Boolean,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
header: "Example usage",
|
|
60
|
+
content: [
|
|
61
|
+
"$ hereby build",
|
|
62
|
+
"$ hereby build lint",
|
|
63
|
+
"$ hereby test --skip someTest --lint=false",
|
|
64
|
+
"$ hereby --tasks",
|
|
65
|
+
],
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
return usage;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=parseArgs.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import foregroundChild from "foreground-child";
|
|
3
|
+
import { resolve } from "import-meta-resolve";
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
5
|
+
export async function reexec(system, herebyfilePath) {
|
|
6
|
+
// If hereby is installed globally, but run against a Herebyfile in some
|
|
7
|
+
// other package, that Herebyfile's import will resolve to a different
|
|
8
|
+
// installation of the hereby package. There's no guarantee that the two
|
|
9
|
+
// are compatible (in fact, are guaranteed not to be as Task is a class).
|
|
10
|
+
//
|
|
11
|
+
// Rather than trying to fix this by messing around with Node's resolution
|
|
12
|
+
// (which won't work in ESM anyway), instead opt to figure out the location
|
|
13
|
+
// of hereby as imported by the Herebyfile, and fork to execute it instead.
|
|
14
|
+
//
|
|
15
|
+
// TODO: Rather than spawning a child process, perhaps we could instead
|
|
16
|
+
// import the CLI from the other version and run it.
|
|
17
|
+
const thisCLI = await resolveToPath("hereby/cli", new URL(import.meta.url));
|
|
18
|
+
const otherCLI = await resolveToPath("hereby/cli", pathToFileURL(herebyfilePath));
|
|
19
|
+
if (thisCLI === otherCLI) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
// TODO: If this turns out to be common, remove this warning.
|
|
23
|
+
system.error(`${chalk.yellow("Warning")}: re-running hereby as imported by the Herebyfile.`);
|
|
24
|
+
const args = [...system.process.execArgv, otherCLI, ...system.process.argv.slice(2)];
|
|
25
|
+
foregroundChild(system.process.execPath, args);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
async function resolveToPath(specifier, url) {
|
|
29
|
+
return fileURLToPath(new URL(await resolve(specifier, url.toString())));
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=reexec.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import assert from "assert";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
import prettyMilliseconds from "pretty-ms";
|
|
4
|
+
import { Runner } from "../runner.js";
|
|
5
|
+
export function runTasksWithCLIRunner(system, ...tasks) {
|
|
6
|
+
return new CLIRunner({ system, concurrency: system.numCPUs }).runTasks(...tasks);
|
|
7
|
+
}
|
|
8
|
+
class CLIRunner extends Runner {
|
|
9
|
+
constructor(options) {
|
|
10
|
+
super(options);
|
|
11
|
+
this._errored = false;
|
|
12
|
+
this._startTimes = new WeakMap();
|
|
13
|
+
this._system = options.system;
|
|
14
|
+
}
|
|
15
|
+
onTaskStart(task) {
|
|
16
|
+
this._startTimes.set(task, Date.now());
|
|
17
|
+
if (this._errored) {
|
|
18
|
+
return; // Skip logging.
|
|
19
|
+
}
|
|
20
|
+
this._system.log(`Starting ${chalk.blue(task.options.name)}`);
|
|
21
|
+
}
|
|
22
|
+
onTaskFinish(task) {
|
|
23
|
+
if (this._errored) {
|
|
24
|
+
return; // Skip logging.
|
|
25
|
+
}
|
|
26
|
+
const took = Date.now() - checkDefined(this._startTimes.get(task));
|
|
27
|
+
this._system.log(`Finished ${chalk.green(task.options.name)} in ${prettyMilliseconds(took)}`);
|
|
28
|
+
}
|
|
29
|
+
onTaskError(task, e) {
|
|
30
|
+
if (this._errored) {
|
|
31
|
+
return; // Skip logging.
|
|
32
|
+
}
|
|
33
|
+
this._errored = true;
|
|
34
|
+
this._system.error(`Error in ${chalk.red(task.options.name)}`);
|
|
35
|
+
this._system.error(`${e}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function checkDefined(value) {
|
|
39
|
+
assert(value);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
export function taskSorter(a, b) {
|
|
4
|
+
return stringSorter(a.options.name, b.options.name);
|
|
5
|
+
}
|
|
6
|
+
export function stringSorter(a, b) {
|
|
7
|
+
return a.localeCompare(b);
|
|
8
|
+
}
|
|
9
|
+
export function simplifyPath(p) {
|
|
10
|
+
let homedir = os.homedir();
|
|
11
|
+
if (!p.endsWith(path.sep)) {
|
|
12
|
+
homedir += path.sep;
|
|
13
|
+
}
|
|
14
|
+
if (p.startsWith(homedir)) {
|
|
15
|
+
p = p.slice(homedir.length);
|
|
16
|
+
return `~${path.sep}${p}`;
|
|
17
|
+
}
|
|
18
|
+
return p;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* UserError is a special error that, when caught in the CLI will be printed
|
|
22
|
+
* as a message only, without stacktrace. Use this instead of process.exit.
|
|
23
|
+
*/
|
|
24
|
+
export class UserError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* When thrown, ExitCodeError causes the process to exit with a specific error code,
|
|
31
|
+
* without logging anything.
|
|
32
|
+
*/
|
|
33
|
+
export class ExitCodeError {
|
|
34
|
+
constructor(exitCode) {
|
|
35
|
+
this.exitCode = exitCode;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
export function createSystem(process) {
|
|
39
|
+
return {
|
|
40
|
+
log(message) {
|
|
41
|
+
process.stdout.write(message + "\n");
|
|
42
|
+
},
|
|
43
|
+
error(message) {
|
|
44
|
+
process.stderr.write(message + "\n");
|
|
45
|
+
},
|
|
46
|
+
process,
|
|
47
|
+
numCPUs: os.cpus().length,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=utils.js.map
|
package/dist/cli.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export interface TaskOptions {
|
|
2
|
+
/**
|
|
3
|
+
* The name of the task, as referenced in logs and the CLI.
|
|
4
|
+
*
|
|
5
|
+
* This name must not start with a "-" in order to prevent conflicts
|
|
6
|
+
* with flags.
|
|
7
|
+
*/
|
|
8
|
+
name: string;
|
|
9
|
+
/**
|
|
10
|
+
* A description of the task, for display in the CLI.
|
|
11
|
+
*/
|
|
12
|
+
description?: string | undefined;
|
|
13
|
+
/**
|
|
14
|
+
* A list of tasks that must has been run to completion before
|
|
15
|
+
* this task can execute.
|
|
16
|
+
*/
|
|
17
|
+
dependencies?: Task[] | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* A function to execute when this task is run.
|
|
20
|
+
*/
|
|
21
|
+
run?: (() => void | Promise<void>) | undefined;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* A hereby Task. To get an instance, call `test`.
|
|
25
|
+
*/
|
|
26
|
+
export declare class Task {
|
|
27
|
+
private constructor();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Creates a new Task.
|
|
31
|
+
*/
|
|
32
|
+
export declare function task(options: TaskOptions): Task;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A hereby Task. To get an instance, call `test`.
|
|
3
|
+
*/
|
|
4
|
+
export class Task {
|
|
5
|
+
constructor(options) {
|
|
6
|
+
if (!options.name) {
|
|
7
|
+
throw new Error("Task name must not be empty.");
|
|
8
|
+
}
|
|
9
|
+
if (options.name.startsWith("-")) {
|
|
10
|
+
throw new Error('Task name must not start with "-".');
|
|
11
|
+
}
|
|
12
|
+
if (!options.dependencies?.length && !options.run) {
|
|
13
|
+
throw new Error("Task must have at run function or dependencies.");
|
|
14
|
+
}
|
|
15
|
+
this.options = options;
|
|
16
|
+
}
|
|
17
|
+
/* @internal */
|
|
18
|
+
static create(options) {
|
|
19
|
+
return new Task(options);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a new Task.
|
|
24
|
+
*/
|
|
25
|
+
export function task(options) {
|
|
26
|
+
return Task.create(options);
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=index.js.map
|
package/dist/runner.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import PQueue from "p-queue";
|
|
2
|
+
export class Runner {
|
|
3
|
+
constructor(options) {
|
|
4
|
+
this._addedTasks = new WeakMap();
|
|
5
|
+
this._queue = new PQueue({ concurrency: options?.concurrency ?? Infinity });
|
|
6
|
+
}
|
|
7
|
+
async runTasks(...tasks) {
|
|
8
|
+
await Promise.all(tasks.map((task) => {
|
|
9
|
+
const cached = this._addedTasks.get(task);
|
|
10
|
+
if (cached) {
|
|
11
|
+
return cached;
|
|
12
|
+
}
|
|
13
|
+
const promise = this._runTask(task);
|
|
14
|
+
this._addedTasks.set(task, promise);
|
|
15
|
+
return promise;
|
|
16
|
+
}));
|
|
17
|
+
}
|
|
18
|
+
async _runTask(task) {
|
|
19
|
+
this.onTaskAdd?.(task);
|
|
20
|
+
const { dependencies, run } = task.options;
|
|
21
|
+
if (dependencies) {
|
|
22
|
+
await this.runTasks(...dependencies);
|
|
23
|
+
}
|
|
24
|
+
if (!run) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
return this._queue.add(async () => {
|
|
28
|
+
try {
|
|
29
|
+
this.onTaskStart?.(task);
|
|
30
|
+
await run();
|
|
31
|
+
this.onTaskFinish?.(task);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
this.onTaskError?.(task, e);
|
|
35
|
+
throw e;
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=runner.js.map
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function forEachTask(tasks, fn, seen = new Set()) {
|
|
2
|
+
tasks.forEach(visit);
|
|
3
|
+
return;
|
|
4
|
+
function visit(task) {
|
|
5
|
+
if (seen.has(task)) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
seen.add(task);
|
|
9
|
+
fn(task);
|
|
10
|
+
task.options.dependencies?.forEach(visit);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=utils.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hereby",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple task runner",
|
|
5
|
+
"repository": "github:jakebailey/hereby",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": "./dist/cli.js",
|
|
8
|
+
"main": "./dist/index.js",
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts"
|
|
14
|
+
},
|
|
15
|
+
"./cli": "./dist/cli.js",
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"author": "Jake Bailey",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"keywords": [
|
|
21
|
+
"hereby",
|
|
22
|
+
"herebyfile",
|
|
23
|
+
"task",
|
|
24
|
+
"runner",
|
|
25
|
+
"build",
|
|
26
|
+
"gulp",
|
|
27
|
+
"make",
|
|
28
|
+
"makefile"
|
|
29
|
+
],
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">= 14.16"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE",
|
|
36
|
+
"./dist/**/*.js",
|
|
37
|
+
"!**/__tests__/**",
|
|
38
|
+
"./dist/index.d.ts",
|
|
39
|
+
"!./dist/testUtils.js"
|
|
40
|
+
],
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"chalk": "^5.0.1",
|
|
43
|
+
"command-line-args": "^5.2.1",
|
|
44
|
+
"command-line-usage": "^6.1.3",
|
|
45
|
+
"foreground-child": "^2.0.0",
|
|
46
|
+
"import-meta-resolve": "^2.1.0",
|
|
47
|
+
"p-queue": "^7.3.0",
|
|
48
|
+
"pretty-ms": "^8.0.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@ava/typescript": "^3.0.1",
|
|
52
|
+
"@tsconfig/node14": "^1.0.3",
|
|
53
|
+
"@types/command-line-args": "^5.2.0",
|
|
54
|
+
"@types/command-line-usage": "^5.0.2",
|
|
55
|
+
"@types/node": "^14.18.24",
|
|
56
|
+
"@typescript-eslint/eslint-plugin": "^5.33.1",
|
|
57
|
+
"@typescript-eslint/parser": "^5.33.1",
|
|
58
|
+
"ava": "^4.3.1",
|
|
59
|
+
"c8": "^7.12.0",
|
|
60
|
+
"cross-env": "^7.0.3",
|
|
61
|
+
"eslint": "^8.22.0",
|
|
62
|
+
"eslint-config-prettier": "^8.5.0",
|
|
63
|
+
"eslint-plugin-ava": "^13.2.0",
|
|
64
|
+
"eslint-plugin-simple-import-sort": "^7.0.0",
|
|
65
|
+
"esmock": "^1.9.4",
|
|
66
|
+
"execa": "^6.1.0",
|
|
67
|
+
"moq.ts": "^9.0.2",
|
|
68
|
+
"prettier": "^2.7.1",
|
|
69
|
+
"release-it": "^15.3.0",
|
|
70
|
+
"rimraf": "^3.0.2",
|
|
71
|
+
"tempy": "^3.0.0",
|
|
72
|
+
"typescript": "^4.7.4"
|
|
73
|
+
},
|
|
74
|
+
"packageManager": "npm@8.17.0",
|
|
75
|
+
"volta": {
|
|
76
|
+
"node": "14.20.0",
|
|
77
|
+
"npm": "8.17.0"
|
|
78
|
+
},
|
|
79
|
+
"scripts": {
|
|
80
|
+
"build": "tsc",
|
|
81
|
+
"watch": "tsc --watch",
|
|
82
|
+
"test": "ava",
|
|
83
|
+
"coverage": "c8 ava",
|
|
84
|
+
"prepack": "rimraf dist && npm run build"
|
|
85
|
+
},
|
|
86
|
+
"ava": {
|
|
87
|
+
"typescript": {
|
|
88
|
+
"rewritePaths": {
|
|
89
|
+
"src/": "dist/"
|
|
90
|
+
},
|
|
91
|
+
"compile": false
|
|
92
|
+
},
|
|
93
|
+
"environmentVariables": {
|
|
94
|
+
"NODE_OPTIONS": "--loader=esmock",
|
|
95
|
+
"NODE_NO_WARNINGS": "1",
|
|
96
|
+
"FORCE_COLOR": "0"
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
"c8": {
|
|
100
|
+
"all": true,
|
|
101
|
+
"include": "dist",
|
|
102
|
+
"reporter": [
|
|
103
|
+
"text",
|
|
104
|
+
"html",
|
|
105
|
+
"lcov"
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
"release-it": {
|
|
109
|
+
"git": {
|
|
110
|
+
"commitMessage": "Release v${version}",
|
|
111
|
+
"tagName": "v${version}"
|
|
112
|
+
},
|
|
113
|
+
"hooks": {
|
|
114
|
+
"before:init": "npm run test"
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|