Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,18 @@ describe('copyfiles', () => {
});
}));

test('ignores inherited option properties', () =>
new Promise((done: any) => {
writeFileSync('input/a.txt', 'a');
const options = Object.create({ flat: true });

copyfiles('input/a.txt', 'output', options, () => {
expect(existsSync('output/input/a.txt')).toBe(true);
expect(existsSync('output/a.txt')).toBe(false);
done();
});
}));

test('copies files using directory pattern with /**', () =>
new Promise((done: any) => {
writeFileSync('input/a.txt', 'a');
Expand Down Expand Up @@ -789,6 +801,12 @@ describe('copyfiles', () => {
expect(result.replaceAll('\\', '/').endsWith(posixJoin('dest', 'bar.txt'))).toBe(true);
});

it('getDestinationPath ignores inherited option properties', () => {
const options = Object.create({ flat: true });
const result = getDestinationPath('foo/bar.txt', 'dest', options, false);
expect(result.replaceAll('\\', '/')).toBe(posixJoin('dest', 'foo', 'bar.txt'));
});

it('getDestinationPath - up === true branch', () => {
const result = getDestinationPath('foo/bar.txt', 'dest', { up: true }, false);
expect(result.replaceAll('\\', '/').endsWith(posixJoin('dest', 'bar.txt'))).toBe(true);
Expand Down
99 changes: 52 additions & 47 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,57 @@ function handleError(err?: Error) {
}

try {
// cli-nano resolves option names through dynamic property access. Keep this
// lookup table free of inherited keys such as "__proto__" and "constructor".
const optionDefinitions = {
all: {
alias: 'a',
type: 'boolean',
describe: 'Include files & directories begining with a dot (.)',
},
dryRun: {
alias: 'd',
type: 'boolean',
describe: 'Show what would be copied, but do not actually copy any files',
},
error: {
alias: 'E',
type: 'boolean',
describe: 'Throw error if nothing is copied',
},
exclude: {
alias: 'e',
type: 'array',
describe: 'Pattern or glob to exclude (may be passed multiple times)',
},
flat: {
alias: 'f',
type: 'boolean',
describe: 'Flatten the output',
},
follow: {
alias: 'F',
type: 'boolean',
describe: 'Follow symbolink links',
},
stat: {
alias: 's',
type: 'boolean',
describe: 'Show statistics after execution (execution time + file count)',
},
up: {
alias: 'u',
type: 'number',
describe: 'Slice a path off the bottom of the paths',
},
verbose: {
alias: 'V',
type: 'boolean',
describe: 'Print more information to console',
},
} as const;
const options = Object.assign(Object.create(null) as typeof optionDefinitions, optionDefinitions);

const config = {
command: {
name: 'copyfiles',
Expand Down Expand Up @@ -51,53 +102,7 @@ try {
},
],
},
options: {
all: {
alias: 'a',
type: 'boolean',
describe: 'Include files & directories begining with a dot (.)',
},
dryRun: {
alias: 'd',
type: 'boolean',
describe: 'Show what would be copied, but do not actually copy any files',
},
error: {
alias: 'E',
type: 'boolean',
describe: 'Throw error if nothing is copied',
},
exclude: {
alias: 'e',
type: 'array',
describe: 'Pattern or glob to exclude (may be passed multiple times)',
},
flat: {
alias: 'f',
type: 'boolean',
describe: 'Flatten the output',
},
follow: {
alias: 'F',
type: 'boolean',
describe: 'Follow symbolink links',
},
stat: {
alias: 's',
type: 'boolean',
describe: 'Show statistics after execution (execution time + file count)',
},
up: {
alias: 'u',
type: 'number',
describe: 'Slice a path off the bottom of the paths',
},
verbose: {
alias: 'V',
type: 'boolean',
describe: 'Print more information to console',
},
},
options,
version: readPackage().version,
} as const;

Expand Down
8 changes: 8 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ function throwOrCallback(err: Error, cb?: (e?: Error) => void) {
}
}

function createSafeOptions(options: CopyFileOptions): CopyFileOptions {
return Object.assign(Object.create(null) as CopyFileOptions, options);
}

function callRenameWhenDefined(inFile: string, dest: string, options: CopyFileOptions): string {
if (typeof options.rename === 'function') {
return options.rename(inFile, dest);
Expand All @@ -51,6 +55,7 @@ function callRenameWhenDefined(inFile: string, dest: string, options: CopyFileOp
* Calculate the destination path for a given input file and options.
*/
export function getDestinationPath(inFile: string, outDir: string, options: CopyFileOptions, isSingleFileRename = false): string {
options = createSafeOptions(options);
const fileDir = dirname(inFile);
const fileName = basename(inFile);
const srcExt = extname(fileName);
Expand Down Expand Up @@ -184,6 +189,9 @@ function getMatchedFiles(
* @param {(e?: Error) => void} callback - optionally callback that will be executed after copy is finished or when an error occurs
*/
export function copyfiles(sources: string | string[], outPath: string, options: CopyFileOptions = {}, callback?: (e?: Error) => void) {
// Treat options as data rather than inheriting behavior from Object.prototype.
// This also safely preserves an own "__proto__" key if options came from JSON.
options = createSafeOptions(options);
const cb = callback || options.callback;
sources = arrify(sources);

Expand Down