From 227b951a0ac07b212484bf51948616b87ce11c0a Mon Sep 17 00:00:00 2001 From: ghiscoding Date: Wed, 3 Jun 2026 00:21:34 -0400 Subject: [PATCH] fix: remove duplicate code & add consistent curly braces --- biome.json | 2 ++ src/__tests__/index.spec.ts | 64 ++++++++++++++++++++++++------------ src/index.ts | 65 ++++++++++++++++++++++--------------- 3 files changed, 84 insertions(+), 47 deletions(-) diff --git a/biome.json b/biome.json index bf08cd7..73004f0 100644 --- a/biome.json +++ b/biome.json @@ -72,6 +72,8 @@ "style": { "noNonNullAssertion": "off", "noParameterAssign": "off", + "useBlockStatements": "error", + "useConsistentCurlyBraces": "error", "useExponentiationOperator": "off" }, "nursery": { diff --git a/src/__tests__/index.spec.ts b/src/__tests__/index.spec.ts index 09cf900..51104e9 100644 --- a/src/__tests__/index.spec.ts +++ b/src/__tests__/index.spec.ts @@ -68,7 +68,7 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/c.js', 'c'); copyfiles('input/*.txt', 'output', {}, () => { - readdir('output/input', async (_err, files) => { + readdir('output/input', async (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); done(); }); @@ -81,7 +81,7 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/c.js', 'c'); copyfiles('input/*.txt', 'output', undefined, () => { - readdir('output/input', async (_err, files) => { + readdir('output/input', async (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); done(); }); @@ -109,7 +109,7 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/c.js', 'c'); copyfiles(['input/*.txt'], 'output', {}, () => { - readdir('output/input', (_err, files) => { + readdir('output/input', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); // 'correct mode' // expect(statSync('output/input/a.txt').mode).toBe(33261); @@ -131,7 +131,7 @@ describe('copyfiles', () => { exclude: ['**/*.js.txt', '**/*.ps.txt'], }, () => { - readdir('output/input', (_err, files) => { + readdir('output/input', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); done(); }); @@ -154,7 +154,7 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/.c.txt', 'c'); copyfiles(['input/*.txt'], 'output', { all: true }, () => { - readdir('output/input', (_err, files) => { + readdir('output/input', (_err: Error | null, files: string[]) => { expect(files).toEqual(['.c.txt', 'a.txt', 'b.txt']); done(); }); @@ -167,7 +167,7 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/c.js', 'c'); copyfiles(['input/*.txt'], 'output', { up: 1 }, () => { - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); done(); }); @@ -182,7 +182,7 @@ describe('copyfiles', () => { writeFileSync('input/c.js', 'c'); writeFileSync('input/deep/d.txt', 'd'); copyfiles('input/**/*.txt', 'output', { up: true }, () => { - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt', 'd.txt']); done(); }); @@ -195,7 +195,7 @@ describe('copyfiles', () => { writeFileSync('input/other/b.txt', 'b'); writeFileSync('input/other/c.js', 'c'); copyfiles(['input/**/*.txt'], 'output', { up: 2 }, () => { - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); done(); }); @@ -221,7 +221,7 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/other/c.js', 'c'); copyfiles(['input/**/*.txt'], 'output', { flat: true }, () => { - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); done(); }); @@ -248,6 +248,7 @@ describe('copyfiles', () => { const logSpy = vi.spyOn(console, 'log'); copyfiles(['input/**/*'], 'output', { dryRun: true }); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('=== dry-run ===')); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/a.txt → output/input/a.txt')); expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/other/c.js → output/input/other/c.js')); @@ -255,6 +256,24 @@ describe('copyfiles', () => { logSpy.mockRestore(); }); + test('dryRun does not copy files but logs actions and has no error in callback', () => + new Promise((done: any) => { + writeFileSync('input/a.txt', 'a'); + writeFileSync('input/other/c.js', 'c'); + const logSpy = vi.spyOn(console, 'log'); + + copyfiles(['input/**/*'], 'output', { dryRun: true }, err => { + expect(err).toBeUndefined(); + done(); + }); + + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('=== dry-run ===')); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/a.txt → output/input/a.txt')); + expect(logSpy).toHaveBeenCalledWith(expect.stringContaining('copy: input/other/c.js → output/input/other/c.js')); + expect(existsSync('output/a.txt')).toBe(false); + logSpy.mockRestore(); + })); + test('dryRun with rename does not copy files but logs actions', () => { mkdirSync('input/sub'); writeFileSync('input/foo.css', 'foo'); @@ -277,9 +296,9 @@ describe('copyfiles', () => { writeFileSync('input/b.txt', 'b'); writeFileSync('input/other/c.js', 'c'); copyfiles('input/**/*.txt', 'output', { flat: true, verbose: true }, () => { - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); - const globCall = logSpy.mock.calls.find(call => call[0] === 'glob found'); + const globCall = logSpy.mock.calls.find((call: any[]) => call[0] === 'glob found'); expect(globCall).toBeTruthy(); expect(new Set(globCall![1])).toEqual(new Set(['input/b.txt', 'input/other/a.txt'])); expect(logSpy).toHaveBeenCalledWith('copy:', { from: 'input/other/a.txt', to: 'output/a.txt' }); @@ -366,7 +385,9 @@ describe('copyfiles', () => { }); test('sets followSymbolicLinks when options.follow is true', async () => { - if (process.platform === 'win32') return; + if (process.platform === 'win32') { + return; + } mkdirSync('input/real', { recursive: true }); writeFileSync('input/real/a.txt', 'test'); symlinkSync('real', 'input/link'); @@ -374,10 +395,13 @@ describe('copyfiles', () => { copyfiles('input/link/*.txt', 'output', { follow: true }, err => { const files = globSync('output/**/*'); console.log('output contents:', files); - const found = files.some(f => f.endsWith('a.txt')); + const found = files.some((f: string) => f.endsWith('a.txt')); expect(found).toBe(true); - if (err) reject(err); - else resolve(); + if (err) { + reject(err); + } else { + resolve(); + } }); }); }); @@ -389,7 +413,7 @@ describe('copyfiles', () => { writeFileSync('input/other/b.txt', 'b'); writeFileSync('input/other/c.js', 'c'); copyfiles('input/**/*.txt', 'output', { up: 2, verbose: true }, () => { - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toEqual(['a.txt', 'b.txt']); expect(logSpy).toHaveBeenCalledWith('glob found', ['input/other/a.txt', 'input/other/b.txt']); expect(logSpy).toHaveBeenCalledWith('copy:', { from: 'input/other/a.txt', to: 'output/a.txt' }); @@ -405,7 +429,7 @@ describe('copyfiles', () => { writeFileSync('input/.env.production', 'SOME=VALUE'); copyfiles('input/.env.production', 'output/.env', {}, err => { expect(err).toBeUndefined(); - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toContain('.env'); // Check file contents const content = readFileSync('output/.env', 'utf8'); @@ -420,7 +444,7 @@ describe('copyfiles', () => { writeFileSync('input/original.txt', 'HELLO WORLD'); copyfiles(['input/original.txt'], 'output/renamed.txt', {}, err => { expect(err).toBeUndefined(); - readdir('output', (_err, files) => { + readdir('output', (_err: Error | null, files: string[]) => { expect(files).toContain('renamed.txt'); // Check file contents const content = readFileSync('output/renamed.txt', 'utf8'); @@ -787,7 +811,7 @@ describe('copyfiles', () => { writeFileSync('input/bar2.txt', 'bar'); writeFileSync('input/baz3.txt', 'baz'); copyfiles(['input/{foo,bar}*.txt'], 'output', {}, () => { - readdir('output/input', (_err, files) => { + readdir('output/input', (_err: Error | null, files: string[]) => { expect(files.sort()).toEqual(['bar2.txt', 'foo1.txt']); done(); }); @@ -801,7 +825,7 @@ describe('copyfiles', () => { writeFileSync('input/c.test.js', 'c'); writeFileSync('input/d.js', 'd'); copyfiles(['input/*.js', '!input/*.spec.js', '!input/*.test.js'], 'output', {}, () => { - readdir('output/input', (_err, files) => { + readdir('output/input', (_err: Error | null, files: string[]) => { expect((files || []).sort()).toEqual(['a.js', 'd.js']); done(); }); diff --git a/src/index.ts b/src/index.ts index e1fcab4..73d8445 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,11 @@ import type { CopyFileOptions } from './interfaces.js'; export type * from './interfaces.js'; +/** Convert an item to an array if it is not already one */ +function arrify(item: T | T[]): T[] { + return Array.isArray(item) ? item : [item]; +} + /** * Check if a directory exists, if not then create it * @param {String} dir - directory to create @@ -54,8 +59,7 @@ export function getDestinationPath(inFile: string, outDir: string, options: Copy // 1. Single file rename (no glob, dest is not a directory, no *) if (isSingleFileRename && !outDir.includes('*')) { - const dest = outDir; - return callRenameWhenDefined(inFile, dest, options); + return callRenameWhenDefined(inFile, outDir, options); } // 2. Wildcard pattern in destination @@ -82,12 +86,7 @@ export function getDestinationPath(inFile: string, outDir: string, options: Copy } // 3. Flat or up logic (no wildcard) - let baseDir: string; - if (options.flat || upCount === true) { - baseDir = outDir; - } else { - baseDir = join(outDir, dealWith(fileDir, upCount)); - } + const baseDir = options.flat || upCount === true ? outDir : join(outDir, dealWith(fileDir, upCount)); const dest = join(baseDir, fileName); return callRenameWhenDefined(inFile, dest, options); @@ -107,7 +106,9 @@ function displayStatWhenEnabled(options: CopyFileOptions, count: number) { * @param dot - if true, include dotfiles/folders; otherwise, filter them out */ export function filterDotFiles(paths: string[], dot: boolean): string[] { - if (dot) return paths; + if (dot) { + return paths; + } return paths.filter(p => { // Remove files/dirs starting with a dot after last slash const base = p.split(/[\\/]/).pop(); @@ -139,7 +140,8 @@ function getMatchedFiles( const allFilesSet = new Set(); for (const pattern of sources) { const isNegated = typeof pattern === 'string' && pattern.startsWith('!'); - const adjustedPattern = tryCreatingDir(isNegated ? pattern.slice(1) : pattern, isNegated ? pattern.slice(1) : pattern); + const dirPart = isNegated ? pattern.slice(1) : pattern; + const adjustedPattern = tryCreatingDir(dirPart, dirPart); let files = globSync(adjustedPattern, { exclude: excludeGlobs }) || []; if (options.all && adjustedPattern.includes('*') && !adjustedPattern.startsWith('.')) { const dotPattern = adjustedPattern.replace(/(\*\.[^/]+$|\*$)/, '.$1'); @@ -147,7 +149,7 @@ function getMatchedFiles( files = files.concat(globSync(dotPattern, { exclude: excludeGlobs })); } } - files = Array.isArray(files) ? files : [files]; + files = arrify(files); files = files.map(f => f.replaceAll('\\', '/')); files = files.filter(f => !tryCreatingDir(f, false)); @@ -183,7 +185,7 @@ function getMatchedFiles( */ export function copyfiles(sources: string | string[], outPath: string, options: CopyFileOptions = {}, callback?: (e?: Error) => void) { const cb = callback || options.callback; - sources = Array.isArray(sources) ? sources : [sources]; + sources = arrify(sources); if (options.verbose || options.stat) { console.time('Execution time'); @@ -226,12 +228,8 @@ export function copyfiles(sources: string | string[], outPath: string, options: } // Set default excludeGlobs only if not provided by user - let excludeGlobs: string[]; - if (Array.isArray(options.exclude) && options.exclude.length > 0) { - excludeGlobs = options.exclude; - } else { - excludeGlobs = ['**/.git/**', '**/node_modules/**']; - } + const excludeGlobs = + Array.isArray(options.exclude) && options.exclude.length > 0 ? options.exclude : ['**/.git/**', '**/node_modules/**']; // Use a Set for deduplication from the start const allFilesSet = getMatchedFiles(sources, excludeGlobs, isSingleFile, isDestFile, options); @@ -242,8 +240,11 @@ export function copyfiles(sources: string | string[], outPath: string, options: if (options.error && allFilesSet.size < 1) { const err = new Error('nothing copied'); - if (typeof cb === 'function') cb(err); - else throw err; + if (typeof cb === 'function') { + cb(err); + } else { + throw err; + } return; } @@ -255,7 +256,9 @@ export function copyfiles(sources: string | string[], outPath: string, options: console.log(`Files copied: 0`); console.timeEnd('Execution time'); } - if (typeof cb === 'function') cb(); + if (typeof cb === 'function') { + cb(); + } return; } @@ -269,7 +272,9 @@ export function copyfiles(sources: string | string[], outPath: string, options: displayStatWhenEnabled(options, allFilesSet.size); console.log(head); - if (typeof cb === 'function') cb(); + if (typeof cb === 'function') { + cb(); + } return; } @@ -279,16 +284,22 @@ export function copyfiles(sources: string | string[], outPath: string, options: outPath, options, err => { - if (hasError) return; + if (hasError) { + return; + } if (err) { hasError = true; - if (typeof cb === 'function') cb(err); + if (typeof cb === 'function') { + cb(err); + } return; } completed++; if (completed === allFilesSet.size) { displayStatWhenEnabled(options, allFilesSet.size); - if (typeof cb === 'function') cb(); + if (typeof cb === 'function') { + cb(); + } } }, isSingleFile && isDestFile, // pass as single rename mode @@ -324,12 +335,12 @@ function copyFileStream(inFile: string, outDir: string, options: CopyFileOptions const writeStream = createWriteStream(dest); let called = false; - function onceCallback(err?: Error) { + const onceCallback = (err?: Error) => { if (!called) { called = true; cb(err); } - } + }; readStream.on('error', onceCallback); writeStream.on('error', onceCallback);