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).
initializing
-- Setting generator's configuration and behavior.prompting
-- Step for user input.configuring
-- Create project configuration(?)- default -- Not really a step name, this step just runs all non-Yeoman functions.
writing
-- File generation.conflicts
-- Where conflicts are handled (used internally).install
-- Where NPM, Bower, and external commands are run.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
How to manually create a React/Typescript project: /create-react-project-using-typescript-and-webpack/ ↩︎
Yeoman template file manipulation API: http://yeoman.io/generator/actions_actions.html#.copy ↩︎
For React/typescript generator code used in this article, see https://github.com/ruxo/generator-react-typescript/blob/d72bbdeb740c1281487e99e55220117a2c57eb9f/app/index.js. ↩︎