Composability is a way to combine smaller parts to make one large thing. Sort of like Voltron®
Yeoman offers multiple ways for generators to build upon common ground. There’s no sense in rewriting the same functionality, so an API is provided to use generators inside other generators.
In Yeoman, composability can be initiated in two ways:
- A generator can decide to compose itself with another generator (e.g.,
generator-backbone
usesgenerator-mocha
). - An end user may also initiate the composition (e.g., Simon wants to generate a Backbone project with SASS and Rails). Note: end user initiated composition is a planned feature and currently not available.
this.composeWith()
The composeWith
method allows the generator to run side-by-side with another generator (or subgenerator). That way it can use features from the other generator instead of having to do it all by itself.
When composing, don’t forget about the running context and the run loop. On a given priority group execution, all composed generators will execute functions in that group. Afterwards, this will repeat for the next group. Execution between the generators is the same order as composeWith
was called, see execution example.
API
composeWith
takes two parameters.
generatorPath
- A full path pointing to the generator you want to compose with (usually usingrequire.resolve()
).options
- An Object containing options to pass to the composed generator once it runs.
When composing with a peerDependencies
generator:
this.composeWith(require.resolve('generator-bootstrap/generators/app'), {preprocessor: 'sass'});
require.resolve()
returns the path from where Node.js would load the provided module.
Note: If you need to pass arguments
to a Generator based on a version of yeoman-generator
older than 1.0, you can do that by providing an Array
as the options.arguments
key.
Even though it is not an encouraged practice, you can also pass a generator namespace to composeWith
. In that case, Yeoman will try to find that generator installed as a peerDependencies
or globally on the end user system.
this.composeWith('backbone:route', {rjs: true});
composing with a Generator class
composeWith
can also take an object as its first argument. The object should have the following properties defined:
Generator
- The generator class to compose withpath
- The path to the generator files
This will let you compose with generator classes defined in your project or imported from other modules. Passing options
as the second argument to composeWith
works as expected.
// Import generator-node's main generator
const NodeGenerator = require('generator-node/generators/app/index.js');
// Compose with it
this.composeWith({
Generator: NodeGenerator,
path: require.resolve('generator-node/generators/app')
});
execution example
// In my-generator/generators/turbo/index.js
module.exports = class extends Generator {
prompting() {
this.log('prompting - turbo');
}
writing() {
this.log('writing - turbo');
}
};
// In my-generator/generators/electric/index.js
module.exports = class extends Generator {
prompting() {
this.log('prompting - zap');
}
writing() {
this.log('writing - zap');
}
};
// In my-generator/generators/app/index.js
module.exports = class extends Generator {
initializing() {
this.composeWith(require.resolve('../turbo'));
this.composeWith(require.resolve('../electric'));
}
};
Upon running yo my-generator
, this will result in:
prompting - turbo
prompting - zap
writing - turbo
writing - zap
You can alter the function call order by reversing the calls for composeWith
.
Keep in mind you can compose with other public generators available on npm.
For a more complex example of composability, check out generator-generator which is composed of generator-node.
dependencies or peerDependencies
npm allows three types of dependencies:
dependencies
get installed local to the generator. It is the best option to control the version of the dependency used. This is the preferred option.peerDependencies
get installed alongside the generator, as a sibling. For example, ifgenerator-backbone
declaredgenerator-gruntfile
as a peer dependency, the folder tree would look this way:├───generator-backbone/ └───generator-gruntfile/
devDependencies
for testing and development utility. This is not needed here.
When using peerDependencies
, be aware other modules may also need the requested module. Take care not to create version conflicts by requesting a specific version (or a narrow range of versions). Yeoman’s recommendation with peerDependencies
is to always request higher or equal to (>=) or any (*) available versions. For example:
{
"peerDependencies": {
"generator-gruntfile": "*",
"generator-bootstrap": ">=1.0.0"
}
}
Note: as of npm@3, peerDependencies
are no longer automatically installed. To install these dependencies, they must be manually installed: npm install generator-yourgenerator generator-gruntfile generator-bootstrap@">=1.0.0"