File Operation Tasks
Nadle ships built-in tasks for everyday file operations: CopyTask, MoveTask, SyncTask,
ZipTask, UnzipTask, DownloadTask, and DeleteTask. They share one source vocabulary and
one rule for destinations, so once you've learned CopyTask you know them all.
File Selections
Every task that reads files accepts the same from shapes — a path, a selector, or an array
mixing both:
tasks.register("collect", CopyTask, {
from: [
"README.md", // a single file
"assets", // a whole directory
{ dir: "docs", include: "**/*.md", exclude: "**/draft-*.md" } // a selector
],
into: "dist"
});
- A string pointing to a file selects that file; pointing to a directory selects the files inside it.
- A selector (
{ dir, include?, exclude? }) filters a directory with glob patterns. - Task-level
include/excludeact as defaults for directory selections without their own. - A missing source logs a warning and is skipped — set
strict: trueto fail instead.
The destination (into) is always a directory and is created if missing.
CopyTask
tasks.register("stageConfigs", CopyTask, {
from: { dir: "configs", include: "*.prod.json" },
into: "build",
flatten: true, // drop source directory structure
rename: { "app.prod.json": "app.json" }, // rename by exact base name
overwrite: "error", // replace (default) | skip | error
strict: true // fail on missing source or zero matches
});
If two sources map to the same destination (e.g. via flatten), the task fails instead of
silently overwriting. Files are copied with bounded concurrency.
MoveTask
Same options as CopyTask; sources are removed after they reach their destination (a
filesystem rename when possible, copy-then-delete across devices). Files skipped by the
overwrite policy keep their source.
tasks.register("archiveLogs", MoveTask, {
from: { dir: "logs", include: "*.log" },
into: "logs/archive",
overwrite: "skip"
});
SyncTask
Makes the destination an exact mirror of the sources: copies everything (always replacing),
then deletes files in into that have no corresponding source and prunes empty directories.
Use preserve for files that must survive:
tasks.register("syncPublic", SyncTask, {
from: "static",
into: "dist/public",
preserve: ["cache-manifest.json"]
});
ZipTask and UnzipTask
tasks.register("bundle", ZipTask, {
from: "dist",
archive: "out/bundle.zip",
prefix: "bundle" // entries stored as bundle/...
});
tasks.register("extract", UnzipTask, {
archive: "out/bundle.zip",
into: "extracted",
include: "bundle/assets/**" // optional entry filter
});
Archive entry names are the selection-relative paths (always forward slashes). UnzipTask
refuses entries that would escape the destination directory (zip-slip protection).
DownloadTask
tasks.register("fetchSchema", DownloadTask, {
url: "https://example.com/schema.json",
into: "vendor",
sha256: "ab12…" // optional integrity check
});
filenamedefaults to the last segment of the URL path.- With
sha256, an existing file with a matching digest skips the download entirely — re-runs are cheap and offline-safe. A mismatch after download fails the task and removes the file.
DeleteTask
tasks.register("clean", DeleteTask, { paths: ["dist", "**/*.tmp"] });
Glob patterns are resolved once; the logged list is exactly what gets deleted.
Caching
All options are plain data, so they participate in the cache key automatically. Declare
inputs/outputs to make file operations cacheable:
import { tasks, Inputs, Outputs, CopyTask } from "nadle";
tasks.register("bundleAssets", CopyTask, { from: "assets", into: "dist/assets" }).config({
inputs: [Inputs.dirs("assets")],
outputs: [Outputs.dirs("dist/assets")]
});