Copy files with glob pattern support, progress reporting, and advanced file operations
npx @tessl/cli install tessl/npm-cpy@12.0.0Cpy is a high-performance Node.js file copying utility that supports glob patterns, directory traversal, and advanced file operations. It provides fast file copying through cloning mechanisms when possible, resilient operations using graceful-fs, and comprehensive copying options including progress reporting, file filtering, and concurrent operations.
npm install cpyimport cpy from "cpy";For CommonJS:
const cpy = require("cpy");With TypeScript types:
import cpy, { type Options, type Entry, type ProgressData, type ProgressEmitter } from "cpy";import cpy from "cpy";
// Copy single file
await cpy("source.txt", "destination");
// Copy multiple files with glob patterns
await cpy([
"source/*.png", // Copy all .png files
"!source/goat.png", // Ignore goat.png
], "destination");
// Copy with options
await cpy("src/**/*.js", "destination", {
flat: true,
onProgress: progress => {
console.log(`Progress: ${Math.round(progress.percent * 100)}%`);
}
});
console.log("Files copied!");Cpy is built around several key components:
cpy() that handles all file copying operationsCpyError class for detailed error reportingCopy files and directories with comprehensive options and glob pattern support.
/**
* Copy files from source to destination with optional configuration
* @param source - Files to copy (string or array of strings/globs)
* @param destination - Destination directory
* @param options - Copy configuration options
* @returns Promise resolving to array of destination file paths, with progress event emitter
*/
function cpy(
source: string | readonly string[],
destination: string,
options?: Options
): Promise<string[]> & ProgressEmitter;Usage Examples:
// Copy node_modules to destination/node_modules
await cpy("node_modules", "destination");
// Copy node_modules content to destination
await cpy("node_modules/**", "destination");
// Copy specific file types with exclusions
await cpy([
"src/**/*.{js,ts}",
"!src/**/*.test.{js,ts}",
"!src/temp/**"
], "build");
// Copy with file renaming
await cpy("*.txt", "destination", {
rename: basename => `backup-${basename}`
});
// Advanced filtering
await cpy("**/*", "destination", {
filter: file => !file.name.startsWith('.') && file.extension !== 'tmp'
});Monitor copy operations with detailed progress information.
interface ProgressData {
/** Number of files copied so far */
completedFiles: number;
/** Total number of files to copy */
totalFiles: number;
/** Number of bytes copied so far */
completedSize: number;
/** Progress percentage as a value between 0 and 1 */
percent: number;
/** Absolute source path of the current file being copied */
sourcePath: string;
/** Absolute destination path of the current file being copied */
destinationPath: string;
}Usage Examples:
// Using onProgress callback (recommended)
await cpy("source/**", "destination", {
onProgress: progress => {
console.log(`${progress.completedFiles}/${progress.totalFiles} files`);
console.log(`${Math.round(progress.percent * 100)}% complete`);
console.log(`Current: ${progress.sourcePath}`);
}
});
// Using event emitter (deprecated)
await cpy("source/**", "destination").on("progress", progress => {
console.log(`Progress: ${Math.round(progress.percent * 100)}%`);
});Filter files during copying with custom predicates.
interface Entry {
/** Resolved absolute path to the file */
readonly path: string;
/** Relative path to the file from cwd */
readonly relativePath: string;
/** Filename with extension */
readonly name: string;
/** Filename without extension */
readonly nameWithoutExtension: string;
/** File extension without dot */
readonly extension: string;
}Usage Examples:
// Filter by file extension
await cpy("src/**/*", "destination", {
filter: file => file.extension === "js" || file.extension === "ts"
});
// Filter by file size or other criteria
await cpy("**/*", "destination", {
filter: async file => {
const stats = await fs.stat(file.path);
return stats.size < 1000000; // Files smaller than 1MB
}
});
// Complex filtering logic
await cpy("**/*", "destination", {
filter: file => {
// Skip hidden files
if (file.name.startsWith(".")) return false;
// Skip backup files
if (file.name.endsWith(".bak")) return false;
// Only include specific directories
return file.relativePath.includes("src/") || file.relativePath.includes("lib/");
}
});interface Options {
/** Working directory to find source files (default: process.cwd()) */
readonly cwd?: string;
/** Flatten directory tree - put all files in same directory (default: false) */
readonly flat?: boolean;
/** Filename or function returning filename to rename every file */
readonly rename?: string | ((basename: string) => string);
/** Number of files being copied concurrently (default: os.availableParallelism()) */
readonly concurrency?: number;
/** Ignore junk files (default: true) */
readonly ignoreJunk?: boolean;
/** Function to filter files to copy */
readonly filter?: (file: Entry) => boolean | Promise<boolean>;
/** Progress callback function */
readonly onProgress?: (progress: ProgressData) => void;
/** Overwrite existing files (default: true) */
readonly overwrite?: boolean;
}
// Note: Options interface extends GlobOptions from 'globby' package and CopyFileOptions from 'copy-file' package,
// providing additional options for glob pattern matching and file copying behavior.Usage Examples:
// Flatten directory structure
await cpy("src/**/*.js", "destination", { flat: true });
// Set working directory
await cpy("*.js", "destination", { cwd: "/path/to/source" });
// Control concurrency
await cpy("**/*", "destination", { concurrency: 2 });
// Preserve junk files
await cpy("**/*", "destination", { ignoreJunk: false });
// Prevent overwriting existing files
await cpy("*.js", "destination", { overwrite: false });
// Rename during copy
await cpy("*.js", "destination", {
rename: basename => `processed-${basename}`
});
// Function-based renaming
await cpy("**/*.js", "destination", {
rename: basename => {
const name = basename.replace(".js", "");
return `${name}.processed.js`;
}
});interface ProgressEmitter {
/**
* @deprecated Use onProgress option instead
* Add progress event listener to the copy operation
*/
on(
event: "progress",
handler: (progress: ProgressData) => void
): Promise<string[]>;
}Cpy uses a custom error class for detailed error reporting. The CpyError class is not directly exportable but can be identified by checking the error name:
// CpyError class (internal, not directly importable)
class CpyError extends Error {
name: "CpyError";
cause?: Error;
}Common Error Scenarios:
try {
await cpy("nonexistent.txt", "destination");
} catch (error) {
if (error.name === "CpyError") {
console.log("Cpy error:", error.message);
if (error.cause) {
console.log("Caused by:", error.cause.message);
}
}
}
// Errors thrown for:
// - Missing source or destination parameters
// - Non-existent files (when not using globs)
// - Self-copy attempts
// - Permission errors
// - Invalid rename functions
// - File system errors// Mix of files, directories, and globs
await cpy([
"readme.md", // Single file
"src/**/*.js", // Glob pattern
"assets/", // Directory
"!assets/temp/**", // Negation pattern
], "destination");// Copy directory structure
await cpy("src", "destination"); // → destination/src/
await cpy("src/**", "destination"); // → destination/ (contents only)
// Glob vs explicit behavior differences
await cpy("src/*.js", "destination"); // Preserves relative to glob parent
await cpy("src/file.js", "destination"); // Preserves relative to cwd// Multi-step copy with progress tracking
let totalFiles = 0;
let copiedFiles = 0;
const copyTasks = [
{ src: "src/**/*.js", dest: "build/js" },
{ src: "assets/**/*", dest: "build/assets" },
{ src: "docs/**/*.md", dest: "build/docs" }
];
for (const task of copyTasks) {
await cpy(task.src, task.dest, {
onProgress: progress => {
console.log(`${task.src}: ${Math.round(progress.percent * 100)}%`);
}
});
}