firest commit

This commit is contained in:
wwweww
2026-02-21 22:48:40 +08:00
commit 55e8053e07
1034 changed files with 99049 additions and 0 deletions
+29
View File
@@ -0,0 +1,29 @@
import commandLineUsage from "command-line-usage";
import pc from "picocolors";
import { compareTaskNames } from "./utils.js";
export function formatTasks(format, tasks, defaultTask) {
const visibleTasks = [...tasks].filter(isTaskVisible).sort(compareTaskNames);
if (format === "simple") {
return visibleTasks.map((task) => task.options.name).join("\n");
}
return commandLineUsage({
header: "Available tasks",
content: visibleTasks.map((task) => {
var _a;
const name = task === defaultTask
? `${pc.green(task.options.name)} (default)`
: pc.blue(task.options.name);
let descriptionParts = task.options.description ? [task.options.description] : undefined;
const deps = (_a = task.options.dependencies) === null || _a === void 0 ? void 0 : _a.filter(isTaskVisible).sort(compareTaskNames);
if (deps === null || deps === void 0 ? void 0 : deps.length) {
const depNames = deps.map((task) => pc.blue(task.options.name));
(descriptionParts !== null && descriptionParts !== void 0 ? descriptionParts : (descriptionParts = [])).push(`Depends on: ${depNames.join(", ")}`);
}
return { name, description: descriptionParts === null || descriptionParts === void 0 ? void 0 : descriptionParts.join("\n") };
}),
});
}
function isTaskVisible(task) {
return !task.options.hiddenFromTaskList;
}
//# sourceMappingURL=formatTasks.js.map
+95
View File
@@ -0,0 +1,95 @@
import path from "node:path";
import { performance } from "node:perf_hooks";
import { types } from "node:util";
import pc from "picocolors";
import { formatTasks } from "./formatTasks.js";
import { findHerebyfile, loadHerebyfile } from "./loadHerebyfile.js";
import { getUsage, parseArgs } from "./parseArgs.js";
import { reexec } from "./reexec.js";
import { Runner } from "./runner.js";
import { UserError } from "./utils.js";
export async function main(d) {
try {
await mainWorker(d);
}
catch (e) {
if (e instanceof UserError) {
d.error(`${pc.red("Error")}: ${e.message}`);
}
else if (types.isNativeError(e) && e.stack) { // eslint-disable-line @typescript-eslint/no-deprecated
d.error(e.stack);
}
else {
d.error(`${e}`);
}
d.setExitCode(1);
}
}
async function mainWorker(d) {
var _a;
const args = parseArgs(d.argv.slice(2));
if (args.help) {
d.log(getUsage());
return;
}
const herebyfilePath = path.resolve(d.cwd(), (_a = args.herebyfile) !== null && _a !== void 0 ? _a : findHerebyfile(d.cwd()));
if (await reexec(herebyfilePath))
return;
if (args.version) {
d.log(`hereby ${d.version()}`);
return;
}
d.chdir(path.dirname(herebyfilePath));
const herebyfile = await loadHerebyfile(herebyfilePath);
if (args.printTasks) {
d.log(formatTasks(args.printTasks, herebyfile.tasks.values(), herebyfile.defaultTask));
return;
}
const tasks = await selectTasks(d, herebyfile, herebyfilePath, args.run);
const taskNames = tasks.map((task) => pc.blue(task.options.name)).join(", ");
d.log(`Using ${pc.yellow(d.simplifyPath(herebyfilePath))} to run ${taskNames}`);
const start = performance.now();
const runner = new Runner(d);
try {
await runner.runTasks(...tasks);
}
catch {
// We will have already printed some message here.
// Set the error code and let the process run to completion,
// so we don't end up with an unflushed output.
d.setExitCode(1);
}
finally {
const took = performance.now() - start;
const failed = runner.failedTasks.length > 0;
d.log(`Completed ${taskNames}${failed ? pc.red(" with errors") : ""} in ${d.prettyMilliseconds(took)}`);
if (failed) {
const names = runner.failedTasks.sort().map((task) => pc.red(task)).join(", ");
d.log(`Failed tasks: ${names}`);
}
}
}
// Exported for testing.
export async function selectTasks(d, herebyfile, herebyfilePath, taskNames) {
if (taskNames.length === 0) {
if (herebyfile.defaultTask)
return [herebyfile.defaultTask];
throw new UserError(`No default task has been exported from ${d.simplifyPath(herebyfilePath)}; please specify a task name.`);
}
const tasks = [];
for (const name of taskNames) {
const task = herebyfile.tasks.get(name);
if (!task) {
let message = `Task "${name}" does not exist or is not exported from ${d.simplifyPath(herebyfilePath)}.`;
const { closest, distance } = await import("fastest-levenshtein");
const candidate = closest(name, [...herebyfile.tasks.keys()]);
if (distance(name, candidate) < name.length * 0.4) {
message += ` Did you mean "${candidate}"?`;
}
throw new UserError(message);
}
tasks.push(task);
}
return tasks;
}
//# sourceMappingURL=index.js.map
+89
View File
@@ -0,0 +1,89 @@
import fs from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import pc from "picocolors";
import { Task } from "../index.js";
import { findUp, UserError } from "./utils.js";
const herebyfileRegExp = /^herebyfile\.m?[jt]s$/i;
export function findHerebyfile(dir) {
const result = findUp(dir, (dir) => {
const entries = fs.readdirSync(dir, { withFileTypes: true });
const matching = entries.filter((e) => herebyfileRegExp.test(e.name));
if (matching.length > 1) {
throw new UserError(`Found more than one Herebyfile: ${matching.map((e) => e.name).join(", ")}`);
}
if (matching.length === 1) {
const candidate = matching[0];
if (!candidate.isFile()) {
throw new UserError(`${candidate.name} is not a file.`);
}
return path.join(dir, candidate.name);
}
if (entries.some((e) => e.name === "package.json")) {
return false; // TODO: Is this actually desirable? What about monorepos?
}
return undefined;
});
if (result) {
return result;
}
throw new UserError("Unable to find Herebyfile.");
}
export async function loadHerebyfile(herebyfilePath) {
// Note: calling pathToFileURL is required on Windows to disambiguate URLs
// from drive letters.
const herebyfile = await import(pathToFileURL(herebyfilePath).toString());
const exportedTasks = new Set();
let defaultTask;
for (const [key, value] of Object.entries(herebyfile)) {
if (!(value instanceof Task))
continue;
if (key === "default") {
defaultTask = value;
}
else if (exportedTasks.has(value)) {
throw new UserError(`Task "${pc.blue(value.options.name)}" has been exported twice.`);
}
else {
exportedTasks.add(value);
}
}
if (defaultTask) {
exportedTasks.add(defaultTask);
}
if (exportedTasks.size === 0) {
throw new UserError("No tasks found. Did you forget to export your tasks?");
}
// We check this here by walking the DAG, as some dependencies may not be
// exported and therefore would not be seen by the above loop.
checkTaskInvariants(exportedTasks);
const tasks = new Map([...exportedTasks].map((task) => [task.options.name, task]));
return { tasks, defaultTask };
}
function checkTaskInvariants(tasks) {
const checkedTasks = new Set();
const taskStack = new Set();
const seenNames = new Set();
checkTaskInvariantsWorker(tasks);
function checkTaskInvariantsWorker(tasks) {
for (const task of tasks) {
if (checkedTasks.has(task))
continue;
if (taskStack.has(task)) {
throw new UserError(`Task "${pc.blue(task.options.name)}" references itself.`);
}
const name = task.options.name;
if (seenNames.has(name)) {
throw new UserError(`Task "${pc.blue(name)}" was declared twice.`);
}
seenNames.add(name);
if (task.options.dependencies) {
taskStack.add(task);
checkTaskInvariantsWorker(task.options.dependencies);
taskStack.delete(task);
}
checkedTasks.add(task);
}
}
}
//# sourceMappingURL=loadHerebyfile.js.map
+74
View File
@@ -0,0 +1,74 @@
import commandLineUsage from "command-line-usage";
import minimist from "minimist";
export function parseArgs(argv) {
let parseUnknownAsTask = true;
const options = minimist(argv, {
"--": true,
string: ["herebyfile"],
boolean: ["tasks", "tasks-simple", "help", "version"],
alias: {
"h": "help",
"T": "tasks",
},
unknown: (name) => parseUnknownAsTask && (parseUnknownAsTask = !name.startsWith("-")),
});
return {
help: options["help"],
run: options._,
herebyfile: options["herebyfile"],
printTasks: options["tasks"] ? "normal" : (options["tasks-simple"] ? "simple" : undefined),
version: options["version"],
};
}
export function getUsage() {
const usage = commandLineUsage([
{
header: "hereby",
content: "A simple task runner.",
},
{
header: "Synopsis",
content: "$ hereby <task>",
},
{
header: "Options",
optionList: [
{
name: "help",
description: "Display this usage guide.",
alias: "h",
type: Boolean,
},
{
name: "herebyfile",
description: "A path to a Herebyfile. Optional.",
type: String,
defaultOption: true,
typeLabel: "{underline path}",
},
{
name: "tasks",
description: "Print a listing of the available tasks.",
alias: "T",
type: Boolean,
},
{
name: "version",
description: "Print the current hereby version.",
type: Boolean,
},
],
},
{
header: "Example usage",
content: [
"$ hereby build",
"$ hereby build lint",
"$ hereby test --skip someTest --lint=false",
"$ hereby --tasks",
],
},
]);
return usage;
}
//# sourceMappingURL=parseArgs.js.map
+58
View File
@@ -0,0 +1,58 @@
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { findUp, UserError } from "./utils.js";
const thisCLI = fileURLToPath(new URL("../cli.js", import.meta.url));
const distCLIPath = path.join("dist", "cli.js");
const expectedCLIPath = path.join("node_modules", "hereby", distCLIPath);
/**
* Checks to see if we need to re-exec another version of hereby.
* If this function returns true, the caller should return immediately
* and do no further work.
*/
export async function reexec(herebyfilePath) {
// If hereby is installed globally, but run against a Herebyfile in some
// other package, that Herebyfile's import will resolve to a different
// installation of the hereby package. There's no guarantee that the two
// are compatible (in fact, they are guaranteed not to as Task is a class).
//
// Rather than trying to fix this by messing around with Node's resolution
// (which won't work in ESM anyway), instead opt to figure out the location
// of hereby as imported by the Herebyfile, and then "reexec" it by importing.
//
// This code used to use `import.meta.resolve` to find `hereby/cli`, but
// manually encoding this behavior is faster and avoids the dependency.
// If Node ever makes the two-argument form of `import.meta.resolve` unflagged,
// we could switch to that.
const otherCLI = findUp(path.dirname(herebyfilePath), (dir) => {
const p = path.resolve(dir, expectedCLIPath);
// This is the typical case; we've walked up and found it in node_modules.
if (fs.existsSync(p))
return p;
// Otherwise, we check to see if we're self-resolving. Realistically,
// this only happens when developing hereby itself.
//
// Technically, this should go before the above check since self-resolution
// comes before node_modules resolution, but this could only happen if hereby
// happened to depend on itself somehow.
const packageJsonPath = path.join(dir, "package.json");
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
if (packageJson.name === "hereby") {
return path.resolve(dir, distCLIPath);
}
}
return undefined;
});
if (!otherCLI) {
throw new UserError("Unable to find hereby; ensure hereby is installed in your package.");
}
if (fs.realpathSync(thisCLI) === fs.realpathSync(otherCLI)) {
return false;
}
// Note: calling pathToFileURL is required on Windows to disambiguate URLs
// from drive letters.
await import(pathToFileURL(otherCLI).toString());
return true;
}
//# sourceMappingURL=reexec.js.map
+65
View File
@@ -0,0 +1,65 @@
import { performance } from "node:perf_hooks";
import pc from "picocolors";
export class Runner {
constructor(_d) {
this._d = _d;
this._addedTasks = new Map();
this.failedTasks = [];
this._startTimes = new Map();
}
async runTasks(...tasks) {
// Using allSettled here so that we don't immediately exit; it could be
// the case that a task has code that needs to run before, e.g. a
// cleanup function in a "finally" or something.
const results = await Promise.allSettled(tasks.map((task) => {
const cached = this._addedTasks.get(task);
if (cached)
return cached;
const promise = this._runTask(task);
this._addedTasks.set(task, promise);
return promise;
}));
for (const result of results) {
if (result.status === "rejected") {
throw result.reason;
}
}
}
async _runTask(task) {
const { dependencies, run } = task.options;
if (dependencies) {
await this.runTasks(...dependencies);
}
if (!run)
return;
try {
this.onTaskStart(task);
await run();
this.onTaskFinish(task);
}
catch (e) {
this.onTaskError(task, e);
throw e;
}
}
onTaskStart(task) {
this._startTimes.set(task, performance.now());
if (this.failedTasks.length > 0)
return; // Skip logging.
this._d.log(`Starting ${pc.blue(task.options.name)}`);
}
onTaskFinish(task) {
if (this.failedTasks.length > 0)
return; // Skip logging.
const took = performance.now() - this._startTimes.get(task);
this._d.log(`Finished ${pc.green(task.options.name)} in ${this._d.prettyMilliseconds(took)}`);
}
onTaskError(task, e) {
this.failedTasks.push(task.options.name);
if (this.failedTasks.length > 1)
return; // Skip logging.
const took = performance.now() - this._startTimes.get(task);
this._d.error(`Error in ${pc.red(task.options.name)} in ${this._d.prettyMilliseconds(took)}\n${e}`);
}
}
//# sourceMappingURL=runner.js.map
+61
View File
@@ -0,0 +1,61 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
export function compareTaskNames(a, b) {
return compareStrings(a.options.name, b.options.name);
}
// eslint-disable-next-line @typescript-eslint/unbound-method
const compareStrings = new Intl.Collator(undefined, { numeric: true }).compare;
// Exported for testing.
export function simplifyPath(p) {
p = path.normalize(p);
const homedir = path.normalize(os.homedir() + path.sep);
if (p.startsWith(homedir)) {
p = p.slice(homedir.length);
return `~${path.sep}${p}`;
}
return p;
}
export function findUp(dir, predicate) {
const root = path.parse(dir).root;
while (true) {
const result = predicate(dir);
if (result !== undefined)
return result;
if (dir === root)
break;
dir = path.dirname(dir);
}
return undefined;
}
/**
* UserError is a special error that, when caught in the CLI will be printed
* as a message only, without stacktrace. Use this instead of process.exit.
*/
export class UserError extends Error {
}
export async function real() {
const { default: prettyMilliseconds } = await import("pretty-ms");
/* eslint-disable no-restricted-globals */
return {
log: console.log,
error: console.error,
// eslint-disable-next-line @typescript-eslint/unbound-method
cwd: process.cwd,
// eslint-disable-next-line @typescript-eslint/unbound-method
chdir: process.chdir,
simplifyPath,
argv: process.argv,
setExitCode: (code) => {
process.exitCode = code;
},
version: () => {
const packageJsonURL = new URL("../../package.json", import.meta.url);
const packageJson = fs.readFileSync(packageJsonURL, "utf8");
return JSON.parse(packageJson).version;
},
prettyMilliseconds,
};
/* eslint-enable no-restricted-globals */
}
//# sourceMappingURL=utils.js.map