Skip to content

Commit 7df88b8

Browse files
Yanis Bensonsindresorhus
andauthored
Add filter option (#66)
Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 85831f1 commit 7df88b8

File tree

6 files changed

+221
-15
lines changed

6 files changed

+221
-15
lines changed

index.d.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,43 @@ import {GlobbyOptions} from 'globby';
22
import {Options as CpFileOptions} from 'cp-file';
33

44
declare namespace cpy {
5+
interface SourceFile {
6+
/**
7+
Resolved path to the file.
8+
9+
@example '/tmp/dir/foo.js'
10+
*/
11+
readonly path: string;
12+
13+
/**
14+
Relative path to the file from `cwd`.
15+
16+
@example 'dir/foo.js' if `cwd` was '/tmp'
17+
*/
18+
readonly relativePath: string;
19+
20+
/**
21+
Filename with extension.
22+
23+
@example 'foo.js'
24+
*/
25+
readonly name: string;
26+
27+
/**
28+
Filename without extension.
29+
30+
@example 'foo'
31+
*/
32+
readonly nameWithoutExtension: string;
33+
34+
/**
35+
File extension.
36+
37+
@example 'js'
38+
*/
39+
readonly extension: string;
40+
}
41+
542
interface Options extends Readonly<GlobbyOptions>, CpFileOptions {
643
/**
744
Working directory to find source files.
@@ -46,6 +83,26 @@ declare namespace cpy {
4683
@default true
4784
*/
4885
readonly ignoreJunk?: boolean;
86+
87+
/**
88+
Function to filter files to copy.
89+
90+
Receives a source file object as the first argument.
91+
92+
Return true to include, false to exclude. You can also return a Promise that resolves to true or false.
93+
94+
@example
95+
```
96+
import cpy = require('cpy');
97+
98+
(async () => {
99+
await cpy('foo', 'destination', {
100+
filter: file => file.extension !== '.nocopy'
101+
});
102+
})();
103+
```
104+
*/
105+
readonly filter?: (file: SourceFile) => (boolean | Promise<boolean>);
49106
}
50107

51108
interface ProgressData {

index.js

Lines changed: 48 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,40 @@
22
const EventEmitter = require('events');
33
const path = require('path');
44
const os = require('os');
5-
const pAll = require('p-all');
5+
const pMap = require('p-map');
66
const arrify = require('arrify');
77
const globby = require('globby');
88
const hasGlob = require('has-glob');
99
const cpFile = require('cp-file');
1010
const junk = require('junk');
11+
const pFilter = require('p-filter');
1112
const CpyError = require('./cpy-error');
1213

1314
const defaultOptions = {
1415
ignoreJunk: true
1516
};
1617

17-
const preprocessSourcePath = (source, options) => options.cwd ? path.resolve(options.cwd, source) : source;
18+
class SourceFile {
19+
constructor(relativePath, path) {
20+
this.path = path;
21+
this.relativePath = relativePath;
22+
Object.freeze(this);
23+
}
24+
25+
get name() {
26+
return path.basename(this.relativePath);
27+
}
28+
29+
get nameWithoutExtension() {
30+
return path.basename(this.relativePath, path.extname(this.relativePath));
31+
}
32+
33+
get extension() {
34+
return path.extname(this.relativePath).slice(1);
35+
}
36+
}
37+
38+
const preprocessSourcePath = (source, options) => path.resolve(options.cwd ? options.cwd : process.cwd(), source);
1839

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

98+
let sources = files.map(sourcePath => new SourceFile(sourcePath, preprocessSourcePath(sourcePath, options)));
99+
100+
if (options.filter !== undefined) {
101+
const filteredSources = await pFilter(sources, options.filter, {concurrency: 1024});
102+
sources = filteredSources;
103+
}
104+
105+
if (sources.length === 0) {
106+
progressEmitter.emit('progress', {
107+
totalFiles: 0,
108+
percent: 1,
109+
completedFiles: 0,
110+
completedSize: 0
111+
});
112+
}
113+
77114
const fileProgressHandler = event => {
78115
const fileStatus = copyStatus.get(event.src) || {written: 0, percent: 0};
79116

@@ -99,20 +136,17 @@ module.exports = (source, destination, {
99136
}
100137
};
101138

102-
return pAll(files.map(sourcePath => {
103-
return async () => {
104-
const from = preprocessSourcePath(sourcePath, options);
105-
const to = preprocessDestinationPath(sourcePath, destination, options);
139+
return pMap(sources, async source => {
140+
const to = preprocessDestinationPath(source.relativePath, destination, options);
106141

107-
try {
108-
await cpFile(from, to, options).on('progress', fileProgressHandler);
109-
} catch (error) {
110-
throw new CpyError(`Cannot copy from \`${from}\` to \`${to}\`: ${error.message}`, error);
111-
}
142+
try {
143+
await cpFile(source.path, to, options).on('progress', fileProgressHandler);
144+
} catch (error) {
145+
throw new CpyError(`Cannot copy from \`${source.relativePath}\` to \`${to}\`: ${error.message}`, error);
146+
}
112147

113-
return to;
114-
};
115-
}), {concurrency});
148+
return to;
149+
}, {concurrency});
116150
})();
117151

118152
promise.on = (...arguments_) => {

index.test-d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ expectType<Promise<string[]> & ProgressEmitter>(
2727
cpy('foo.js', 'destination', {concurrency: 2})
2828
);
2929

30+
expectType<Promise<string[]> & ProgressEmitter>(
31+
cpy('foo.js', 'destination', {filter: (file: cpy.SourceFile) => true})
32+
);
33+
expectType<Promise<string[]> & ProgressEmitter>(
34+
cpy('foo.js', 'destination', {filter: async (file: cpy.SourceFile) => true})
35+
);
3036

3137
expectType<Promise<string[]>>(
3238
cpy('foo.js', 'destination').on('progress', progress => {

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,9 @@
4949
"has-glob": "^1.0.0",
5050
"junk": "^3.1.0",
5151
"nested-error-stacks": "^2.1.0",
52-
"p-all": "^2.1.0"
52+
"p-all": "^2.1.0",
53+
"p-filter": "^2.1.0",
54+
"p-map": "^3.0.0"
5355
},
5456
"devDependencies": {
5557
"ava": "^2.1.0",

readme.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,63 @@ Default: `true`
106106

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

109+
##### filter
110+
111+
Type: `Function`
112+
113+
Function to filter files to copy.
114+
115+
Receives a source file object as the first argument.
116+
117+
Return true to include, false to exclude. You can also return a Promise that resolves to true or false.
118+
119+
```js
120+
const cpy = require('cpy');
121+
122+
(async () => {
123+
await cpy('foo', 'destination', {
124+
filter: file => file.extension !== '.nocopy'
125+
});
126+
})();
127+
```
128+
129+
##### Source file object
130+
131+
###### path
132+
133+
Type: `string`\
134+
Example: `'/tmp/dir/foo.js'`
135+
136+
Resolved path to the file.
137+
138+
###### relativePath
139+
140+
Type: `string`\
141+
Example: `'dir/foo.js'` if `cwd` was `'/tmp'`
142+
143+
Relative path to the file from `cwd`.
144+
145+
###### name
146+
147+
Type: `string`\
148+
Example: `'foo.js'`
149+
150+
Filename with extension.
151+
152+
###### nameWithoutExtension
153+
154+
Type: `string`\
155+
Example: `'foo'`
156+
157+
Filename without extension.
158+
159+
###### extension
160+
161+
Type: `string`\
162+
Example: `'js'`
163+
164+
File extension.
165+
109166
## Progress reporting
110167

111168
### cpy.on('progress', handler)

test.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,56 @@ test('throws on invalid concurrency value', async t => {
5454
await t.throwsAsync(cpy(['license', 'package.json'], t.context.tmp, {concurrency: 'foo'}));
5555
});
5656

57+
test('copy array of files with filter', async t => {
58+
await cpy(['license', 'package.json'], t.context.tmp, {
59+
filter: file => {
60+
if (file.path.endsWith('/license')) {
61+
t.is(file.path, path.join(process.cwd(), 'license'));
62+
t.is(file.relativePath, 'license');
63+
t.is(file.name, 'license');
64+
t.is(file.nameWithoutExtension, 'license');
65+
t.is(file.extension, '');
66+
} else if (file.path.endsWith('/package.json')) {
67+
t.is(file.path, path.join(process.cwd(), 'package.json'));
68+
t.is(file.relativePath, 'package.json');
69+
t.is(file.name, 'package.json');
70+
t.is(file.nameWithoutExtension, 'package');
71+
t.is(file.extension, 'json');
72+
}
73+
74+
return !file.path.endsWith('/license');
75+
}
76+
});
77+
78+
t.false(fs.existsSync(path.join(t.context.tmp, 'license')));
79+
t.is(read('package.json'), read(t.context.tmp, 'package.json'));
80+
});
81+
82+
test('copy array of files with async filter', async t => {
83+
await cpy(['license', 'package.json'], t.context.tmp, {
84+
filter: async file => {
85+
if (file.path.endsWith('/license')) {
86+
t.is(file.path, path.join(process.cwd(), 'license'));
87+
t.is(file.relativePath, 'license');
88+
t.is(file.name, 'license');
89+
t.is(file.nameWithoutExtension, 'license');
90+
t.is(file.extension, '');
91+
} else if (file.path.endsWith('/package.json')) {
92+
t.is(file.path, path.join(process.cwd(), 'package.json'));
93+
t.is(file.relativePath, 'package.json');
94+
t.is(file.name, 'package.json');
95+
t.is(file.nameWithoutExtension, 'package');
96+
t.is(file.extension, 'json');
97+
}
98+
99+
return !file.path.endsWith('/license');
100+
}
101+
});
102+
103+
t.false(fs.existsSync(path.join(t.context.tmp, 'license')));
104+
t.is(read('package.json'), read(t.context.tmp, 'package.json'));
105+
});
106+
57107
test('cwd', async t => {
58108
fs.mkdirSync(t.context.tmp);
59109
fs.mkdirSync(path.join(t.context.tmp, 'cwd'));

0 commit comments

Comments
 (0)