How to speed up kickstarting new projects with Yeoman

Vincent Will - Mar 13 '20 - - Dev Community

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

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

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;
    });
}

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
}

For more information about user interactions check the Yeoman documentation.

writing()

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

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()

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 }
);

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 %>

install()

install() {
    this.installDependencies();
}

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
});

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 });
    }
}

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;
    });
}

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 },
    )
}

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')
}

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

Now enter the email and password of your npm account.

The last thing you have to do is typing:

npm publish

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

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .