Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generator "templates" / "base generators" / mixins #105

Closed
macrozone opened this issue Jan 5, 2018 · 10 comments
Closed

Generator "templates" / "base generators" / mixins #105

macrozone opened this issue Jan 5, 2018 · 10 comments

Comments

@macrozone
Copy link
Collaborator

macrozone commented Jan 5, 2018

I want to allow my users to create generators that all follow a certain convention.

E.g. the generator will place every generated element in a certain module-folder (which has a modulename) and which will create that folder if it does not exist.

What i did now is create a helper function that wraps plop.setGenerator and mutates the prompts and actions:

const makeModuleGenerator = (name, generatorConfig) =>
  plop.setGenerator(name, {
    ...generatorConfig,
    prompts: [
      {
        type: 'input',
        name: 'moduleName',
        message: "What's the module name?"
      },
      ...generatorConfig.prompts,
      {
        type: 'confirm',
        name: 'createModule',
        when(data) {
          return !exists(data, modulePath);
        },
        message: 'This module does not exist. Should I create this module?'
      }
    ],
    actions: data => {
      if (data.createModule === false) {
        return []; // abort
      }
      if (isFunction(generatorConfig.actions)) {
        return generatorConfig.actions(data);
      }
      return generatorConfig.actions;
    }
  });

and you can use it like this:

makeModuleGenerator('component', {
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: "What's the component name?"
      }
    ],
    actions: [
      {
        type: 'add',
        template: 'templates/components/component.js',
        path: '{{moduleName}}/components/{{name}}.js',
      }
    ]
  });

While this works, you can't use this helper function out of the box in a plopfile.

Imaging you want to load this helper functions in a plop-pack from npm. Usually, you can load this with plop.load("plop-pack-name") which will add generators, actions, helpers, etc.

But there is no way to provide such a helper function without any changes. You would need to import it additionally through npm.

An approach would be to provide a new generatorTemplate.
This could look like this (same example as above in makeModuleGenerator )

  plop.setGeneratorTemplate("withModule", config => ({
    prompts: [
       {
        type: 'input',
        name: 'moduleName',
        message: "What's the module name?"
      },
      ...config.prompts,
      {
        type: 'confirm',
        name: 'createModule',
        when(data) {
          return !exists(data, modulePath);
        },
        message: 'This module does not exist. Should I create this module?'
      }
    ],
    actions: data => {
      if (data.createModule === false) {
        return []; // abort
      }
      if (isFunction(config.actions)) {
        return config.actions(data);
      }
      return config.actions;
    }
  })

So its basically a function that will mutate the config of a generator.

And you could use this like this (same example as above):

plop.setGenerator('component', {
  generatorTemplate: "withModule",
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: "What's the component name?"
      }
    ],
    actions: [
      {
        type: 'add',
        template: 'templates/components/component.js',
        path: '{{moduleName}}/components/{{name}}.js',
      }
    ]
  });

Apart from managing an additional entity "generatorTemplate", it would be easy to implement:

// where the generator is called
let actualConfig = config;
  if(config.template) {
    templateFunc = getGeneratorTemplates(config.template)
    actualConfig = templateFunc(config)
  } 

(I don't like the name "generatorTemplate", as "template" is already overloaded in plop, maybe we find a better name for that?

@macrozone
Copy link
Collaborator Author

macrozone commented Jan 5, 2018

There is a better approach that does not need to introduce a new type:

  • generators can have a new config property baseGenerator
  • in setGenerator the config can now be a function, identical to the plop.setGeneratorTemplate example above . If this generator is used as a base, the extended config is passed to that function (otherway, its just an empty object)

So taking the example from above, it would look like this:

  plop.setGenerator("module-base", config => ({
    prompts: [
       {
        type: 'input',
        name: 'moduleName',
        message: "What's the module name?"
      },
      ...config.prompts,
      {
        type: 'confirm',
        name: 'createModule',
        when(data) {
          return !exists(data, modulePath);
        },
        message: 'This module does not exist. Should I create this module?'
      }
    ],
    actions: [ ... ]
  })

// and the extended generator:

plop.setGenerator('component', {
  baseGenerator: "module-base",
    prompts: [
      {
        type: 'input',
        name: 'name',
        message: "What's the component name?"
      }
    ],
    actions: [
       // ...
    ]
  });


This could be implemented like this:

// where the generator is used
let actualConfig = generatorConfig;
if (generatorConfig.baseGenerator) {
  baseGenerator = getGenerators(generatorConfig.baseGenerator);
  if (isFunction(baseGenerator.config)) {
    // the actual config is composed by running the current generators config run though the basegenerator's config function
    actualConfig = baseGenerator.config(generatorConfig);
  } else {
    // optional: if the baseGenerator has no function as config but a object literal (like all generators until now, merge the configs in sequence.
    // e.g. prompts are in sequence and actions are in sequence
    actualConfig = {
      ...baseGenerator.config,
      ...generatorConfig,
      prompts: [...baseGenerator.config.prompts, ...generatorConfig.prompts],
      // in this example i don't take into account that actions can be a function
      // but that would just need some additional code
      actions: [...baseGenerator.config.actions, ...config.actions]
    };
  }
}

@macrozone macrozone changed the title Generator "templates" Generator "templates" / base generators Jan 5, 2018
@macrozone macrozone changed the title Generator "templates" / base generators Generator "templates" / base generators / mixins Jan 7, 2018
@macrozone
Copy link
Collaborator Author

ok i played around with it and I realized that the cleanest way is probably introducing a new entity "Generator mixin", which is similar to the approach above with the "baseGenerator".

Here is an example (taken from the unit test):

module.exports = function (plop) {
	'use strict';

	plop.setGeneratorMixin('withModule', ({
		description: 'adds module to the generator',
		prompts:prompts => [
			{
				type: 'input',
				name: 'moduleName',
				message: 'What is the modulename?',
				validate: function (value) {
					if ((/.+/).test(value)) { return true; }
					return 'moduleName is required';
				}
			}
		].concat(prompts),
		actions: (baseActions) => {
			const modulePath = 'src/{{moduleName}}';
			return [
				{
					type: 'add',
					path: modulePath+'/index.txt',
					templateFile: 'plop-templates/moduleIndex.txt',
					abortOnFail: true
				},
			].concat(
				// also extend `path` on every base action
				(baseActions || []).map(
					config => Object.assign({}, config, {path: modulePath+'/'+config.path})
				)
			);
		}
	}));

	plop.setGenerator('module', {
		description: 'adds only the module',
		mixins: ['withModule'],
	});

	plop.setGenerator('module-file', {
		description: 'adds a file inside a module',
		mixins: ['withModule'],
		prompts: [
			{
				type: 'input',
				name: 'name',
				message: 'What is the file name?',
				validate: function (value) {
					if ((/.+/).test(value)) { return true; }
					return 'name is required';
				}
			},
		],
		actions: [
			{
				type: 'add',
				path: 'files/{{name}}.txt',
				templateFile: 'plop-templates/moduleFile.txt',
				abortOnFail: true
			}
		]
	});
};

@macrozone macrozone changed the title Generator "templates" / base generators / mixins Generator "templates" / "base generators" / mixins Jan 7, 2018
@amwmedia
Copy link
Member

amwmedia commented Feb 1, 2018

hey @macrozone, somehow I didn't see this until now... not sure how I missed it ☹️. I'm not sure that what you are proposing here belongs as a new entity within the plop api. You can achieve the same result with a function (as you mention) or a Class that generates the generator config.

example: plop.setGenerator('something', new ModuleGenerator({config}));

I think that adding this as a new entity would introduce unneeded cognitive load for others using plop. I'll give this some more thought, but I would lean toward distributing something like this as a standard npm module that exports a function or a class.

I'll leave this open for now for more discussion.

@macrozone
Copy link
Collaborator Author

@amwmedia

A:


plop.load('my-plop-pack')

plop.setGenerator('components", {
   mixins: ["withModules],
....
})

B:

import { ModuleGenerator } from 'my-plop-pack'

plop.load('my-plop-pack')

plop.setGenerator('components", new ModuleGenerator({config}))

A fits more to how plop handles generators, actions and entities.
Not sure if B is really less "cognitive overhead" as it all of sudden introduce an import statement where everything else is loaded via plop.

@amwmedia
Copy link
Member

amwmedia commented Feb 1, 2018

I believe B is easier to understand because I know that setGenerator takes a name and a config object. I also know that constructor functions are a way to generate objects in a consistent way. So I understand B without knowing what a mixin is and how it relates to generators. I also think that you could run into some real trouble when you try to combine mixins and they both control the output, but that's just a gut feeling.

I guess the real question here is... how often do people want to create generators that inherit some of their behavior from somewhere else? My assumption is that this is a bit of an edge case. Plop is currently designed to have generators whose behavior is primarily owned by a single codebase.

@macrozone
Copy link
Collaborator Author

"I believe B is easier to understand because I know that setGenerator takes a name and a config object. " --> that makes no sense. This is the case in A, not B (mixins is just a config property).

"My assumption is that this is a bit of an edge case."

Propably, probably not. I am missing this feature. You could also providing mixins like linting or pretty every file that has been generated. I think there are a lot more use case

But yeah, it can probably also be solved by saying the user to import some config generator functions manually and using them. Less elegant, but also less complex

@amwmedia
Copy link
Member

amwmedia commented Feb 1, 2018

"that makes no sense. This is the case in A, not B (mixins is just a config property)" --> What I'm trying to say here is that the concept of a "mixin" is extra. A constructor function is a standard way to create objects in JavaScript, so I already know what is happening here. The constructor function will generate an object and the setGenerator method takes an object.

I would think that linting and prettier should be handled by build process instead of generators, but I don't doubt that other use cases could be found.

I'll take a look at the PR, maybe seeing your implementation will shed more light on this for me.

@macrozone
Copy link
Collaborator Author

I tried to revisit that and tried to remove the mixins from my ploppack as this not seems to get merged. But its hard to do. I need the plopfileconfig in my mixins and without a first-citizen api, this will get nasty:

const { moduleGenerator } = require('myploppack');

module.exports = function(plop, config) {
const customConfig = {
  foo: "bar"
}
  plop.load('myploppack',customConfig);
  plop.setGenerator(
    'reducer',
// need to pass in config somehow. This was already handled in my PR
    moduleGenerator(customConfig)({
      description: 'bla',
      /// ... config
    })
  );
};

@amwmedia
Copy link
Member

is the part that "gets nasty" the fact that you have to pass the config in at 2 places? just want to be sure I understand your concern.

@macrozone
Copy link
Collaborator Author

@amwmedia yes, exactly! Sorry for the bad wording.

  • you have to pass the config twice
  • and you have to load the "ploppack" twice (once with require/import and once with plop.load)

I think the underlying problem is because node-plop's architecture does not allow customizing it in a more advance way, you are stuck with the existing api (setGenerator, setAction, etc.), otherwise you have to monkeypatch it

My proposal does not improve the architecture and so does not solve this underlying problem, but it adds an additional api which is similar to the existing api and solves certain use cases.

Could we give it a chance as it is already implemented with tests?

cspotcode pushed a commit to cspotcode/plop that referenced this issue Mar 20, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants