How to speed up kickstarting new projects with Yeoman
Tags: webdev, yeoman, generator, javascript
Published: 2020-03-13 09:53:17

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](https://github.com/Vincenius/generator-next-styled-components), as this is one of my most commonly used base structures.

![yeoman generator demonstration](https://wweb.dev/blog/yeoman/demo.gif)

## 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](https://yeoman.io/) and the [generator-generator](https://github.com/yeoman/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](https://wweb.dev/blog/yeoman/structure.png)

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](https://yeoman.io/authoring/user-interactions.html).

### 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](https://github.com/sboudrias/mem-fs-editor), so check their documentation if you want to know more details. As templating engine Yeoman is using [ejs](https://github.com/mde/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](https://wweb.dev/blog/yeoman/overwriting-example.png)

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](https://www.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](https://yeoman.io/generators/) for others to be discovered.

To read more about publishing on npm, check this [article](https://zellwk.com/blog/publish-to-npm/).

Cheers,
Vincent