diff --git a/README.md b/README.md index e5c5b68..cf4cea1 100644 --- a/README.md +++ b/README.md @@ -384,6 +384,20 @@ Property | Type | Default | Description **data** | *Object* | `{}` | *inherited from [ActionConfig](#interface-actionconfig)* **abortOnFail** | *Boolean* | `true` | *inherited from [ActionConfig](#interface-actionconfig)* +## Prepend +The `prepend` action is a similar to `append`. The only difference is that it used to prepend the data in a file at a particular location. + +Property | Type | Default | Description +-------- | ---- | ------- | ----------- +**path** | *String* | | handlebars template that (when rendered) is the path of the file to be modified +**pattern** | *RegExp, String* | | regular expression used to match text where the prepend should happen +**unique** | *Boolean* | `true` | whether identical entries should be removed +**separator** | *String* | `new line` | the value that separates entries +**template** | *String* | | handlebars template to be used for the entry +**templateFile** | *String* | | path a file containing the `template` +**data** | *Object* | `{}` | *inherited from [ActionConfig](#interface-actionconfig)* +**abortOnFail** | *Boolean* | `true` | *inherited from [ActionConfig](#interface-actionconfig)* + ## Custom (Action Function) The `Add` and `Modify` actions will take care of almost every case that plop is designed to handle. However, plop does offer custom action functions for the node/js guru. A custom action function is a function that is provided in the actions array. - Custom action functions are executed by plop with the same [CustomAction](#functionsignature-custom-action) function signature. diff --git a/README.zh.md b/README.zh.md index 448167d..5c8b0c2 100644 --- a/README.zh.md +++ b/README.zh.md @@ -174,7 +174,7 @@ import { NodePlopAPI } from 'plop'; export default function (plop: NodePlopAPI) { -  // plop generator code + // plop generator code }; ``` @@ -184,13 +184,13 @@ export default function (plop: NodePlopAPI) { module.exports = function ( -  /** @type {import('plop').NodePlopAPI} */ + /** @type {import('plop').NodePlopAPI} */ -  plop + plop ) { -  // plop generator code + // plop generator code }; ``` @@ -316,7 +316,7 @@ export default function (plop) { | **prompts** | *Array[[InquirerQuestion](https://github.com/SBoudrias/Inquirer.js/#question)]* | | 需要询问用户的问题 | | **actions** | *Array[[ActionConfig](#interface-actionconfig)]* | | 需要执行的操作 | -  >如果你的Action列表有动态需求,你可以查看[使用动态action数组](#using-a-dynamic-actions-array)部分内容。 + >如果你的Action列表有动态需求,你可以查看[使用动态action数组](#using-a-dynamic-actions-array)部分内容。 ### *接口* `ActionConfig` diff --git a/packages/node-plop/.gitignore b/packages/node-plop/.gitignore index 0838192..f9d9f55 100644 --- a/packages/node-plop/.gitignore +++ b/packages/node-plop/.gitignore @@ -19,6 +19,7 @@ lib # Test mocks tests/*-mock/src +tests/*/src # IDE files .idea \ No newline at end of file diff --git a/packages/node-plop/src/actions/index.js b/packages/node-plop/src/actions/index.js index 180540c..c02eac5 100644 --- a/packages/node-plop/src/actions/index.js +++ b/packages/node-plop/src/actions/index.js @@ -2,5 +2,6 @@ import add from "./add.js"; import addMany from "./addMany.js"; import modify from "./modify.js"; import append from "./append.js"; +import prepend from "./prepend.js"; -export { add, addMany, modify, append }; +export { add, addMany, modify, append, prepend }; diff --git a/packages/node-plop/src/actions/prepend.js b/packages/node-plop/src/actions/prepend.js new file mode 100644 index 0000000..4b50515 --- /dev/null +++ b/packages/node-plop/src/actions/prepend.js @@ -0,0 +1,64 @@ +import * as fspp from "../fs-promise-proxy.js"; + +import { + getRenderedTemplate, + getRenderedTemplatePath, + makeDestPath, + throwStringifiedError, + getRelativeToBasePath, +} from "./_common-action-utils.js"; + +import actionInterfaceTest from "./_common-action-interface-check.js"; + +const doPrepend = async function (data, cfg, plop, fileData) { + const stringToPrepend = await getRenderedTemplate(data, cfg, plop); + // if the prepended string should be unique (default), + // remove any occurence of it (but only if pattern would match) + + const { separator = "\n" } = cfg; + if (cfg.unique !== false) { + // only remove after "pattern", so that we remove not too much accidentally + const parts = fileData.split(cfg.pattern); + const firstPart = parts[0]; + const firstPartWithoutDuplicates = firstPart.replace( + new RegExp(stringToPrepend + separator, "g"), + "", + ); + fileData = fileData.replace(firstPart, firstPartWithoutDuplicates); + } + + // add the prepended string to the start of the "fileData" if "pattern" + // was not provided, i.e. null or false + if (!cfg.pattern) { + // make sure to add a "separator" if "fileData" is not empty + if (fileData.length > 0) { + fileData = separator + fileData; + } + return stringToPrepend + fileData; + } + + return fileData.replace(cfg.pattern, stringToPrepend + separator + "$&"); +}; + +export default async function (data, cfg, plop) { + const interfaceTestResult = actionInterfaceTest(cfg); + if (interfaceTestResult !== true) { + throw interfaceTestResult; + } + const fileDestPath = makeDestPath(data, cfg, plop); + try { + // check path + const pathExists = await fspp.fileExists(fileDestPath); + if (!pathExists) { + throw "File does not exist"; + } else { + let fileData = await fspp.readFile(fileDestPath); + cfg.templateFile = getRenderedTemplatePath(data, cfg, plop); + fileData = await doPrepend(data, cfg, plop, fileData); + await fspp.writeFile(fileDestPath, fileData); + } + return getRelativeToBasePath(fileDestPath, plop); + } catch (err) { + throwStringifiedError(err); + } +} diff --git a/packages/node-plop/tests/prepend/package.json b/packages/node-plop/tests/prepend/package.json new file mode 100644 index 0000000..f02c72a --- /dev/null +++ b/packages/node-plop/tests/prepend/package.json @@ -0,0 +1,11 @@ +{ + "name": "prepend-test", + "private": true, + "type": "module", + "config": { + "nested": [ + "basic-plopfile-test-propertyPath-value-index-0", + "basic-plopfile-test-propertyPath-value-index-1" + ] + } +} diff --git a/packages/node-plop/tests/prepend/plop-templates/list.txt b/packages/node-plop/tests/prepend/plop-templates/list.txt new file mode 100644 index 0000000..3435341 --- /dev/null +++ b/packages/node-plop/tests/prepend/plop-templates/list.txt @@ -0,0 +1,7 @@ +Don't remove me: Plop + +-- PREPEND ITEMS HERE -- + +/* PREPEND OTHER ITEMS HERE */ + ++++++++++++++++++++++++++++++++++++++++ diff --git a/packages/node-plop/tests/prepend/plopfile.js b/packages/node-plop/tests/prepend/plopfile.js new file mode 100644 index 0000000..6cac6d5 --- /dev/null +++ b/packages/node-plop/tests/prepend/plopfile.js @@ -0,0 +1,120 @@ +export default function (plop) { + plop.setGenerator("make-list", { + prompts: [ + { + type: "input", + name: "listName", + message: "What's the list name?", + validate: function (value) { + if (/.+/.test(value)) { + return true; + } + return "name is required"; + }, + }, + ], + actions: [ + { + type: "add", + path: "src/{{listName}}.txt", + templateFile: "plop-templates/list.txt", + }, + ], + }); + + plop.setGenerator("prepend-to-list", { + description: "prepend entry to a list", + prompts: [ + { + type: "input", + name: "listName", + message: "What's the list name?", + validate: function (value) { + if (/.+/.test(value)) { + return true; + } + return "name is required"; + }, + }, + { + type: "input", + name: "name", + message: "What is your name?", + validate: function (value) { + if (/.+/.test(value)) { + return true; + } + return "name is required"; + }, + }, + { + type: "confirm", + name: "allowDuplicates", + message: "Allow Duplicates?", + }, + ], + actions: ({ allowDuplicates }) => [ + { + type: "prepend", + path: "src/{{listName}}.txt", + pattern: /-- PREPEND ITEMS HERE --/gi, + template: "name: {{name}}1", + unique: !allowDuplicates, + }, + { + type: "prepend", + path: "src/{{listName}}.txt", + pattern: "/* PREPEND OTHER ITEMS HERE */", + template: "name: {{name}}2", + unique: !allowDuplicates, + }, + ], + }); + + plop.setGenerator("prepend-without-pattern", { + description: "prepend entry to a list without pattern", + prompts: [ + { + type: "input", + name: "listName", + message: "What's the list name?", + validate: function (value) { + if (/.+/.test(value)) { + return true; + } + return "name is required"; + }, + }, + { + type: "input", + name: "name", + message: "What is your name?", + validate: function (value) { + if (/.+/.test(value)) { + return true; + } + return "name is required"; + }, + }, + { + type: "confirm", + name: "allowDuplicates", + message: "Allow Duplicates?", + }, + ], + actions: ({ allowDuplicates }) => [ + { + type: "prepend", + path: "src/{{listName}}.txt", + template: "name: {{name}}1", + unique: !allowDuplicates, + }, + { + type: "prepend", + path: "src/{{listName}}.txt", + template: "name: {{name}}2", + unique: !allowDuplicates, + }, + ], + }); +} diff --git a/packages/node-plop/tests/prepend/prepend.spec.js b/packages/node-plop/tests/prepend/prepend.spec.js new file mode 100644 index 0000000..416ec78 --- /dev/null +++ b/packages/node-plop/tests/prepend/prepend.spec.js @@ -0,0 +1,124 @@ +import fs from "fs"; +import path from "path"; +import nodePlop from "../../src/index.js"; +import { setupMockPath } from "../helpers/path.js"; + +const { clean, testSrcPath, mockPath } = setupMockPath(import.meta.url); + +describe("prepend", function () { + afterEach(clean); + + let plop; + let makeList; + let prependToList; + let prependWithoutPattern; + + beforeEach(async () => { + plop = await nodePlop(`${mockPath}/plopfile.js`); + makeList = plop.getGenerator("make-list"); + prependToList = plop.getGenerator("prepend-to-list"); + prependWithoutPattern = plop.getGenerator("prepend-without-pattern"); + }); + + test("Check if list has been created", async function () { + await makeList.runActions({ listName: "test" }); + const filePath = path.resolve(testSrcPath, "test.txt"); + expect(fs.existsSync(filePath)).toBe(true); + }); + + test("Check if entry will be prepended", async function () { + await makeList.runActions({ listName: "list1" }); + await prependToList.runActions({ + listName: "list1", + name: "Marco", + allowDuplicates: false, + }); + await prependToList.runActions({ + listName: "list1", + name: "Polo", + allowDuplicates: false, + }); + const filePath = path.resolve(testSrcPath, "list1.txt"); + const content = fs.readFileSync(filePath).toString(); + + expect( + ( + content.match(/name: Marco1\nname: Polo1\n-- PREPEND ITEMS HERE --/g) || + [] + ).length, + ).toBe(1); + expect( + ( + content.match( + /name: Marco2\nname: Polo2\n\/\* PREPEND OTHER ITEMS HERE \*\//g, + ) || [] + ).length, + ).toBe(1); + }); + + test("Check if duplicates get filtered", async function () { + await makeList.runActions({ listName: "list2" }); + + await prependToList.runActions({ + listName: "list2", + name: "Marco", + allowDuplicates: false, + }); + await prependToList.runActions({ + listName: "list2", + name: "Polo", + allowDuplicates: false, + }); + await prependToList.runActions({ + listName: "list2", + name: "Marco", + allowDuplicates: false, + }); + const filePath = path.resolve(testSrcPath, "list2.txt"); + const content = fs.readFileSync(filePath).toString(); + + expect((content.match(/Marco1/g) || []).length).toBe(1); + expect((content.match(/Polo1/g) || []).length).toBe(1); + expect((content.match(/Marco2/g) || []).length).toBe(1); + expect((content.match(/Polo2/g) || []).length).toBe(1); + }); + + test("Check if duplicates are kept, if allowed", async function () { + await makeList.runActions({ listName: "list3" }); + await prependToList.runActions({ + listName: "list3", + name: "Marco", + allowDuplicates: true, + }); + await prependToList.runActions({ + listName: "list3", + name: "Polo", + allowDuplicates: true, + }); + await prependToList.runActions({ + listName: "list3", + name: "Marco", + allowDuplicates: true, + }); + const filePath = path.resolve(testSrcPath, "list3.txt"); + const content = fs.readFileSync(filePath).toString(); + + expect((content.match(/Marco1/g) || []).length).toBe(2); + expect((content.match(/Polo1/g) || []).length).toBe(1); + expect((content.match(/Marco2/g) || []).length).toBe(2); + expect((content.match(/Polo2/g) || []).length).toBe(1); + }); + + test("Check if prepend happen at the top of the file in case of no pattern", async function () { + await makeList.runActions({ listName: "list4" }); + + await prependWithoutPattern.runActions({ + listName: "list4", + name: "Marco", + allowDuplicates: true, + }); + const filePath = path.resolve(testSrcPath, "list4.txt"); + const content = fs.readFileSync(filePath).toString(); + expect(content.match(/name: Marco2\nname: Marco1\nDon't remove me: Plop/g)); + }); +}); diff --git a/packages/node-plop/types/index.d.ts b/packages/node-plop/types/index.d.ts index 377f6ac..6164154 100644 --- a/packages/node-plop/types/index.d.ts +++ b/packages/node-plop/types/index.d.ts @@ -187,7 +187,9 @@ type TemplateStrOrFile = Template | TemplateFile; */ export interface CustomActionConfig extends Omit { - type: TypeString extends "addMany" | "modify" | "append" ? never : TypeString; + type: TypeString extends "addMany" | "modify" | "append" | "prepend" + ? never + : TypeString; [key: string]: any; } @@ -215,6 +217,7 @@ export type ActionType = | AddManyActionConfig | ModifyActionConfig | AppendActionConfig + | PrependActionConfig | CustomActionFunction; export interface ActionConfig { @@ -278,6 +281,16 @@ interface AppendActionConfigBase extends ActionConfig { export type AppendActionConfig = AppendActionConfigBase & TemplateStrOrFile; +interface PrependActionConfigBase extends ActionConfig { + type: "prepend"; + path: string; + pattern: string | RegExp; + unique: boolean; + separator: string; +} + +export type PrependActionConfig = PrependActionConfigBase & TemplateStrOrFile; + export interface PlopCfg { force: boolean; destBasePath: string | undefined; diff --git a/packages/node-plop/types/test.ts b/packages/node-plop/types/test.ts index 6abf173..99d310a 100644 --- a/packages/node-plop/types/test.ts +++ b/packages/node-plop/types/test.ts @@ -7,6 +7,7 @@ import nodePlop, { Actions, ModifyActionConfig, AppendActionConfig, + PrependActionConfig, } from "./index"; import inquirer from "inquirer"; import prompt from "inquirer-autocomplete-prompt"; diff --git a/packages/plop/src/console-out.js b/packages/plop/src/console-out.js index 7999df6..9809c1c 100644 --- a/packages/plop/src/console-out.js +++ b/packages/plop/src/console-out.js @@ -116,6 +116,7 @@ const typeDisplay = { addMany: chalk.green("+!"), modify: `${chalk.green("+")}${chalk.red("-")}`, append: chalk.green("_+"), + prepend: chalk.green("+_"), skip: chalk.green("--"), }; const typeMap = (name, noMap) => {