How-to: Yeoman for React-Typescript-Webpack project

This is summary of how I create a Yeoman generator for React/Typescript/Webpack project. This imitates the producedure used in React project manual steps post.[1]

What is Yeoman IMO?

Yeoman is a tool to generate a project scaffold. It works by using a generator written with Yeoman framework (yeoman-generator) to generate a specific project scaffold. It also hosts Generator Respository, allowing anyone to write a generator and contribute to Yeoman repository.

How Yeoman generator works?

(This article only introduces basic workflow. Yeoman website has more comprehensive guide on http://yeoman.io/authoring/index.html)

A Yeoman generator is a NodeJS project which includes yeoman-generator library and has a specific directory structure like following.

generator-your-generator-name
├───package.json
└───app/
    └───index.js

Your generator project has to be prefixed with generator- as Yeoman searching generator by name convention.

The generator entry point is in app/index.js, which is simply a Node module.

The module has to export a generator class which inherits Yeoman's generator.Base class.

const generators = require('yeoman-generator')

module.exports = generators.Base.extend({
  stepName() {
    this.log('some generator step...')
  }
})

Note: The sample code is written with ES2015.

generator's Base class has many properties and methods to help generating files. You can see its document in http://yeoman.io/generator/Base.html.

Steps of a generator can be described by functions in the generator class. For example, the above code snippet has one step, named stepName.

To run this generator, you have to put it in NPM global repo. Normally you would install a generator with npm install -g but if you are developing a generator, you may just issue npm link (or sudo npm link for Linux) on the generator root directory.

Then you can call your generator with yo your-generator-name. Note that the working directory for generator is where you run the command. Most generators require you to make a directory and cd it before running.

Generator's special steps

There are 8 pre-defined generator steps (see Running Context).

  1. initializing -- Setting generator's configuration and behavior.
  2. prompting -- Step for user input.
  3. configuring -- Create project configuration(?)
  4. default -- Not really a step name, this step just runs all non-Yeoman functions.
  5. writing -- File generation.
  6. conflicts -- Where conflicts are handled (used internally).
  7. install -- Where NPM, Bower, and external commands are run.
  8. end -- Clean up.

Yeoman's "Virtual File System"

Yeoman has composability concept where a generator can be a composition of other generators. With this concept, Yeoman has to merge generators' outputs before producing the final files. Yeoman uses mem-fs for file operations before committing it to disk.

Template workflow

Template is a set of pre-defined files that are used to generate the scaffold. All template files should be under app/templates directory, like following:

generator-your-generator-name
├───package.json
└───app/
    ├───templates/
    |   └───index.html
    └───index.js

Template supports EJS placeholder. See the full document on https://github.com/SBoudrias/mem-fs-editor.

Time for React-Typescript generator

We are going to create generator-react-typescript to generate a scaffold as described in Create React project using Typescript and Webpack.

Create a generator directory

First, create a directory and initialize Node project.

mkdir generator-react-typescript
cd generator-react-typescript
mkdir app
npm init
npm install --save yeoman-generator

Minimum configuration needed for package.json looks like the snippet below. We need yeoman-generator to inherit generator class.

{
  "name": "generator-react-typescript",
  "version": "0.1.0",
  "description": "Yeoman generator for Typescript/React stack",
  "files": [
    "app"
  ],
  "keywords": ["yeoman-generator"],
  "dependencies": {
    "yeoman-generator": "^0.24.1"
  }
}

Simple generator for user input inquiry

Implement app/index.ts to ask user about project name, description, and option to include Redux.

const generators = require('yeoman-generator')

const isBlank = s => s.match(/^\W*$/) !== null
const containsWS = s => s.match(/\W/) !== null

module.exports = generators.Base.extend({
  prompting() {
    this.log('Welcome to React/Typescript generator.')

    const self = this
    return this.prompt([
      { type: 'input'
      , name: 'slug'
      , message: 'Your project slug'
      , validate: s => !isBlank(s) && !containsWS(s) },
      { type: 'input'
      , name: 'desc'
      , message: 'Description'
      , default: '' },
      { type: 'confirm'
      , name: 'use_redux'
      , message: 'Use Redux'
      , default: false }
    ]).then(answers => self.answers = answers)
  }
})

Yeoman uses Inquirer.js' prompt function. Its philosophy is to allow generator to be run on any UI technology (e.g. GUI client), not just command line on console.

Also note that we have to store result of user input ourselves.

Writing a JSON file

First file to be generated is package.json. The answers from last section are used to create the file. Here the sample of generator:

  createPackageJson() {
    this.fs.writeJSON(
      this.destinationPath('package.json'),
      { name: this.answers.slug
      , version: '1.0.0'
      , description: this.answers.description }
    )
  }

this.fs refers to the Virtual File System. This code schedules JSON file writing with name package.json directly into the "destination path" (which is the working directory where yo is run, by default).

Package dependencies installation

There are specific API for installing either NPM and Bower, or both (see Manage Dependencies). Since we do not use bower here, so we only use npmInstall(packages, options) function.

  installNpmLibs() {
    const reacts = ['react', 'react-dom']
    const additions = this.answers.use_redux? ['redux', 'react-redux'] : []
    const webpacks = ['webpack-bundle-tracker', 'ts-loader', 'source-map-loader']
    const mandatoryApps = ['typescript', 'webpack']
    
    const packages = reacts.concat(additions).concat(webpacks).concat(mandatoryApps)
    this.npmInstall(packages, { saveDev: true })
  }

Copying template files

Yeoman has concept of Source Path and Destination Path, where Source Path is the template directory (e.g. app/templates/) and Destination Path is the working directory, which is usually where yo command is run (but this can be changed by an API destinationRoot(path)).

There are a few template-related APIs[2]. The one I used here is just plain file copying copy(relative_source_path, relative_destination_path). See other versions in Mixin: actions/actions API reference.

The content of the template files can be found in GitHub repo.

  copyFiles() {
    this.copy('tsconfig.json', 'tsconfig.json')
    this.copy('webpack.config.js', 'webpack.config.js')
    this.copy('src/index.tsx', 'src/js/index.tsx')
    this.copy('index.html', 'index.html')
  }

Running a custom command

spawnCommand(program_name, argument_array) is an API for running external command.

I found that running sudo here does not work as the password prompt is not shown and sudo just fails. I have yet no idea how to run command in administrator privilege. Few generator codes I looked at are just asking user to type command in its stead.

  install() {
    const reactTypes = ['dt~react', 'dt~react-dom']
    const reduxTypes = ['dt~redux', 'dt~react-redux']
    const types = reactTypes.concat(this.answers.use_redux? reduxTypes : [])
    this.spawnCommand('typings', ['install', '--save', '--global'].concat(types))
  }

Done!

All above steps are just what we need to generate React/Typescript project. The whole example of this generator file can be found in the references[3].

There are a few more features in Yeoman that are interesting such as command line arguments (it's called options), generator composition, unit testing, etc.

References


  1. How to manually create a React/Typescript project: /create-react-project-using-typescript-and-webpack/ ↩︎

  2. Yeoman template file manipulation API: http://yeoman.io/generator/actions_actions.html#.copy ↩︎

  3. For React/typescript generator code used in this article, see https://github.com/ruxo/generator-react-typescript/blob/d72bbdeb740c1281487e99e55220117a2c57eb9f/app/index.js. ↩︎