Skip to content

Commit 83bf2dd

Browse files
authored
Support flat copy (#111)
1 parent 2c8e590 commit 83bf2dd

File tree

7 files changed

+121
-14
lines changed

7 files changed

+121
-14
lines changed

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
strategy:
1212
matrix:
1313
os: [ubuntu-latest, windows-latest, macos-latest]
14-
node-version: [12.x, 14.x, 16.x]
14+
node-version: [12.x, 14.x, 16.x, 17.x]
1515

1616
name: Test - ${{ matrix.os }} - Node v${{ matrix.node-version }}
1717

README.md

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,18 @@ Copy individual files or entire directories from a source folder to a destinatio
118118
```js
119119
[
120120
{ source: '/path/from', destination: '/path/to' },
121-
{ source: '/path/**/*.js', destination: '/path' },
121+
{
122+
source: '/path/**/*.js',
123+
destination: '/path',
124+
options: {
125+
flat: false,
126+
preserveTimestamps: true,
127+
overwite: true,
128+
},
129+
globOptions: {
130+
dot: true,
131+
},
132+
},
122133
{ source: '/path/fromfile.txt', destination: '/path/tofile.txt' },
123134
{ source: '/path/**/*.{html,js}', destination: '/path/to' },
124135
{ source: '/path/{file1,file2}.js', destination: '/path/to' },
@@ -129,6 +140,8 @@ Copy individual files or entire directories from a source folder to a destinatio
129140

130141
- source[`string`] - a file or a directory or a glob
131142
- destination[`string`] - a file or a directory.
143+
- options ['object`] - copy options
144+
- globOptions [`object`] - options to forward to glob options
132145

133146
**Caveats**
134147

src/actions/copy.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,26 @@ const fsExtraDefaultOptions = {
1111
};
1212

1313
const copy = async (task, { logger }) => {
14-
const { source, absoluteSource, destination, absoluteDestination, context, toType } = task;
14+
const {
15+
source,
16+
absoluteSource,
17+
destination,
18+
absoluteDestination,
19+
context,
20+
toType,
21+
options = {},
22+
globOptions = {},
23+
} = task;
1524

1625
logger.log(`copying from ${source} to ${destination}`);
1726

1827
if (isGlob(source)) {
1928
const cpOptions = {
29+
...options,
2030
cwd: context,
2131
};
2232

23-
await globCopy(source, absoluteDestination, cpOptions);
33+
await globCopy(source, absoluteDestination, cpOptions, globOptions);
2434
} else {
2535
const isSourceFile = fs.lstatSync(absoluteSource).isFile();
2636

src/options-schema.js

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,33 @@ export default {
2323
type: 'string',
2424
minLength: 1,
2525
},
26+
options: {
27+
additionalProperties: false,
28+
type: 'object',
29+
description: 'Options to forward to archiver',
30+
properties: {
31+
flat: {
32+
description: 'Flatten the directory structure. All copied files will be put in the same directory',
33+
type: 'boolean',
34+
default: false,
35+
},
36+
overwrite: {
37+
description: 'overwrite existing file or directory',
38+
type: 'boolean',
39+
default: true,
40+
},
41+
preserveTimestamps: {
42+
description: 'Set last modification and access times to the ones of the original source files',
43+
type: 'boolean',
44+
default: false,
45+
},
46+
},
47+
},
48+
globOptions: {
49+
additionalProperties: true,
50+
type: 'object',
51+
description: 'Options to forward to fast-glob',
52+
},
2653
},
2754
},
2855
],
@@ -187,7 +214,7 @@ export default {
187214
runOnceInWatchMode: {
188215
type: 'boolean',
189216
default: false,
190-
description: 'Run tasks only at first compilation in watch mode'
191-
}
217+
description: 'Run tasks only at first compilation in watch mode',
218+
},
192219
},
193220
};

src/utils/glob-copy.js

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,35 @@ import fse from 'fs-extra';
33
import fg from 'fast-glob';
44

55
const defaultOptions = {
6+
flat: false,
67
cwd: process.cwd(),
78
};
89

9-
const destPath = (pattern, file, cwd) => {
10+
const destPath = (pattern, file, options = defaultOptions) => {
11+
if (options.flat) {
12+
return path.posix.basename(file);
13+
}
14+
1015
const pathArr = pattern.split('/');
1116
const globIndex = pathArr.findIndex((item) => (item ? fg.isDynamicPattern(item) : false));
1217
const normalized = pathArr.slice(0, globIndex).join('/');
1318

14-
const absolutePath = path.isAbsolute(normalized) ? normalized : path.posix.join(cwd, normalized);
19+
const absolutePath = path.isAbsolute(normalized) ? normalized : path.posix.join(options.cwd, normalized);
1520

1621
return path.relative(absolutePath, file);
1722
};
1823

19-
const globCopy = async (pattern, destination, options = defaultOptions) => {
24+
const globCopy = async (pattern, destination, options = defaultOptions, globOptions = {}) => {
2025
await fse.ensureDir(destination);
2126

22-
const matches = await fg(pattern, { dot: true, absolute: true, cwd: options.cwd });
27+
const matches = await fg(pattern, { dot: true, ...globOptions, absolute: true, cwd: options.cwd });
2328

2429
const entries = matches.map((file) => {
2530
const destDir = path.isAbsolute(destination) ? destination : path.posix.join(options.cwd, destination);
26-
const destFileName = destPath(pattern, file, options.cwd);
31+
const destFileName = destPath(pattern, file, options);
2732

2833
return {
2934
from: file,
30-
3135
destDir,
3236
destFileName,
3337
to: path.posix.join(destDir, destFileName),

tests/copy.test.js

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { existsSync } from 'node:fs';
2-
import { basename, join } from 'node:path';
2+
import { basename, join, sep } from 'node:path';
33

44
import test from 'ava';
55
import del from 'del';
@@ -94,7 +94,43 @@ test('should deep copy files to directory given a glob source', async (t) => {
9494

9595
t.true(existsSync(join(tmpdir, dirName)));
9696
t.true(existsSync(join(tmpdir, dirName, basename(file1))));
97-
t.true(existsSync(join(nestedDir, basename(file2))));
97+
t.true(existsSync(join(tmpdir, dirName, nestedDir.split(sep).pop(), basename(file2))));
98+
});
99+
100+
test('should flat copy the files to directory given a glob source', async (t) => {
101+
const { tmpdir } = t.context;
102+
103+
const file1 = await tempy.file(tmpdir);
104+
const nestedDir = await tempy.dir({ root: tmpdir });
105+
const file2 = await tempy.file(nestedDir);
106+
107+
const dirName = tempy.getDirName();
108+
109+
const config = {
110+
context: tmpdir,
111+
events: {
112+
onEnd: {
113+
copy: [
114+
{
115+
source: '**/*',
116+
destination: dirName,
117+
options: {
118+
flat: true,
119+
},
120+
globOptions: {},
121+
},
122+
],
123+
},
124+
},
125+
};
126+
127+
const compiler = getCompiler();
128+
new FileManagerPlugin(config).apply(compiler);
129+
await compile(compiler);
130+
131+
t.true(existsSync(join(tmpdir, dirName)));
132+
t.true(existsSync(join(tmpdir, dirName, basename(file1))));
133+
t.true(existsSync(join(tmpdir, dirName, basename(file2))));
98134
});
99135

100136
test(`should create destination directory if it doesn't exist and copy files`, async (t) => {

types.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
import type { ArchiverOptions } from 'archiver';
22
import type { Options as DelOptions } from 'del';
33
import type { Compiler, WebpackPluginInstance } from 'webpack';
4+
import type { CopyOptions } from 'fs-extra';
5+
import type { Options as FgOptions } from 'fast-glob';
6+
7+
type FsCopyOptions = Pick<CopyOptions, 'overwrite' | 'preserveTimestamps'>;
8+
9+
interface CopyActionOptions extends FsCopyOptions {
10+
/**
11+
* Flatten directory structure. All copied files will be put in the same directory.
12+
* disabled by default
13+
*/
14+
flat: boolean;
15+
}
416

517
/** Copy individual files or entire directories from a source folder to a destination folder */
618
type Copy = {
719
/** Copy source. A file or directory or a glob */
820
source: string;
921
/** Copy destination */
1022
destination: string;
23+
/** Copy Options */
24+
options: CopyActionOptions;
25+
/** Glob options */
26+
globOptions: Omit<FgOptions, 'absolute' | 'cwd'>;
1127
}[];
1228

1329
/** Delete individual files or entire directories */
1430
type Delete = (
1531
| {
32+
/** A folder or file or a glob to delete */
1633
source: string;
1734
/** Options to forward to del */
1835
options: DelOptions;

0 commit comments

Comments
 (0)