Skip to content

Commit df73230

Browse files
authored
feat: add --dry-run option (#22)
1 parent ffe8d06 commit df73230

8 files changed

Lines changed: 103 additions & 26 deletions

File tree

.vscode/extensions.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"recommendations": ["biomejs.biome"]
3+
}

.vscode/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"editor.defaultFormatter": "biomejs.biome",
3+
"editor.formatOnSave": true,
4+
"editor.formatOnPaste": false,
5+
"editor.codeActionsOnSave":{
6+
"source.organizeImports.biome": "explicit"
7+
},
8+
"[typescript]": {
9+
"editor.defaultFormatter": "biomejs.biome"
10+
}
11+
}

README.md

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,13 @@
66
[![npm](https://img.shields.io/npm/dy/native-copyfiles)](https://www.npmjs.com/package/native-copyfiles)
77
[![npm bundle size](https://img.shields.io/bundlephobia/minzip/native-copyfiles?color=success&label=gzip)](https://bundlephobia.com/result?p=native-copyfiles)
88

9-
## Copyfiles
10-
#### native-copyfiles
9+
## native-copyfiles
1110

1211
Copy files easily via JavaScript or the CLI, it uses [tinyglobby](https://www.npmjs.com/package/tinyglobby) internally for glob patterns and [yargs](https://www.npmjs.com/package/yargs) for the CLI.
1312

14-
The library is very similar to the [copyfiles](https://www.npmjs.com/package/copyfiles) package, it is however written with more native NodeJS code and less dependencies (3 instead of 7). The package options are the same (except for `--soft` which is not implemented), some new options were also added in this project here (see below).
13+
The library is very similar from the outside to the [copyfiles](https://www.npmjs.com/package/copyfiles) package, however its internal is quite different, it uses more native NodeJS code and less dependencies (3 instead of 7). The package options are the same (except for `--soft` which is not implemented), some new options were also added in this project here (see below).
1514

16-
> Note: there is 1 major difference with `copyfiles`, any options must be provided as a suffix after the source/target directories command (the original project had them as prefix)<br>
15+
> Note: there is 1 noticeable difference with `copyfiles`, any options must be provided as a suffix after the source/target directories command (the original project had them as prefix).<br>
1716
> This mean calling: `copyfiles source target [options]` instead of `copyfiles [options] source target`
1817
1918
### Install
@@ -28,16 +27,17 @@ npm install native-copyfiles -D
2827
Usage: copyfiles inFile [more files ...] outDirectory [options]
2928

3029
Options:
31-
-u, --up slice a path off the bottom of the paths [number]
32-
-a, --all include files & directories begining with a dot (.) [boolean]
33-
-f, --flat flatten the output [boolean]
34-
-e, --exclude pattern or glob to exclude (may be passed multiple times) [string|string[]]
35-
-E, --error throw error if nothing is copied [boolean]
36-
-V, --verbose print more information to console [boolean]
37-
-F, --follow follow symbolic links [boolean]
38-
-s, --stat show statistics after execution (time + file count) [boolean]
39-
-v, --version show version number [boolean]
40-
-h, --help show help [boolean]
30+
-u, --up slice a path off the bottom of the paths [number]
31+
-a, --all include files & directories begining with a dot (.) [boolean]
32+
-d, --dry-run show what would be copied, without actually copying anything [boolean]
33+
-f, --flat flatten the output [boolean]
34+
-e, --exclude pattern or glob to exclude (may be passed multiple times) [string|string[]]
35+
-E, --error throw error if nothing is copied [boolean]
36+
-V, --verbose print more information to console [boolean]
37+
-F, --follow follow symbolic links [boolean]
38+
-s, --stat show statistics after execution (time + file count) [boolean]
39+
-v, --version show version number [boolean]
40+
-h, --help show help [boolean]
4141
```
4242
4343
> [!NOTE]
@@ -202,6 +202,7 @@ and finally the third and last argument is a callback function which is executed
202202
up: number, // slice a path off the bottom of the paths
203203
exclude: string, // exclude pattern
204204
all: bool, // include dot files
205+
dryRun: bool, // show what would be copied, without actually copying anything
205206
follow: bool, // follow symlinked directories when expanding ** patterns
206207
error: bool // raise errors if no files copied
207208
stat: bool // show statistics after execution (time + file count)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
"biome:lint:write": "biome lint --write ./src",
4444
"biome:format:check": "biome format ./src",
4545
"biome:format:write": "biome format --write ./src",
46-
"preview:copy": "node dist/cli.js test-copyin test-copyout --flat --verbose",
46+
"preview:copy": "node dist/cli.js test-copyin test-copyout --dry-run --flat --stat",
4747
"preview:release": "release-it --only-version --dry-run",
4848
"release": "release-it --only-version",
4949
"test": "vitest --watch --config ./vitest.config.mts",

src/__tests__/index.spec.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { mkdirSync, readdir, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
1+
import { existsSync, mkdirSync, readdir, readdirSync, readFileSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
22
import { Readable } from 'node:stream';
33
import { globSync } from 'tinyglobby';
44
import { afterAll, afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
@@ -215,6 +215,34 @@ describe('copyfiles', () => {
215215
}
216216
});
217217

218+
test('dryRun does not copy files but logs actions', () => {
219+
writeFileSync('input/a.txt', 'a');
220+
writeFileSync('input/other/c.js', 'c');
221+
const logSpy = vi.spyOn(console, 'log');
222+
223+
copyfiles(['input/**/*', 'output'], { dryRun: true });
224+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('=== dry-run ==='));
225+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/a.txt → output/input/a.txt'));
226+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/other/c.js → output/input/other/c.js'));
227+
expect(existsSync('output/a.txt')).toBe(false);
228+
logSpy.mockRestore();
229+
});
230+
231+
test('dryRun with rename does not copy files but logs actions', () => {
232+
createDir('input/sub');
233+
writeFileSync('input/foo.css', 'foo');
234+
writeFileSync('input/sub/bar.css', 'bar');
235+
const logSpy = vi.spyOn(console, 'log');
236+
237+
copyfiles(['input/**/*.css', 'output/*.scss'], { dryRun: true, stat: true });
238+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('=== dry-run ==='));
239+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/foo.css → output/input/foo.scss'));
240+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/sub/bar.css → output/input/sub/bar.scss'));
241+
expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('Files copied: 2'));
242+
expect(existsSync('output/a.txt')).toBe(false);
243+
logSpy.mockRestore();
244+
});
245+
218246
test('verbose flat', () =>
219247
new Promise((done: any) => {
220248
const logSpy = vi.spyOn(global.console, 'log').mockReturnValue();

src/cli.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ const argv = cli
2020
type: 'boolean',
2121
description: 'include files & directories begining with a dot (.)',
2222
})
23+
.option('dryRun', {
24+
alias: 'd',
25+
type: 'boolean',
26+
description: 'Show what would be copied, but do not actually copy any files',
27+
})
2328
.option('error', {
2429
alias: 'E',
2530
type: 'boolean',

src/index.ts

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { createReadStream, createWriteStream, existsSync, mkdirSync, statSync } from 'node:fs';
22
import { basename, dirname, extname, join, normalize, posix, sep } from 'node:path';
3-
import untildify from 'untildify';
43
import { type GlobOptions, globSync } from 'tinyglobby';
4+
import untildify from 'untildify';
55

66
import type { CopyFileOptions } from './interfaces.js';
77

@@ -15,6 +15,15 @@ export function createDir(dir: string) {
1515
}
1616
}
1717

18+
/**
19+
* Converts a path from any platform to posix
20+
* @param {String} pathStr - the path to convert
21+
* @returns {String} - the converted posix path
22+
*/
23+
export function convertToPosix(pathStr: string) {
24+
return pathStr.replaceAll(sep, posix.sep);
25+
}
26+
1827
/**
1928
* Helper to throw or callback with error
2029
*/
@@ -84,6 +93,15 @@ function getDestinationPath(inFile: string, outDir: string, options: CopyFileOpt
8493
return callRenameWhenDefined(inFile, dest, options);
8594
}
8695

96+
/** Show statistics when `verbose` and/or `stat` are enabled */
97+
function displayStatWhenEnabled(options: CopyFileOptions, count: number) {
98+
if (options.verbose || options.stat) {
99+
console.log('\n');
100+
console.log(`Files copied: ${count}`);
101+
console.timeEnd('Execution time');
102+
}
103+
}
104+
87105
/**
88106
* Copy the files per a glob pattern, the first item(s) can be a 1 or more files to copy
89107
* while the last item in the array is the output outDirectory directory
@@ -134,8 +152,8 @@ export function copyfiles(paths: string[], options: CopyFileOptions, callback?:
134152
}
135153
}
136154

155+
// create destination directory if not exists
137156
if (!isDestFile) {
138-
// create destination directory if not exists
139157
createDir(dirname(outPath));
140158
}
141159

@@ -175,6 +193,20 @@ export function copyfiles(paths: string[], options: CopyFileOptions, callback?:
175193
return;
176194
}
177195

196+
if (options.dryRun) {
197+
const head = '=== dry-run ===';
198+
console.log(head);
199+
allFiles.forEach(inFile => {
200+
const dest = getDestinationPath(inFile, outPath, options, isSingleFile && isDestFile);
201+
console.log(`copy: ${convertToPosix(inFile)}${convertToPosix(dest)}`);
202+
});
203+
displayStatWhenEnabled(options, allFiles.length);
204+
console.log(head);
205+
206+
if (typeof cb === 'function') cb();
207+
return;
208+
}
209+
178210
allFiles.forEach(inFile => {
179211
copyFileStream(
180212
inFile,
@@ -189,10 +221,7 @@ export function copyfiles(paths: string[], options: CopyFileOptions, callback?:
189221
}
190222
completed++;
191223
if (completed === allFiles.length) {
192-
if (options.verbose || options.stat) {
193-
console.log(`Files copied: ${allFiles.length}`);
194-
console.timeEnd('Execution time');
195-
}
224+
displayStatWhenEnabled(options, allFiles.length);
196225
if (typeof cb === 'function') cb();
197226
}
198227
},
@@ -207,6 +236,7 @@ export function copyfiles(paths: string[], options: CopyFileOptions, callback?:
207236
* @param {String} outDir
208237
* @param {CopyFileOptions} options
209238
* @param {(e?: Error) => void} cb
239+
* @param {Boolean} isSingleFileRename - whether the operation is a single file rename (no glob, dest is not a directory, no *)
210240
*/
211241
function copyFileStream(inFile: string, outDir: string, options: CopyFileOptions, cb: (e?: Error) => void, isSingleFileRename = false) {
212242
outDir = outDir.startsWith('~') ? untildify(outDir) : outDir;
@@ -245,10 +275,6 @@ function copyFileStream(inFile: string, outDir: string, options: CopyFileOptions
245275
});
246276

247277
readStream.pipe(writeStream);
248-
249-
function convertToPosix(pathStr: string) {
250-
return pathStr.replaceAll(sep, posix.sep);
251-
}
252278
}
253279

254280
function depth(str: string) {

src/interfaces.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ export interface CopyFileOptions {
22
/** Include files & directories beginning with a dot (.) */
33
all?: boolean;
44

5+
/** Show what would be copied, but do not actually copy any files */
6+
dryRun?: boolean;
7+
58
/** Throw error if nothing is copied */
69
error?: boolean;
710

0 commit comments

Comments
 (0)