plopjs / plop

Consistency Made Simple
http://plopjs.com
MIT License
7.12k stars 277 forks source link

Using plop programmatically #214

Closed troncoso closed 4 years ago

troncoso commented 4 years ago

I am building a cli tool that generates code in a very specific way. Essentially, I have a configuration file and I need to add/update code in accordance with that configuration. I would like to use Plop as the engine for the code generation part, but it seems designed specifically to be used from the CLI.

Can plop be used programmatically such that my application calls on it as needed rather than a user? Obviously I could use the child_process package to programmatically call the plop CLI, but I was hoping I could just directly command plop from my code.

crutchcorn commented 4 years ago

Plop can be called programmatically. For example, I'm wrapping plop into my own code editor for a project for work:

#!/usr/bin/env node
const path = require('path');
const args = process.argv.slice(2);
const {Plop, run} = require('plop');
const argv = require('minimist')(args);

Plop.launch({
  cwd: argv.cwd,
  configPath: path.join(__dirname, 'plopfile.js'),
  require: argv.require,
  completion: argv.completion
}, run);

And then I update my package.json to include the bin prop:

  "bin": {
    "create-project-app": "./index.js"
  },

This just allows you to wrap Plop in another command. However, you can also do things like use the underlying node-plop, effectively forking plop and using it's template resolution engine for free.

I would suggest reading through the plop docs and familiarizing yourself with custom actions and such. I've been able to add "npm install" functionality after code generation and you can do so much more as well:

const { spawn } = require('child_process');
const { resolve } = require('path');

module.exports = function(plop) {
  plop.setActionType('npmInstall', function(answers, config, plop) {
    return new Promise((resolve, reject) => {
        const ls = spawn('npm', ['install'], {
          cwd: config.path,
          shell: true
        });

        ls.stdout.pipe(process.stdout);

        ls.stderr.pipe(process.stderr);

        ls.on('close', (code) => {
          resolve(code);
        });
      }
    );
  });
// ...
};
crutchcorn commented 4 years ago

Documentation on how to do this (wrapping plop in your own CLI) more effectively has been created:

https://plopjs.com/documentation/#wrapping-plop

Closing this issue

sangdth commented 3 years ago

I'm interested in this topic, and found this one https://github.com/plopjs/node-plop <- is this still on development? Can we use it now?

crutchcorn commented 3 years ago

@sangdth yes. node-plop is what powers plop and can absolutely be used if you'd prefer. Keep in mind that you'd lose much of the CLI functionality that makes plop plop

sangdth commented 3 years ago

Thank you, currently I just want to create a file (based on template) in a callback, inside a node.js app. Do you think it's enough?

derekdon commented 3 years ago

@crutchcorn plop is pretty sweet, thank you... When it comes to wrapping it however, plop vs node-plop via liftoff, I find the docs a bit unclear.

For example...

PlopLiftoff.launch({
    cwd: argv.cwd,
    configPath: path.join(__dirname, '../../dist/plop/plopfile.js'),
    require: argv.require,
    completion: argv.completion,
  }, (env: LiftoffEnv) =>
    PlopRun(env, undefined, true));

This seems like how you wrap plop, but I also want my generators to have the runPrompts and runActions apis (which they don't seem to get), have more control over the running... as the above PlopRun (renamed run exported from plop) will check generator count, throw errors etc. But in my case I'd like to have a the flow: prompts, actions, prompts, actions, prompts, actions... done. Actions need to run before the next prompts so we can read the file system of say a cloned repo in this case. These being more modular generators, we may not want them selectable on their own. Instead a top level generator / function runs them all... So add a few modular generators, not have plop bring up the choices, and instead programmably run some generates (both prompts and actions), and perhaps do this sequentially across several generators, answers and data chained across each. I know internally plop uses node-plop, but the fact that the apis are missing when I get them is annoyingly confusing.

From the README.md on node-plop. Not sure how liftoff, bypass, force fits in to fulfil the above wrapping plop setup.

const nodePlop = require('node-plop');
// load an instance of plop from a plopfile
const plop = nodePlop(`./path/to/plopfile.js`);
// get a generator by name
const basicAdd = plop.getGenerator('basic-add');

// run all the generator actions using the data specified
basicAdd.runActions({name: 'this is a test'}).then(function (results) {
  // do something after the actions have run
})

So there's a few questions here:

  1. How-to wrap and using plop programmatically via node-plop while still ensuring env argv bypass are all in place and passed along.
  2. How we might autostart a specific generator without raising choices. Assume this is just plop component like the docs, but say based on a conditional flow logic inside the custom run function / plopfile / launch... ie. plop.getGenerator, and runPrompts etc.
  3. How we might chain generators to run one after the other, keep thing modular, answers and bypasses being passed along.

Tried a number of things, ie.

export const defaultGenerator: PlopGeneratorConfig = {
  description: generatorDescription('Chained example'),
  async prompts(inquirer) { return {}; },
  actions: () => {
    return [
      <GeneratorActionConfig.Run>{
        type: GeneratorActionType.run,
        generator: step1Generator,
      },
      <GeneratorActionConfig.Run>{
        type: GeneratorActionType.run,
        generator: step2Generator,
      },
    ];
  },
}; 
export const run = async (generator: PlopGeneratorConfig, prevAns: Answers, plop: NodePlopAPI, bypass?: string[]) => {
    if (!generator) {
      throw new Error(`Invalid generator, unable to run.`);
    }
    const runner = generatorRunner(plop);
    // progressSpinner.clear();
    const nextAns = await runner.runGeneratorPrompts(generator, bypass);
    await runner.runGeneratorActions(generator, {
      ...prevAns,
      ...nextAns,
    });
  };

... but then the spinner keeps interfering with the prompt output, as I assume it's added during the action phase of the top level defaultGenerator, yet we're off doing additional async prompts and actions. So the top level defaultGenerator I moved calls into the prompts to avoid the spinner issue but then in the async prompts(inquirer) function you don't have access to plop: NodePlopAPI instance driving the session, or the bypass etc, so I can't call through to the run generator function I show above.

I tried to not adding a generator and just calling a method in the launch flow / plopfile that does these chained generator run calls, but then the default run that plop provides throws an error as there's no generator... so the I looked at.

export const launcher = (external: boolean) => {
  /**
   * cli 1 - General user - Just start prompting, chained generators.
   */
  if (external) {
    nodePlop(path.join(__dirname, '../../dist/plop/plopfile.external.js'));
    return;
  }

  /**
   * cli 2 - Power user - List of choices.
   */
  PlopLiftoff.launch({
    cwd: argv.cwd,
    configPath: path.join(__dirname, '../../dist/plop/plopfile.js'),
    require: argv.require,
    completion: argv.completion,
  }, (env: LiftoffEnv) =>
    PlopRun(env, undefined, true));
};

... which in the external case I get full control as not calling PlopRun (plop default run export) , but then I'm not following the wrapping example. Unless I call (env: LiftoffEnv) => nodePlop(..) and pass things along. Something like:

const run = (env: LiftoffEnv) => {
  const plopfilePath = env.configPath;
  const destBasePath = argv.dest || env['dest']; // < dest isn't in the LiftoffEnv type information
  const plop = nodePlop(plopfilePath, {
    destBasePath: destBasePath ? path.resolve(destBasePath) : undefined,
    force: argv.force === true || argv.f === true || false,
  });
};

export const launcher = (external: boolean) => {
  const configPath = external ?
    path.join(__dirname, '../../dist/plop/plopfile.external.js') :
    path.join(__dirname, '../../dist/plop/plopfile.js');
  Plop.launch({
    cwd: argv.cwd,
    configPath,
    require: argv.require,
    completion: argv.completion,
  }, run);
};

Sorry for the long nature of this post... Any advice on best practice would be appreciated.

crutchcorn commented 3 years ago

Haha @derekdon glad you're enjoing it. I'm hardly the one to thank - @amwmedia is the amazing creator of the project. I'm just a (admittedly somewhat lackluster) maintainer

I mean, FWIW the APIs of runPrompts and runActions are not public. They're internal methods exposed by node-plop, not for general plop usage. We generally advice highly advanced/customized usage to use node-plop directly

1) However, your use-case might instead benefit from the skip function on an action:

https://plopjs.com/documentation/#interface-actionconfig

Using this, you should be able to decide when to skip a prompt based off of previous answers.


2) This can be done, but it would require some janky process.arg manipulation before passing to plop.run. I'd be 100% down to see a PR to allowing env to pass in a custom arg/argv values - bypassing the arg logic. This would allow you to pass a generator name programmatically (and the default value args) fairly easily.

However, to do this you'd need to change this function without removing any of the edgecases, so that might be a bit tricky. (unless you simply swap out argv/arg here and pass it in everywhere else)

I'm admittedly working right now on adding e2e tests to Plop in prep for some big rework efforts, but if you'd want to submit a PR I'd be happy to review


3) I'd honestly try to use the skip property mentioned above (in 1.)

If all of that doesn't suit your needs however, to make both the argv bypass and answer-based generation loop, you'd need to implement all of the code present in this repo. node-plop contains all of the code related to running the actions and such, this repo handles all of the CLI interaction niceties (loading spinners, argv passing)

For the smallest amount of code change, modify the doThePlop function, then modify the run function to run that new implementation (you'd likely have to copy+paste). Finally, you could probably import the bypass.js file from plop itself.

derekdon commented 3 years ago

Thanks for the speedy response, and the detail around arg / bypass, and the links into the code that I might need to think about... it's really useful.

I assume you mean when in the context of prompts, and skip actions right? Either way, I do use both but they don't really fulfil my requirements. To recap my flow ideally needs to be:

This allows me to separate generators into distinct tasks. For example:

The idea here you could reconfigure an existing seed / workspace at a later point, so use that generator to do that in isolation, outside of the full chain.

I think I'm actually close to having something working (famous last words)... but sure if I get something nice in place, I'll look at if any of it could be moved into the core, and raise a PR.

Thanks again.

paulfrench commented 2 years ago

I want to use plop programmatically but cannot see how? I'm writing a VSCode extension and would like to use plop to do the code gen, this means creating a single webpack bundle file for the VSCode extension. I'm no javascript or webpack expert etc so any pointers to this would be great. I'm assuming I will need to use some form of webpack file loaders to allow the loading of the plopfile.js and the mustache templates from the webpack bundle?