UNPKG

nadle

Version:

A type-safe, Gradle-inspired task runner for Node.js. Sharp tasks. Fast builds.

577 lines (553 loc) 20.6 kB
#!/usr/bin/env node 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 };