nadle
Version:
A type-safe, Gradle-inspired task runner for Node.js. Sharp tasks. Fast builds.
577 lines (553 loc) • 20.6 kB
JavaScript
import {
SupportReporters
} from "./chunk-AHRK4KLP.js";
import {
ConfigurationError,
CyclicDependencyError,
MaybeArray,
Messages,
NadleError,
SupportLogLevels,
TaskExecutionError,
TaskNotFoundError,
getCurrentInstance,
isPathExists,
normalizeGlobPath
} from "./chunk-U2TDD2B5.js";
// packages/nadle/src/core/models/cache/declarations-dsl.ts
var Inputs;
((Inputs2) => {
function files(...patterns) {
return { patterns, type: "file" };
}
Inputs2.files = files;
function dirs(...patterns) {
return { patterns, type: "dir" };
}
Inputs2.dirs = dirs;
})(Inputs || (Inputs = {}));
var Outputs;
((Outputs2) => {
Outputs2.files = Inputs.files;
Outputs2.dirs = Inputs.dirs;
})(Outputs || (Outputs = {}));
// packages/nadle/src/core/options/file-options-validator.ts
var WORKER_PATTERN = /^\d+(\.\d+)?%$|^\d+$/;
function fail(option, expected, value) {
throw new ConfigurationError(`Invalid value for \`${option}\`: expected ${expected}, received ${JSON.stringify(value)}.`);
}
function assertBoolean(options, key) {
const value = options[key];
if (value !== void 0 && typeof value !== "boolean") {
fail(key, "a boolean", value);
}
}
function assertWorker(options, key) {
const value = options[key];
if (value === void 0) {
return;
}
if (typeof value === "number") {
if (!Number.isInteger(value) || value <= 0) {
fail(key, "a positive integer or a percentage string", value);
}
return;
}
if (typeof value !== "string" || !WORKER_PATTERN.test(value)) {
fail(key, 'a positive integer or a percentage string (e.g. "50%")', value);
}
}
function assertCacheDir(options) {
if (options.cacheDir !== void 0 && (typeof options.cacheDir !== "string" || options.cacheDir.length === 0)) {
fail("cacheDir", "a non-empty string", options.cacheDir);
}
}
function assertMaxCacheEntries(options) {
const value = options.maxCacheEntries;
if (value !== void 0 && (typeof value !== "number" || !Number.isInteger(value) || value <= 0)) {
fail("maxCacheEntries", "a positive integer", value);
}
}
function assertEnum(value, key, allowed) {
if (value !== void 0 && !allowed.includes(value)) {
fail(key, `one of ${allowed.map((entry) => `"${entry}"`).join(", ")}`, value);
}
}
function assertAlias(options) {
const value = options.alias;
if (value !== void 0 && typeof value !== "function" && (typeof value !== "object" || value === null)) {
fail("alias", "an object or a function", value);
}
}
function validateFileOptions(options) {
assertBoolean(options, "cache");
assertBoolean(options, "footer");
assertBoolean(options, "parallel");
assertBoolean(options, "implicitDependencies");
assertWorker(options, "minWorkers");
assertWorker(options, "maxWorkers");
assertCacheDir(options);
assertMaxCacheEntries(options);
assertEnum(options.logLevel, "logLevel", SupportLogLevels);
assertEnum(options.reporter, "reporter", SupportReporters);
assertAlias(options);
}
// packages/nadle/src/core/options/configure.ts
function configure(options) {
if (typeof options !== "object" || options === null) {
throw new TypeError("Options must be an object");
}
validateFileOptions(options);
getCurrentInstance().fileOptionRegistry.register(options);
}
// packages/nadle/src/core/registration/api.ts
import { VALID_TASK_NAME_PATTERN } from "@nadle/kernel";
var tasks = {
register: (name, task, optionsResolver) => {
const { taskRegistry } = getCurrentInstance();
validateTaskName(name);
if (taskRegistry.hasTaskName(name)) {
throw new ConfigurationError(Messages.DuplicatedTaskName(name, taskRegistry.workspaceId ?? ""));
}
let configCollector = () => ({});
const register = () => {
taskRegistry.register({
name,
configResolver: () => typeof configCollector === "function" ? configCollector() : configCollector,
...computeTaskInfo(task, optionsResolver)
});
};
register();
return {
config: (collector) => {
configCollector = collector;
register();
}
};
}
};
function computeTaskInfo(task, optionsResolver) {
if (task === void 0) {
return { empty: true, run: () => {
}, optionsResolver: void 0 };
}
if (typeof task === "function") {
return { run: task, empty: false, optionsResolver: void 0 };
}
return { ...task, empty: false, optionsResolver: optionsResolver ?? (() => ({})) };
}
function validateTaskName(name) {
if (!VALID_TASK_NAME_PATTERN.test(name)) {
throw new ConfigurationError(Messages.InvalidTaskName(`[${name}]`));
}
}
// packages/nadle/src/core/registration/define-task.ts
function defineTask(params) {
return params;
}
// packages/nadle/src/builtin-tasks/run-command.ts
import { execa, parseCommandString } from "execa";
function normalizeArgs(args) {
return args == null ? [] : typeof args === "string" ? parseCommandString(args) : [...args];
}
async function runCommand(context, { args, command, doneMessage, startMessage }) {
const finalArgs = [...args, ...context.passthroughArgs];
context.logger.info(startMessage(finalArgs));
const subprocess = execa(command, finalArgs, { all: true, cwd: context.workingDir, env: { FORCE_COLOR: "1" } });
subprocess.all?.on("data", (chunk) => {
context.logger.log(chunk.toString());
});
try {
await subprocess;
} catch (error) {
const exitCode = error.exitCode;
throw new TaskExecutionError(`Command failed${exitCode !== void 0 ? ` with exit code ${exitCode}` : ""}: ${command} ${finalArgs.join(" ")}`, {
cause: error
});
}
context.logger.info(doneMessage);
}
// packages/nadle/src/builtin-tasks/npm-task.ts
var NpmTask = defineTask({
run: async ({ options, context }) => {
await runCommand(context, {
command: "npm",
args: normalizeArgs(options.args),
doneMessage: `npm command completed successfully.`,
startMessage: (finalArgs) => `Running npm command: npm ${finalArgs.join(" ")}`
});
}
});
// packages/nadle/src/builtin-tasks/npx-task.ts
var NpxTask = defineTask({
run: async ({ options, context }) => {
await runCommand(context, {
command: "npx",
doneMessage: `npx command completed successfully.`,
args: [options.command, ...normalizeArgs(options.args)],
startMessage: (finalArgs) => `Running npx command: npx ${finalArgs.join(" ")}`
});
}
});
// packages/nadle/src/builtin-tasks/zip-task.ts
import Path2 from "path";
import Fs2 from "fs/promises";
import { zipSync } from "fflate";
// packages/nadle/src/builtin-tasks/file-selection.ts
import Path from "path";
import Fs from "fs/promises";
import fg from "fast-glob";
async function resolveFileSelections(params) {
const selected = [];
for (const selection of MaybeArray.toArray(params.selections)) {
selected.push(...await resolveSelection(selection, params));
}
if (params.strict && selected.length === 0) {
throw new TaskExecutionError(`No files matched the configured sources.`);
}
return selected;
}
async function resolveSelection(selection, params) {
const selector = typeof selection === "string" ? void 0 : selection;
const sourcePath = Path.resolve(params.workingDir, selector?.dir ?? selection);
if (!await isPathExists(sourcePath)) {
if (params.strict) {
throw new TaskExecutionError(`Source path '${sourcePath}' does not exist.`);
}
params.logger.warn(`Source path '${sourcePath}' does not exist. Skipping.`);
return [];
}
if (selector === void 0 && !(await Fs.stat(sourcePath)).isDirectory()) {
return [{ source: sourcePath, relativePath: Path.basename(sourcePath) }];
}
const { include, exclude } = resolvePatterns(selector, params);
const files = await fg(include, { dot: true, cwd: sourcePath, ignore: exclude, onlyFiles: true });
return files.map((file) => ({ relativePath: file, source: Path.join(sourcePath, file) }));
}
function resolvePatterns(selector, params) {
return {
exclude: MaybeArray.toArray(selector?.exclude ?? params.defaultExclude ?? []),
include: MaybeArray.toArray(selector?.include ?? params.defaultInclude ?? "**/*")
};
}
// packages/nadle/src/builtin-tasks/zip-task.ts
var ZipTask = defineTask({
run: async ({ options, context }) => {
const files = await resolveFileSelections({
logger: context.logger,
selections: options.from,
workingDir: context.workingDir,
strict: options.strict ?? false,
defaultInclude: options.include,
defaultExclude: options.exclude
});
const entries = {};
for (const file of files) {
const entryName = options.prefix ? Path2.posix.join(options.prefix, toPosixPath(file.relativePath)) : toPosixPath(file.relativePath);
if (entries[entryName] !== void 0) {
throw new TaskExecutionError(`Multiple source files map to the same archive entry '${entryName}'.`);
}
entries[entryName] = await Fs2.readFile(file.source);
}
const archivePath = Path2.resolve(context.workingDir, options.archive);
await Fs2.mkdir(Path2.dirname(archivePath), { recursive: true });
await Fs2.writeFile(archivePath, zipSync(entries));
context.logger.info(`Zipped ${files.length} file(s) into ${options.archive}`);
}
});
function toPosixPath(filePath) {
return filePath.split(Path2.sep).join("/");
}
// packages/nadle/src/builtin-tasks/node-task.ts
var NodeTask = defineTask({
run: async ({ options, context }) => {
await runCommand(context, {
command: "node",
doneMessage: `Node script completed successfully.`,
args: [options.script, ...normalizeArgs(options.args)],
startMessage: (finalArgs) => `Running node script: node ${finalArgs.join(" ")}`
});
}
});
// packages/nadle/src/builtin-tasks/pnpm-task.ts
var PnpmTask = defineTask({
run: async ({ options, context }) => {
const filterArgs = MaybeArray.toArray(options.filter ?? []).flatMap((filter) => ["--filter", filter]);
await runCommand(context, {
command: "pnpm",
doneMessage: `pnpm command completed successfully.`,
args: [...filterArgs, ...normalizeArgs(options.args)],
startMessage: (finalArgs) => `Running pnpm command: pnpm ${finalArgs.join(" ")}`
});
}
});
// packages/nadle/src/builtin-tasks/pnpx-task.ts
var PnpxTask = defineTask({
run: async ({ options, context }) => {
await runCommand(context, {
command: "pnpm",
doneMessage: `pnpm exec command completed successfully.`,
args: ["exec", options.command, ...normalizeArgs(options.args)],
startMessage: (finalArgs) => `Running pnpm exec command: pnpm ${finalArgs.join(" ")}`
});
}
});
// packages/nadle/src/builtin-tasks/exec-task.ts
var ExecTask = defineTask({
run: async ({ options, context }) => {
const { args, command } = options;
await runCommand(context, {
command,
args: normalizeArgs(args),
doneMessage: `Run completed successfully.`,
startMessage: (finalArgs) => `Running command: ${command} ${finalArgs.join(" ")}`
});
}
});
// packages/nadle/src/builtin-tasks/copy-task.ts
import Path4 from "path";
import Fs3 from "fs/promises";
import pLimit from "p-limit";
// packages/nadle/src/builtin-tasks/file-operations.ts
import Path3 from "path";
async function resolveTargets(options, context) {
if (options.into === void 0) {
throw new TaskExecutionError(`The 'into' option is required.`);
}
const files = await resolveFileSelections({
logger: context.logger,
selections: options.from,
workingDir: context.workingDir,
strict: options.strict ?? false,
defaultInclude: options.include,
defaultExclude: options.exclude
});
const intoPath = Path3.resolve(context.workingDir, options.into);
const targets = /* @__PURE__ */ new Map();
for (const file of files) {
const target = Path3.join(intoPath, computeRelativePath(file.relativePath, options));
const existingSource = targets.get(target);
if (existingSource !== void 0) {
throw new TaskExecutionError(`Both '${existingSource}' and '${file.source}' map to the same destination '${target}'.`);
}
targets.set(target, file.source);
}
return targets;
}
function computeRelativePath(relativePath, options) {
const flattened = options.flatten ? Path3.basename(relativePath) : relativePath;
const renamed = options.rename?.[Path3.basename(flattened)];
return renamed === void 0 ? flattened : Path3.join(Path3.dirname(flattened), renamed);
}
async function shouldWrite(target, overwrite, context) {
if (overwrite === "replace" || !await isPathExists(target)) {
return true;
}
if (overwrite === "error") {
throw new TaskExecutionError(`Destination '${target}' already exists.`);
}
context.logger.info(`Skip existing ${Path3.relative(context.workingDir, target)}`);
return false;
}
// packages/nadle/src/builtin-tasks/copy-task.ts
var COPY_CONCURRENCY = 8;
var CopyTask = defineTask({
run: async ({ options, context }) => {
const targets = await resolveTargets(options, context);
const overwrite = options.overwrite ?? "replace";
const limit = pLimit(COPY_CONCURRENCY);
await Promise.all(
[...targets.entries()].map(
([target, source]) => limit(async () => {
if (!await shouldWrite(target, overwrite, context)) {
return;
}
await Fs3.mkdir(Path4.dirname(target), { recursive: true });
context.logger.log(`Copy ${Path4.relative(context.workingDir, source)} -> ${Path4.relative(context.workingDir, target)}`);
await Fs3.cp(source, target);
})
)
);
context.logger.info(`Copied ${targets.size} file(s) into ${options.into}`);
}
});
// packages/nadle/src/builtin-tasks/move-task.ts
import Path5 from "path";
import Fs4 from "fs/promises";
var MoveTask = defineTask({
run: async ({ options, context }) => {
const targets = await resolveTargets(options, context);
const overwrite = options.overwrite ?? "replace";
for (const [target, source] of targets) {
if (!await shouldWrite(target, overwrite, context)) {
continue;
}
await Fs4.mkdir(Path5.dirname(target), { recursive: true });
context.logger.log(`Move ${Path5.relative(context.workingDir, source)} -> ${Path5.relative(context.workingDir, target)}`);
try {
await Fs4.rename(source, target);
} catch {
await Fs4.cp(source, target);
await Fs4.rm(source);
}
}
context.logger.info(`Moved ${targets.size} file(s) into ${options.into}`);
}
});
// packages/nadle/src/builtin-tasks/sync-task.ts
import Path6 from "path";
import Fs5 from "fs/promises";
import fg2 from "fast-glob";
import micromatch from "micromatch";
var SyncTask = defineTask({
run: async ({ options, context }) => {
const targets = await resolveTargets(options, context);
const intoPath = Path6.resolve(context.workingDir, options.into);
for (const [target, source] of targets) {
await Fs5.mkdir(Path6.dirname(target), { recursive: true });
context.logger.log(`Sync ${Path6.relative(context.workingDir, source)} -> ${Path6.relative(context.workingDir, target)}`);
await Fs5.cp(source, target);
}
const deleted = await deleteExtraneous({ context, targets, intoPath, preserve: MaybeArray.toArray(options.preserve ?? []) });
await pruneEmptyDirectories(intoPath);
context.logger.info(`Synced ${targets.size} file(s) into ${options.into}${deleted > 0 ? `, deleted ${deleted} extraneous file(s)` : ""}`);
}
});
async function deleteExtraneous({ context, targets, intoPath, preserve }) {
if (!await isPathExists(intoPath)) {
return 0;
}
const existingFiles = await fg2("**/*", { dot: true, cwd: intoPath, onlyFiles: true });
let deleted = 0;
for (const relativePath of existingFiles) {
const absolutePath = Path6.join(intoPath, relativePath);
if (targets.has(absolutePath) || preserve.length > 0 && micromatch.isMatch(relativePath, preserve)) {
continue;
}
context.logger.log(`Delete extraneous ${Path6.relative(context.workingDir, absolutePath)}`);
await Fs5.rm(absolutePath);
deleted += 1;
}
return deleted;
}
async function pruneEmptyDirectories(intoPath) {
const directories = await fg2("**/*", { dot: true, cwd: intoPath, onlyDirectories: true });
directories.sort((left, right) => right.split("/").length - left.split("/").length);
for (const directory of directories) {
try {
await Fs5.rmdir(Path6.join(intoPath, directory));
} catch {
}
}
}
// packages/nadle/src/builtin-tasks/unzip-task.ts
import Path7 from "path";
import Fs6 from "fs/promises";
import { unzipSync } from "fflate";
import micromatch2 from "micromatch";
var UnzipTask = defineTask({
run: async ({ options, context }) => {
const archivePath = Path7.resolve(context.workingDir, options.archive);
if (!await isPathExists(archivePath)) {
throw new TaskExecutionError(`Archive '${archivePath}' does not exist.`);
}
const intoPath = Path7.resolve(context.workingDir, options.into);
const include = MaybeArray.toArray(options.include ?? []);
const entries = unzipSync(await Fs6.readFile(archivePath));
let extracted = 0;
for (const [entryName, content] of Object.entries(entries)) {
if (entryName.endsWith("/") || include.length > 0 && !micromatch2.isMatch(entryName, include)) {
continue;
}
const target = Path7.join(intoPath, entryName);
if (Path7.relative(intoPath, target).startsWith("..")) {
throw new TaskExecutionError(`Archive entry '${entryName}' escapes the destination directory.`);
}
await Fs6.mkdir(Path7.dirname(target), { recursive: true });
await Fs6.writeFile(target, content);
extracted += 1;
}
context.logger.info(`Extracted ${extracted} entry(ies) from ${options.archive} into ${options.into}`);
}
});
// packages/nadle/src/builtin-tasks/delete-task.ts
import Path8 from "path";
import { glob } from "glob";
import { rimraf } from "rimraf";
var DeleteTask = defineTask({
run: async ({ options, context }) => {
const { paths, ...restOptions } = options;
const { workingDir } = context;
const matchPaths = await glob(paths, { cwd: workingDir });
context.logger.info(`Current working dir: ${workingDir}`);
context.logger.info("Deleting paths:", matchPaths.map(normalizeGlobPath).join(", "));
await rimraf(
matchPaths.map((matchPath) => Path8.resolve(workingDir, matchPath)),
restOptions
);
}
});
// packages/nadle/src/builtin-tasks/download-task.ts
import Path9 from "path";
import Crypto from "crypto";
import Fs7 from "fs/promises";
var DownloadTask = defineTask({
run: async ({ options, context }) => {
const filename = options.filename ?? Path9.posix.basename(new URL(options.url).pathname);
if (filename === "") {
throw new TaskExecutionError(`Cannot derive a file name from '${options.url}'. Set the 'filename' option.`);
}
const target = Path9.resolve(context.workingDir, options.into, filename);
if (options.sha256 !== void 0 && await isPathExists(target) && await sha256Of(target) === options.sha256.toLowerCase()) {
context.logger.info(`Skip download: ${filename} already matches the expected digest.`);
return;
}
context.logger.info(`Downloading ${options.url}`);
const response = await fetch(options.url);
if (!response.ok) {
throw new TaskExecutionError(`Download of '${options.url}' failed with status ${response.status}.`);
}
await Fs7.mkdir(Path9.dirname(target), { recursive: true });
await Fs7.writeFile(target, new Uint8Array(await response.arrayBuffer()));
if (options.sha256 !== void 0) {
const actual = await sha256Of(target);
if (actual !== options.sha256.toLowerCase()) {
await Fs7.rm(target);
throw new TaskExecutionError(`Digest mismatch for '${options.url}': expected ${options.sha256.toLowerCase()}, got ${actual}.`);
}
}
context.logger.info(`Downloaded ${options.url} to ${Path9.relative(context.workingDir, target)}`);
}
});
async function sha256Of(filePath) {
return Crypto.createHash("sha256").update(await Fs7.readFile(filePath)).digest("hex");
}
export {
ConfigurationError,
CopyTask,
CyclicDependencyError,
DeleteTask,
DownloadTask,
ExecTask,
Inputs,
MaybeArray,
MoveTask,
NadleError,
NodeTask,
NpmTask,
NpxTask,
Outputs,
PnpmTask,
PnpxTask,
SupportLogLevels,
SupportReporters,
SyncTask,
TaskExecutionError,
TaskNotFoundError,
UnzipTask,
ZipTask,
configure,
defineTask,
tasks
};