Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add filter option #66

Merged
merged 15 commits into from Mar 7, 2020
49 changes: 49 additions & 0 deletions index.d.ts
Expand Up @@ -2,6 +2,38 @@ import {GlobbyOptions} from 'globby';
import {Options as CpFileOptions} from 'cp-file';

declare namespace cpy {
interface SourceFile {
/**
Resolved path to file.
@example `/tmp/dir/foo.bar`
*/
readonly path: string,

stroncium marked this conversation as resolved.
Show resolved Hide resolved
/**
Relative path to file from `cwd`.
@example 'dir/foo.bar' if `cwd` was '/tmp'
*/
readonly relativePath: string,

/**
Filename.
@example `foo.bar`
*/
readonly name: string,

/**
Filename without extension.
@example `foo`
*/
readonly nameWithoutExtension: string,

/**
File extension.
@example `.bar`
stroncium marked this conversation as resolved.
Show resolved Hide resolved
*/
readonly extension: string,
}

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

/**
Function to filter files to copy. Should accept source file object as argument.
Return true to include, false to exclude. 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'
stroncium marked this conversation as resolved.
Show resolved Hide resolved
});
})();
```
*/
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
44 changes: 44 additions & 0 deletions readme.md
Expand Up @@ -106,6 +106,50 @@ Default: `true`

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

##### filter

Type: `Function`

Function to filter files to copy. Should accept source file object as argument.
Return true to include, false to exclude. 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`

Resolved path to file. Example: `/tmp/foo.bar`.

###### name

Type: `string`

Filename. Example: `foo.bar`.

###### nameWithoutExtension

Type: `string`

File name without extension. Example: `foo`.

###### extension

Type: `string`
stroncium marked this conversation as resolved.
Show resolved Hide resolved

File extension. Example: `.bar`.


## 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