Skip to content

Commit

Permalink
Add filter option (#66)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Yanis Benson and sindresorhus committed Mar 7, 2020
1 parent 85831f1 commit 7df88b8
Show file tree
Hide file tree
Showing 6 changed files with 221 additions and 15 deletions.
57 changes: 57 additions & 0 deletions index.d.ts
Expand Up @@ -2,6 +2,43 @@ import {GlobbyOptions} from 'globby';
import {Options as CpFileOptions} from 'cp-file';

declare namespace cpy {
interface SourceFile {
/**
Resolved path to the file.
@example '/tmp/dir/foo.js'
*/
readonly path: string;

/**
Relative path to the file from `cwd`.
@example 'dir/foo.js' if `cwd` was '/tmp'
*/
readonly relativePath: string;

/**
Filename with extension.
@example 'foo.js'
*/
readonly name: string;

/**
Filename without extension.
@example 'foo'
*/
readonly nameWithoutExtension: string;

/**
File extension.
@example 'js'
*/
readonly extension: string;
}

interface Options extends Readonly<GlobbyOptions>, CpFileOptions {
/**
Working directory to find source files.
Expand Down Expand Up @@ -46,6 +83,26 @@ declare namespace cpy {
@default true
*/
readonly ignoreJunk?: boolean;

/**
Function to filter files to copy.
Receives a source file object as the first argument.
Return true to include, false to exclude. You can also return a Promise that resolves to true or false.
@example
```
import cpy = require('cpy');
(async () => {
await cpy('foo', 'destination', {
filter: file => file.extension !== '.nocopy'
});
})();
```
*/
readonly filter?: (file: SourceFile) => (boolean | Promise<boolean>);
}

interface ProgressData {
Expand Down
62 changes: 48 additions & 14 deletions index.js
Expand Up @@ -2,19 +2,40 @@
const EventEmitter = require('events');
const path = require('path');
const os = require('os');
const pAll = require('p-all');
const pMap = require('p-map');
const arrify = require('arrify');
const globby = require('globby');
const hasGlob = require('has-glob');
const cpFile = require('cp-file');
const junk = require('junk');
const pFilter = require('p-filter');
const CpyError = require('./cpy-error');

const defaultOptions = {
ignoreJunk: true
};

const preprocessSourcePath = (source, options) => options.cwd ? path.resolve(options.cwd, source) : source;
class SourceFile {
constructor(relativePath, path) {
this.path = path;
this.relativePath = relativePath;
Object.freeze(this);
}

get name() {
return path.basename(this.relativePath);
}

get nameWithoutExtension() {
return path.basename(this.relativePath, path.extname(this.relativePath));
}

get extension() {
return path.extname(this.relativePath).slice(1);
}
}

const preprocessSourcePath = (source, options) => path.resolve(options.cwd ? options.cwd : process.cwd(), source);

const preprocessDestinationPath = (source, destination, options) => {
let basename = path.basename(source);
Expand Down Expand Up @@ -74,6 +95,22 @@ module.exports = (source, destination, {
throw new CpyError(`Cannot copy \`${source}\`: the file doesn't exist`);
}

let sources = files.map(sourcePath => new SourceFile(sourcePath, preprocessSourcePath(sourcePath, options)));

if (options.filter !== undefined) {
const filteredSources = await pFilter(sources, options.filter, {concurrency: 1024});
sources = filteredSources;
}

if (sources.length === 0) {
progressEmitter.emit('progress', {
totalFiles: 0,
percent: 1,
completedFiles: 0,
completedSize: 0
});
}

const fileProgressHandler = event => {
const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0};

Expand All @@ -99,20 +136,17 @@ module.exports = (source, destination, {
}
};

return pAll(files.map(sourcePath => {
return async () => {
const from = preprocessSourcePath(sourcePath, options);
const to = preprocessDestinationPath(sourcePath, destination, options);
return pMap(sources, async source => {
const to = preprocessDestinationPath(source.relativePath, destination, options);

try {
await cpFile(from, to, options).on('progress', fileProgressHandler);
} catch (error) {
throw new CpyError(`Cannot copy from \`${from}\` to \`${to}\`: ${error.message}`, error);
}
try {
await cpFile(source.path, to, options).on('progress', fileProgressHandler);
} catch (error) {
throw new CpyError(`Cannot copy from \`${source.relativePath}\` to \`${to}\`: ${error.message}`, error);
}

return to;
};
}), {concurrency});
return to;
}, {concurrency});
})();

promise.on = (...arguments_) => {
Expand Down
6 changes: 6 additions & 0 deletions index.test-d.ts
Expand Up @@ -27,6 +27,12 @@ expectType<Promise<string[]> & ProgressEmitter>(
cpy('foo.js', 'destination', {concurrency: 2})
);

expectType<Promise<string[]> & ProgressEmitter>(
cpy('foo.js', 'destination', {filter: (file: cpy.SourceFile) => true})
);
expectType<Promise<string[]> & ProgressEmitter>(
cpy('foo.js', 'destination', {filter: async (file: cpy.SourceFile) => true})
);

expectType<Promise<string[]>>(
cpy('foo.js', 'destination').on('progress', progress => {
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -49,7 +49,9 @@
"has-glob": "^1.0.0",
"junk": "^3.1.0",
"nested-error-stacks": "^2.1.0",
"p-all": "^2.1.0"
"p-all": "^2.1.0",
"p-filter": "^2.1.0",
"p-map": "^3.0.0"
},
"devDependencies": {
"ava": "^2.1.0",
Expand Down
57 changes: 57 additions & 0 deletions readme.md
Expand Up @@ -106,6 +106,63 @@ Default: `true`

Ignores [junk](https://github.com/sindresorhus/junk) files.

##### filter

Type: `Function`

Function to filter files to copy.

Receives a source file object as the first argument.

Return true to include, false to exclude. You can also return a Promise that resolves to true or false.

```js
const cpy = require('cpy');

(async () => {
await cpy('foo', 'destination', {
filter: file => file.extension !== '.nocopy'
});
})();
```

##### Source file object

###### path

Type: `string`\
Example: `'/tmp/dir/foo.js'`

Resolved path to the file.

###### relativePath

Type: `string`\
Example: `'dir/foo.js'` if `cwd` was `'/tmp'`

Relative path to the file from `cwd`.

###### name

Type: `string`\
Example: `'foo.js'`

Filename with extension.

###### nameWithoutExtension

Type: `string`\
Example: `'foo'`

Filename without extension.

###### extension

Type: `string`\
Example: `'js'`

File extension.

## Progress reporting

### cpy.on('progress', handler)
Expand Down
50 changes: 50 additions & 0 deletions test.js
Expand Up @@ -54,6 +54,56 @@ test('throws on invalid concurrency value', async t => {
await t.throwsAsync(cpy(['license', 'package.json'], t.context.tmp, {concurrency: 'foo'}));
});

test('copy array of files with filter', async t => {
await cpy(['license', 'package.json'], t.context.tmp, {
filter: file => {
if (file.path.endsWith('/license')) {
t.is(file.path, path.join(process.cwd(), 'license'));
t.is(file.relativePath, 'license');
t.is(file.name, 'license');
t.is(file.nameWithoutExtension, 'license');
t.is(file.extension, '');
} else if (file.path.endsWith('/package.json')) {
t.is(file.path, path.join(process.cwd(), 'package.json'));
t.is(file.relativePath, 'package.json');
t.is(file.name, 'package.json');
t.is(file.nameWithoutExtension, 'package');
t.is(file.extension, 'json');
}

return !file.path.endsWith('/license');
}
});

t.false(fs.existsSync(path.join(t.context.tmp, 'license')));
t.is(read('package.json'), read(t.context.tmp, 'package.json'));
});

test('copy array of files with async filter', async t => {
await cpy(['license', 'package.json'], t.context.tmp, {
filter: async file => {
if (file.path.endsWith('/license')) {
t.is(file.path, path.join(process.cwd(), 'license'));
t.is(file.relativePath, 'license');
t.is(file.name, 'license');
t.is(file.nameWithoutExtension, 'license');
t.is(file.extension, '');
} else if (file.path.endsWith('/package.json')) {
t.is(file.path, path.join(process.cwd(), 'package.json'));
t.is(file.relativePath, 'package.json');
t.is(file.name, 'package.json');
t.is(file.nameWithoutExtension, 'package');
t.is(file.extension, 'json');
}

return !file.path.endsWith('/license');
}
});

t.false(fs.existsSync(path.join(t.context.tmp, 'license')));
t.is(read('package.json'), read(t.context.tmp, 'package.json'));
});

test('cwd', async t => {
fs.mkdirSync(t.context.tmp);
fs.mkdirSync(path.join(t.context.tmp, 'cwd'));
Expand Down

0 comments on commit 7df88b8

Please sign in to comment.