UNPKG

@cjsa/cpy

Version:

Copy files

223 lines (219 loc) 7.54 kB
// index.js import process from "process"; import EventEmitter from "events"; import path2 from "path"; import os from "os"; import pMap from "p-map"; import arrify from "arrify"; import cpFile from "cp-file"; import pFilter from "p-filter"; import { isDynamicPattern as isDynamicPattern2 } from "globby"; import micromatch from "micromatch"; // cpy-error.js import NestedError from "nested-error-stacks"; var CpyError = class extends NestedError { constructor(message, nested) { super(message, nested); Object.assign(this, nested); this.name = "CpyError"; } }; // glob-pattern.js import path from "path"; import fs from "fs"; import { globbySync, isDynamicPattern } from "globby"; import { isNotJunk } from "junk"; var GlobPattern = class { constructor(pattern, destination, options) { this.path = pattern; this.originalPath = pattern; this.destination = destination; this.options = options; this.isDirectory = false; if (!isDynamicPattern(pattern) && fs.existsSync(pattern) && fs.lstatSync(pattern).isDirectory()) { this.path = [pattern, "**"].join("/"); this.isDirectory = true; } } get name() { return path.basename(this.originalPath); } get normalizedPath() { const segments = this.originalPath.split("/"); const magicIndex = segments.findIndex((item) => item ? isDynamicPattern(item) : false); const normalized = segments.slice(0, magicIndex).join("/"); if (normalized) { return path.isAbsolute(normalized) ? normalized : path.join(this.options.cwd, normalized); } return this.destination; } hasMagic() { return isDynamicPattern(this.options.flat ? this.path : this.originalPath); } getMatches() { let matches = globbySync(this.path, { ...this.options, dot: true, absolute: true, onlyFiles: true }); if (this.options.ignoreJunk) { matches = matches.filter((file) => isNotJunk(path.basename(file))); } return matches; } }; // index.js var defaultConcurrency = (os.cpus().length || 1) * 2; var defaultOptions = { ignoreJunk: true, flat: false, cwd: process.cwd() }; var Entry = class { constructor(source, relativePath, pattern) { this.path = source.split("/").join(path2.sep); this.relativePath = relativePath.split("/").join(path2.sep); this.pattern = pattern; Object.freeze(this); } get name() { return path2.basename(this.path); } get nameWithoutExtension() { return path2.basename(this.path, path2.extname(this.path)); } get extension() { return path2.extname(this.path).slice(1); } }; var expandPatternsWithBraceExpansion = (patterns) => patterns.flatMap((pattern) => micromatch.braces(pattern, { expand: true, nodupes: true })); var preprocessDestinationPath = ({ entry, destination, options }) => { if (entry.pattern.hasMagic()) { if (options.flat) { if (path2.isAbsolute(destination)) { return path2.join(destination, entry.name); } return path2.join(options.cwd, destination, entry.name); } return path2.join(destination, path2.relative(entry.pattern.normalizedPath, entry.path)); } if (path2.isAbsolute(destination)) { return path2.join(destination, entry.name); } if (entry.pattern.isDirectory && path2.relative(options.cwd, entry.path).startsWith("..")) { return path2.join(options.cwd, destination, path2.basename(entry.pattern.originalPath), path2.relative(entry.pattern.originalPath, entry.path)); } if (!entry.pattern.isDirectory && entry.path === entry.relativePath) { return path2.join(options.cwd, destination, path2.basename(entry.pattern.originalPath), path2.relative(entry.pattern.originalPath, entry.path)); } if (!entry.pattern.isDirectory && options.flat) { return path2.join(options.cwd, destination, path2.basename(entry.pattern.originalPath)); } return path2.join(options.cwd, destination, path2.relative(options.cwd, entry.path)); }; var renameFile = (source, rename) => { const filename = path2.basename(source, path2.extname(source)); const fileExtension = path2.extname(source); const directory = path2.dirname(source); if (typeof rename === "string") { return path2.join(directory, rename); } if (typeof rename === "function") { return path2.join(directory, `${rename(filename)}${fileExtension}`); } return source; }; function cpy(source, destination, { concurrency = defaultConcurrency, ...options } = {}) { const copyStatus = /* @__PURE__ */ new Map(); const progressEmitter = new EventEmitter(); options = { ...defaultOptions, ...options }; const promise = (async () => { let entries = []; let completedFiles = 0; let completedSize = 0; let patterns = expandPatternsWithBraceExpansion(arrify(source)).map((string) => string.replace(/\\/g, "/")); const sources = patterns.filter((item) => !item.startsWith("!")); const ignore = patterns.filter((item) => item.startsWith("!")); if (sources.length === 0 || !destination) { throw new CpyError("`source` and `destination` required"); } patterns = patterns.map((pattern) => new GlobPattern(pattern, destination, { ...options, ignore })); for (const pattern of patterns) { let matches = []; try { matches = pattern.getMatches(); } catch (error) { throw new CpyError(`Cannot glob \`${pattern.originalPath}\`: ${error.message}`, error); } if (matches.length === 0 && !isDynamicPattern2(pattern.originalPath) && !isDynamicPattern2(ignore)) { throw new CpyError(`Cannot copy \`${pattern.originalPath}\`: the file doesn't exist`); } entries = [ ...entries, ...matches.map((sourcePath) => new Entry(sourcePath, path2.relative(options.cwd, sourcePath), pattern)) ]; } if (options.filter !== void 0) { entries = await pFilter(entries, options.filter, { concurrency: 1024 }); } if (entries.length === 0) { progressEmitter.emit("progress", { totalFiles: 0, percent: 1, completedFiles: 0, completedSize: 0 }); } const fileProgressHandler = (event) => { const fileStatus = copyStatus.get(event.sourcePath) || { writtenBytes: 0, percent: 0 }; if (fileStatus.writtenBytes !== event.writtenBytes || fileStatus.percent !== event.percent) { completedSize -= fileStatus.writtenBytes; completedSize += event.writtenBytes; if (event.percent === 1 && fileStatus.percent !== 1) { completedFiles++; } copyStatus.set(event.sourcePath, { writtenBytes: event.writtenBytes, percent: event.percent }); progressEmitter.emit("progress", { totalFiles: entries.length, percent: completedFiles / entries.length, completedFiles, completedSize }); } }; return pMap(entries, async (entry) => { const to = renameFile(preprocessDestinationPath({ entry, destination, options }), options.rename); try { await cpFile(entry.path, to, options).on("progress", fileProgressHandler); } catch (error) { throw new CpyError(`Cannot copy from \`${entry.relativePath}\` to \`${to}\`: ${error.message}`, error); } return to; }, { concurrency }); })(); promise.on = (...arguments_) => { progressEmitter.on(...arguments_); return promise; }; return promise; } export { cpy as default };