├── .editorconfig ├── .gitattributes ├── .github ├── security.md └── workflows │ └── main.yml ├── .gitignore ├── .npmrc ├── cpy-error.js ├── glob-pattern.js ├── index.d.ts ├── index.js ├── index.test-d.ts ├── license ├── package.json ├── readme.md └── test.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.yml] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/security.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure. 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - push 4 | - pull_request 5 | jobs: 6 | test: 7 | name: Node.js ${{ matrix.node-version }} on ${{ matrix.os }} 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | fail-fast: false 11 | matrix: 12 | node-version: 13 | - 20 14 | - 18 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm install 25 | - run: npm test 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /cpy-error.js: -------------------------------------------------------------------------------- 1 | export default class CpyError extends Error { 2 | constructor(message, {cause} = {}) { 3 | super(message, {cause}); 4 | Object.assign(this, cause); 5 | this.name = 'CpyError'; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /glob-pattern.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import fs from 'node:fs'; 3 | import {globbySync, isDynamicPattern} from 'globby'; 4 | import {isNotJunk} from 'junk'; 5 | 6 | export default class GlobPattern { 7 | /** 8 | @param {string} pattern 9 | @param {string} destination 10 | @param {import('.').Options} options 11 | */ 12 | constructor(pattern, destination, options) { 13 | this.path = pattern; 14 | this.originalPath = pattern; 15 | this.destination = destination; 16 | this.options = options; 17 | this.isDirectory = false; 18 | 19 | if ( 20 | !isDynamicPattern(pattern) 21 | && fs.existsSync(pattern) 22 | && fs.lstatSync(pattern).isDirectory() 23 | ) { 24 | this.path = [pattern, '**'].join('/'); 25 | this.isDirectory = true; 26 | } 27 | } 28 | 29 | get name() { 30 | return path.basename(this.originalPath); 31 | } 32 | 33 | get normalizedPath() { 34 | const segments = this.originalPath.split('/'); 35 | const magicIndex = segments.findIndex(item => item ? isDynamicPattern(item) : false); 36 | const normalized = segments.slice(0, magicIndex).join('/'); 37 | 38 | if (normalized) { 39 | return path.isAbsolute(normalized) ? normalized : path.join(this.options.cwd, normalized); 40 | } 41 | 42 | return this.destination; 43 | } 44 | 45 | hasMagic() { 46 | return isDynamicPattern(this.options.flat ? this.path : this.originalPath); 47 | } 48 | 49 | getMatches() { 50 | let matches = globbySync(this.path, { 51 | ...this.options, 52 | dot: true, 53 | absolute: true, 54 | onlyFiles: true, 55 | }); 56 | 57 | if (this.options.ignoreJunk) { 58 | matches = matches.filter(file => isNotJunk(path.basename(file))); 59 | } 60 | 61 | return matches; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import {type Options as GlobOptions} from 'globby'; 2 | import {type Options as CopyFileOptions} from 'copy-file'; 3 | 4 | export type Entry = { 5 | /** 6 | Resolved path to the file. 7 | 8 | @example '/tmp/dir/foo.js' 9 | */ 10 | readonly path: string; 11 | 12 | /** 13 | Relative path to the file from cwd. 14 | 15 | @example 'dir/foo.js' 16 | */ 17 | readonly relativePath: string; 18 | 19 | /** 20 | Filename with extension. 21 | 22 | @example 'foo.js' 23 | */ 24 | readonly name: string; 25 | 26 | /** 27 | Filename without extension. 28 | 29 | @example 'foo' 30 | */ 31 | readonly nameWithoutExtension: string; 32 | 33 | /** 34 | File extension. 35 | 36 | @example 'js' 37 | */ 38 | readonly extension: string; 39 | }; 40 | 41 | export type Options = { 42 | /** 43 | Working directory to find source files. 44 | 45 | @default process.cwd() 46 | */ 47 | readonly cwd?: string; 48 | 49 | /** 50 | Flatten directory tree. 51 | 52 | @default false 53 | */ 54 | readonly flat?: boolean; 55 | 56 | /** 57 | Filename or function returning a filename used to rename every file in `source`. 58 | 59 | @example 60 | ``` 61 | import cpy from 'cpy'; 62 | 63 | await cpy('foo.js', 'destination', { 64 | // The `basename` is the filename with extension. 65 | rename: basename => `prefix-${basename}` 66 | }); 67 | 68 | await cpy('foo.js', 'destination', { 69 | rename: 'new-name' 70 | }); 71 | ``` 72 | */ 73 | readonly rename?: string | ((basename: string) => string); 74 | 75 | /** 76 | Number of files being copied concurrently. 77 | 78 | @default (os.cpus().length || 1) * 2 79 | */ 80 | readonly concurrency?: number; 81 | 82 | /** 83 | Ignore junk files. 84 | 85 | @default true 86 | */ 87 | readonly ignoreJunk?: boolean; 88 | 89 | /** 90 | Function to filter files to copy. 91 | 92 | Receives a source file object as the first argument. 93 | 94 | Return true to include, false to exclude. You can also return a Promise that resolves to true or false. 95 | 96 | @example 97 | ``` 98 | import cpy from 'cpy'; 99 | 100 | await cpy('foo', 'destination', { 101 | filter: file => file.extension !== 'nocopy' 102 | }); 103 | ``` 104 | */ 105 | readonly filter?: (file: Entry) => boolean | Promise; 106 | } & Readonly & CopyFileOptions; 107 | 108 | export type ProgressData = { 109 | /** 110 | Copied file count. 111 | */ 112 | completedFiles: number; 113 | 114 | /** 115 | Overall file count. 116 | */ 117 | totalFiles: number; 118 | 119 | /** 120 | Completed size in bytes. 121 | */ 122 | completedSize: number; 123 | 124 | /** 125 | Completed percentage. A value between `0` and `1`. 126 | */ 127 | percent: number; 128 | 129 | /** 130 | The absolute source path of the current file being copied. 131 | */ 132 | sourcePath: string; 133 | 134 | /** 135 | The absolute destination path of the current file being copied. 136 | */ 137 | destinationPath: string; 138 | }; 139 | 140 | export type ProgressEmitter = { 141 | on( 142 | event: 'progress', 143 | handler: (progress: ProgressData) => void 144 | ): Promise; 145 | }; 146 | 147 | /** 148 | Copy files. 149 | 150 | @param source - Files to copy. If any of the files do not exist, an error will be thrown (does not apply to globs). 151 | @param destination - Destination directory. 152 | @param options - In addition to the options defined here, options are passed to [globby](https://github.com/sindresorhus/globby#options). 153 | 154 | @example 155 | ``` 156 | import cpy from 'cpy'; 157 | 158 | await cpy([ 159 | 'source/*.png', // Copy all .png files 160 | '!source/goat.png', // Ignore goat.png 161 | ], 'destination'); 162 | 163 | // Copy node_modules to destination/node_modules 164 | await cpy('node_modules', 'destination'); 165 | 166 | // Copy node_modules content to destination 167 | await cpy('node_modules/**', 'destination'); 168 | 169 | // Copy node_modules structure but skip all files except any .json files 170 | await cpy('node_modules/**\/*.json', 'destination'); 171 | 172 | // Copy all png files into destination without keeping directory structure 173 | await cpy('**\/*.png', 'destination', {flat: true}); 174 | 175 | console.log('Files copied!'); 176 | ``` 177 | */ 178 | export default function cpy( 179 | source: string | readonly string[], 180 | destination: string, 181 | options?: Options 182 | ): Promise & ProgressEmitter; 183 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import EventEmitter from 'node:events'; 3 | import path from 'node:path'; 4 | import os from 'node:os'; 5 | import pMap from 'p-map'; 6 | import {copyFile} from 'copy-file'; 7 | import pFilter from 'p-filter'; 8 | import {isDynamicPattern} from 'globby'; 9 | import micromatch from 'micromatch'; 10 | import CpyError from './cpy-error.js'; 11 | import GlobPattern from './glob-pattern.js'; 12 | 13 | /** 14 | @type {import('./index').Options} 15 | */ 16 | const defaultOptions = { 17 | ignoreJunk: true, 18 | flat: false, 19 | cwd: process.cwd(), 20 | }; 21 | 22 | class Entry { 23 | /** 24 | @param {string} source 25 | @param {string} relativePath 26 | @param {GlobPattern} pattern 27 | */ 28 | constructor(source, relativePath, pattern) { 29 | /** 30 | @type {string} 31 | */ 32 | this.path = source.split('/').join(path.sep); 33 | 34 | /** 35 | @type {string} 36 | */ 37 | this.relativePath = relativePath.split('/').join(path.sep); 38 | 39 | this.pattern = pattern; 40 | 41 | Object.freeze(this); 42 | } 43 | 44 | get name() { 45 | return path.basename(this.path); 46 | } 47 | 48 | get nameWithoutExtension() { 49 | return path.basename(this.path, path.extname(this.path)); 50 | } 51 | 52 | get extension() { 53 | return path.extname(this.path).slice(1); 54 | } 55 | } 56 | 57 | /** 58 | Expand patterns like `'node_modules/{globby,micromatch}'` into `['node_modules/globby', 'node_modules/micromatch']`. 59 | 60 | @param {string[]} patterns 61 | @returns {string[]} 62 | */ 63 | const expandPatternsWithBraceExpansion = patterns => patterns.flatMap(pattern => ( 64 | micromatch.braces(pattern, { 65 | expand: true, 66 | nodupes: true, 67 | }) 68 | )); 69 | 70 | /** 71 | @param {object} props 72 | @param {Entry} props.entry 73 | @param {import('./index').Options} 74 | @param {string} props.destination 75 | @returns {string} 76 | */ 77 | const preprocessDestinationPath = ({entry, destination, options}) => { 78 | if (entry.pattern.hasMagic()) { 79 | if (options.flat) { 80 | if (path.isAbsolute(destination)) { 81 | return path.join(destination, entry.name); 82 | } 83 | 84 | return path.join(options.cwd, destination, entry.name); 85 | } 86 | 87 | return path.join( 88 | destination, 89 | path.relative(entry.pattern.normalizedPath, entry.path), 90 | ); 91 | } 92 | 93 | if (path.isAbsolute(destination)) { 94 | return path.join(destination, entry.name); 95 | } 96 | 97 | // TODO: This check will not work correctly if `options.cwd` and `entry.path` are on different partitions on Windows, see: https://github.com/sindresorhus/import-local/pull/12 98 | if (entry.pattern.isDirectory && path.relative(options.cwd, entry.path).startsWith('..')) { 99 | return path.join(options.cwd, destination, path.basename(entry.pattern.originalPath), path.relative(entry.pattern.originalPath, entry.path)); 100 | } 101 | 102 | if (!entry.pattern.isDirectory && entry.path === entry.relativePath) { 103 | return path.join(options.cwd, destination, path.basename(entry.pattern.originalPath), path.relative(entry.pattern.originalPath, entry.path)); 104 | } 105 | 106 | if (!entry.pattern.isDirectory && options.flat) { 107 | return path.join(options.cwd, destination, path.basename(entry.pattern.originalPath)); 108 | } 109 | 110 | return path.join(options.cwd, destination, path.relative(options.cwd, entry.path)); 111 | }; 112 | 113 | /** 114 | @param {string} source 115 | @param {string|Function} rename 116 | */ 117 | const renameFile = (source, rename) => { 118 | const directory = path.dirname(source); 119 | if (typeof rename === 'string') { 120 | return path.join(directory, rename); 121 | } 122 | 123 | if (typeof rename === 'function') { 124 | const filename = path.basename(source); 125 | return path.join(directory, rename(filename)); 126 | } 127 | 128 | return source; 129 | }; 130 | 131 | /** 132 | @param {string|string[]} source 133 | @param {string} destination 134 | @param {import('./index').Options} options 135 | */ 136 | export default function cpy( 137 | source, 138 | destination, 139 | {concurrency = os.availableParallelism(), ...options} = {}, // eslint-disable-line n/no-unsupported-features/node-builtins 140 | ) { 141 | const copyStatus = new Map(); 142 | 143 | /** 144 | @type {import('events').EventEmitter} 145 | */ 146 | const progressEmitter = new EventEmitter(); 147 | 148 | options = { 149 | ...defaultOptions, 150 | ...options, 151 | }; 152 | 153 | const promise = (async () => { 154 | /** 155 | @type {Entry[]} 156 | */ 157 | let entries = []; 158 | let completedFiles = 0; 159 | let completedSize = 0; 160 | 161 | /** 162 | @type {GlobPattern[]} 163 | */ 164 | let patterns = expandPatternsWithBraceExpansion([source ?? []].flat()) 165 | .map(string => string.replaceAll('\\', '/')); 166 | const sources = patterns.filter(item => !item.startsWith('!')); 167 | const ignore = patterns.filter(item => item.startsWith('!')); 168 | 169 | if (sources.length === 0 || !destination) { 170 | throw new CpyError('`source` and `destination` required'); 171 | } 172 | 173 | patterns = patterns.map(pattern => new GlobPattern(pattern, destination, {...options, ignore})); 174 | 175 | for (const pattern of patterns) { 176 | /** 177 | @type {string[]} 178 | */ 179 | let matches = []; 180 | 181 | try { 182 | matches = pattern.getMatches(); 183 | } catch (error) { 184 | throw new CpyError(`Cannot glob \`${pattern.originalPath}\`: ${error.message}`, {cause: error}); 185 | } 186 | 187 | if (matches.length === 0 && !isDynamicPattern(pattern.originalPath) && !isDynamicPattern(ignore)) { 188 | throw new CpyError(`Cannot copy \`${pattern.originalPath}\`: the file doesn't exist`); 189 | } 190 | 191 | entries = [ 192 | ...entries, 193 | ...matches.map(sourcePath => new Entry(sourcePath, path.relative(options.cwd, sourcePath), pattern)), 194 | ]; 195 | } 196 | 197 | if (options.filter !== undefined) { 198 | entries = await pFilter(entries, options.filter, {concurrency: 1024}); 199 | } 200 | 201 | if (entries.length === 0) { 202 | progressEmitter.emit('progress', { 203 | totalFiles: 0, 204 | percent: 1, 205 | completedFiles: 0, 206 | completedSize: 0, 207 | }); 208 | } 209 | 210 | /** 211 | @param {import('copy-file').ProgressData} event 212 | */ 213 | const fileProgressHandler = event => { 214 | const fileStatus = copyStatus.get(event.sourcePath) || { 215 | writtenBytes: 0, 216 | percent: 0, 217 | }; 218 | 219 | if ( 220 | fileStatus.writtenBytes !== event.writtenBytes 221 | || fileStatus.percent !== event.percent 222 | ) { 223 | completedSize -= fileStatus.writtenBytes; 224 | completedSize += event.writtenBytes; 225 | 226 | if (event.percent === 1 && fileStatus.percent !== 1) { 227 | completedFiles++; 228 | } 229 | 230 | copyStatus.set(event.sourcePath, { 231 | writtenBytes: event.writtenBytes, 232 | percent: event.percent, 233 | }); 234 | 235 | progressEmitter.emit('progress', { 236 | totalFiles: entries.length, 237 | percent: completedFiles / entries.length, 238 | completedFiles, 239 | completedSize, 240 | sourcePath: event.sourcePath, 241 | destinationPath: event.destinationPath, 242 | }); 243 | } 244 | }; 245 | 246 | return pMap( 247 | entries, 248 | async entry => { 249 | const to = renameFile( 250 | preprocessDestinationPath({ 251 | entry, 252 | destination, 253 | options, 254 | }), 255 | options.rename, 256 | ); 257 | 258 | try { 259 | await copyFile(entry.path, to, {...options, onProgress: fileProgressHandler}); 260 | } catch (error) { 261 | throw new CpyError(`Cannot copy from \`${entry.relativePath}\` to \`${to}\`: ${error.message}`, {cause: error}); 262 | } 263 | 264 | return to; 265 | }, 266 | {concurrency}, 267 | ); 268 | })(); 269 | 270 | promise.on = (...arguments_) => { 271 | progressEmitter.on(...arguments_); 272 | return promise; 273 | }; 274 | 275 | return promise; 276 | } 277 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import {expectType} from 'tsd'; 2 | import cpy, {type ProgressEmitter, type ProgressData, type Entry} from './index.js'; 3 | 4 | expectType & ProgressEmitter>( 5 | cpy(['source/*.png', '!source/goat.png'], 'destination'), 6 | ); 7 | expectType & ProgressEmitter>( 8 | cpy('foo.js', 'destination', {rename: 'foobar'}), 9 | ); 10 | expectType & ProgressEmitter>( 11 | 12 | cpy('foo.js', 'destination', {rename: basename => `prefix-${basename}`}), 13 | ); 14 | expectType & ProgressEmitter>( 15 | cpy('foo.js', 'destination', {cwd: '/'}), 16 | ); 17 | expectType & ProgressEmitter>( 18 | cpy('foo.js', 'destination', {flat: true}), 19 | ); 20 | expectType & ProgressEmitter>( 21 | cpy('foo.js', 'destination', {overwrite: false}), 22 | ); 23 | expectType & ProgressEmitter>( 24 | cpy('foo.js', 'destination', {concurrency: 2}), 25 | ); 26 | 27 | expectType & ProgressEmitter>( 28 | cpy('foo.js', 'destination', { 29 | filter(file) { 30 | expectType(file); 31 | 32 | expectType(file.path); 33 | expectType(file.relativePath); 34 | expectType(file.name); 35 | expectType(file.nameWithoutExtension); 36 | expectType(file.extension); 37 | return true; 38 | }, 39 | }), 40 | ); 41 | expectType & ProgressEmitter>( 42 | cpy('foo.js', 'destination', {filter: async (_file: Entry) => true}), 43 | ); 44 | 45 | expectType>( 46 | cpy('foo.js', 'destination').on('progress', progress => { 47 | expectType(progress); 48 | 49 | expectType(progress.completedFiles); 50 | expectType(progress.totalFiles); 51 | expectType(progress.completedSize); 52 | expectType(progress.percent); 53 | expectType(progress.sourcePath); 54 | expectType(progress.destinationPath); 55 | }), 56 | ); 57 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Sindre Sorhus (https://sindresorhus.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cpy", 3 | "version": "11.1.0", 4 | "description": "Copy files", 5 | "license": "MIT", 6 | "repository": "sindresorhus/cpy", 7 | "funding": "https://github.com/sponsors/sindresorhus", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "https://sindresorhus.com" 12 | }, 13 | "type": "module", 14 | "exports": { 15 | "types": "./index.d.ts", 16 | "default": "./index.js" 17 | }, 18 | "sideEffects": false, 19 | "engines": { 20 | "node": ">=18" 21 | }, 22 | "scripts": { 23 | "test": "xo && ava && tsd" 24 | }, 25 | "files": [ 26 | "cpy-error.js", 27 | "glob-pattern.js", 28 | "index.js", 29 | "index.d.ts" 30 | ], 31 | "keywords": [ 32 | "copy", 33 | "cp", 34 | "cpy", 35 | "file", 36 | "files", 37 | "clone", 38 | "fs", 39 | "stream", 40 | "glob", 41 | "file-system", 42 | "ncp", 43 | "fast", 44 | "quick", 45 | "data", 46 | "content", 47 | "contents", 48 | "cpx", 49 | "directory", 50 | "directories" 51 | ], 52 | "dependencies": { 53 | "copy-file": "^11.0.0", 54 | "globby": "^14.0.2", 55 | "junk": "^4.0.1", 56 | "micromatch": "^4.0.7", 57 | "p-filter": "^4.1.0", 58 | "p-map": "^7.0.2" 59 | }, 60 | "devDependencies": { 61 | "ava": "^6.1.3", 62 | "proxyquire": "^2.1.3", 63 | "rimraf": "^5.0.5", 64 | "tempy": "^3.1.0", 65 | "tsd": "^0.31.1", 66 | "xo": "^0.59.2" 67 | }, 68 | "xo": { 69 | "rules": { 70 | "unicorn/prefer-event-target": "off" 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # cpy 2 | 3 | > Copy files 4 | 5 | **IMPORTANT:** This package has a lot of problems and I unfortunately don't have time to fix them. I would recommend against using this package until these problems are resolved. Help welcome (see the issue tracker) 🙏 6 | 7 | ## Why 8 | 9 | - Fast by [cloning](https://stackoverflow.com/questions/71629903/node-js-why-we-should-use-copyfile-ficlone-and-copyfile-ficlone-force-what-is) the files whenever possible. 10 | - Resilient by using [graceful-fs](https://github.com/isaacs/node-graceful-fs). 11 | - User-friendly by accepting [globs](https://github.com/sindresorhus/globby#globbing-patterns) and creating non-existent destination directories. 12 | - User-friendly error messages. 13 | - Progress reporting. 14 | 15 | ## Install 16 | 17 | ```sh 18 | npm install cpy 19 | ``` 20 | 21 | ## Usage 22 | 23 | ```js 24 | import cpy from 'cpy'; 25 | 26 | await cpy([ 27 | 'source/*.png', // Copy all .png files 28 | '!source/goat.png', // Ignore goat.png 29 | ], 'destination'); 30 | 31 | // Copy node_modules to destination/node_modules 32 | await cpy('node_modules', 'destination'); 33 | 34 | // Copy node_modules content to destination 35 | await cpy('node_modules/**', 'destination'); 36 | 37 | // Copy node_modules structure but skip all files except package.json files 38 | await cpy('node_modules/**/*.json', 'destination'); 39 | 40 | // Copy all png files into destination without keeping directory structure 41 | await cpy('**/*.png', 'destination', {flat: true}); 42 | 43 | console.log('Files copied!'); 44 | ``` 45 | 46 | ## API 47 | 48 | ### cpy(source, destination, options?) 49 | 50 | Returns a `Promise` with the destination file paths. 51 | 52 | #### source 53 | 54 | Type: `string | string[]` 55 | 56 | Files to copy. 57 | 58 | If any of the files do not exist, an error will be thrown (does not apply to globs). 59 | 60 | #### destination 61 | 62 | Type: `string` 63 | 64 | Destination directory. 65 | 66 | #### options 67 | 68 | Type: `object` 69 | 70 | Options are passed to [globby](https://github.com/sindresorhus/globby#options). 71 | 72 | In addition, you can specify the below options. 73 | 74 | ##### cwd 75 | 76 | Type: `string`\ 77 | Default: `process.cwd()` 78 | 79 | Working directory to find source files. 80 | 81 | ##### overwrite 82 | 83 | Type: `boolean`\ 84 | Default: `true` 85 | 86 | Overwrite existing files. 87 | 88 | ##### flat 89 | 90 | Type: `boolean`\ 91 | Default: `false` 92 | 93 | Flatten directory structure. All copied files will be put in the same directory. 94 | 95 | ```js 96 | import cpy from 'cpy'; 97 | 98 | await cpy('src/**/*.js', 'destination', { 99 | flat: true 100 | }); 101 | ``` 102 | 103 | ##### rename 104 | 105 | Type: `string | Function` 106 | 107 | Filename or function returning a filename used to rename every file in `source`. 108 | 109 | ```js 110 | import cpy from 'cpy'; 111 | 112 | await cpy('foo.js', 'destination', { 113 | // The `basename` is the filename with extension. 114 | rename: basename => `prefix-${basename}` 115 | }); 116 | 117 | await cpy('foo.js', 'destination', { 118 | rename: 'new-name' 119 | }); 120 | ``` 121 | 122 | ##### concurrency 123 | 124 | Type: `number`\ 125 | Default: `(os.cpus().length || 1) * 2` 126 | 127 | Number of files being copied concurrently. 128 | 129 | ##### ignoreJunk 130 | 131 | Type: `boolean`\ 132 | Default: `true` 133 | 134 | Ignores [junk](https://github.com/sindresorhus/junk) files. 135 | 136 | ##### filter 137 | 138 | Type: `Function` 139 | 140 | Function to filter files to copy. 141 | 142 | Receives a source file object as the first argument. 143 | 144 | Return true to include, false to exclude. You can also return a Promise that resolves to true or false. 145 | 146 | ```js 147 | import cpy from 'cpy'; 148 | 149 | await cpy('foo', 'destination', { 150 | filter: file => file.extension !== 'nocopy' 151 | }); 152 | ``` 153 | 154 | ##### Source file object 155 | 156 | ###### path 157 | 158 | Type: `string`\ 159 | Example: `'/tmp/dir/foo.js'` 160 | 161 | Resolved path to the file. 162 | 163 | ###### relativePath 164 | 165 | Type: `string`\ 166 | Example: `'dir/foo.js'` if `cwd` was `'/tmp'` 167 | 168 | Relative path to the file from `cwd`. 169 | 170 | ###### name 171 | 172 | Type: `string`\ 173 | Example: `'foo.js'` 174 | 175 | Filename with extension. 176 | 177 | ###### nameWithoutExtension 178 | 179 | Type: `string`\ 180 | Example: `'foo'` 181 | 182 | Filename without extension. 183 | 184 | ###### extension 185 | 186 | Type: `string`\ 187 | Example: `'js'` 188 | 189 | File extension. 190 | 191 | ## Progress reporting 192 | 193 | ### cpy.on('progress', handler) 194 | 195 | #### handler(progress) 196 | 197 | Type: `Function` 198 | 199 | ##### progress 200 | 201 | ```js 202 | { 203 | completedFiles: number, 204 | totalFiles: number, 205 | completedSize: number, 206 | percent: number, 207 | sourcePath: string, 208 | destinationPath: string, 209 | } 210 | ``` 211 | 212 | - `completedSize` is in bytes 213 | - `percent` is a value between `0` and `1` 214 | - `sourcePath` is the absolute source path of the current file being copied. 215 | - `destinationPath` is The absolute destination path of the current file being copied. 216 | 217 | Note that the `.on()` method is available only right after the initial `cpy` call, so make sure you add a `handler` before awaiting the promise: 218 | 219 | ```js 220 | import cpy from 'cpy'; 221 | 222 | await cpy(source, destination).on('progress', progress => { 223 | // … 224 | }); 225 | ``` 226 | 227 | ## Related 228 | 229 | - [cpy-cli](https://github.com/sindresorhus/cpy-cli) - CLI for this module 230 | - [copy-file](https://github.com/sindresorhus/copy-file) - Copy a single file 231 | - [move-file](https://github.com/sindresorhus/move-file) - Move a file 232 | - [make-dir](https://github.com/sindresorhus/make-dir) - Make a directory and its parents if needed 233 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import path from 'node:path'; 3 | import fs from 'node:fs'; 4 | import crypto from 'node:crypto'; 5 | import {rimrafSync} from 'rimraf'; 6 | import test from 'ava'; 7 | import {temporaryFile, temporaryDirectory} from 'tempy'; 8 | import proxyquire from 'proxyquire'; 9 | import CpyError from './cpy-error.js'; 10 | import cpy from './index.js'; 11 | 12 | const read = (...arguments_) => fs.readFileSync(path.join(...arguments_), 'utf8'); 13 | 14 | const cpyMockedError = module => proxyquire('.', { 15 | [module]() { 16 | throw new Error(`${module}:\tERROR`); 17 | }, 18 | }); 19 | 20 | test.beforeEach(t => { 21 | t.context.tmp = temporaryFile(); 22 | t.context.dir = temporaryDirectory(); 23 | }); 24 | 25 | test.afterEach(t => { 26 | rimrafSync(t.context.tmp); 27 | rimrafSync(t.context.dir); 28 | }); 29 | 30 | test('reject Errors on missing `source`', async t => { 31 | await t.throwsAsync(cpy, {message: /`source`/, instanceOf: CpyError}); 32 | 33 | await t.throwsAsync(cpy(null, 'destination'), {message: /`source`/, instanceOf: CpyError}); 34 | 35 | await t.throwsAsync(cpy([], 'destination'), {message: /`source`/, instanceOf: CpyError}); 36 | }); 37 | 38 | test('reject Errors on missing `destination`', async t => { 39 | await t.throwsAsync(cpy('TARGET'), {message: /`destination`/, instanceOf: CpyError}); 40 | }); 41 | 42 | test('copy single file', async t => { 43 | await cpy('license', t.context.tmp); 44 | 45 | t.is(read('license'), read(t.context.tmp, 'license')); 46 | }); 47 | 48 | test('copy array of files', async t => { 49 | await cpy(['license', 'package.json'], t.context.tmp); 50 | 51 | t.is(read('license'), read(t.context.tmp, 'license')); 52 | t.is(read('package.json'), read(t.context.tmp, 'package.json')); 53 | }); 54 | 55 | test('throws on invalid concurrency value', async t => { 56 | await t.throwsAsync( 57 | cpy(['license', 'package.json'], t.context.tmp, {concurrency: -2}), 58 | ); 59 | await t.throwsAsync( 60 | cpy(['license', 'package.json'], t.context.tmp, {concurrency: 'foo'}), 61 | ); 62 | }); 63 | 64 | test('copy array of files with filter', async t => { 65 | await cpy(['license', 'package.json'], t.context.tmp, { 66 | filter(file) { 67 | if (file.path.endsWith('license')) { 68 | t.is(file.path, path.join(process.cwd(), 'license')); 69 | t.is(file.name, 'license'); 70 | t.is(file.nameWithoutExtension, 'license'); 71 | t.is(file.extension, ''); 72 | } else if (file.path.endsWith('package.json')) { 73 | t.is(file.path, path.join(process.cwd(), 'package.json')); 74 | t.is(file.name, 'package.json'); 75 | t.is(file.nameWithoutExtension, 'package'); 76 | t.is(file.extension, 'json'); 77 | } 78 | 79 | return !file.path.endsWith('license'); 80 | }, 81 | }); 82 | 83 | t.false(fs.existsSync(path.join(t.context.tmp, 'license'))); 84 | t.is(read('package.json'), read(t.context.tmp, 'package.json')); 85 | }); 86 | 87 | test('copy array of files with async filter', async t => { 88 | await cpy(['license', 'package.json'], t.context.tmp, { 89 | async filter(file) { 90 | if (file.path.endsWith(`${path.sep}license`)) { 91 | t.is(file.path, path.join(process.cwd(), 'license')); 92 | t.is(file.name, 'license'); 93 | t.is(file.nameWithoutExtension, 'license'); 94 | t.is(file.extension, ''); 95 | } else if (file.path.endsWith(`${path.sep}package.json`)) { 96 | t.is(file.path, path.join(process.cwd(), 'package.json')); 97 | t.is(file.name, 'package.json'); 98 | t.is(file.nameWithoutExtension, 'package'); 99 | t.is(file.extension, 'json'); 100 | } 101 | 102 | return !file.path.endsWith(`${path.sep}license`); 103 | }, 104 | }); 105 | 106 | t.false(fs.existsSync(path.join(t.context.tmp, 'license'))); 107 | t.is(read('package.json'), read(t.context.tmp, 'package.json')); 108 | }); 109 | 110 | test('cwd', async t => { 111 | fs.mkdirSync(t.context.tmp); 112 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 113 | fs.writeFileSync( 114 | path.join(t.context.tmp, 'cwd/hello.js'), 115 | 'console.log("hello");', 116 | ); 117 | 118 | await cpy(['hello.js'], 'destination', { 119 | cwd: path.join(t.context.tmp, 'cwd'), 120 | }); 121 | 122 | t.is( 123 | read(t.context.tmp, 'cwd/hello.js'), 124 | read(t.context.tmp, 'cwd/destination/hello.js'), 125 | ); 126 | }); 127 | 128 | test('do not overwrite', async t => { 129 | fs.mkdirSync(t.context.tmp); 130 | fs.writeFileSync(path.join(t.context.tmp, 'license'), ''); 131 | 132 | await t.throwsAsync(cpy(['license'], t.context.tmp, {overwrite: false})); 133 | 134 | t.is(read(t.context.tmp, 'license'), ''); 135 | }); 136 | 137 | test('do not keep path structure', async t => { 138 | fs.mkdirSync(t.context.tmp); 139 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 140 | fs.writeFileSync( 141 | path.join(t.context.tmp, 'cwd/hello.js'), 142 | 'console.log("hello");', 143 | ); 144 | 145 | await cpy([path.join(t.context.tmp, 'cwd/hello.js')], t.context.tmp); 146 | 147 | t.is(read(t.context.tmp, 'cwd/hello.js'), read(t.context.tmp, 'hello.js')); 148 | }); 149 | 150 | test('path structure', async t => { 151 | fs.mkdirSync(t.context.tmp); 152 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 153 | fs.mkdirSync(path.join(t.context.tmp, 'out')); 154 | fs.writeFileSync( 155 | path.join(t.context.tmp, 'cwd/hello.js'), 156 | 'console.log("hello");', 157 | ); 158 | 159 | await cpy([path.join(t.context.tmp, '**')], path.join(t.context.tmp, 'out')); 160 | 161 | t.is( 162 | read(t.context.tmp, 'cwd/hello.js'), 163 | read(t.context.tmp, 'out', 'cwd/hello.js'), 164 | ); 165 | }); 166 | 167 | test('rename filenames but not filepaths', async t => { 168 | fs.mkdirSync(t.context.tmp); 169 | fs.mkdirSync(path.join(t.context.tmp, 'source')); 170 | fs.writeFileSync( 171 | path.join(t.context.tmp, 'hello.js'), 172 | 'console.log("hello");', 173 | ); 174 | fs.writeFileSync( 175 | path.join(t.context.tmp, 'source/hello.js'), 176 | 'console.log("hello");', 177 | ); 178 | 179 | await cpy(['hello.js', 'source/hello.js'], 'destination/subdir', { 180 | cwd: t.context.tmp, 181 | rename: 'hi.js', 182 | }); 183 | 184 | t.is( 185 | read(t.context.tmp, 'hello.js'), 186 | read(t.context.tmp, 'destination/subdir/hi.js'), 187 | ); 188 | t.is( 189 | read(t.context.tmp, 'source/hello.js'), 190 | read(t.context.tmp, 'destination/subdir/source/hi.js'), 191 | ); 192 | }); 193 | 194 | test('rename filenames using a function', async t => { 195 | fs.mkdirSync(t.context.tmp); 196 | fs.mkdirSync(path.join(t.context.tmp, 'source')); 197 | fs.writeFileSync(path.join(t.context.tmp, 'foo.js'), 'console.log("foo");'); 198 | fs.writeFileSync( 199 | path.join(t.context.tmp, 'source/bar.js'), 200 | 'console.log("bar");', 201 | ); 202 | 203 | await cpy(['foo.js', 'source/bar.js'], 'destination/subdir', { 204 | cwd: t.context.tmp, 205 | rename: basename => `prefix-${basename}`, 206 | }); 207 | 208 | t.is( 209 | read(t.context.tmp, 'foo.js'), 210 | read(t.context.tmp, 'destination/subdir/prefix-foo.js'), 211 | ); 212 | t.is( 213 | read(t.context.tmp, 'source/bar.js'), 214 | read(t.context.tmp, 'destination/subdir/source/prefix-bar.js'), 215 | ); 216 | }); 217 | 218 | test('rename function receives the basename argument with the file extension', async t => { 219 | fs.mkdirSync(t.context.tmp); 220 | fs.writeFileSync(path.join(t.context.tmp, 'foo.js'), ''); 221 | fs.writeFileSync(path.join(t.context.tmp, 'foo.ts'), ''); 222 | 223 | const visited = []; 224 | await cpy(['foo.js', 'foo.ts'], 'destination/subdir', { 225 | cwd: t.context.tmp, 226 | rename(basename) { 227 | visited.push(basename); 228 | return basename; 229 | }, 230 | }); 231 | 232 | t.is(visited.length, 2); 233 | t.true(visited.includes('foo.js')); 234 | t.true(visited.includes('foo.ts')); 235 | }); 236 | 237 | test('flatten directory tree', async t => { 238 | fs.mkdirSync(t.context.tmp); 239 | fs.mkdirSync(path.join(t.context.tmp, 'source')); 240 | fs.mkdirSync(path.join(t.context.tmp, 'source', 'nested')); 241 | fs.writeFileSync(path.join(t.context.tmp, 'foo.js'), 'console.log("foo");'); 242 | fs.writeFileSync( 243 | path.join(t.context.tmp, 'source/bar.js'), 244 | 'console.log("bar");', 245 | ); 246 | fs.writeFileSync( 247 | path.join(t.context.tmp, 'source/nested/baz.ts'), 248 | 'console.log("baz");', 249 | ); 250 | 251 | await cpy('**/*.js', 'destination/subdir', { 252 | cwd: t.context.tmp, 253 | flat: true, 254 | }); 255 | 256 | t.is( 257 | read(t.context.tmp, 'foo.js'), 258 | read(t.context.tmp, 'destination/subdir/foo.js'), 259 | ); 260 | t.is( 261 | read(t.context.tmp, 'source/bar.js'), 262 | read(t.context.tmp, 'destination/subdir/bar.js'), 263 | ); 264 | t.falsy( 265 | fs.existsSync(path.join(t.context.tmp, 'destination/subdir/baz.ts')), 266 | ); 267 | }); 268 | 269 | test('flatten single file', async t => { 270 | fs.mkdirSync(t.context.tmp); 271 | fs.mkdirSync(path.join(t.context.tmp, 'source')); 272 | fs.writeFileSync( 273 | path.join(t.context.tmp, 'source/bar.js'), 274 | 'console.log("bar");', 275 | ); 276 | 277 | await cpy('source/bar.js', 'destination', { 278 | cwd: t.context.tmp, 279 | flat: true, 280 | }); 281 | 282 | t.is( 283 | read(t.context.tmp, 'source/bar.js'), 284 | read(t.context.tmp, 'destination/bar.js'), 285 | ); 286 | }); 287 | 288 | // TODO: Enable again when ESM supports mocking. 289 | // eslint-disable-next-line ava/no-skip-test 290 | test.skip('copy-file errors are CpyErrors', async t => { 291 | const cpy = cpyMockedError('copy-file'); 292 | await t.throwsAsync(cpy('license', t.context.dir), {message: /copy-file/, instanceOf: CpyError}); 293 | }); 294 | 295 | test('throws on non-existing file', async t => { 296 | fs.mkdirSync(t.context.tmp); 297 | 298 | await t.throwsAsync(cpy(['no-file'], t.context.tmp), { 299 | instanceOf: CpyError, 300 | }); 301 | }); 302 | 303 | test('throws on multiple non-existing files', async t => { 304 | fs.mkdirSync(t.context.tmp); 305 | 306 | await t.throwsAsync(cpy(['no-file1', 'no-file2'], t.context.tmp), { 307 | instanceOf: CpyError, 308 | }); 309 | }); 310 | 311 | test('does not throw when not matching any file on glob pattern', async t => { 312 | fs.mkdirSync(t.context.tmp); 313 | 314 | await t.notThrowsAsync(cpy(['*.nonexistent'], t.context.tmp)); 315 | }); 316 | 317 | test('junk files are ignored', async t => { 318 | fs.mkdirSync(t.context.tmp); 319 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 320 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/Thumbs.db'), 'lorem ipsum'); 321 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/foo'), 'lorem ipsum'); 322 | 323 | let report; 324 | 325 | await cpy('*', t.context.tmp, { 326 | cwd: path.join(t.context.tmp, 'cwd'), 327 | ignoreJunk: true, 328 | }).on('progress', event => { 329 | report = event; 330 | }); 331 | 332 | t.not(report, undefined); 333 | t.is(report.totalFiles, 1); 334 | t.is(report.completedFiles, 1); 335 | t.is(report.completedSize, 11); 336 | t.is(report.percent, 1); 337 | t.is(read(report.sourcePath), read(t.context.tmp + '/cwd/foo')); 338 | t.is(read(report.destinationPath), read(t.context.tmp + '/cwd/foo')); 339 | }); 340 | 341 | test('junk files are copied', async t => { 342 | fs.mkdirSync(t.context.tmp); 343 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 344 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/Thumbs.db'), 'lorem ipsum'); 345 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/foo'), 'lorem ipsum'); 346 | 347 | let report; 348 | 349 | await cpy('*', t.context.tmp, { 350 | cwd: path.join(t.context.tmp, 'cwd'), 351 | ignoreJunk: false, 352 | }).on('progress', event => { 353 | report = event; 354 | }); 355 | 356 | t.not(report, undefined); 357 | t.is(report.totalFiles, 2); 358 | t.is(report.completedFiles, 2); 359 | t.is(report.completedSize, 22); 360 | t.is(report.percent, 1); 361 | t.is(read(report.sourcePath), read(t.context.tmp + '/cwd/foo')); 362 | t.is(read(report.destinationPath), read(t.context.tmp + '/cwd/foo')); 363 | }); 364 | 365 | test('nested junk files are ignored', async t => { 366 | fs.mkdirSync(t.context.tmp); 367 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 368 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/Thumbs.db'), 'lorem ispum'); 369 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/test'), 'lorem ispum'); 370 | 371 | let report; 372 | 373 | await cpy(['cwd/*'], t.context.tmp, { 374 | cwd: t.context.tmp, 375 | ignoreJunk: true, 376 | }).on('progress', event => { 377 | report = event; 378 | }); 379 | 380 | t.not(report, undefined); 381 | t.is(report.totalFiles, 1); 382 | t.is(report.completedFiles, 1); 383 | t.is(report.completedSize, 11); 384 | t.is(report.percent, 1); 385 | t.is(read(report.sourcePath), read(t.context.tmp + '/cwd/test')); 386 | t.is(read(report.destinationPath), read(t.context.tmp + '/test')); 387 | }); 388 | 389 | test('reports copy progress of single file', async t => { 390 | fs.mkdirSync(t.context.tmp); 391 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 392 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/foo'), 'lorem ipsum'); 393 | 394 | let report; 395 | 396 | await cpy(['foo'], t.context.tmp, { 397 | cwd: path.join(t.context.tmp, 'cwd'), 398 | }).on('progress', event => { 399 | report = event; 400 | }); 401 | 402 | t.not(report, undefined); 403 | t.is(report.totalFiles, 1); 404 | t.is(report.completedFiles, 1); 405 | t.is(report.completedSize, 11); 406 | t.is(report.percent, 1); 407 | t.is(read(report.sourcePath), read(t.context.tmp + '/cwd/foo')); 408 | t.is(read(report.destinationPath), read(t.context.tmp + '/foo')); 409 | }); 410 | 411 | test('reports copy progress of multiple files', async t => { 412 | fs.mkdirSync(t.context.tmp); 413 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 414 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/foo'), 'lorem ipsum'); 415 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/bar'), 'dolor sit amet'); 416 | 417 | let report; 418 | 419 | await cpy(['foo', 'bar'], t.context.tmp, { 420 | cwd: path.join(t.context.tmp, 'cwd'), 421 | }).on('progress', event => { 422 | report = event; 423 | }); 424 | 425 | t.not(report, undefined); 426 | t.is(report.totalFiles, 2); 427 | t.is(report.completedFiles, 2); 428 | t.is(report.completedSize, 25); 429 | t.is(report.percent, 1); 430 | t.is(read(report.sourcePath), read(t.context.tmp + '/cwd/bar')); 431 | t.is(read(report.destinationPath), read(t.context.tmp + '/bar')); 432 | }); 433 | 434 | test('reports correct completedSize', async t => { 435 | const ONE_MEGABYTE = (1 * 1024 * 1024) + 1; 436 | const buf = crypto.randomBytes(ONE_MEGABYTE); 437 | 438 | fs.mkdirSync(t.context.tmp); 439 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 440 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/fatfile'), buf); 441 | 442 | let report; 443 | let chunkCount = 0; 444 | 445 | await cpy(['fatfile'], t.context.tmp, { 446 | cwd: path.join(t.context.tmp, 'cwd'), 447 | }).on('progress', event => { 448 | chunkCount++; 449 | report = event; 450 | }); 451 | 452 | t.not(report, undefined); 453 | t.is(report.totalFiles, 1); 454 | t.is(report.completedFiles, 1); 455 | t.is(report.completedSize, ONE_MEGABYTE); 456 | t.is(read(report.sourcePath), read(t.context.tmp, 'cwd/fatfile')); 457 | t.is(read(report.destinationPath), read(t.context.tmp, 'fatfile')); 458 | t.true(chunkCount > 1); 459 | t.is(report.percent, 1); 460 | }); 461 | 462 | test('returns the event emitter on early rejection', t => { 463 | const rejectedPromise = cpy(null, null); 464 | t.is(typeof rejectedPromise.on, 'function'); 465 | rejectedPromise.catch(() => {}); // eslint-disable-line promise/prefer-await-to-then 466 | }); 467 | 468 | test('returns destination path', async t => { 469 | fs.mkdirSync(t.context.tmp); 470 | fs.mkdirSync(path.join(t.context.tmp, 'cwd')); 471 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/foo'), 'lorem ipsum'); 472 | fs.writeFileSync(path.join(t.context.tmp, 'cwd/bar'), 'dolor sit amet'); 473 | 474 | const to = await cpy(['foo', 'bar'], t.context.tmp, { 475 | cwd: path.join(t.context.tmp, 'cwd'), 476 | }); 477 | 478 | t.deepEqual(to, [ 479 | path.join(t.context.tmp, 'foo'), 480 | path.join(t.context.tmp, 'bar'), 481 | ]); 482 | }); 483 | --------------------------------------------------------------------------------