DEV Community

Cover image for How to speed up kickstarting new projects with Yeoman
Vincent Will
Vincent Will

Posted on

How to speed up kickstarting new projects with Yeoman

I found myself often copy-pasting code from other projects when starting new projects. This is why I created a Yeoman generator, which setups a nextjs project with styled components, as this is one of my most commonly used base structures.

yeoman generator demonstration

Creating your own generator

In this post I'll explain how Yeoman works and how you can set up your own generator. First of all you'll have to globally install Yeoman and the generator-generator from Yeoman, which helps setting up new generators.

npm install -g yo generator-generator
Enter fullscreen mode Exit fullscreen mode

After the installation is done, you can scaffold your generator by typing yo generator and going through the wizard. Now the structure of your project should look like this:

yeoman generator structure

To be able to test your generator locally, you'll have to symlink a global module to your local file by going into your generated directory and typing:

npm link
Enter fullscreen mode Exit fullscreen mode

Now you'll be able to run your generator by typing yo name-of-your-generator. I'd recommend opening a new workspace for that, so you're not messing up your generator project.

If you do that right away you'll get an error, if you don't have bower installed. That's because yeoman is trying to install dependencies with npm and bower by default. But don't worry, we'll cover this later.

The interesting part of the generator is happening inside generators/app/. Let's have a look at the index.js in the app folder first. The exported class includes three functions: prompting(), writing() and install()

prompting()

This function is executed first when running your generator.

prompting() {
    // Have Yeoman greet the user.
    this.log(
        yosay(`Welcome to the slick ${chalk.red('generator-yeoman-demo')} generator!`)
    );

    const prompts = [
        {
            type: 'confirm',
            name: 'someAnswer',
            message: 'Would you like to enable this option?',
            default: true
        }
    ];

    return this.prompt(prompts).then(props => {
        // To access props later use this.props.someAnswer;
        this.props = props;
    });
}
Enter fullscreen mode Exit fullscreen mode

In the beginning, the function greets the user with this.log(). Afterward, the questions for the user of the generator are defined in the constant prompts. In the end, the answers to these prompts are stored in this.props by their name. So the answer to the question above will be accessible through this.prompt.someAnswer.

To add prompts for the user, you just need to extend the prompts array. A question for the name of the project would look like this:

{
        type: "input",
        name: "projectName",
        message: "Your project name",
        default: this.appname // Default to current folder name
}
Enter fullscreen mode Exit fullscreen mode

For more information about user interactions check the Yeoman documentation.

writing()

writing() {
    this.fs.copy(
        this.templatePath('dummyfile.txt'),
        this.destinationPath('dummyfile.txt')
    );
}
Enter fullscreen mode Exit fullscreen mode

This is where the magic happens. This default code takes the file dummyfile.txt from the directory generators/app/templates and copies it to the directory from where the generator is called. If you want to just copy all files from the templates folder you can also use wildcard selectors:

this.templatePath('**/*'),
this.destinationPath()
Enter fullscreen mode Exit fullscreen mode

Of course, we also want to make use of the prompts the user answered. Therefore we have to change the this.fs.copy function to this.fs.copyTpl and pass the prop to the function:

this.fs.copyTpl(
    this.templatePath('**/*'),
    this.destinationPath(),
    { projectName: this.props.projectName }
);
Enter fullscreen mode Exit fullscreen mode

For the filesystem Yeoman is using the mem-fs-editor, so check their documentation if you want to know more details. As templating engine Yeoman is using ejs. So to make use of the passed variable you can include it in your files (eg. dummyfile.txt) with the following syntax:

Welcome to your project: <%= projectName %>
Enter fullscreen mode Exit fullscreen mode

install()

install() {
    this.installDependencies();
}
Enter fullscreen mode Exit fullscreen mode

This will run npm and bower install by default. But you can also pass parameters to specify what should be called.

this.installDependencies({
    npm: false,
    bower: true,
    yarn: true
});
Enter fullscreen mode Exit fullscreen mode

It is also possible to install specific packages programmatically by using npmInstall() or yarnInstall(). This makes the most sense in combination with a check for what the user selected in the prompting() function:

install() {
    if (this.props.installLodash) {
        this.npmInstall(['lodash'], { 'save-dev': true });
    }
}
Enter fullscreen mode Exit fullscreen mode

Also, you can just remove the whole install() function if you don't want anything to be installed.

Handling user options

Let's have a look at how to work with user input. For that I'll add two demo options to the prompting() function:

prompting() {
    // Have Yeoman greet the user.
    this.log(
        yosay(`Welcome to the slick ${chalk.red('generator-yeoman-demo')} generator!`)
    );

    const prompts = [
        {
            type: "input",
            name: "projectName",
            message: "Your project name",
            default: this.appname // Default to current folder name
        },
        {
            type: 'confirm',
            name: 'someAnswer',
            message: 'Would you like to enable this option?',
            default: true
        },
        {
            type: 'confirm',
            name: 'anotherAnswer',
            message: 'Would you like to enable this option too?',
            default: true
        }
    ];

    return this.prompt(prompts).then(props => {
        // To access props later use this.props.someAnswer;
        this.props = props;
    });
}
Enter fullscreen mode Exit fullscreen mode

Now we'll have this.props.someAnswer and this.props.anotherAnswer available in our writing() function.

Overwriting files

Of course, you can just copy file by file depending on the chosen options. But this is not very scalable. So create a new function for copying in your index.js file.

_generateFiles(path) {
    this.fs.copyTpl(
        this.templatePath(`${path}/**/*`),
        this.destinationPath(),
        { projectName: this.props.projectName },
    )
}
Enter fullscreen mode Exit fullscreen mode

This is almost the same function we have in the writing() function. The underscore _ indicates, that this is a private function. It accepts a path parameter and copies everything from the corresponding folder. So if we would call _generateFiles('base'), it would copy all files from generators/app/templates/base.

So now let's update our writing() function to use _generateFiles().

writing() {
    this._generateFiles('base')

    if (this.props.someAnswer)
        this._generateFiles('option')

    if (this.props.anotherAnswer)
        this._generateFiles('anotherOption')
}
Enter fullscreen mode Exit fullscreen mode

So this code will first copy everything from templates/base. Then it would copy the files templates/option if the user selected someAnswer. Files with the same path and title will be overwritten. Afterward, it will do the same for anotherAnswer and templates/anotherOption. Let's take following example:

file tree in templates directory with subfolders

This would mean that we end up with testFile.txt from templates/base if we answered no to the generators prompts. If we answer yes to the first question (someAnswer), we'd end up with testFile.txt and textFile2.txt from templates/option. And if we answered also yes to the third question (anotherAnswer), we'd have testFile.txt from option, but testFile2.txt and testFile3.txt from templates/anotherOption.

Publishing your generator to the npm registry

When you're done developing your generator, you can push it to the npm registry to be able to install it globally on any machine. If you don't want it to be available on npm you can still always use your generator by cloning your repository and doing npm link.

First you need to have an npm account. If you don't have one yet, head to npmjs.com/signup.

Afterward, head back to your project and type in the console

npm login
Enter fullscreen mode Exit fullscreen mode

Now enter the email and password of your npm account.

The last thing you have to do is typing:

npm publish
Enter fullscreen mode Exit fullscreen mode

After up to one day, your generator will then also be listed on the yeoman website for others to be discovered.

To read more about publishing on npm, check this article.

Cheers,
Vincent

Top comments (0)