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

stroncium marked this conversation as resolved.
Show resolved Hide resolved
/**
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'
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
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