Skip to main content

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", {
run: CopyTask,
options: {
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/exclude act as defaults for directory selections without their own.
  • A missing source logs a warning and is skipped — set strict: true to fail instead.

The destination (into) is always a directory and is created if missing.

CopyTask​

tasks.register("stageConfigs", {
run: CopyTask,
options: {
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", {
run: MoveTask,
options: {
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", {
run: SyncTask,
options: {
from: "static",
into: "dist/public",
preserve: ["cache-manifest.json"]
}
});

ZipTask and UnzipTask​

tasks.register("bundle", {
run: ZipTask,
options: {
from: "dist",
archive: "out/bundle.zip",
prefix: "bundle" // entries stored as bundle/...
}
});

tasks.register("extract", {
run: UnzipTask,
options: {
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", {
run: DownloadTask,
options: {
url: "https://example.com/schema.json",
into: "vendor",
sha256: "ab12…" // optional integrity check
}
});
  • filename defaults 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", { run: DeleteTask, options: { 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", {
run: CopyTask,
options: { from: "assets", into: "dist/assets" },
inputs: [Inputs.dirs("assets")],
outputs: [Outputs.dirs("dist/assets")]
});