diff --git a/README.md b/README.md index 37266984..3c464746 100644 --- a/README.md +++ b/README.md @@ -245,6 +245,43 @@ export default function (plop) { }); }; ``` +## setActionTypeDisplay +`setActionTypeDisplay` allows you to change the characters shown on the output of a specific action. For instance, the `add` action's output is prefixed with `++`. + +You may need to write a custom action that fetches a file from an API and adds it, and you may want to use the `++` prefix for consistency. This could be done with the following: + +``` javascript +import chalk from 'chalk'; + +export default function (plop) { + // Define your custom action that asynchronously adds a file + plop.setActionType('fetchAndAddAsync', function (answers, config, plop) { + return new Promise((resolve, reject) => { + if (success) { + resolve('success status message'); + } else { + reject('error message'); + } + }); + }); + + // Use the same action type as 'add' for consistency + plop.setActionTypeDisplay('fetchAndAddAsync', chalk.green('++')); +}; +``` + +By default, the following type displays are set: + +``` javascript +const typeDisplay = { + function: chalk.yellow("->"), + add: chalk.green("++"), + addMany: chalk.green("+!"), + modify: `${chalk.green("+")}${chalk.red("-")}`, + append: chalk.green("_+"), + skip: chalk.green("--"), +}; +``` ## setPrompt [Inquirer](https://github.com/SBoudrias/Inquirer.js) provides many types of prompts out of the box, but it also allows developers to build prompt plugins. If you'd like to use a prompt plugin, you can register it with `setPrompt`. For more details see the [Inquirer documentation for registering prompts](https://github.com/SBoudrias/Inquirer.js#inquirerregisterpromptname-prompt). Also check out the [plop community driven list of custom prompts](https://github.com/plopjs/awesome-plop#inquirer-prompts). diff --git a/packages/node-plop/src/node-plop.js b/packages/node-plop/src/node-plop.js index 043bd123..eef60339 100644 --- a/packages/node-plop/src/node-plop.js +++ b/packages/node-plop/src/node-plop.js @@ -21,6 +21,7 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { const generators = {}; const partials = {}; const actionTypes = {}; + const actionTypeDisplays = {}; const helpers = Object.assign( { pkg: (propertyPath) => _get(pkgJson, propertyPath, ""), @@ -39,6 +40,9 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { const setPartial = (name, str) => { partials[name] = str; }; + const setActionTypeDisplay = (name, typeDisplay) => { + actionTypeDisplays[name] = typeDisplay; + }; const setActionType = (name, fn) => { actionTypes[name] = fn; }; @@ -57,6 +61,7 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { const getHelper = (name) => helpers[name]; const getPartial = (name) => partials[name]; const getActionType = (name) => actionTypes[name]; + const getActionTypeDisplay = (name) => actionTypeDisplays[name]; const getGenerator = (name) => generators[name]; function setGenerator(name = "", config = {}) { // if no name is provided, use a default @@ -75,6 +80,7 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { Object.keys(helpers).filter((h) => !baseHelpers.includes(h)); const getPartialList = () => Object.keys(partials); const getActionTypeList = () => Object.keys(actionTypes); + const getActionTypeDisplayList = () => Object.keys(actionTypeDisplays); function getGeneratorList() { return Object.keys(generators).map(function (name) { const { description } = generators[name]; @@ -118,6 +124,7 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { helpers: false, partials: false, actionTypes: false, + actionTypeDisplays: false, }, includeCfg ); @@ -147,6 +154,12 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { setActionType, proxy.getActionType ); + loadAsset( + proxy.getActionTypeDisplayList(), + includeCfg === true || include.actionTypeDisplays, + setActionTypeDisplay, + proxy.getActionTypeDisplay + ); }) ); } @@ -202,6 +215,9 @@ async function nodePlop(plopfilePath = "", plopCfg = {}) { setActionType, getActionType, getActionTypeList, + setActionTypeDisplay, + getActionTypeDisplay, + getActionTypeDisplayList, // path context methods setPlopfilePath, diff --git a/packages/node-plop/tests/imported-custom-action/imported-custom-action.spec.js b/packages/node-plop/tests/imported-custom-action/imported-custom-action.spec.js index 5c313530..b703498f 100644 --- a/packages/node-plop/tests/imported-custom-action/imported-custom-action.spec.js +++ b/packages/node-plop/tests/imported-custom-action/imported-custom-action.spec.js @@ -62,4 +62,24 @@ describe("imported-custom-action", function () { true ); }); + + test("imported custom action can define a custom type display string", async function () { + const plop = await nodePlop(); + const testFilePath = path.resolve(testSrcPath, "test.txt"); + plop.setActionType("custom-del", customAction); + plop.setActionTypeDisplay("custom-del", "><"); + + // add the file + const addTestFile = { type: "add", path: testFilePath }; + // remove the file + const deleteTestFile = { type: "custom-del", path: testFilePath }; + + const generator = plop.setGenerator("", { + actions: [addTestFile, deleteTestFile], + }); + + expect(typeof plop.getActionType("custom-del")).toBe("function"); + expect(typeof plop.getActionTypeDisplay("custom-del")).toBe("string"); + expect(plop.getActionTypeDisplay("custom-del")).toBe("><"); + }); }); diff --git a/packages/node-plop/tests/load-assets-from-plopfile/load-assets-from-plopfile.spec.js b/packages/node-plop/tests/load-assets-from-plopfile/load-assets-from-plopfile.spec.js index a51e8378..1f2fbb3c 100644 --- a/packages/node-plop/tests/load-assets-from-plopfile/load-assets-from-plopfile.spec.js +++ b/packages/node-plop/tests/load-assets-from-plopfile/load-assets-from-plopfile.spec.js @@ -79,6 +79,7 @@ describe("load-assets-from-plopfile", function () { helpers: true, partials: true, actionTypes: true, + actionTypeDisplays: true, } ); @@ -86,11 +87,19 @@ describe("load-assets-from-plopfile", function () { expect(gNameList.length).toBe(3); expect(plop.getHelperList().length).toBe(3); expect(plop.getPartialList().length).toBe(3); - expect(plop.getActionTypeList().length).toBe(1); + expect(plop.getActionTypeList().length).toBe(2); + expect(plop.getActionTypeDisplayList().length).toBe(1); expect(gNameList.includes("test-generator1")).toBe(true); expect(plop.getHelperList().includes("test-helper2")).toBe(true); expect(plop.getPartialList().includes("test-partial3")).toBe(true); expect(plop.getActionTypeList().includes("test-actionType1")).toBe(true); + expect(plop.getActionTypeList().includes("test-actionType2")).toBe(true); + expect(plop.getActionTypeDisplayList().includes("test-actionType1")).toBe( + false + ); + expect(plop.getActionTypeDisplayList().includes("test-actionType2")).toBe( + true + ); }); test("plop.load passes a config option that can be used to include all the plopfile output", async function () { @@ -101,11 +110,19 @@ describe("load-assets-from-plopfile", function () { expect(gNameList.length).toBe(3); expect(plop.getHelperList().length).toBe(3); expect(plop.getPartialList().length).toBe(3); - expect(plop.getActionTypeList().length).toBe(1); + expect(plop.getActionTypeList().length).toBe(2); + expect(plop.getActionTypeDisplayList().length).toBe(1); expect(gNameList.includes("test-generator1")).toBe(true); expect(plop.getHelperList().includes("test-helper2")).toBe(true); expect(plop.getPartialList().includes("test-partial3")).toBe(true); expect(plop.getActionTypeList().includes("test-actionType1")).toBe(true); + expect(plop.getActionTypeList().includes("test-actionType2")).toBe(true); + expect(plop.getActionTypeDisplayList().includes("test-actionType1")).toBe( + false + ); + expect(plop.getActionTypeDisplayList().includes("test-actionType2")).toBe( + true + ); }); test("plop.load should import functioning assets", async function () { @@ -124,6 +141,7 @@ describe("load-assets-from-plopfile", function () { expect(plop.getHelper("test-helper2")("test")).toBe("helper 2: test"); expect(plop.getPartial("test-partial3")).toBe("partial 3: {{name}}"); expect(plop.getActionType("test-actionType1")()).toBe("test"); + expect(plop.getActionType("test-actionType2")()).toBe("test"); }); test("plop.load can include only helpers", async function () { diff --git a/packages/node-plop/tests/load-assets-from-plopfile/plopfile.js b/packages/node-plop/tests/load-assets-from-plopfile/plopfile.js index 56191766..0878e22f 100644 --- a/packages/node-plop/tests/load-assets-from-plopfile/plopfile.js +++ b/packages/node-plop/tests/load-assets-from-plopfile/plopfile.js @@ -10,6 +10,8 @@ export default function (plop, config = {}) { plop.setPartial(`${cfg.prefix}partial3`, "partial 3: {{name}}"); plop.setActionType(`${cfg.prefix}actionType1`, () => "test"); + plop.setActionType(`${cfg.prefix}actionType2`, () => "test"); + plop.setActionTypeDisplay(`${cfg.prefix}actionType2`, "><"); const generatorObject = { actions: [{ type: "add", path: "src/{{name}}.txt" }], diff --git a/packages/node-plop/types/index.d.ts b/packages/node-plop/types/index.d.ts index a0c0f18d..560bc0cb 100644 --- a/packages/node-plop/types/index.d.ts +++ b/packages/node-plop/types/index.d.ts @@ -11,6 +11,7 @@ export interface IncludeDefinitionConfig { helpers?: boolean; partials?: boolean; actionTypes?: boolean; + actionTypeDisplays?: boolean; } export type IncludeDefinition = boolean | string[] | IncludeDefinitionConfig; @@ -46,6 +47,8 @@ export interface NodePlopAPI { setActionType(name: string, fn: CustomActionFunction): void; + setActionTypeDisplay(name: string, typeDisplay: string): void; + /** * This does not include a `CustomActionConfig` for the same reasons * Listed in the `ActionType` declaration. Please see that JSDoc for more @@ -54,6 +57,10 @@ export interface NodePlopAPI { getActionTypeList(): string[]; + getActionTypeDisplay(name: string): string; + + getActionTypeDisplayList(): string[]; + setPlopfilePath(filePath: string): void; getPlopfilePath(): string; diff --git a/packages/plop/bin/plop.js b/packages/plop/bin/plop.js old mode 100644 new mode 100755 diff --git a/packages/plop/src/console-out.js b/packages/plop/src/console-out.js index bda7b30e..24b5acb9 100644 --- a/packages/plop/src/console-out.js +++ b/packages/plop/src/console-out.js @@ -117,6 +117,11 @@ const typeDisplay = { append: chalk.green("_+"), skip: chalk.green("--"), }; + +const addToTypeDisplay = (name, characters) => { + typeDisplay[name] = characters; +}; + const typeMap = (name, noMap) => { const dimType = chalk.dim(name); return noMap ? dimType : typeDisplay[name] || dimType; @@ -126,6 +131,7 @@ export { chooseOptionFromList, displayHelpScreen, createInitPlopfile, + addToTypeDisplay, typeMap, getHelpMessage, }; diff --git a/packages/plop/src/plop.js b/packages/plop/src/plop.js index 6f33d572..e069ade3 100644 --- a/packages/plop/src/plop.js +++ b/packages/plop/src/plop.js @@ -70,6 +70,10 @@ async function run(env, _, passArgsBeforeDashes) { plop, passArgsBeforeDashes ); + const actionTypeDisplays = plop.getActionTypeDisplayList(); + actionTypeDisplays.forEach((type) => { + out.addToTypeDisplay(type, plop.getActionTypeDisplay(type)); + }); // look up a generator and run it with calculated bypass data const runGeneratorByName = (name) => { diff --git a/packages/plop/tests/actions.spec.js b/packages/plop/tests/actions.spec.js index 25c51418..54e073cd 100644 --- a/packages/plop/tests/actions.spec.js +++ b/packages/plop/tests/actions.spec.js @@ -27,6 +27,7 @@ test("Plop to add and rename files", async () => { const data = fs.readFileSync(expectedFilePath, "utf8"); expect(data).toMatch(/Hello/); + expect(await findByText("++ /output/new-output.txt")).toBeInTheConsole(); }); test("Plop to add and change file contents", async () => { @@ -48,9 +49,24 @@ test("Plop to add and change file contents", async () => { const data = await fs.promises.readFile(expectedFilePath, "utf8"); expect(data).toMatch(/Hi Corbin!/); + expect(await findByText("++ /output/new-output.txt")).toBeInTheConsole(); }); test.todo("Test modify"); test.todo("Test append"); test.todo("Test built-in helpers"); test.todo("Test custom helpers"); + +test("Plop to display a custom string for a given action type", async () => { + const expectedFilePath = await getFilePath( + "./examples/custom-action-display/output/out.txt" + ); + + const { findByText } = await renderPlop(["addFile"], { + cwd: resolve(__dirname, "./examples/custom-action-display"), + }); + + await waitFor(() => fs.promises.stat(expectedFilePath)); + + expect(await findByText(">< /output/out.txt")).toBeInTheConsole(); +}); diff --git a/packages/plop/tests/examples/custom-action-display/output/.gitkeep b/packages/plop/tests/examples/custom-action-display/output/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/packages/plop/tests/examples/custom-action-display/package.json b/packages/plop/tests/examples/custom-action-display/package.json new file mode 100644 index 00000000..440ecbf6 --- /dev/null +++ b/packages/plop/tests/examples/custom-action-display/package.json @@ -0,0 +1,7 @@ +{ + "name": "plop-example-custom-action-display", + "type": "module", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } +} diff --git a/packages/plop/tests/examples/custom-action-display/plopfile.js b/packages/plop/tests/examples/custom-action-display/plopfile.js new file mode 100644 index 00000000..16f10a66 --- /dev/null +++ b/packages/plop/tests/examples/custom-action-display/plopfile.js @@ -0,0 +1,14 @@ +export default function (plop) { + plop.setActionTypeDisplay("add", "><"); + plop.setGenerator("addFile", { + description: "Add a file", + prompts: [], + actions: [ + { + type: "add", + path: "./output/out.txt", + templateFile: "./templates/to-add.txt", + }, + ], + }); +} diff --git a/packages/plop/tests/examples/custom-action-display/templates/to-add.txt b/packages/plop/tests/examples/custom-action-display/templates/to-add.txt new file mode 100644 index 00000000..e965047a --- /dev/null +++ b/packages/plop/tests/examples/custom-action-display/templates/to-add.txt @@ -0,0 +1 @@ +Hello