├── .gitignore ├── test ├── fixtures │ └── source │ │ ├── junk │ │ ├── a │ │ ├── b │ │ ├── Thumbs.db │ │ └── npm-debug.log │ │ ├── symlink │ │ ├── directory │ │ ├── a │ │ ├── b │ │ └── c │ │ ├── dotfiles │ │ ├── a │ │ ├── b │ │ ├── .a │ │ └── .b │ │ ├── empty │ │ └── .gitignore │ │ ├── file │ │ ├── file-symlink │ │ ├── nested-directory │ │ ├── 1 │ │ │ ├── 1-a │ │ │ ├── 1-b │ │ │ ├── 1-1 │ │ │ │ ├── 1-1-a │ │ │ │ └── 1-1-b │ │ │ └── 1-2 │ │ │ │ ├── 1-2-a │ │ │ │ └── 1-2-b │ │ ├── 2 │ │ │ ├── 2-a │ │ │ ├── 2-b │ │ │ ├── 2-1 │ │ │ │ ├── 2-1-a │ │ │ │ └── 2-1-b │ │ │ └── 2-2 │ │ │ │ ├── 2-2-a │ │ │ │ └── 2-2-b │ │ ├── a │ │ └── b │ │ ├── nested-symlinks │ │ ├── file │ │ ├── directory │ │ └── nested │ │ │ └── directory │ │ ├── directory-symlink │ │ ├── nested-file │ │ └── file │ │ └── executable ├── mocha.opts ├── .eslintrc └── spec │ ├── index.spec.js │ └── copy.spec.js ├── .eslintignore ├── index.js ├── lib ├── slash.js ├── maximatch.js └── copy.js ├── .editorconfig ├── .eslintrc ├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── package.json ├── index.test-d.ts ├── index.d.ts └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /test/fixtures/source/junk/a: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/junk/b: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/symlink: -------------------------------------------------------------------------------- 1 | . -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures/**/* 2 | -------------------------------------------------------------------------------- /test/fixtures/source/directory/a: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/directory/b: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/directory/c: -------------------------------------------------------------------------------- 1 | c 2 | -------------------------------------------------------------------------------- /test/fixtures/source/dotfiles/a: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/dotfiles/b: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/empty/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixtures/source/dotfiles/.a: -------------------------------------------------------------------------------- 1 | .a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/dotfiles/.b: -------------------------------------------------------------------------------- 1 | .b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/file: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /test/fixtures/source/file-symlink: -------------------------------------------------------------------------------- 1 | ./file -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/a: -------------------------------------------------------------------------------- 1 | a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/b: -------------------------------------------------------------------------------- 1 | b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-symlinks/file: -------------------------------------------------------------------------------- 1 | ../file -------------------------------------------------------------------------------- /test/fixtures/source/directory-symlink: -------------------------------------------------------------------------------- 1 | ./directory -------------------------------------------------------------------------------- /test/fixtures/source/junk/Thumbs.db: -------------------------------------------------------------------------------- 1 | Thumbs.db 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/1/1-a: -------------------------------------------------------------------------------- 1 | 1-a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/1/1-b: -------------------------------------------------------------------------------- 1 | 1-b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/2/2-a: -------------------------------------------------------------------------------- 1 | 2-a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/2/2-b: -------------------------------------------------------------------------------- 1 | 2-b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/junk/npm-debug.log: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-file/file: -------------------------------------------------------------------------------- 1 | Hello, world! 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-symlinks/directory: -------------------------------------------------------------------------------- 1 | ../directory -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/1/1-1/1-1-a: -------------------------------------------------------------------------------- 1 | 1-1-a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/1/1-1/1-1-b: -------------------------------------------------------------------------------- 1 | 1-1-b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/1/1-2/1-2-a: -------------------------------------------------------------------------------- 1 | 1-2-a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/1/1-2/1-2-b: -------------------------------------------------------------------------------- 1 | 1-2-b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/2/2-1/2-1-a: -------------------------------------------------------------------------------- 1 | 2-1-a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/2/2-1/2-1-b: -------------------------------------------------------------------------------- 1 | 2-1-b 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/2/2-2/2-2-a: -------------------------------------------------------------------------------- 1 | 2-2-a 2 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-directory/2/2-2/2-2-b: -------------------------------------------------------------------------------- 1 | 2-2-b 2 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | ./test/**/*.spec.js 2 | --timeout 10000 3 | -------------------------------------------------------------------------------- /test/fixtures/source/nested-symlinks/nested/directory: -------------------------------------------------------------------------------- 1 | ../../directory -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./lib/copy'); 4 | -------------------------------------------------------------------------------- /test/fixtures/source/executable: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | echo "Hello, world!" 3 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "no-unused-expressions": "off" 4 | }, 5 | "env": { 6 | "mocha": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/slash.js: -------------------------------------------------------------------------------- 1 | // via https://github.com/sindresorhus/slash/blob/main/index.js (MIT license) 2 | module.exports = function slash(path) { 3 | const isExtendedLengthPath = path.startsWith('\\\\?\\'); 4 | 5 | if (isExtendedLengthPath) { 6 | return path; 7 | } 8 | 9 | return path.replace(/\\/g, '/'); 10 | } 11 | -------------------------------------------------------------------------------- /test/spec/index.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | var expect = chai.expect; 5 | 6 | describe('recursive-copy', function() { 7 | var copy; 8 | before(function() { 9 | copy = require('../..'); 10 | }); 11 | 12 | it('Should export a function', function() { 13 | expect(copy).to.be.a('function'); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | # Apply for all files 8 | [*] 9 | 10 | charset = utf-8 11 | 12 | indent_style = tab 13 | indent_size = 2 14 | 15 | end_of_line = lf 16 | insert_final_newline = true 17 | trim_trailing_whitespace = true 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "eslint:recommended" 5 | ], 6 | "rules": { 7 | "quotes": ["error", "single"], 8 | "dot-notation": "off", 9 | "no-underscore-dangle": "off", 10 | "no-shadow": "off", 11 | "no-use-before-define": ["error", "nofunc"], 12 | "no-unused-vars": ["error", { 13 | "vars": "all", 14 | "args": "none" 15 | }], 16 | "consistent-return": "off", 17 | "no-console": "error" 18 | }, 19 | "env": { 20 | "node": true 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Node Unit Tests 2 | on: 3 | push: 4 | branches-ignore: 5 | - "gh-pages" 6 | jobs: 7 | build: 8 | runs-on: ${{ matrix.os }} 9 | strategy: 10 | matrix: 11 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 12 | node: ["18", "20", "22", "24"] 13 | name: Node.js ${{ matrix.node }} on ${{ matrix.os }} 14 | steps: 15 | - run: git config --global core.autocrlf input 16 | - uses: actions/checkout@v4 17 | - name: Setup node 18 | uses: actions/setup-node@v4 19 | with: 20 | node-version: ${{ matrix.node }} 21 | # cache: npm 22 | - run: npm install 23 | - run: npm test 24 | env: 25 | YARN_GPG: no 26 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Release to npm 2 | on: 3 | release: 4 | types: [published] 5 | permissions: read-all 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | environment: GitHub Publish 10 | permissions: 11 | contents: read 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # 4.1.7 15 | - uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # 4.0.3 16 | with: 17 | node-version: "20" 18 | registry-url: 'https://registry.npmjs.org' 19 | - run: npm install -g npm@latest 20 | - run: npm ci 21 | - run: npm test 22 | - if: ${{ github.event.release.tag_name != '' && env.NPM_PUBLISH_TAG != '' }} 23 | run: npm publish --provenance --access=public --tag=${{ env.NPM_PUBLISH_TAG }} 24 | env: 25 | NPM_PUBLISH_TAG: ${{ contains(github.event.release.tag_name, '-beta.') && 'beta' || 'latest' }} 26 | -------------------------------------------------------------------------------- /lib/maximatch.js: -------------------------------------------------------------------------------- 1 | var minimatch = require("minimatch"); 2 | 3 | // via https://github.com/sindresorhus/array-union (MIT license) 4 | function arrayUnion(...args) { 5 | return [...new Set(args.flat())]; 6 | } 7 | 8 | // via https://github.com/sindresorhus/array-differ/blob/main/index.js (MIT license) 9 | function arrayDiffer(array, ...values) { 10 | const rest = new Set(values.flat()); 11 | return array.filter(element => !rest.has(element)); 12 | } 13 | 14 | // via https://github.com/sindresorhus/arrify/blob/main/index.js (MIT license) 15 | function arrify(value) { 16 | if (value === null || value === undefined) { 17 | return []; 18 | } 19 | 20 | if (Array.isArray(value)) { 21 | return value; 22 | } 23 | 24 | if (typeof value === 'string') { 25 | return [value]; 26 | } 27 | 28 | if (typeof value[Symbol.iterator] === 'function') { 29 | return [...value]; 30 | } 31 | 32 | return [value]; 33 | } 34 | 35 | // via https://www.npmjs.com/package/maximatch (MIT license) 36 | module.exports = function (list, patterns, options) { 37 | list = arrify(list); 38 | 39 | patterns = arrify(patterns); 40 | 41 | if (list.length === 0 || patterns.length === 0) { 42 | return []; 43 | } 44 | 45 | options = options || {}; 46 | 47 | return patterns.reduce(function (ret, pattern) { 48 | if (typeof pattern === "function") { 49 | return arrayUnion(ret, list.filter(pattern)); 50 | } else if (pattern instanceof RegExp) { 51 | return arrayUnion( 52 | ret, 53 | list.filter(function (item) { 54 | return pattern.test(item); 55 | }), 56 | ); 57 | } else { 58 | var process = arrayUnion; 59 | 60 | if (pattern[0] === "!") { 61 | pattern = pattern.slice(1); 62 | 63 | process = arrayDiffer; 64 | } 65 | 66 | return process(ret, minimatch.match(list, pattern, options)); 67 | } 68 | }, []); 69 | }; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@11ty/recursive-copy", 3 | "version": "5.0.0", 4 | "description": "A fork of `recursive-copy`: Simple, flexible file copy utility", 5 | "engines": { 6 | "node": ">=20" 7 | }, 8 | "main": "index.js", 9 | "types": "index.d.ts", 10 | "directories": { 11 | "lib": "lib", 12 | "test": "test" 13 | }, 14 | "files": [ 15 | "index.js", 16 | "index.d.ts", 17 | "lib" 18 | ], 19 | "scripts": { 20 | "test": "npm run test:lint && npm run test:mocha && if-node-version '>=10' npm run test:typings", 21 | "test:lint": "if-node-version '>=4' eslint index.js test", 22 | "test:mocha": "mocha --reporter spec", 23 | "test:typings": "tsd && echo 'TypeScript definitions are valid'", 24 | "prepublishOnly": "npm run test" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/11ty/recursive-copy.git" 29 | }, 30 | "keywords": [ 31 | "copy", 32 | "recursive", 33 | "file", 34 | "directory", 35 | "folder", 36 | "symlink", 37 | "fs", 38 | "rename", 39 | "filter", 40 | "transform", 41 | "glob", 42 | "regex", 43 | "regexp" 44 | ], 45 | "author": "Tim Kendrick ", 46 | "contributors": [ 47 | "Zach Leatherman (https://zachleat.com/)" 48 | ], 49 | "license": "ISC", 50 | "bugs": { 51 | "url": "https://github.com/11ty/recursive-copy/issues" 52 | }, 53 | "homepage": "https://github.com/11ty/recursive-copy", 54 | "dependencies": { 55 | "errno": "^1.0.0", 56 | "junk": "^3.1.0", 57 | "minimatch": "^10.1.1" 58 | }, 59 | "devDependencies": { 60 | "@types/node": "^14.18.63", 61 | "chai": "^3.5.0", 62 | "chai-as-promised": "^5.3.0", 63 | "eslint": "^2.13.1", 64 | "if-node-version": "^1.1.1", 65 | "mocha": "^2.5.3", 66 | "read-dir-files": "^0.1.1", 67 | "rewire": "^2.5.2", 68 | "through2": "^2.0.5", 69 | "tsd": "0.31.2" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /index.test-d.ts: -------------------------------------------------------------------------------- 1 | import copy, { CopyErrorInfo, CopyEventType, CopyOperation } from '.'; 2 | import { Stream } from 'stream'; 3 | import { expectError, expectType } from 'tsd'; 4 | 5 | // Promise interface 6 | copy('source', 'dest') 7 | .on(copy.events.ERROR, (error, info) => {}) 8 | .on(copy.events.COMPLETE, (info) => {}) 9 | .on(copy.events.CREATE_DIRECTORY_START, (info) => {}) 10 | .on(copy.events.CREATE_DIRECTORY_ERROR, (error, info) => {}) 11 | .on(copy.events.CREATE_DIRECTORY_COMPLETE, (info) => {}) 12 | .on(copy.events.CREATE_SYMLINK_START, (info) => {}) 13 | .on(copy.events.CREATE_SYMLINK_ERROR, (error, info) => {}) 14 | .on(copy.events.CREATE_SYMLINK_COMPLETE, (info) => {}) 15 | .on(copy.events.COPY_FILE_START, (info) => {}) 16 | .on(copy.events.COPY_FILE_ERROR, (error, info) => {}) 17 | .on(copy.events.COPY_FILE_COMPLETE, (info) => {}) 18 | .then(() => {}) 19 | .catch(e => {}); 20 | 21 | // Callback interface 22 | copy('source', 'dest', (error, results) => {}) 23 | .on(copy.events.ERROR, (error, info) => {}) 24 | .on(copy.events.COMPLETE, (info) => {}) 25 | .on(copy.events.CREATE_DIRECTORY_START, (info) => {}) 26 | .on(copy.events.CREATE_DIRECTORY_ERROR, (error, info) => {}) 27 | .on(copy.events.CREATE_DIRECTORY_COMPLETE, (info) => {}) 28 | .on(copy.events.CREATE_SYMLINK_START, (info) => {}) 29 | .on(copy.events.CREATE_SYMLINK_ERROR, (error, info) => {}) 30 | .on(copy.events.CREATE_SYMLINK_COMPLETE, (info) => {}) 31 | .on(copy.events.COPY_FILE_START, (info) => {}) 32 | .on(copy.events.COPY_FILE_ERROR, (error, info) => {}) 33 | .on(copy.events.COPY_FILE_COMPLETE, (info) => {}); 34 | 35 | // Prevent specifying both callback and promise interfaces 36 | expectError(copy('source', 'dest', (error, results) => {}).then(() => {})); 37 | 38 | // All options should be optional. 39 | copy('source', 'dest', {}) 40 | .then(() => {}) 41 | .catch(e => {}); 42 | 43 | copy('source', 'dest', { 44 | overwrite: true, 45 | expand: true, 46 | dot: true, 47 | junk: true, 48 | rename: (path: string) => 'abc/' + path, 49 | transform: (src: string, dest: string, stats) => { 50 | if (stats.isDirectory()) { 51 | return new Stream(); 52 | } else { 53 | return new Stream(); 54 | } 55 | }, 56 | results: true, 57 | concurrency: 123, 58 | debug: true, 59 | }) 60 | .then(() => {}) 61 | .catch(e => {}); 62 | 63 | // Test each 'filter' type. 64 | copy('source', 'dest', {filter: 'abc'}); 65 | copy('source', 'dest', {filter: /abc/}); 66 | copy('source', 'dest', {filter: ['abc', 'def']}); 67 | copy('source', 'dest', {filter: (path) => false}); 68 | 69 | expectType>>>(copy('source', 'dest')); 70 | 71 | expectType>(copy('source', 'dest', () => {})); 72 | 73 | type WithCopyEvents = T & { 74 | on(event: CopyEventType.ERROR, callback: (error: Error, info: CopyErrorInfo) => void): WithCopyEvents; 75 | on(event: CopyEventType.COMPLETE, callback: (info: Array) => void): WithCopyEvents; 76 | on(event: CopyEventType.CREATE_DIRECTORY_START, callback: (info: CopyOperation) => void): WithCopyEvents; 77 | on(event: CopyEventType.CREATE_DIRECTORY_ERROR, callback: (error: Error, info: CopyOperation) => void): WithCopyEvents; 78 | on(event: CopyEventType.CREATE_DIRECTORY_COMPLETE, callback: (info: CopyOperation) => void): WithCopyEvents; 79 | on(event: CopyEventType.CREATE_SYMLINK_START, callback: (info: CopyOperation) => void): WithCopyEvents; 80 | on(event: CopyEventType.CREATE_SYMLINK_ERROR, callback: (error: Error, info: CopyOperation) => void): WithCopyEvents; 81 | on(event: CopyEventType.CREATE_SYMLINK_COMPLETE, callback: (info: CopyOperation) => void): WithCopyEvents; 82 | on(event: CopyEventType.COPY_FILE_START, callback: (info: CopyOperation) => void): WithCopyEvents; 83 | on(event: CopyEventType.COPY_FILE_ERROR, callback: (error: Error, info: CopyOperation) => void): WithCopyEvents; 84 | on(event: CopyEventType.COPY_FILE_COMPLETE, callback: (info: CopyOperation) => void): WithCopyEvents; 85 | } 86 | 87 | expectError(copy(123, 'dest')); 88 | expectError(copy('source', 123)); 89 | expectError(copy('source', 'dest', 'options')); 90 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from 'fs'; 2 | import { Stream } from 'stream'; 3 | 4 | interface Options { 5 | /** 6 | * Whether to overwrite destination files. 7 | */ 8 | overwrite?: boolean; 9 | /** 10 | * Whether to expand symbolic links. 11 | */ 12 | expand?: boolean; 13 | /** 14 | * Whether to copy files beginning with a `.` 15 | */ 16 | dot?: boolean; 17 | /** 18 | * Whether to copy OS junk files (e.g. `.DS_Store`, `Thumbs.db`). 19 | */ 20 | junk?: boolean; 21 | /** 22 | * Filter function / regular expression / glob that determines which files to copy (uses maximatch). 23 | */ 24 | filter?: string | string[] | RegExp | ((path: string) => boolean); 25 | /** 26 | * Function that maps source paths to destination paths. 27 | */ 28 | rename?: (path: string) => string; 29 | /** 30 | * Function that returns a transform stream used to modify file contents. 31 | */ 32 | transform?: (src: string, dest: string, stats: Stats) => Stream | null | undefined; 33 | /** 34 | * Whether to return an array of copy results. 35 | * 36 | * Defaults to true. 37 | */ 38 | results?: boolean; 39 | /** 40 | * Maximum number of simultaneous copy operations. 41 | * 42 | * Defaults to 255. 43 | */ 44 | concurrency?: number; 45 | /** 46 | * Whether to log debug information. 47 | */ 48 | debug?: boolean; 49 | } 50 | 51 | interface CopyFn { 52 | ( 53 | source: string, 54 | dest: string, 55 | options?: Options, 56 | ): WithCopyEvents>>; 57 | ( 58 | source: string, 59 | dest: string, 60 | callback: (error: Error | null, results?: Array) => void, 61 | ): WithCopyEvents<{}>; 62 | events: { 63 | ERROR: CopyEventType.ERROR; 64 | COMPLETE: CopyEventType.COMPLETE; 65 | CREATE_DIRECTORY_START: CopyEventType.CREATE_DIRECTORY_START; 66 | CREATE_DIRECTORY_ERROR: CopyEventType.CREATE_DIRECTORY_ERROR; 67 | CREATE_DIRECTORY_COMPLETE: CopyEventType.CREATE_DIRECTORY_COMPLETE; 68 | CREATE_SYMLINK_START: CopyEventType.CREATE_SYMLINK_START; 69 | CREATE_SYMLINK_ERROR: CopyEventType.CREATE_SYMLINK_ERROR; 70 | CREATE_SYMLINK_COMPLETE: CopyEventType.CREATE_SYMLINK_COMPLETE; 71 | COPY_FILE_START: CopyEventType.COPY_FILE_START; 72 | COPY_FILE_ERROR: CopyEventType.COPY_FILE_ERROR; 73 | COPY_FILE_COMPLETE: CopyEventType.COPY_FILE_COMPLETE; 74 | }; 75 | } 76 | 77 | declare const copy: CopyFn; 78 | export default copy; 79 | 80 | export interface CopyErrorInfo { 81 | src: string; 82 | dest: string; 83 | } 84 | 85 | export interface CopyOperation { 86 | src: string; 87 | dest: string; 88 | stats: Stats; 89 | } 90 | 91 | type WithCopyEvents = T & { 92 | on(event: CopyEventType.ERROR, callback: (error: Error, info: CopyErrorInfo) => void): WithCopyEvents; 93 | on(event: CopyEventType.COMPLETE, callback: (info: Array) => void): WithCopyEvents; 94 | on(event: CopyEventType.CREATE_DIRECTORY_START, callback: (info: CopyOperation) => void): WithCopyEvents; 95 | on(event: CopyEventType.CREATE_DIRECTORY_ERROR, callback: (error: Error, info: CopyOperation) => void): WithCopyEvents; 96 | on(event: CopyEventType.CREATE_DIRECTORY_COMPLETE, callback: (info: CopyOperation) => void): WithCopyEvents; 97 | on(event: CopyEventType.CREATE_SYMLINK_START, callback: (info: CopyOperation) => void): WithCopyEvents; 98 | on(event: CopyEventType.CREATE_SYMLINK_ERROR, callback: (error: Error, info: CopyOperation) => void): WithCopyEvents; 99 | on(event: CopyEventType.CREATE_SYMLINK_COMPLETE, callback: (info: CopyOperation) => void): WithCopyEvents; 100 | on(event: CopyEventType.COPY_FILE_START, callback: (info: CopyOperation) => void): WithCopyEvents; 101 | on(event: CopyEventType.COPY_FILE_ERROR, callback: (error: Error, info: CopyOperation) => void): WithCopyEvents; 102 | on(event: CopyEventType.COPY_FILE_COMPLETE, callback: (info: CopyOperation) => void): WithCopyEvents; 103 | } 104 | 105 | export enum CopyEventType { 106 | ERROR = 'error', 107 | COMPLETE = 'complete', 108 | CREATE_DIRECTORY_START = 'createDirectoryStart', 109 | CREATE_DIRECTORY_ERROR = 'createDirectoryError', 110 | CREATE_DIRECTORY_COMPLETE = 'createDirectoryComplete', 111 | CREATE_SYMLINK_START = 'createSymlinkStart', 112 | CREATE_SYMLINK_ERROR = 'createSymlinkError', 113 | CREATE_SYMLINK_COMPLETE = 'createSymlinkComplete', 114 | COPY_FILE_START = 'copyFileStart', 115 | COPY_FILE_ERROR = 'copyFileError', 116 | COPY_FILE_COMPLETE = 'copyFileComplete', 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `@11ty/recursive-copy` 2 | 3 | A temporary fork of [`timkendrick/recursive-copy`](https://github.com/timkendrick/recursive-copy) to satisfy https://github.com/11ty/eleventy/issues/3299 as Eleventy slowly [moves to use Node native API `fs.cp`](https://github.com/11ty/eleventy/issues/3360). 4 | 5 | - v5.x requires Node 20+ 6 | - v4.x requires Node 18+ 7 | - v3.x requires Node 0.10+ 8 | - v2.x requires Node 0.10+ 9 | 10 | > Simple, flexible file copy utility 11 | 12 | ## Features 13 | 14 | - Recursively copy whole directory hierarchies 15 | - Choose which files are copied by passing a filter function, regular expression or glob 16 | - Rename files dynamically, including changing the output path 17 | - Transform file contents using streams 18 | - Choose whether to overwrite existing files 19 | - Choose whether to copy system files 20 | - Filters out [junk](https://www.npmjs.com/package/junk) files by default 21 | - Emits start, finish and error events for each file that is processed 22 | - Optional promise-based interface 23 | 24 | ## Examples 25 | 26 | #### Node-style callback interface 27 | 28 | ```javascript 29 | var copy = require('@11ty/recursive-copy'); 30 | 31 | copy('src', 'dest', function(error, results) { 32 | if (error) { 33 | console.error('Copy failed: ' + error); 34 | } else { 35 | console.info('Copied ' + results.length + ' files'); 36 | } 37 | }); 38 | ``` 39 | 40 | #### Promise interface 41 | 42 | ```javascript 43 | var copy = require('@11ty/recursive-copy'); 44 | 45 | copy('src', 'dest') 46 | .then(function(results) { 47 | console.info('Copied ' + results.length + ' files'); 48 | }) 49 | .catch(function(error) { 50 | console.error('Copy failed: ' + error); 51 | }); 52 | ``` 53 | 54 | #### ES2015+ usage 55 | 56 | ```javascript 57 | import copy from '@11ty/recursive-copy'; 58 | 59 | try { 60 | const results = await copy('src', 'dest'); 61 | console.info('Copied ' + results.length + ' files'); 62 | } catch (error) { 63 | console.error('Copy failed: ' + error); 64 | } 65 | ``` 66 | 67 | #### Advanced options 68 | 69 | ```javascript 70 | var copy = require('@11ty/recursive-copy'); 71 | 72 | var path = require('path'); 73 | var through = require('through2'); 74 | 75 | var options = { 76 | overwrite: true, 77 | expand: true, 78 | dot: true, 79 | junk: true, 80 | filter: [ 81 | '**/*', 82 | '!.htpasswd' 83 | ], 84 | rename: function(filePath) { 85 | return filePath + '.orig'; 86 | }, 87 | transform: function(src, dest, stats) { 88 | if (path.extname(src) !== '.txt') { return null; } 89 | return through(function(chunk, enc, done) { 90 | var output = chunk.toString().toUpperCase(); 91 | done(null, output); 92 | }); 93 | } 94 | }; 95 | 96 | copy('src', 'dest', options) 97 | .on(copy.events.COPY_FILE_START, function(copyOperation) { 98 | console.info('Copying file ' + copyOperation.src + '...'); 99 | }) 100 | .on(copy.events.COPY_FILE_COMPLETE, function(copyOperation) { 101 | console.info('Copied to ' + copyOperation.dest); 102 | }) 103 | .on(copy.events.ERROR, function(error, copyOperation) { 104 | console.error('Unable to copy ' + copyOperation.dest); 105 | }) 106 | .then(function(results) { 107 | console.info(results.length + ' file(s) copied'); 108 | }) 109 | .catch(function(error) { 110 | return console.error('Copy failed: ' + error); 111 | }); 112 | ``` 113 | 114 | 115 | ## Usage 116 | 117 | ### `copy(src, dest, [options], [callback])` 118 | 119 | Recursively copy files and folders from `src` to `dest` 120 | 121 | #### Arguments: 122 | 123 | | Name | Type | Required | Default | Description | 124 | | ---- | ---- | -------- | ------- | ----------- | 125 | | `src` | `string` | Yes | N/A | Source file/folder path | 126 | | `dest` | `string` | Yes | N/A | Destination file/folder path | 127 | | `options.overwrite` | `boolean` | No | `false` | Whether to overwrite destination files | 128 | | `options.expand` | `boolean` | No | `false` | Whether to expand symbolic links | 129 | | `options.dot` | `boolean` | No | `false` | Whether to copy files beginning with a `.` | 130 | | `options.junk` | `boolean` | No | `false` | Whether to copy OS junk files (e.g. `.DS_Store`, `Thumbs.db`) | 131 | | `options.filter` | `function`, `RegExp`, `string`, `array` | No | `null` | Filter function / regular expression / glob that determines which files to copy (uses [maximatch](https://www.npmjs.com/package/maximatch)) | 132 | | `options.rename` | `function` | No | `null` | Function that maps source paths to destination paths | 133 | | `options.transform` | `function` | No | `null` | Function that returns a transform stream used to modify file contents | 134 | | `options.results` | `boolean` | No | `true` | Whether to return an array of copy results | 135 | | `options.concurrency` | `number` | No | `255` | Maximum number of simultaneous copy operations | 136 | | `options.debug` | `boolean` | No | `false` | Whether to log debug information | 137 | | `callback` | `function` | No | `null` | Callback, invoked on success/failure | 138 | 139 | 140 | #### Returns: 141 | 142 | `Promise` Promise, fulfilled with array of copy results: 143 | 144 | ```json 145 | [ 146 | { 147 | "src": "/path/to/src", 148 | "dest": "/path/to/dest", 149 | "stats": 150 | }, 151 | { 152 | "src": "/path/to/src/file.txt", 153 | "dest": "/path/to/dest/file.txt", 154 | "stats": 155 | }, 156 | { 157 | "src": "/path/to/src/subfolder", 158 | "dest": "/path/to/dest/subfolder", 159 | "stats": 160 | }, 161 | { 162 | "src": "/path/to/src/subfolder/nested.txt", 163 | "dest": "/path/to/dest/subfolder/nested.txt", 164 | "stats": 165 | } 166 | ] 167 | ``` 168 | 169 | ## Events 170 | 171 | The value returned by the `copy` function implements the `EventEmitter` interface, and emits the following events: 172 | 173 | | Event | Handler signature | 174 | | ----- | ----------------- | 175 | | `copy.events.ERROR` | `function(error, ErrorInfo)` | 176 | | `copy.events.COMPLETE` | `function(Array)` | 177 | | `copy.events.CREATE_DIRECTORY_START` | `function(CopyOperation)` | 178 | | `copy.events.CREATE_DIRECTORY_ERROR` | `function(error, CopyOperation)` | 179 | | `copy.events.CREATE_DIRECTORY_COMPLETE` | `function(CopyOperation)` | 180 | | `copy.events.CREATE_SYMLINK_START` | `function(CopyOperation)` | 181 | | `copy.events.CREATE_SYMLINK_ERROR` | `function(error, CopyOperation)` | 182 | | `copy.events.CREATE_SYMLINK_COMPLETE` | `function(CopyOperation)` | 183 | | `copy.events.COPY_FILE_START` | `function(CopyOperation)` | 184 | | `copy.events.COPY_FILE_ERROR` | `function(error, CopyOperation)` | 185 | | `copy.events.COPY_FILE_COMPLETE` | `function(CopyOperation)` | 186 | 187 | ...where the types referred to in the handler signature are as follows: 188 | 189 | ### `ErrorInfo` 190 | 191 | | Property | Type | Description | 192 | | -------- | ---- | ----------- | 193 | | `src` | `string` | Source path of the file/folder/symlink that failed to copy | 194 | | `dest` | `string` | Destination path of the file/folder/symlink that failed to copy | 195 | 196 | ### `CopyOperation` 197 | 198 | | Property | Type | Description | 199 | | -------- | ---- | ----------- | 200 | | `src` | `string` | Source path of the relevant file/folder/symlink | 201 | | `dest` | `string` | Destination path of the relevant file/folder/symlink | 202 | | `stats ` | `fs.Stats` | Stats for the relevant file/folder/symlink | 203 | -------------------------------------------------------------------------------- /lib/copy.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = global.Promise; 4 | var path = require('node:path'); 5 | var EventEmitter = require('node:events').EventEmitter; 6 | var fs = require('fs'); 7 | var junk = require('junk'); 8 | var errno = require('errno'); 9 | var slash = require('./slash.js'); 10 | var maximatch = require('./maximatch.js'); 11 | 12 | var CopyError = errno.custom.createError('CopyError'); 13 | 14 | var EVENT_ERROR = 'error'; 15 | var EVENT_COMPLETE = 'complete'; 16 | var EVENT_CREATE_DIRECTORY_START = 'createDirectoryStart'; 17 | var EVENT_CREATE_DIRECTORY_ERROR = 'createDirectoryError'; 18 | var EVENT_CREATE_DIRECTORY_COMPLETE = 'createDirectoryComplete'; 19 | var EVENT_CREATE_SYMLINK_START = 'createSymlinkStart'; 20 | var EVENT_CREATE_SYMLINK_ERROR = 'createSymlinkError'; 21 | var EVENT_CREATE_SYMLINK_COMPLETE = 'createSymlinkComplete'; 22 | var EVENT_COPY_FILE_START = 'copyFileStart'; 23 | var EVENT_COPY_FILE_ERROR = 'copyFileError'; 24 | var EVENT_COPY_FILE_COMPLETE = 'copyFileComplete'; 25 | 26 | var mkdirp = fs.promises.mkdir; 27 | var mkdir = mkdirp; 28 | var stat = fs.promises.stat; 29 | var lstat = fs.promises.lstat; 30 | var readlink = fs.promises.readlink 31 | var symlink = fs.promises.symlink; 32 | var readdir = fs.promises.readdir; 33 | 34 | module.exports = function(src, dest, options, callback) { 35 | if ((arguments.length === 3) && (typeof options === 'function')) { 36 | callback = options; 37 | options = undefined; 38 | } 39 | options = options || {}; 40 | 41 | var parentDirectory = path.dirname(dest); 42 | var shouldExpandSymlinks = Boolean(options.expand); 43 | 44 | var emitter; 45 | var hasFinished = false; 46 | if (options.debug) { log('Ensuring output directory exists…'); } 47 | var promise = ensureDirectoryExists(parentDirectory) 48 | .then(function() { 49 | if (options.debug) { log('Fetching source paths…'); } 50 | return getFilePaths(src, shouldExpandSymlinks) 51 | }) 52 | .then(function(filePaths) { 53 | if (options.debug) { log('Filtering source paths…'); } 54 | // must filter out explicit copy attempts for dot files (not post-relative path) 55 | filePaths = getFilteredPaths(filePaths, undefined, { 56 | dot: options.dot, 57 | junk: options.junk 58 | }); 59 | var relativePaths = filePaths.map(function(filePath) { 60 | return path.relative(src, filePath); 61 | }); 62 | var filteredPaths = getFilteredPaths(relativePaths, options.filter, { 63 | dot: options.dot, 64 | junk: options.junk 65 | }); 66 | return filteredPaths.map(function(relativePath) { 67 | var inputPath = relativePath; 68 | var outputPath = options.rename ? options.rename(inputPath) : inputPath; 69 | return { 70 | src: path.join(src, inputPath), 71 | dest: path.join(dest, outputPath) 72 | }; 73 | }) 74 | }) 75 | .then(function(operations) { 76 | if (options.debug) { log('Copying files…'); } 77 | var hasFinishedGetter = function() { return hasFinished; }; 78 | var emitEvent = function() { emitter.emit.apply(emitter, arguments); }; 79 | return batch(operations, function(operation) { 80 | return copy(operation.src, operation.dest, hasFinishedGetter, emitEvent, options); 81 | }, { 82 | results: options.results !== false, 83 | concurrency: options.concurrency || 255 84 | }); 85 | }) 86 | .catch(function(error) { 87 | if (options.debug) { log('Copy failed'); } 88 | if (error instanceof CopyError) { 89 | emitter.emit(EVENT_ERROR, error.error, error.data); 90 | throw error.error; 91 | } else { 92 | throw error; 93 | } 94 | }) 95 | .then(function(results) { 96 | if (options.debug) { log('Copy complete'); } 97 | emitter.emit(EVENT_COMPLETE, results); 98 | return results; 99 | }) 100 | .then(function(results) { 101 | hasFinished = true; 102 | return results; 103 | }) 104 | .catch(function(error) { 105 | hasFinished = true; 106 | throw error; 107 | }); 108 | 109 | if (typeof callback === 'function') { 110 | promise.then(function(results) { 111 | callback(null, results); 112 | }) 113 | .catch(function(error) { 114 | callback(error); 115 | }); 116 | emitter = new EventEmitter(); 117 | } else { 118 | emitter = withEventEmitter(promise); 119 | } 120 | 121 | return emitter; 122 | }; 123 | 124 | function batch(inputs, iteratee, options) { 125 | var results = options.results ? [] : undefined; 126 | if (inputs.length === 0) { return Promise.resolve(results); } 127 | return new Promise(function(resolve, reject) { 128 | var currentIndex = -1; 129 | var activeWorkers = 0; 130 | while (currentIndex < Math.min(inputs.length, options.concurrency) - 1) { 131 | startWorker(inputs[++currentIndex]); 132 | } 133 | 134 | function startWorker(input) { 135 | ++activeWorkers; 136 | iteratee(input).then(function(result) { 137 | --activeWorkers; 138 | if (results) { results.push(result); } 139 | if (currentIndex < inputs.length - 1) { 140 | startWorker(inputs[++currentIndex]); 141 | } else if (activeWorkers === 0) { 142 | resolve(results); 143 | } 144 | }).catch(reject); 145 | } 146 | }); 147 | } 148 | 149 | function getFilePaths(src, shouldExpandSymlinks) { 150 | return (shouldExpandSymlinks ? stat : lstat)(src) 151 | .then(function(stats) { 152 | if (stats.isDirectory()) { 153 | return getFileListing(src, shouldExpandSymlinks) 154 | .then(function(filenames) { 155 | return [src].concat(filenames); 156 | }); 157 | } else { 158 | return [src]; 159 | } 160 | }); 161 | } 162 | 163 | function getFilteredPaths(paths, filter, options) { 164 | var useDotFilter = !options.dot; 165 | var useJunkFilter = !options.junk; 166 | if (!filter && !useDotFilter && !useJunkFilter) { return paths; } 167 | return paths.filter(function(path) { 168 | if(!useDotFilter || dotFilter(path)) { 169 | if(!useJunkFilter || junkFilter(path)) { 170 | if(!filter) { 171 | return true; 172 | } 173 | var p = slash(path); 174 | // filter might be a string, array, function 175 | var m = maximatch(p, filter, options); 176 | if(m.length > 0) { 177 | return true; 178 | } 179 | } 180 | } 181 | return false; 182 | }); 183 | } 184 | 185 | function dotFilter(relativePath) { 186 | var filename = path.basename(relativePath); 187 | return filename.charAt(0) !== '.'; 188 | } 189 | 190 | function junkFilter(relativePath) { 191 | var filename = path.basename(relativePath); 192 | return !junk.is(filename); 193 | } 194 | 195 | function ensureDirectoryExists(path) { 196 | return mkdir(path, { recursive: true }); 197 | } 198 | 199 | function getFileListing(srcPath, shouldExpandSymlinks) { 200 | return readdir(srcPath) 201 | .then(function(filenames) { 202 | return Promise.all( 203 | filenames.map(function(filename) { 204 | var filePath = path.join(srcPath, filename); 205 | return (shouldExpandSymlinks ? stat : lstat)(filePath) 206 | .then(function(stats) { 207 | if (stats.isDirectory()) { 208 | return getFileListing(filePath, shouldExpandSymlinks) 209 | .then(function(childPaths) { 210 | return [filePath].concat(childPaths); 211 | }); 212 | } else { 213 | return [filePath]; 214 | } 215 | }); 216 | }) 217 | ) 218 | .then(function mergeArrays(arrays) { 219 | return Array.prototype.concat.apply([], arrays); 220 | }); 221 | }); 222 | } 223 | 224 | function copy(srcPath, destPath, hasFinished, emitEvent, options) { 225 | if (options.debug) { log('Preparing to copy ' + srcPath + '…'); } 226 | return prepareForCopy(srcPath, destPath, options) 227 | .then(function(stats) { 228 | if (options.debug) { log('Copying ' + srcPath + '…'); } 229 | var copyFunction = getCopyFunction(stats, hasFinished, emitEvent); 230 | return copyFunction(srcPath, destPath, stats, options); 231 | }) 232 | .catch(function(error) { 233 | if (error instanceof CopyError) { 234 | throw error; 235 | } 236 | var copyError = new CopyError(error.message); 237 | copyError.error = error; 238 | copyError.data = { 239 | src: srcPath, 240 | dest: destPath 241 | }; 242 | throw copyError; 243 | }) 244 | .then(function(result) { 245 | if (options.debug) { log('Copied ' + srcPath); } 246 | return result; 247 | }); 248 | } 249 | 250 | function prepareForCopy(srcPath, destPath, options) { 251 | var shouldExpandSymlinks = Boolean(options.expand); 252 | var shouldOverwriteExistingFiles = Boolean(options.overwrite); 253 | return (shouldExpandSymlinks ? stat : lstat)(srcPath) 254 | .then(function(stats) { 255 | return ensureDestinationIsWritable(destPath, stats, shouldOverwriteExistingFiles) 256 | .then(function() { 257 | return stats; 258 | }); 259 | }); 260 | } 261 | 262 | function ensureDestinationIsWritable(destPath, srcStats, shouldOverwriteExistingFiles) { 263 | return lstat(destPath) 264 | .catch(function(error) { 265 | var shouldIgnoreError = error.code === 'ENOENT'; 266 | if (shouldIgnoreError) { return null; } 267 | throw error; 268 | }) 269 | .then(function(destStats) { 270 | var destExists = Boolean(destStats); 271 | if (!destExists) { return true; } 272 | 273 | var isMergePossible = srcStats.isDirectory() && destStats.isDirectory(); 274 | if (isMergePossible) { return true; } 275 | 276 | if (shouldOverwriteExistingFiles) { 277 | return fs.promises.rm(destPath, { recursive: true, force: true }).then(function(paths) { 278 | return true; 279 | }); 280 | } else { 281 | throw fsError('EEXIST', destPath); 282 | } 283 | }); 284 | } 285 | 286 | function getCopyFunction(stats, hasFinished, emitEvent) { 287 | if (stats.isDirectory()) { 288 | return createCopyFunction(copyDirectory, stats, hasFinished, emitEvent, { 289 | startEvent: EVENT_CREATE_DIRECTORY_START, 290 | completeEvent: EVENT_CREATE_DIRECTORY_COMPLETE, 291 | errorEvent: EVENT_CREATE_DIRECTORY_ERROR 292 | }); 293 | } else if (stats.isSymbolicLink()) { 294 | return createCopyFunction(copySymlink, stats, hasFinished, emitEvent, { 295 | startEvent: EVENT_CREATE_SYMLINK_START, 296 | completeEvent: EVENT_CREATE_SYMLINK_COMPLETE, 297 | errorEvent: EVENT_CREATE_SYMLINK_ERROR 298 | }); 299 | } else { 300 | return createCopyFunction(copyFile, stats, hasFinished, emitEvent, { 301 | startEvent: EVENT_COPY_FILE_START, 302 | completeEvent: EVENT_COPY_FILE_COMPLETE, 303 | errorEvent: EVENT_COPY_FILE_ERROR 304 | }); 305 | } 306 | } 307 | 308 | function createCopyFunction(fn, stats, hasFinished, emitEvent, events) { 309 | var startEvent = events.startEvent; 310 | var completeEvent = events.completeEvent; 311 | var errorEvent = events.errorEvent; 312 | return function(srcPath, destPath, stats, options) { 313 | // Multiple chains of promises are fired in parallel, 314 | // so when one fails we need to prevent any future 315 | // copy operations 316 | if (hasFinished()) { return Promise.reject(); } 317 | var metadata = { 318 | src: srcPath, 319 | dest: destPath, 320 | stats: stats 321 | }; 322 | emitEvent(startEvent, metadata); 323 | var parentDirectory = path.dirname(destPath); 324 | return ensureDirectoryExists(parentDirectory) 325 | .then(function() { 326 | return fn(srcPath, destPath, stats, options); 327 | }) 328 | .then(function() { 329 | if (!hasFinished()) { emitEvent(completeEvent, metadata); } 330 | return metadata; 331 | }) 332 | .catch(function(error) { 333 | if (!hasFinished()) { emitEvent(errorEvent, error, metadata); } 334 | throw error; 335 | }); 336 | }; 337 | } 338 | 339 | function copyFile(srcPath, destPath, stats, options) { 340 | return new Promise(function(resolve, reject) { 341 | var hasFinished = false; 342 | 343 | var read = fs.createReadStream(srcPath); 344 | read.on('error', handleCopyFailed); 345 | 346 | var write = fs.createWriteStream(destPath, { 347 | flags: 'w', 348 | mode: stats.mode 349 | }); 350 | write.on('error', handleCopyFailed); 351 | write.on('finish', function() { 352 | fs.utimes(destPath, stats.atime, stats.mtime, function() { 353 | hasFinished = true; 354 | resolve(); 355 | }); 356 | }); 357 | 358 | var transformStream = null; 359 | if (options.transform) { 360 | transformStream = options.transform(srcPath, destPath, stats); 361 | if (transformStream) { 362 | transformStream.on('error', handleCopyFailed); 363 | read.pipe(transformStream).pipe(write); 364 | } else { 365 | read.pipe(write); 366 | } 367 | } else { 368 | read.pipe(write); 369 | } 370 | 371 | 372 | function handleCopyFailed(error) { 373 | if (hasFinished) { return; } 374 | hasFinished = true; 375 | if (typeof read.close === 'function') { 376 | read.close(); 377 | } 378 | if (typeof write.close === 'function') { 379 | write.close(); 380 | } 381 | return reject(error); 382 | } 383 | }); 384 | } 385 | 386 | function copySymlink(srcPath, destPath, stats, options) { 387 | return readlink(srcPath) 388 | .then(function(link) { 389 | return symlink(link, destPath); 390 | }); 391 | } 392 | 393 | function copyDirectory(srcPath, destPath, stats, options) { 394 | return mkdir(destPath, { recusirve: true }) 395 | .catch(function(error) { 396 | var shouldIgnoreError = error.code === 'EEXIST'; 397 | if (shouldIgnoreError) { return; } 398 | throw error; 399 | }); 400 | } 401 | 402 | function fsError(code, path) { 403 | var errorType = errno.code[code]; 404 | var message = errorType.code + ', ' + errorType.description + ' ' + path; 405 | var error = new Error(message); 406 | error.errno = errorType.errno; 407 | error.code = errorType.code; 408 | error.path = path; 409 | return error; 410 | } 411 | 412 | function log(message) { 413 | process.stdout.write(message + '\n'); 414 | } 415 | 416 | function withEventEmitter(target) { 417 | for (var key in EventEmitter.prototype) { 418 | target[key] = EventEmitter.prototype[key]; 419 | } 420 | EventEmitter.call(target); 421 | return target; 422 | } 423 | 424 | module.exports.events = { 425 | ERROR: EVENT_ERROR, 426 | COMPLETE: EVENT_COMPLETE, 427 | CREATE_DIRECTORY_START: EVENT_CREATE_DIRECTORY_START, 428 | CREATE_DIRECTORY_ERROR: EVENT_CREATE_DIRECTORY_ERROR, 429 | CREATE_DIRECTORY_COMPLETE: EVENT_CREATE_DIRECTORY_COMPLETE, 430 | CREATE_SYMLINK_START: EVENT_CREATE_SYMLINK_START, 431 | CREATE_SYMLINK_ERROR: EVENT_CREATE_SYMLINK_ERROR, 432 | CREATE_SYMLINK_COMPLETE: EVENT_CREATE_SYMLINK_COMPLETE, 433 | COPY_FILE_START: EVENT_COPY_FILE_START, 434 | COPY_FILE_ERROR: EVENT_COPY_FILE_ERROR, 435 | COPY_FILE_COMPLETE: EVENT_COPY_FILE_COMPLETE 436 | }; 437 | -------------------------------------------------------------------------------- /test/spec/copy.spec.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Promise = global.Promise; 4 | var fs = require('node:fs'); 5 | var path = require('node:path'); 6 | var chai = require('chai'); 7 | var expect = chai.expect; 8 | var chaiAsPromised = require('chai-as-promised'); 9 | var readDirFiles = require('read-dir-files'); 10 | var through = require('through2'); 11 | var rewire = require('rewire'); 12 | 13 | var slash = require('../../lib/slash.js'); 14 | var copy = rewire('../../lib/copy'); 15 | 16 | var SOURCE_PATH = path.resolve(__dirname, '../fixtures/source'); 17 | var DESTINATION_PATH = path.resolve(__dirname, '../fixtures/destination'); 18 | 19 | var COPY_EVENTS = Object.keys(copy.events).map(function(key) { 20 | return copy.events[key]; 21 | }); 22 | 23 | chai.use(chaiAsPromised); 24 | 25 | describe('copy()', function() { 26 | beforeEach(function(done) { 27 | fs.mkdir(DESTINATION_PATH, function(error) { 28 | if (error) { 29 | return fs.promises.rm(path.join(DESTINATION_PATH, '**/*'), { recursive: true, force: true }).then(function() { 30 | done(); 31 | }); 32 | } else { 33 | done(); 34 | } 35 | }); 36 | }); 37 | 38 | afterEach(function(done) { 39 | return fs.promises.rm(DESTINATION_PATH, { recursive: true, force: true }).then(function() { 40 | done(); 41 | }); 42 | }); 43 | 44 | function getSourcePath(filename) { 45 | return path.join(SOURCE_PATH, filename); 46 | } 47 | 48 | function getDestinationPath(filename) { 49 | if (!filename) { return DESTINATION_PATH; } 50 | return path.join(DESTINATION_PATH, filename); 51 | } 52 | 53 | function getOutputFiles() { 54 | return new Promise(function(resolve, reject) { 55 | readDirFiles.read(DESTINATION_PATH, 'utf8', function(error, files) { 56 | if (error) { 57 | return reject(error); 58 | } 59 | return resolve(files); 60 | }); 61 | }); 62 | } 63 | 64 | function checkResults(results, expectedResults) { 65 | var actual, expected; 66 | actual = results.reduce(function(paths, copyOperation) { 67 | paths[copyOperation.src] = copyOperation.dest; 68 | return paths; 69 | }, {}); 70 | expected = Object.keys(expectedResults).map(function(filename) { 71 | return { 72 | src: getSourcePath(filename), 73 | dest: getDestinationPath(filename) 74 | }; 75 | }).reduce(function(paths, copyOperation) { 76 | paths[copyOperation.src] = copyOperation.dest; 77 | return paths; 78 | }, {}); 79 | expect(actual).to.eql(expected); 80 | 81 | actual = results.reduce(function(stats, copyOperation) { 82 | stats[copyOperation.dest] = getFileType(copyOperation.stats); 83 | return stats; 84 | }, {}); 85 | expected = Object.keys(expectedResults).map(function(filename) { 86 | return { 87 | dest: getDestinationPath(filename), 88 | type: expectedResults[filename] 89 | }; 90 | }).reduce(function(paths, copyOperation) { 91 | paths[copyOperation.dest] = copyOperation.type; 92 | return paths; 93 | }, {}); 94 | expect(actual).to.eql(expected); 95 | 96 | 97 | function getFileType(stats) { 98 | if (stats.isDirectory()) { return 'dir'; } 99 | if (stats.isSymbolicLink()) { return 'symlink'; } 100 | return 'file'; 101 | } 102 | } 103 | 104 | function createSymbolicLink(src, dest, type) { 105 | var stats; 106 | try { 107 | stats = fs.lstatSync(dest); 108 | } catch (error) { 109 | if (error.code !== 'ENOENT') { 110 | throw error; 111 | } 112 | } 113 | if (!stats) { 114 | fs.symlinkSync(src, dest, type); 115 | } else if (!stats.isSymbolicLink()) { 116 | fs.unlinkSync(dest); 117 | fs.symlinkSync(src, dest, type); 118 | } 119 | } 120 | 121 | function listenTo(emitter, eventNames) { 122 | var events = []; 123 | eventNames.forEach(function(eventName) { 124 | emitter.on(eventName, createListener(eventName)); 125 | }); 126 | return events; 127 | 128 | 129 | function createListener(eventName) { 130 | return function(args) { 131 | events.push({ 132 | name: eventName, 133 | args: Array.prototype.slice.call(arguments) 134 | }); 135 | }; 136 | } 137 | } 138 | 139 | function mockMkdirp(subject, errors) { 140 | return subject.__set__('mkdirp', mkdirp); 141 | 142 | function mkdirp(path, mode, callback) { 143 | if ((arguments.length === 2) && (typeof mode === 'function')) { 144 | callback = mode; 145 | mode = undefined; 146 | } 147 | setTimeout(function() { 148 | if (errors && errors[path]) { 149 | callback(errors[path]); 150 | } else { 151 | callback(null); 152 | } 153 | }); 154 | } 155 | } 156 | 157 | function mockSymlink(subject) { 158 | var originalSymlink = subject.__get__('fs').symlink; 159 | subject.__get__('fs').symlink = symlink; 160 | return function() { 161 | subject.__get__('fs').symlink = originalSymlink; 162 | }; 163 | 164 | function symlink(srcPath, dstPath, type, callback) { 165 | if ((arguments.length === 3) && (typeof type === 'function')) { 166 | callback = type; 167 | type = undefined; 168 | } 169 | setTimeout(function() { 170 | callback(new Error('Test error')); 171 | }); 172 | } 173 | } 174 | 175 | describe('basic operation', function() { 176 | it('should copy single files', function() { 177 | return copy( 178 | getSourcePath('file'), 179 | getDestinationPath('file') 180 | ).then(function(results) { 181 | return getOutputFiles() 182 | .then(function(files) { 183 | var actual, expected; 184 | actual = files; 185 | expected = { 186 | file: 'Hello, world!\n' 187 | }; 188 | expect(actual).to.eql(expected); 189 | }); 190 | }); 191 | }); 192 | 193 | it('should return results for single files', function() { 194 | return copy( 195 | getSourcePath('file'), 196 | getDestinationPath('file') 197 | ).then(function(results) { 198 | checkResults(results, { 199 | 'file': 'file' 200 | }); 201 | }); 202 | }); 203 | 204 | it('should retain file modification dates', function() { 205 | return new Promise(function(resolve) { 206 | setTimeout(resolve, 1000) 207 | }).then(function() { 208 | return copy( 209 | getSourcePath('file'), 210 | getDestinationPath('file') 211 | ); 212 | }).then(function(results) { 213 | var actual = fs.statSync(getDestinationPath('file')).mtime; 214 | var expected = fs.statSync(getSourcePath('file')).mtime; 215 | actual.setMilliseconds(0); 216 | expected.setMilliseconds(0); 217 | expect(actual).to.eql(expected); 218 | }); 219 | }); 220 | 221 | it('should retain file permissions', function() { 222 | return copy( 223 | getSourcePath('executable'), 224 | getDestinationPath('executable') 225 | ).then(function(results) { 226 | var actual = fs.statSync(getDestinationPath('executable')).mode; 227 | var expected = fs.statSync(getSourcePath('executable')).mode; 228 | expect(actual).to.equal(expected); 229 | }); 230 | }); 231 | 232 | it('should create parent directory if it does not exist', function() { 233 | return copy( 234 | getSourcePath('nested-file/file'), 235 | getDestinationPath('nested-file/file') 236 | ).then(function(results) { 237 | checkResults(results, { 238 | 'nested-file/file': 'file' 239 | }); 240 | }); 241 | }); 242 | 243 | it('should copy empty directories', function() { 244 | return copy( 245 | getSourcePath('empty'), 246 | getDestinationPath('empty') 247 | ).then(function(results) { 248 | return getOutputFiles() 249 | .then(function(files) { 250 | var actual, expected; 251 | actual = files; 252 | expected = { 253 | 'empty': {} 254 | }; 255 | expect(actual).to.eql(expected); 256 | }); 257 | }); 258 | }); 259 | 260 | it('should return results for empty directories', function() { 261 | return copy( 262 | getSourcePath('empty'), 263 | getDestinationPath('empty') 264 | ).then(function(results) { 265 | checkResults(results, { 266 | 'empty': 'dir' 267 | }); 268 | }); 269 | }); 270 | 271 | it('should copy directories', function() { 272 | return copy( 273 | getSourcePath('directory'), 274 | getDestinationPath('directory') 275 | ).then(function(results) { 276 | return getOutputFiles() 277 | .then(function(files) { 278 | var actual, expected; 279 | actual = files; 280 | expected = { 281 | directory: { 282 | a: 'a\n', 283 | b: 'b\n', 284 | c: 'c\n' 285 | } 286 | }; 287 | expect(actual).to.eql(expected); 288 | }); 289 | }); 290 | }); 291 | 292 | it('should return results for directories', function() { 293 | return copy( 294 | getSourcePath('directory'), 295 | getDestinationPath('directory') 296 | ).then(function(results) { 297 | checkResults(results, { 298 | 'directory': 'dir', 299 | 'directory/a': 'file', 300 | 'directory/b': 'file', 301 | 'directory/c': 'file' 302 | }); 303 | }); 304 | }); 305 | 306 | it('should copy nested directories', function() { 307 | return copy( 308 | getSourcePath('nested-directory'), 309 | getDestinationPath('nested-directory') 310 | ).then(function(results) { 311 | return getOutputFiles() 312 | .then(function(files) { 313 | var actual, expected; 314 | actual = files; 315 | expected = { 316 | 'nested-directory': { 317 | '1': { 318 | '1-1': { 319 | '1-1-a': '1-1-a\n', 320 | '1-1-b': '1-1-b\n' 321 | }, 322 | '1-2': { 323 | '1-2-a': '1-2-a\n', 324 | '1-2-b': '1-2-b\n' 325 | }, 326 | '1-a': '1-a\n', 327 | '1-b': '1-b\n' 328 | }, 329 | '2': { 330 | '2-1': { 331 | '2-1-a': '2-1-a\n', 332 | '2-1-b': '2-1-b\n' 333 | }, 334 | '2-2': { 335 | '2-2-a': '2-2-a\n', 336 | '2-2-b': '2-2-b\n' 337 | }, 338 | '2-a': '2-a\n', 339 | '2-b': '2-b\n' 340 | }, 341 | 'a': 'a\n', 342 | 'b': 'b\n' 343 | } 344 | }; 345 | expect(actual).to.eql(expected); 346 | }); 347 | }); 348 | }); 349 | 350 | it('should return results for directories', function() { 351 | return copy( 352 | getSourcePath('nested-directory'), 353 | getDestinationPath('nested-directory') 354 | ).then(function(results) { 355 | checkResults(results, { 356 | 'nested-directory': 'dir', 357 | 'nested-directory/1': 'dir', 358 | 'nested-directory/1/1-1': 'dir', 359 | 'nested-directory/1/1-1/1-1-a': 'file', 360 | 'nested-directory/1/1-1/1-1-b': 'file', 361 | 'nested-directory/1/1-2': 'dir', 362 | 'nested-directory/1/1-2/1-2-a': 'file', 363 | 'nested-directory/1/1-2/1-2-b': 'file', 364 | 'nested-directory/1/1-a': 'file', 365 | 'nested-directory/1/1-b': 'file', 366 | 'nested-directory/2': 'dir', 367 | 'nested-directory/2/2-1': 'dir', 368 | 'nested-directory/2/2-1/2-1-a': 'file', 369 | 'nested-directory/2/2-1/2-1-b': 'file', 370 | 'nested-directory/2/2-2': 'dir', 371 | 'nested-directory/2/2-2/2-2-a': 'file', 372 | 'nested-directory/2/2-2/2-2-b': 'file', 373 | 'nested-directory/2/2-a': 'file', 374 | 'nested-directory/2/2-b': 'file', 375 | 'nested-directory/a': 'file', 376 | 'nested-directory/b': 'file' 377 | }); 378 | }); 379 | }); 380 | 381 | it('should merge directories into existing directories', function() { 382 | return copy( 383 | getSourcePath('nested-directory'), 384 | getDestinationPath() 385 | ).then(function(results) { 386 | return getOutputFiles() 387 | .then(function(files) { 388 | var actual, expected; 389 | actual = files; 390 | expected = { 391 | '1': { 392 | '1-1': { 393 | '1-1-a': '1-1-a\n', 394 | '1-1-b': '1-1-b\n' 395 | }, 396 | '1-2': { 397 | '1-2-a': '1-2-a\n', 398 | '1-2-b': '1-2-b\n' 399 | }, 400 | '1-a': '1-a\n', 401 | '1-b': '1-b\n' 402 | }, 403 | '2': { 404 | '2-1': { 405 | '2-1-a': '2-1-a\n', 406 | '2-1-b': '2-1-b\n' 407 | }, 408 | '2-2': { 409 | '2-2-a': '2-2-a\n', 410 | '2-2-b': '2-2-b\n' 411 | }, 412 | '2-a': '2-a\n', 413 | '2-b': '2-b\n' 414 | }, 415 | 'a': 'a\n', 416 | 'b': 'b\n' 417 | }; 418 | expect(actual).to.eql(expected); 419 | }); 420 | }); 421 | }); 422 | 423 | it('should copy symlinks', function() { 424 | createSymbolicLink('.', getSourcePath('symlink'), 'dir'); 425 | return copy( 426 | getSourcePath('symlink'), 427 | getDestinationPath('symlink') 428 | ).then(function(results) { 429 | var actual, expected; 430 | actual = fs.readlinkSync(getDestinationPath('symlink')); 431 | expected = '.'; 432 | expect(actual).to.equal(expected); 433 | }); 434 | }); 435 | 436 | it('should return results for symlinks', function() { 437 | createSymbolicLink('.', getSourcePath('symlink'), 'dir'); 438 | return copy( 439 | getSourcePath('symlink'), 440 | getDestinationPath('symlink') 441 | ).then(function(results) { 442 | checkResults(results, { 443 | 'symlink': 'symlink' 444 | }); 445 | }); 446 | }); 447 | 448 | it('should copy nested symlinks', function() { 449 | createSymbolicLink('../file', getSourcePath('nested-symlinks/file'), 'file'); 450 | createSymbolicLink('../directory', getSourcePath('nested-symlinks/directory'), 'dir'); 451 | createSymbolicLink('../../directory', getSourcePath('nested-symlinks/nested/directory'), 'dir'); 452 | return copy( 453 | getSourcePath('nested-symlinks'), 454 | getDestinationPath('nested-symlinks') 455 | ).then(function(results) { 456 | var actual, expected; 457 | actual = slash(fs.readlinkSync(getDestinationPath('nested-symlinks/file'))); 458 | expected = '../file'; 459 | expect(actual).to.equal(expected); 460 | actual = slash(fs.readlinkSync(getDestinationPath('nested-symlinks/directory'))); 461 | expected = '../directory'; 462 | expect(actual).to.equal(expected); 463 | actual = slash(fs.readlinkSync(getDestinationPath('nested-symlinks/nested/directory'))); 464 | expected = '../../directory'; 465 | expect(actual).to.equal(expected); 466 | }); 467 | }); 468 | 469 | it('should return results for nested symlinks', function() { 470 | createSymbolicLink('../file', getSourcePath('nested-symlinks/file'), 'file'); 471 | createSymbolicLink('../directory', getSourcePath('nested-symlinks/directory'), 'dir'); 472 | createSymbolicLink('../../directory', getSourcePath('nested-symlinks/nested/directory'), 'dir'); 473 | return copy( 474 | getSourcePath('nested-symlinks'), 475 | getDestinationPath('nested-symlinks') 476 | ).then(function(results) { 477 | checkResults(results, { 478 | 'nested-symlinks': 'dir', 479 | 'nested-symlinks/file': 'symlink', 480 | 'nested-symlinks/directory': 'symlink', 481 | 'nested-symlinks/nested': 'dir', 482 | 'nested-symlinks/nested/directory': 'symlink' 483 | }); 484 | }); 485 | }); 486 | }); 487 | 488 | describe('options', function() { 489 | 490 | it('should overwrite destination file if overwrite is specified', function() { 491 | fs.writeFileSync(getDestinationPath('file'), 'Goodbye, world!'); 492 | 493 | return copy( 494 | getSourcePath('file'), 495 | getDestinationPath('file'), 496 | { 497 | overwrite: true 498 | } 499 | ).then(function(results) { 500 | return getOutputFiles() 501 | .then(function(files) { 502 | var actual, expected; 503 | actual = files; 504 | expected = { 505 | file: 'Hello, world!\n' 506 | }; 507 | expect(actual).to.eql(expected); 508 | }); 509 | }); 510 | }); 511 | 512 | it('should overwrite destination symlink if overwrite is specified', function() { 513 | fs.symlinkSync('./symlink', getDestinationPath('file'), 'file'); 514 | 515 | return copy( 516 | getSourcePath('file'), 517 | getDestinationPath('file'), 518 | { 519 | overwrite: true 520 | } 521 | ).then(function(results) { 522 | return getOutputFiles() 523 | .then(function(files) { 524 | var actual, expected; 525 | actual = files; 526 | expected = { 527 | file: 'Hello, world!\n' 528 | }; 529 | expect(actual).to.eql(expected); 530 | }); 531 | }); 532 | }); 533 | 534 | it('should overwrite destination directory if overwrite is specified', function() { 535 | fs.mkdirSync(getDestinationPath('file')); 536 | 537 | return copy( 538 | getSourcePath('file'), 539 | getDestinationPath('file'), 540 | { 541 | overwrite: true 542 | } 543 | ).then(function(results) { 544 | return getOutputFiles() 545 | .then(function(files) { 546 | var actual, expected; 547 | actual = files; 548 | expected = { 549 | file: 'Hello, world!\n' 550 | }; 551 | expect(actual).to.eql(expected); 552 | }); 553 | }); 554 | }); 555 | 556 | it('should not copy dotfiles if dotfiles is not specified', function() { 557 | return copy( 558 | getSourcePath('dotfiles'), 559 | getDestinationPath() 560 | ).then(function(results) { 561 | return getOutputFiles() 562 | .then(function(files) { 563 | var actual, expected; 564 | actual = files; 565 | expected = { 566 | 'a': 'a\n', 567 | 'b': 'b\n' 568 | }; 569 | expect(actual).to.eql(expected); 570 | }); 571 | }); 572 | }); 573 | 574 | it('should not copy dotfiles if dotfile is referenced directly', function() { 575 | return copy( 576 | getSourcePath('dotfiles/.a'), 577 | getDestinationPath() 578 | ).then(function(results) { 579 | return getOutputFiles() 580 | .then(function(files) { 581 | var actual, expected; 582 | actual = files; 583 | expected = {}; 584 | expect(actual).to.eql(expected); 585 | }); 586 | }); 587 | }); 588 | 589 | it('should copy dotfiles if dotfiles is specified', function() { 590 | return copy( 591 | getSourcePath('dotfiles'), 592 | getDestinationPath(), 593 | { 594 | dot: true 595 | } 596 | ).then(function(results) { 597 | return getOutputFiles() 598 | .then(function(files) { 599 | var actual, expected; 600 | actual = files; 601 | expected = { 602 | '.a': '.a\n', 603 | '.b': '.b\n', 604 | 'a': 'a\n', 605 | 'b': 'b\n' 606 | }; 607 | expect(actual).to.eql(expected); 608 | }); 609 | }); 610 | }); 611 | 612 | it('should not copy junk files if junk is not specified', function() { 613 | return copy( 614 | getSourcePath('junk'), 615 | getDestinationPath() 616 | ).then(function(results) { 617 | return getOutputFiles() 618 | .then(function(files) { 619 | var actual, expected; 620 | actual = files; 621 | expected = { 622 | 'a': 'a\n', 623 | 'b': 'b\n' 624 | }; 625 | expect(actual).to.eql(expected); 626 | }); 627 | }); 628 | }); 629 | 630 | it('should copy junk files if junk is specified', function() { 631 | return copy( 632 | getSourcePath('junk'), 633 | getDestinationPath(), 634 | { 635 | junk: true 636 | } 637 | ).then(function(results) { 638 | return getOutputFiles() 639 | .then(function(files) { 640 | var actual, expected; 641 | actual = files; 642 | expected = { 643 | 'a': 'a\n', 644 | 'b': 'b\n', 645 | 'npm-debug.log': 'npm-debug.log\n', 646 | 'Thumbs.db': 'Thumbs.db\n' 647 | }; 648 | expect(actual).to.eql(expected); 649 | }); 650 | }); 651 | }); 652 | 653 | it('should expand symlinked source files if expand is specified', function() { 654 | createSymbolicLink('./file', getSourcePath('file-symlink'), 'file'); 655 | return copy( 656 | getSourcePath('file-symlink'), 657 | getDestinationPath('expanded-file-symlink'), 658 | { 659 | expand: true 660 | } 661 | ).then(function(results) { 662 | var actual = fs.lstatSync(getDestinationPath('expanded-file-symlink')).isSymbolicLink(); 663 | var expected = false; 664 | expect(actual).to.equal(expected); 665 | return getOutputFiles() 666 | .then(function(files) { 667 | var actual, expected; 668 | actual = files; 669 | expected = { 670 | 'expanded-file-symlink': 'Hello, world!\n' 671 | }; 672 | expect(actual).to.eql(expected); 673 | }); 674 | }); 675 | }); 676 | 677 | it('should expand symlinked source directories if expand is specified', function() { 678 | createSymbolicLink('./directory', getSourcePath('directory-symlink'), 'dir'); 679 | return copy( 680 | getSourcePath('directory-symlink'), 681 | getDestinationPath('directory-symlink'), 682 | { 683 | expand: true 684 | } 685 | ).then(function(results) { 686 | var actual = fs.lstatSync(getDestinationPath('directory-symlink')).isSymbolicLink(); 687 | var expected = false; 688 | expect(actual).to.equal(expected); 689 | return getOutputFiles() 690 | .then(function(files) { 691 | var actual, expected; 692 | actual = files; 693 | expected = { 694 | 'directory-symlink': { 695 | 'a': 'a\n', 696 | 'b': 'b\n', 697 | 'c': 'c\n' 698 | } 699 | }; 700 | expect(actual).to.eql(expected); 701 | }); 702 | }); 703 | }); 704 | 705 | it('should expand nested symlinks if expand is specified', function() { 706 | createSymbolicLink('../file', getSourcePath('nested-symlinks/file'), 'file'); 707 | createSymbolicLink('../directory', getSourcePath('nested-symlinks/directory'), 'dir'); 708 | createSymbolicLink('../../directory', getSourcePath('nested-symlinks/nested/directory'), 'dir'); 709 | return copy( 710 | getSourcePath('nested-symlinks'), 711 | getDestinationPath('expanded-nested-symlinks'), 712 | { 713 | expand: true 714 | } 715 | ).then(function(results) { 716 | var actual, expected; 717 | actual = fs.lstatSync(getDestinationPath('expanded-nested-symlinks')).isSymbolicLink(); 718 | expected = false; 719 | expect(actual).to.equal(expected); 720 | return getOutputFiles() 721 | .then(function(files) { 722 | var actual, expected; 723 | actual = files; 724 | expected = { 725 | 'expanded-nested-symlinks': { 726 | 'file': 'Hello, world!\n', 727 | 'directory': { 728 | 'a': 'a\n', 729 | 'b': 'b\n', 730 | 'c': 'c\n' 731 | }, 732 | 'nested': { 733 | 'directory': { 734 | 'a': 'a\n', 735 | 'b': 'b\n', 736 | 'c': 'c\n' 737 | } 738 | } 739 | } 740 | }; 741 | expect(actual).to.eql(expected); 742 | }); 743 | }); 744 | }); 745 | }); 746 | 747 | describe('output transformation', function() { 748 | it('should filter output files via function', function() { 749 | return copy( 750 | getSourcePath('nested-directory'), 751 | getDestinationPath(), 752 | { 753 | filter: function(filePath) { 754 | var filename = path.basename(filePath); 755 | return (filePath === '1') || (filename.charAt(0) !== '1'); 756 | } 757 | } 758 | ).then(function(results) { 759 | return getOutputFiles() 760 | .then(function(files) { 761 | var actual, expected; 762 | actual = files; 763 | expected = { 764 | '1': {}, 765 | '2': { 766 | '2-1': { 767 | '2-1-a': '2-1-a\n', 768 | '2-1-b': '2-1-b\n' 769 | }, 770 | '2-2': { 771 | '2-2-a': '2-2-a\n', 772 | '2-2-b': '2-2-b\n' 773 | }, 774 | '2-a': '2-a\n', 775 | '2-b': '2-b\n' 776 | }, 777 | 'a': 'a\n', 778 | 'b': 'b\n' 779 | }; 780 | expect(actual).to.eql(expected); 781 | }); 782 | }); 783 | }); 784 | 785 | it('should filter output files via regular expression', function() { 786 | return copy( 787 | getSourcePath('nested-directory'), 788 | getDestinationPath(), 789 | { 790 | filter: /(^[^1].*$)|(^1$)/ 791 | } 792 | ).then(function(results) { 793 | return getOutputFiles() 794 | .then(function(files) { 795 | var actual, expected; 796 | actual = files; 797 | expected = { 798 | '1': {}, 799 | '2': { 800 | '2-1': { 801 | '2-1-a': '2-1-a\n', 802 | '2-1-b': '2-1-b\n' 803 | }, 804 | '2-2': { 805 | '2-2-a': '2-2-a\n', 806 | '2-2-b': '2-2-b\n' 807 | }, 808 | '2-a': '2-a\n', 809 | '2-b': '2-b\n' 810 | }, 811 | 'a': 'a\n', 812 | 'b': 'b\n' 813 | }; 814 | expect(actual).to.eql(expected); 815 | }); 816 | }); 817 | }); 818 | 819 | it('should filter output files via glob', function() { 820 | return copy( 821 | getSourcePath('nested-directory'), 822 | getDestinationPath(), 823 | { 824 | filter: '2/**/*' 825 | } 826 | ).then(function(results) { 827 | return getOutputFiles() 828 | .then(function(files) { 829 | var actual, expected; 830 | actual = files; 831 | expected = { 832 | '2': { 833 | '2-1': { 834 | '2-1-a': '2-1-a\n', 835 | '2-1-b': '2-1-b\n' 836 | }, 837 | '2-2': { 838 | '2-2-a': '2-2-a\n', 839 | '2-2-b': '2-2-b\n' 840 | }, 841 | '2-a': '2-a\n', 842 | '2-b': '2-b\n' 843 | } 844 | }; 845 | expect(actual).to.eql(expected); 846 | }); 847 | }); 848 | }); 849 | 850 | it('should combine multiple filters from arrays', function() { 851 | return copy( 852 | getSourcePath('nested-directory'), 853 | getDestinationPath(), 854 | { 855 | filter: [ 856 | '1/**/*', 857 | '!1/1-1/**/*', 858 | /^2\/(?!2-1\/).*$/, 859 | function(filePath) { 860 | return filePath === 'a'; 861 | } 862 | ] 863 | } 864 | ).then(function(results) { 865 | return getOutputFiles() 866 | .then(function(files) { 867 | var actual, expected; 868 | actual = files; 869 | expected = { 870 | '1': { 871 | '1-1': {}, 872 | '1-2': { 873 | '1-2-a': '1-2-a\n', 874 | '1-2-b': '1-2-b\n' 875 | }, 876 | '1-a': '1-a\n', 877 | '1-b': '1-b\n' 878 | }, 879 | '2': { 880 | '2-1': { 881 | }, 882 | '2-2': { 883 | '2-2-a': '2-2-a\n', 884 | '2-2-b': '2-2-b\n' 885 | }, 886 | '2-a': '2-a\n', 887 | '2-b': '2-b\n' 888 | }, 889 | 'a': 'a\n' 890 | }; 891 | expect(actual).to.eql(expected); 892 | }); 893 | }); 894 | }); 895 | 896 | it('should rename files', function() { 897 | return copy( 898 | getSourcePath('nested-directory'), 899 | getDestinationPath(), 900 | { 901 | rename: function(path) { 902 | if (path === 'b') { return 'c'; } 903 | return path; 904 | } 905 | } 906 | ).then(function(results) { 907 | return getOutputFiles() 908 | .then(function(files) { 909 | var actual, expected; 910 | actual = files; 911 | expected = { 912 | '1': { 913 | '1-1': { 914 | '1-1-a': '1-1-a\n', 915 | '1-1-b': '1-1-b\n' 916 | }, 917 | '1-2': { 918 | '1-2-a': '1-2-a\n', 919 | '1-2-b': '1-2-b\n' 920 | }, 921 | '1-a': '1-a\n', 922 | '1-b': '1-b\n' 923 | }, 924 | '2': { 925 | '2-1': { 926 | '2-1-a': '2-1-a\n', 927 | '2-1-b': '2-1-b\n' 928 | }, 929 | '2-2': { 930 | '2-2-a': '2-2-a\n', 931 | '2-2-b': '2-2-b\n' 932 | }, 933 | '2-a': '2-a\n', 934 | '2-b': '2-b\n' 935 | }, 936 | 'a': 'a\n', 937 | 'c': 'b\n' 938 | }; 939 | expect(actual).to.eql(expected); 940 | }); 941 | }); 942 | }); 943 | 944 | it('should rename file paths', function() { 945 | return copy( 946 | getSourcePath('nested-directory'), 947 | getDestinationPath(), 948 | { 949 | rename: function(path) { 950 | return path.replace(/^2/, '3').replace(/[\/\\]2/g, '/3'); 951 | } 952 | } 953 | ).then(function(results) { 954 | return getOutputFiles() 955 | .then(function(files) { 956 | var actual, expected; 957 | actual = files; 958 | expected = { 959 | '1': { 960 | '1-1': { 961 | '1-1-a': '1-1-a\n', 962 | '1-1-b': '1-1-b\n' 963 | }, 964 | '1-2': { 965 | '1-2-a': '1-2-a\n', 966 | '1-2-b': '1-2-b\n' 967 | }, 968 | '1-a': '1-a\n', 969 | '1-b': '1-b\n' 970 | }, 971 | '3': { 972 | '3-1': { 973 | '3-1-a': '2-1-a\n', 974 | '3-1-b': '2-1-b\n' 975 | }, 976 | '3-2': { 977 | '3-2-a': '2-2-a\n', 978 | '3-2-b': '2-2-b\n' 979 | }, 980 | '3-a': '2-a\n', 981 | '3-b': '2-b\n' 982 | }, 983 | 'a': 'a\n', 984 | 'b': 'b\n' 985 | }; 986 | expect(actual).to.eql(expected); 987 | }); 988 | }); 989 | }); 990 | 991 | it('should rename files into parent paths', function() { 992 | return copy( 993 | getSourcePath('nested-directory'), 994 | getDestinationPath('parent'), 995 | { 996 | rename: function(path) { 997 | return path.replace(/^2/, '../3').replace(/[\/\\]2/g, '/3'); 998 | } 999 | } 1000 | ).then(function(results) { 1001 | return getOutputFiles() 1002 | .then(function(files) { 1003 | var actual, expected; 1004 | actual = files; 1005 | expected = { 1006 | 'parent': { 1007 | '1': { 1008 | '1-1': { 1009 | '1-1-a': '1-1-a\n', 1010 | '1-1-b': '1-1-b\n' 1011 | }, 1012 | '1-2': { 1013 | '1-2-a': '1-2-a\n', 1014 | '1-2-b': '1-2-b\n' 1015 | }, 1016 | '1-a': '1-a\n', 1017 | '1-b': '1-b\n' 1018 | }, 1019 | 'a': 'a\n', 1020 | 'b': 'b\n' 1021 | }, 1022 | '3': { 1023 | '3-1': { 1024 | '3-1-a': '2-1-a\n', 1025 | '3-1-b': '2-1-b\n' 1026 | }, 1027 | '3-2': { 1028 | '3-2-a': '2-2-a\n', 1029 | '3-2-b': '2-2-b\n' 1030 | }, 1031 | '3-a': '2-a\n', 1032 | '3-b': '2-b\n' 1033 | } 1034 | }; 1035 | expect(actual).to.eql(expected); 1036 | }); 1037 | }); 1038 | }); 1039 | 1040 | it('should rename files into child paths', function() { 1041 | return copy( 1042 | getSourcePath('nested-directory'), 1043 | getDestinationPath(), 1044 | { 1045 | rename: function(path) { 1046 | return path.replace(/^2/, 'child/3').replace(/[\/\\]2/g, '/3'); 1047 | } 1048 | } 1049 | ).then(function(results) { 1050 | return getOutputFiles() 1051 | .then(function(files) { 1052 | var actual, expected; 1053 | actual = files; 1054 | expected = { 1055 | '1': { 1056 | '1-1': { 1057 | '1-1-a': '1-1-a\n', 1058 | '1-1-b': '1-1-b\n' 1059 | }, 1060 | '1-2': { 1061 | '1-2-a': '1-2-a\n', 1062 | '1-2-b': '1-2-b\n' 1063 | }, 1064 | '1-a': '1-a\n', 1065 | '1-b': '1-b\n' 1066 | }, 1067 | 'a': 'a\n', 1068 | 'b': 'b\n', 1069 | 'child': { 1070 | '3': { 1071 | '3-1': { 1072 | '3-1-a': '2-1-a\n', 1073 | '3-1-b': '2-1-b\n' 1074 | }, 1075 | '3-2': { 1076 | '3-2-a': '2-2-a\n', 1077 | '3-2-b': '2-2-b\n' 1078 | }, 1079 | '3-a': '2-a\n', 1080 | '3-b': '2-b\n' 1081 | } 1082 | } 1083 | }; 1084 | expect(actual).to.eql(expected); 1085 | }); 1086 | }); 1087 | }); 1088 | 1089 | it('should filter files before renaming', function() { 1090 | return copy( 1091 | getSourcePath('nested-directory'), 1092 | getDestinationPath(), 1093 | { 1094 | filter: function(path) { 1095 | return path === 'a'; 1096 | }, 1097 | rename: function(path) { 1098 | if (path === 'a') { return 'b'; } 1099 | return path; 1100 | } 1101 | } 1102 | ).then(function(results) { 1103 | return getOutputFiles() 1104 | .then(function(files) { 1105 | var actual, expected; 1106 | actual = files; 1107 | expected = { 1108 | 'b': 'a\n' 1109 | }; 1110 | expect(actual).to.eql(expected); 1111 | }); 1112 | }); 1113 | }); 1114 | 1115 | it('should transform files', function() { 1116 | var transformArguments = null; 1117 | return copy( 1118 | getSourcePath('file'), 1119 | getDestinationPath('file'), 1120 | { 1121 | transform: function(src, dest, stats) { 1122 | transformArguments = arguments; 1123 | return through(function(chunk, enc, done) { 1124 | done(null, chunk.toString().toUpperCase()); 1125 | }); 1126 | } 1127 | } 1128 | ).then(function(results) { 1129 | return getOutputFiles() 1130 | .then(function(files) { 1131 | var actual, expected; 1132 | actual = files; 1133 | expected = { 1134 | 'file': 'HELLO, WORLD!\n' 1135 | }; 1136 | expect(actual).to.eql(expected); 1137 | expect(transformArguments).to.exist; 1138 | expect(transformArguments.length).to.eql(3); 1139 | expect(transformArguments[0]).to.equal(getSourcePath('file')); 1140 | expect(transformArguments[1]).to.equal(getDestinationPath('file')); 1141 | expect(transformArguments[2]).to.exist; 1142 | expect(transformArguments[2].isFile).to.exist; 1143 | expect(transformArguments[2].isFile()).to.be.truthy; 1144 | }); 1145 | }); 1146 | }); 1147 | 1148 | it('should allow transform to be skipped', function() { 1149 | return copy( 1150 | getSourcePath('directory'), 1151 | getDestinationPath('directory'), 1152 | { 1153 | transform: function(src, dest, stats) { 1154 | if (path.basename(src) === 'b') { return null; } 1155 | return through(function(chunk, enc, done) { 1156 | done(null, chunk.toString().toUpperCase()); 1157 | }); 1158 | } 1159 | } 1160 | ).then(function(results) { 1161 | return getOutputFiles() 1162 | .then(function(files) { 1163 | var actual, expected; 1164 | actual = files; 1165 | expected = { 1166 | 'directory': { 1167 | 'a': 'A\n', 1168 | 'b': 'b\n', 1169 | 'c': 'C\n' 1170 | } 1171 | }; 1172 | expect(actual).to.eql(expected); 1173 | }); 1174 | }); 1175 | }); 1176 | 1177 | it('should throw an error on a transform stream error', function() { 1178 | var actual, expected; 1179 | expected = 'Stream error'; 1180 | actual = copy( 1181 | getSourcePath('file'), 1182 | getDestinationPath('file'), 1183 | { 1184 | transform: function(src, dest, stats) { 1185 | return through(function(chunk, enc, done) { 1186 | done(new Error('Stream error')); 1187 | }); 1188 | } 1189 | } 1190 | ); 1191 | return expect(actual).to.be.rejectedWith(expected); 1192 | }); 1193 | 1194 | it('should throw the original error on nested file error', function() { 1195 | return copy( 1196 | getSourcePath('nested-directory'), 1197 | getDestinationPath('nested-directory'), 1198 | { 1199 | transform: function(src, dest, stats) { 1200 | return through(function(chunk, enc, done) { 1201 | if (src === getSourcePath('nested-directory/1/1-1/1-1-a')) { 1202 | done(new Error('Stream error')); 1203 | } else { 1204 | done(null, chunk); 1205 | } 1206 | }); 1207 | } 1208 | } 1209 | ).then(function() { 1210 | throw new Error('Should throw error'); 1211 | }).catch(function(error) { 1212 | var actual, expected; 1213 | 1214 | actual = error.name; 1215 | expected = 'Error'; 1216 | expect(actual).to.equal(expected); 1217 | 1218 | actual = error.message; 1219 | expected = 'Stream error'; 1220 | expect(actual).to.equal(expected); 1221 | }); 1222 | }); 1223 | }); 1224 | 1225 | describe('argument validation', function() { 1226 | 1227 | it('should throw an error if the source path does not exist', function() { 1228 | var actual, expected; 1229 | actual = copy( 1230 | 'nonexistent', 1231 | getDestinationPath() 1232 | ); 1233 | expected = 'ENOENT'; 1234 | return expect(actual).to.be.rejectedWith(expected); 1235 | }); 1236 | 1237 | it('should throw an error if the destination path exists (single file)', function() { 1238 | fs.writeFileSync(getDestinationPath('file'), ''); 1239 | 1240 | var actual, expected; 1241 | actual = copy( 1242 | getSourcePath('file'), 1243 | getDestinationPath('file') 1244 | ); 1245 | expected = 'EEXIST'; 1246 | return expect(actual).to.be.rejectedWith(expected); 1247 | }); 1248 | 1249 | it('should not throw an error if an nonconflicting file exists within the destination path (single file)', function() { 1250 | fs.writeFileSync(getDestinationPath('pre-existing'), ''); 1251 | 1252 | return copy( 1253 | getSourcePath('file'), 1254 | getDestinationPath('file') 1255 | ).then(function(results) { 1256 | return getOutputFiles() 1257 | .then(function(files) { 1258 | var actual, expected; 1259 | actual = files; 1260 | expected = { 1261 | 'pre-existing': '', 1262 | 'file': 'Hello, world!\n' 1263 | }; 1264 | expect(actual).to.eql(expected); 1265 | }); 1266 | }); 1267 | }); 1268 | 1269 | it('should throw an error if a conflicting file exists within the destination path (directory)', function() { 1270 | fs.writeFileSync(getDestinationPath('a'), ''); 1271 | 1272 | var actual, expected; 1273 | actual = copy( 1274 | getSourcePath('nested-directory'), 1275 | getDestinationPath() 1276 | ); 1277 | expected = 'EEXIST'; 1278 | return expect(actual).to.be.rejectedWith(expected); 1279 | }); 1280 | 1281 | it('should not throw an error if an nonconflicting file exists within the destination path (directory)', function() { 1282 | fs.writeFileSync(getDestinationPath('pre-existing'), ''); 1283 | 1284 | return copy( 1285 | getSourcePath('nested-directory'), 1286 | getDestinationPath() 1287 | ).then(function(results) { 1288 | return getOutputFiles() 1289 | .then(function(files) { 1290 | var actual, expected; 1291 | actual = files; 1292 | expected = { 1293 | 'pre-existing': '', 1294 | '1': { 1295 | '1-1': { 1296 | '1-1-a': '1-1-a\n', 1297 | '1-1-b': '1-1-b\n' 1298 | }, 1299 | '1-2': { 1300 | '1-2-a': '1-2-a\n', 1301 | '1-2-b': '1-2-b\n' 1302 | }, 1303 | '1-a': '1-a\n', 1304 | '1-b': '1-b\n' 1305 | }, 1306 | '2': { 1307 | '2-1': { 1308 | '2-1-a': '2-1-a\n', 1309 | '2-1-b': '2-1-b\n' 1310 | }, 1311 | '2-2': { 1312 | '2-2-a': '2-2-a\n', 1313 | '2-2-b': '2-2-b\n' 1314 | }, 1315 | '2-a': '2-a\n', 1316 | '2-b': '2-b\n' 1317 | }, 1318 | 'a': 'a\n', 1319 | 'b': 'b\n' 1320 | }; 1321 | expect(actual).to.eql(expected); 1322 | }); 1323 | }); 1324 | }); 1325 | }); 1326 | 1327 | describe('callbacks', function() { 1328 | it('should invoke the callback on success (without options)', function(done) { 1329 | copy( 1330 | getSourcePath('file'), 1331 | getDestinationPath('file'), 1332 | function(error, results) { 1333 | expect(results).to.exist; 1334 | expect(error).not.to.exist; 1335 | 1336 | checkResults(results, { 1337 | 'file': 'file' 1338 | }); 1339 | 1340 | done(); 1341 | } 1342 | ); 1343 | }); 1344 | 1345 | it('should invoke the callback on failure (without options)', function(done) { 1346 | copy( 1347 | 'nonexistent', 1348 | getDestinationPath(), 1349 | function(error, results) { 1350 | expect(error).to.exist; 1351 | expect(results).not.to.exist; 1352 | 1353 | var actual, expected; 1354 | actual = error.code; 1355 | expected = 'ENOENT'; 1356 | expect(actual).to.equal(expected); 1357 | 1358 | done(); 1359 | } 1360 | ); 1361 | }); 1362 | 1363 | it('should invoke the callback on success (with options)', function(done) { 1364 | copy( 1365 | getSourcePath('file'), 1366 | getDestinationPath('file'), 1367 | { }, 1368 | function(error, results) { 1369 | expect(results).to.exist; 1370 | expect(error).not.to.exist; 1371 | 1372 | checkResults(results, { 1373 | 'file': 'file' 1374 | }); 1375 | 1376 | done(); 1377 | } 1378 | ); 1379 | }); 1380 | it('should invoke the callback on failure (with options)', function(done) { 1381 | copy( 1382 | 'nonexistent', 1383 | getDestinationPath(), 1384 | {}, 1385 | function(error, results) { 1386 | expect(error).to.exist; 1387 | expect(results).not.to.exist; 1388 | 1389 | var actual, expected; 1390 | actual = error.code; 1391 | expected = 'ENOENT'; 1392 | expect(actual).to.equal(expected); 1393 | 1394 | done(); 1395 | } 1396 | ); 1397 | }); 1398 | }); 1399 | 1400 | describe('events', function() { 1401 | it('should export event names and values', function() { 1402 | var actual, expected; 1403 | actual = copy.events; 1404 | expected = { 1405 | ERROR: 'error', 1406 | COMPLETE: 'complete', 1407 | CREATE_DIRECTORY_START: 'createDirectoryStart', 1408 | CREATE_DIRECTORY_ERROR: 'createDirectoryError', 1409 | CREATE_DIRECTORY_COMPLETE: 'createDirectoryComplete', 1410 | CREATE_SYMLINK_START: 'createSymlinkStart', 1411 | CREATE_SYMLINK_ERROR: 'createSymlinkError', 1412 | CREATE_SYMLINK_COMPLETE: 'createSymlinkComplete', 1413 | COPY_FILE_START: 'copyFileStart', 1414 | COPY_FILE_ERROR: 'copyFileError', 1415 | COPY_FILE_COMPLETE: 'copyFileComplete' 1416 | }; 1417 | expect(actual).to.eql(expected); 1418 | }); 1419 | 1420 | it('should allow event listeners to be chained', function() { 1421 | var copier = copy( 1422 | getSourcePath('file'), 1423 | getDestinationPath('file') 1424 | ); 1425 | expect(function() { 1426 | copier 1427 | .on('error', function() {}) 1428 | .on('complete', function() {}) 1429 | .on('createDirectoryStart', function() {}) 1430 | .on('createDirectoryError', function() {}) 1431 | .on('createDirectoryComplete', function() {}) 1432 | .on('createSymlinkStart', function() {}) 1433 | .on('createSymlinkError', function() {}) 1434 | .on('createSymlinkComplete', function() {}) 1435 | .on('copyFileStart', function() {}) 1436 | .on('copyFileError', function() {}) 1437 | .on('copyFileComplete', function() {}) 1438 | .then(function() { }) 1439 | .catch(function() { }); 1440 | }).to.not.throw(); 1441 | return copier; 1442 | }); 1443 | 1444 | it('should emit file copy events', function() { 1445 | var copier = copy( 1446 | getSourcePath('file'), 1447 | getDestinationPath('file') 1448 | ); 1449 | var events = listenTo(copier, COPY_EVENTS); 1450 | return copier.then(function() { 1451 | var actual, expected; 1452 | 1453 | var eventNames = events.map(function(event) { 1454 | return event.name; 1455 | }); 1456 | 1457 | actual = eventNames; 1458 | expected = ['copyFileStart', 'copyFileComplete', 'complete']; 1459 | expect(actual).to.eql(expected); 1460 | 1461 | var completeEvent = events.filter(function(event) { 1462 | return event.name === 'complete'; 1463 | })[0]; 1464 | var eventArgs = completeEvent.args; 1465 | 1466 | actual = eventArgs.length; 1467 | expected = 1; 1468 | expect(actual).to.equal(expected); 1469 | 1470 | var results = eventArgs[0]; 1471 | checkResults(results, { 1472 | 'file': 'file' 1473 | }); 1474 | }); 1475 | }); 1476 | 1477 | it('should emit error events', function() { 1478 | fs.writeFileSync(getDestinationPath('file'), ''); 1479 | 1480 | var copier = copy( 1481 | getSourcePath('file'), 1482 | getDestinationPath('file') 1483 | ); 1484 | var events = listenTo(copier, COPY_EVENTS); 1485 | return copier.catch(function() { 1486 | var actual, expected; 1487 | 1488 | var eventNames = events.map(function(event) { 1489 | return event.name; 1490 | }); 1491 | 1492 | actual = eventNames; 1493 | expected = ['error']; 1494 | expect(actual).to.eql(expected); 1495 | 1496 | var errorEvent = events.filter(function(event) { 1497 | return event.name === 'error'; 1498 | })[0]; 1499 | var eventArgs = errorEvent.args; 1500 | 1501 | actual = eventArgs.length; 1502 | expected = 2; 1503 | expect(actual).to.equal(expected); 1504 | 1505 | var error = eventArgs[0]; 1506 | var copyOperation = eventArgs[1]; 1507 | 1508 | actual = error.code; 1509 | expected = 'EEXIST'; 1510 | expect(actual).to.equal(expected); 1511 | 1512 | actual = copyOperation.src; 1513 | expected = getSourcePath('file'); 1514 | expect(actual).to.equal(expected); 1515 | 1516 | actual = copyOperation.dest; 1517 | expected = getDestinationPath('file'); 1518 | expect(actual).to.equal(expected); 1519 | }); 1520 | }); 1521 | 1522 | it('should emit file copy error events', function() { 1523 | var copier = copy( 1524 | getSourcePath('file'), 1525 | getDestinationPath('file'), 1526 | { 1527 | transform: function(src, dest, stats) { 1528 | return through(function(chunk, enc, done) { 1529 | done(new Error('Stream error')); 1530 | }); 1531 | } 1532 | } 1533 | ); 1534 | var events = listenTo(copier, COPY_EVENTS); 1535 | return copier.catch(function() { 1536 | var actual, expected; 1537 | 1538 | var eventNames = events.map(function(event) { 1539 | return event.name; 1540 | }); 1541 | 1542 | actual = eventNames; 1543 | expected = ['copyFileStart', 'copyFileError', 'error']; 1544 | expect(actual).to.eql(expected); 1545 | 1546 | 1547 | var errorEvent = events.filter(function(event) { 1548 | return event.name === 'error'; 1549 | })[0]; 1550 | var eventArgs = errorEvent.args; 1551 | 1552 | actual = eventArgs.length; 1553 | expected = 2; 1554 | expect(actual).to.equal(expected); 1555 | 1556 | var error = eventArgs[0]; 1557 | var copyOperation = eventArgs[1]; 1558 | 1559 | actual = error.message; 1560 | expected = 'Stream error'; 1561 | expect(actual).to.equal(expected); 1562 | 1563 | actual = copyOperation.src; 1564 | expected = getSourcePath('file'); 1565 | expect(actual).to.equal(expected); 1566 | 1567 | actual = copyOperation.dest; 1568 | expected = getDestinationPath('file'); 1569 | expect(actual).to.equal(expected); 1570 | 1571 | 1572 | var fileErrorEvent = events.filter(function(event) { 1573 | return event.name === 'copyFileError'; 1574 | })[0]; 1575 | var fileErrorEventArgs = fileErrorEvent.args; 1576 | 1577 | actual = fileErrorEventArgs.length; 1578 | expected = 2; 1579 | expect(actual).to.equal(expected); 1580 | 1581 | var fileError = fileErrorEventArgs[0]; 1582 | var fileCopyOperation = fileErrorEventArgs[1]; 1583 | 1584 | actual = fileError.message; 1585 | expected = 'Stream error'; 1586 | expect(actual).to.equal(expected); 1587 | 1588 | actual = fileCopyOperation.src; 1589 | expected = getSourcePath('file'); 1590 | expect(actual).to.equal(expected); 1591 | 1592 | actual = fileCopyOperation.dest; 1593 | expected = getDestinationPath('file'); 1594 | expect(actual).to.equal(expected); 1595 | 1596 | actual = fileCopyOperation.stats && fileCopyOperation.stats.isDirectory; 1597 | expected = 'function'; 1598 | expect(actual).to.be.a(expected); 1599 | }); 1600 | }); 1601 | 1602 | it('should emit directory copy events', function() { 1603 | var copier = copy( 1604 | getSourcePath('empty'), 1605 | getDestinationPath('empty') 1606 | ); 1607 | var events = listenTo(copier, COPY_EVENTS); 1608 | return copier.then(function() { 1609 | var actual, expected; 1610 | 1611 | var eventNames = events.map(function(event) { 1612 | return event.name; 1613 | }); 1614 | 1615 | actual = eventNames; 1616 | expected = ['createDirectoryStart', 'createDirectoryComplete', 'complete']; 1617 | expect(actual).to.eql(expected); 1618 | 1619 | var completeEvent = events.filter(function(event) { 1620 | return event.name === 'complete'; 1621 | })[0]; 1622 | var eventArgs = completeEvent.args; 1623 | 1624 | actual = eventArgs.length; 1625 | expected = 1; 1626 | expect(actual).to.equal(expected); 1627 | 1628 | var results = eventArgs[0]; 1629 | checkResults(results, { 1630 | 'empty': 'dir' 1631 | }); 1632 | }); 1633 | }); 1634 | 1635 | it('should emit directory copy error events', function() { 1636 | var errors = {}; 1637 | errors[getDestinationPath('empty')] = new Error('Test error'); 1638 | var unmockMkdirp = mockMkdirp(copy, errors); 1639 | 1640 | var copier = copy( 1641 | getSourcePath('empty'), 1642 | getDestinationPath('empty') 1643 | ); 1644 | var events = listenTo(copier, COPY_EVENTS); 1645 | return copier.catch(function() { 1646 | var actual, expected; 1647 | 1648 | var eventNames = events.map(function(event) { 1649 | return event.name; 1650 | }); 1651 | 1652 | actual = eventNames; 1653 | expected = ['createDirectoryStart', 'createDirectoryError', 'error']; 1654 | expect(actual).to.eql(expected); 1655 | 1656 | 1657 | var errorEvent = events.filter(function(event) { 1658 | return event.name === 'error'; 1659 | })[0]; 1660 | var eventArgs = errorEvent.args; 1661 | 1662 | actual = eventArgs.length; 1663 | expected = 2; 1664 | expect(actual).to.equal(expected); 1665 | 1666 | var error = eventArgs[0]; 1667 | var copyOperation = eventArgs[1]; 1668 | 1669 | actual = error.message; 1670 | expected = 'Test error'; 1671 | expect(actual).to.equal(expected); 1672 | 1673 | actual = copyOperation.src; 1674 | expected = getSourcePath('empty'); 1675 | expect(actual).to.equal(expected); 1676 | 1677 | actual = copyOperation.dest; 1678 | expected = getDestinationPath('empty'); 1679 | expect(actual).to.equal(expected); 1680 | 1681 | 1682 | var directoryErrorEvent = events.filter(function(event) { 1683 | return event.name === 'createDirectoryError'; 1684 | })[0]; 1685 | var directoryErrorEventArgs = directoryErrorEvent.args; 1686 | 1687 | actual = directoryErrorEventArgs.length; 1688 | expected = 2; 1689 | expect(actual).to.equal(expected); 1690 | 1691 | var directoryError = directoryErrorEventArgs[0]; 1692 | var directoryCopyOperation = directoryErrorEventArgs[1]; 1693 | 1694 | actual = directoryError.message; 1695 | expected = 'Test error'; 1696 | expect(actual).to.equal(expected); 1697 | 1698 | actual = directoryCopyOperation.src; 1699 | expected = getSourcePath('empty'); 1700 | expect(actual).to.equal(expected); 1701 | 1702 | actual = directoryCopyOperation.dest; 1703 | expected = getDestinationPath('empty'); 1704 | expect(actual).to.equal(expected); 1705 | 1706 | actual = directoryCopyOperation.stats && directoryCopyOperation.stats.isDirectory; 1707 | expected = 'function'; 1708 | expect(actual).to.be.a(expected); 1709 | }) 1710 | .then(function() { 1711 | unmockMkdirp(); 1712 | }) 1713 | .catch(function() { 1714 | unmockMkdirp(); 1715 | }); 1716 | }); 1717 | 1718 | it('should emit symlink copy error events', function() { 1719 | createSymbolicLink('.', getSourcePath('symlink'), 'dir'); 1720 | var unmockSymlink = mockSymlink(copy); 1721 | 1722 | var copier = copy( 1723 | getSourcePath('symlink'), 1724 | getDestinationPath('symlink') 1725 | ); 1726 | var events = listenTo(copier, COPY_EVENTS); 1727 | return copier.catch(function() { 1728 | var actual, expected; 1729 | 1730 | var eventNames = events.map(function(event) { 1731 | return event.name; 1732 | }); 1733 | 1734 | actual = eventNames; 1735 | expected = ['createSymlinkStart', 'createSymlinkError', 'error']; 1736 | expect(actual).to.eql(expected); 1737 | 1738 | 1739 | var errorEvent = events.filter(function(event) { 1740 | return event.name === 'error'; 1741 | })[0]; 1742 | var eventArgs = errorEvent.args; 1743 | 1744 | actual = eventArgs.length; 1745 | expected = 2; 1746 | expect(actual).to.equal(expected); 1747 | 1748 | var error = eventArgs[0]; 1749 | var copyOperation = eventArgs[1]; 1750 | 1751 | actual = error.message; 1752 | expected = 'Test error'; 1753 | expect(actual).to.equal(expected); 1754 | 1755 | actual = copyOperation.src; 1756 | expected = getSourcePath('symlink'); 1757 | expect(actual).to.equal(expected); 1758 | 1759 | actual = copyOperation.dest; 1760 | expected = getDestinationPath('symlink'); 1761 | expect(actual).to.equal(expected); 1762 | 1763 | 1764 | var symlinkErrorEvent = events.filter(function(event) { 1765 | return event.name === 'createSymlinkError'; 1766 | })[0]; 1767 | var symlinkErrorEventArgs = symlinkErrorEvent.args; 1768 | 1769 | actual = symlinkErrorEventArgs.length; 1770 | expected = 2; 1771 | expect(actual).to.equal(expected); 1772 | 1773 | var symlinkError = symlinkErrorEventArgs[0]; 1774 | var symlinkCopyOperation = symlinkErrorEventArgs[1]; 1775 | 1776 | actual = symlinkError.message; 1777 | expected = 'Test error'; 1778 | expect(actual).to.equal(expected); 1779 | 1780 | actual = symlinkCopyOperation.src; 1781 | expected = getSourcePath('symlink'); 1782 | expect(actual).to.equal(expected); 1783 | 1784 | actual = symlinkCopyOperation.dest; 1785 | expected = getDestinationPath('symlink'); 1786 | expect(actual).to.equal(expected); 1787 | 1788 | actual = symlinkCopyOperation.stats && symlinkCopyOperation.stats.isDirectory; 1789 | expected = 'function'; 1790 | expect(actual).to.be.a(expected); 1791 | }) 1792 | .then(function() { 1793 | unmockSymlink(); 1794 | }) 1795 | .catch(function(error) { 1796 | unmockSymlink(); 1797 | throw error; 1798 | }); 1799 | }); 1800 | 1801 | it('should emit symlink copy events', function() { 1802 | createSymbolicLink('.', getSourcePath('symlink'), 'dir'); 1803 | var copier = copy( 1804 | getSourcePath('symlink'), 1805 | getDestinationPath('symlink') 1806 | ); 1807 | var events = listenTo(copier, COPY_EVENTS); 1808 | return copier.then(function() { 1809 | var actual, expected; 1810 | 1811 | var eventNames = events.map(function(event) { 1812 | return event.name; 1813 | }); 1814 | 1815 | actual = eventNames; 1816 | expected = ['createSymlinkStart', 'createSymlinkComplete', 'complete']; 1817 | expect(actual).to.eql(expected); 1818 | 1819 | var completeEvent = events.filter(function(event) { 1820 | return event.name === 'complete'; 1821 | })[0]; 1822 | var eventArgs = completeEvent.args; 1823 | 1824 | actual = eventArgs.length; 1825 | expected = 1; 1826 | expect(actual).to.equal(expected); 1827 | 1828 | var results = eventArgs[0]; 1829 | checkResults(results, { 1830 | 'symlink': 'symlink' 1831 | }); 1832 | }); 1833 | }); 1834 | }); 1835 | }); 1836 | --------------------------------------------------------------------------------