Skip to content

Commit

Permalink
Add Rename Option to Copy Command (#126)
Browse files Browse the repository at this point in the history
  • Loading branch information
biniona-mongodb authored Aug 12, 2022
1 parent b40c5ac commit d97fa76
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 14 deletions.
9 changes: 5 additions & 4 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,32 @@
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"version": "0.3.0",
"configurations": [
{
"name": "Run Tests",
"request": "launch",
"runtimeArgs": ["run", "test"],
"runtimeExecutable": "npm",
"cwd": "${workspaceFolder}",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
"type": "node"
},
{
"name": "Run TS Tests",
"request": "launch",
"runtimeArgs": ["run", "ts-test"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node",
"type": "node"
},
{
"name": "Run JS Tests",
"request": "launch",
"runtimeArgs": ["run", "js-test"],
"runtimeExecutable": "npm",
"skipFiles": ["<node_internals>/**"],
"type": "pwa-node"
"type": "node"
}
]
}
7 changes: 7 additions & 0 deletions docs/docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ ignore patterns that omit matched files from output.
By default, this command generates output files that omit all `state`.
However, you can use the `--state` flag to generate output files that
include content from a single state that you specify.
If you would like to rename files as you copy them, use
the `--rename` flag. The `--rename` flag takes a JSON
string as an argument. The JSON must represent an object whose keys are filenames that are to be renamed and whose values are the new names of those files.
For example, ` --rename '{"test.txt":"test_new.txt"}'` changes the name of any file names `test.txt` to `test_new.txt`. The `--rename` flag cannot accept a JSON
object whose keys or values contain a path. If you
require this functionality, please submit a pull
request or issue on Github.

### Check

Expand Down
86 changes: 85 additions & 1 deletion src/bluehawk/actions/copy.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { ConsoleActionReporter } from "./ConsoleActionReporter";
import * as Path from "path";
import { getBluehawk, System } from "../../bluehawk";
import { copy } from "./copy";
import { copy, RENAME_ERR } from "./copy";

describe("copy", () => {
beforeEach(getBluehawk.reset);
Expand Down Expand Up @@ -32,6 +32,58 @@ describe("copy", () => {
expect(outputList).toStrictEqual(sourceList);
});

it("can rename a file", async () => {
const rootPath = "/path/to/project";
const outputPath = "/output";
await System.fs.mkdir(rootPath, {
recursive: true,
});
await System.fs.mkdir(outputPath, {
recursive: true,
});
const filePath = Path.join(rootPath, "test.bin");
await System.fs.writeFile(filePath, new Uint8Array([11, 12, 13]));
const reporter = new ConsoleActionReporter();
await copy({
reporter,
output: outputPath,
rootPath,
waitForListeners: true,
rename: { "test.bin": "renamed.bin" },
});
expect(reporter.errorCount).toBe(0);
const sourceList = await System.fs.readdir(rootPath);
expect(sourceList).toStrictEqual(["test.bin"]);
const outputList = await System.fs.readdir(outputPath);
expect(outputList).toStrictEqual(["renamed.bin"]);
});

it("fails to rename file with path separator", async () => {
const rootPath = "/path/to/project/a";
const outputPath = "/output";
await System.fs.mkdir(rootPath, {
recursive: true,
});
await System.fs.mkdir(outputPath, {
recursive: true,
});
const filePath = Path.join(rootPath, "test.bin");
await System.fs.writeFile(filePath, new Uint8Array([11, 12, 13]));
const reporter = new ConsoleActionReporter();
const path_with_sep = `a${Path.sep}test.bin`;
try {
await copy({
reporter,
output: outputPath,
rootPath,
waitForListeners: true,
rename: { path_with_sep: "renamed.bin" },
});
} catch (e) {
expect(e).toEqual(RENAME_ERR);
}
});

it("copies binary files", async () => {
const rootPath = "/path/to/project";
const outputPath = "/output";
Expand Down Expand Up @@ -63,6 +115,38 @@ describe("copy", () => {
expect(didCallBinaryFileForPath).toBe(filePath);
});

it("can rename binary files", async () => {
const rootPath = "/path/to/project";
const outputPath = "/output";
await System.fs.mkdir(rootPath, {
recursive: true,
});
await System.fs.mkdir(outputPath, {
recursive: true,
});
const filePath = Path.join(rootPath, "test.bin");
await System.fs.writeFile(filePath, new Uint8Array([0, 1, 2, 3, 4, 5]));
let didCallBinaryFileForPath: string | undefined = undefined;
const reporter = new ConsoleActionReporter();
await copy({
reporter,
output: outputPath,
rootPath,
onBinaryFile(path) {
didCallBinaryFileForPath = path;
},
waitForListeners: true,
rename: { "test.bin": "new_name.bin" },
});

expect(reporter.errorCount).toBe(0);
const sourceList = await System.fs.readdir(rootPath);
expect(sourceList).toStrictEqual(["test.bin"]);
const outputList = await System.fs.readdir(outputPath);
expect(outputList).toStrictEqual(["new_name.bin"]);
expect(didCallBinaryFileForPath).toBe(filePath);
});

it("copies permissions", async () => {
const rootPath = "/path/to/project";
const outputPath = "/output";
Expand Down
36 changes: 33 additions & 3 deletions src/bluehawk/actions/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,24 @@ export interface CopyArgs extends ActionArgs {
output: string;
state?: string;
ignore?: string | string[];
rename?: Record<string, string>;

/**
Hook for additional work after a binary file is processed.
*/
onBinaryFile?(path: string): Promise<void> | void;
}

// this type is necessary as yargs cannot parse directly to a record
export type CopyArgsCli = Omit<CopyArgs, "rename"> & { rename?: string };

export const RENAME_ERR =
"Rename flag does not support specifying a path argument. If you would like to see this functionality, please submit an issue or pull request.";

export const copy = async (
args: WithActionReporter<CopyArgs>
): Promise<void> => {
const { output, ignore, rootPath, waitForListeners, reporter } = args;
const { output, ignore, rootPath, waitForListeners, reporter, rename } = args;
const desiredState = args.state;
const bluehawk = await getBluehawk();
let stats: Stats;
Expand All @@ -33,6 +40,24 @@ export const copy = async (
});
return;
}

// check that args does not contain path separator. Can add this in if a use case arises.
if (rename) {
for (const [key, value] of Object.entries(rename)) {
if (key.includes(path.sep) || value.includes(path.sep)) {
throw Error(RENAME_ERR);
}
}
}

// construct path for file. Renames file if name specified in rename map.
const getRenameAwareTargetPath = (directory: string, name: string) => {
if (rename && rename[name] !== undefined) {
name = rename[name];
}
return path.join(directory, name);
};

const projectDirectory = !stats.isDirectory()
? path.dirname(rootPath)
: rootPath;
Expand All @@ -43,7 +68,11 @@ export const copy = async (
output,
path.relative(projectDirectory, path.dirname(filePath))
);
const targetPath = path.join(directory, path.basename(filePath));

let targetPath = getRenameAwareTargetPath(
directory,
path.basename(filePath)
);
try {
await System.fs.mkdir(directory, { recursive: true });
await System.fs.copyFile(filePath, targetPath);
Expand Down Expand Up @@ -106,7 +135,8 @@ export const copy = async (
output,
path.relative(projectDirectory, path.dirname(document.path))
);
const targetPath = path.join(directory, document.basename);

let targetPath = getRenameAwareTargetPath(directory, document.basename);

try {
await System.fs.mkdir(directory, { recursive: true });
Expand Down
2 changes: 1 addition & 1 deletion src/bluehawk/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ export * from "./processor";
export * from "./project";
export * from "./actions";
export * from "./options";
export * from "./actions/ActionReporter"
export * from "./actions/ActionReporter";
export * from "./actions/ConsoleActionReporter";
10 changes: 10 additions & 0 deletions src/bluehawk/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ export function withStateOption<T>(
});
}

export function withRenameOption<T>(
yargs: Argv<T>
): Argv<T & { rename?: string }> {
return option(yargs, "rename", {
string: true,
describe: "rename files during copy",
once: true,
});
}

export function withIdOption<T>(
yargs: Argv<T>
): Argv<T & { id?: string | string[] }> {
Expand Down
24 changes: 19 additions & 5 deletions src/cli/commandModules/copy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,38 @@ import {
withStateOption,
withIgnoreOption,
withLogLevelOption,
withRenameOption,
ActionArgs,
CopyArgs,
CopyArgsCli,
copy,
} from "../..";

const commandModule: CommandModule<
ActionArgs & { rootPath: string },
CopyArgs
CopyArgsCli
> = {
command: "copy <rootPath>",
builder: (yargs): Argv<CopyArgs> => {
builder: (yargs): Argv<CopyArgsCli> => {
return withLogLevelOption(
withIgnoreOption(withStateOption(withOutputOption(yargs)))
withIgnoreOption(
withStateOption(withRenameOption(withOutputOption(yargs)))
)
);
},
handler: async (args: Arguments<CopyArgs>) => {
handler: async (args: Arguments<CopyArgsCli>) => {
const reporter = new ConsoleActionReporter(args);
await copy({ ...args, reporter });
let parsedRename = undefined;
// parse rename argument to record if specified
if (typeof args.rename === "string") {
try {
parsedRename = JSON.parse(args.rename) as Record<string, string>;
} catch (SyntaxError) {
throw "Unable to parse 'rename' argument. Ensure your 'rename' argument is valid JSON.";
}
}
const argsParsed: CopyArgs = { ...args, rename: parsedRename };
await copy({ ...argsParsed, reporter });
reporter.printSummary();
process.exit(reporter.errorCount > 0 ? 1 : 0);
},
Expand Down

0 comments on commit d97fa76

Please sign in to comment.