Feuille de chaleur Yeoman¶
Yeoman - L'outil d'échafaudage du Web
Yeoman est un système d'échafaudage générique permettant la création de tout type d'application. Il permet de démarrer rapidement de nouveaux projets et de rationaliser le maintien des projets existants. Yeoman est agnostique de langue et peut générer des projets dans n'importe quelle langue (Web, Java, Python, C#, etc.).
Sommaire¶
- [Installation] (#installation)
- [Pour commencer] (#getting-started)
- [Générateurs populaires] (#popular-generators)
- [Utilisation de générateurs] (#using-generators)
- [Création de générateurs personnalisés] (#creating-custom-generators)
- [Développement des créateurs] (#generator-development)
- [Sous-générateurs] (#sub-generators)
- [Configuration] (#configuration)
- [Temples] (#templates)
- [Système de fichiers] (#file-system)
- [Interaction avec l'utilisateur] (#user-interaction)
- [Testing Generators] (#testing-generators)
- [Générateurs d'édition] (#publishing-generators)
- [Caractéristiques avancées] (#advanced-features)
- [Intégration] (#integration)
- [Dépannage] (#troubleshooting)
- [Meilleures pratiques] (#best-practices)
Installation¶
Installation mondiale¶
# Install Yeoman globally
npm install -g yo
# Verify installation
yo --version
# Check Yeoman help
yo --help
Exigences du système¶
# Yeoman requires Node.js and npm
node --version # Should be >= 10.0.0
npm --version # Should be >= 6.0.0
# Git is recommended for many generators
git --version
```_
### Installation de générateurs
```bash
# Install popular generators
npm install -g generator-webapp
npm install -g generator-angular
npm install -g generator-react
npm install -g generator-node
npm install -g generator-express
# Search for generators
npm search yeoman-generator
# Install specific generator
npm install -g generator-fountain-webapp
```_
### Configuration du projet
```bash
# Create project directory
mkdir my-yeoman-project
cd my-yeoman-project
# Run a generator
yo webapp
# Or run with options
yo webapp --skip-install
Commencer¶
Utilisation de base¶
# List installed generators
yo
# Run a specific generator
yo webapp
# Get help for a generator
yo webapp --help
# Run generator with options
yo webapp --skip-install --coffee
# Run sub-generator
yo webapp:route myroute
Premier projet¶
# Create a new web app
mkdir my-webapp
cd my-webapp
# Initialize with webapp generator
yo webapp
# Follow the prompts:
# ? What more would you like? (Press <space> to select, <a> to toggle all, <i> to invert selection)
# ❯◉ Sass
# ◉ Bootstrap
# ◉ Modernizr
# Install dependencies
npm install
# Start development server
npm start
Structure du projet¶
my-webapp/
├── app/
│ ├── index.html
│ ├── scripts/
│ │ └── main.js
│ ├── styles/
│ │ └── main.scss
│ └── images/
├── test/
├── bower.json
├── package.json
├── gulpfile.js
├── .bowerrc
├── .gitignore
└── README.md
Commandes communes¶
# List all available generators
yo
# Update generators
npm update -g generator-webapp
# Uninstall generator
npm uninstall -g generator-webapp
# Clear Yeoman cache
yo --clear-cache
# Get version information
yo --version
Générateurs populaires¶
Applications Web¶
# Modern web app with build tools
npm install -g generator-webapp
yo webapp
# Angular applications
npm install -g generator-angular
yo angular
# React applications
npm install -g generator-react-webpack
yo react-webpack
# Vue.js applications
npm install -g generator-vue
yo vue
# Progressive Web App
npm install -g generator-pwa
yo pwa
Backend & APIs¶
# Node.js applications
npm install -g generator-node
yo node
# Express.js applications
npm install -g generator-express
yo express
# Koa.js applications
npm install -g generator-koa
yo koa
# REST API
npm install -g generator-rest
yo rest
# GraphQL API
npm install -g generator-graphql
yo graphql
Développement mobile¶
# Ionic applications
npm install -g generator-ionic
yo ionic
# React Native
npm install -g generator-rn-toolbox
yo rn-toolbox
# Cordova/PhoneGap
npm install -g generator-cordova
yo cordova
Générateurs statiques de sites¶
# Jekyll sites
npm install -g generator-jekyllrb
yo jekyllrb
# Hugo sites
npm install -g generator-hugo
yo hugo
# Hexo blogs
npm install -g generator-hexo
yo hexo
Bibliothèques des composantes¶
# Storybook
npm install -g generator-storybook
yo storybook
# Web Components
npm install -g generator-polymer
yo polymer
# UI Library
npm install -g generator-ui-library
yo ui-library
Utilisation de générateurs¶
Mode interactif¶
# Run generator interactively
yo webapp
# Example prompts:
# ? What more would you like?
# ◉ Sass
# ◉ Bootstrap
# ◉ Modernizr
# ◯ Babel
# ◯ jQuery
# ? Which version of Bootstrap?
# Bootstrap v4
# Bootstrap v3
# ? Would you like to include jQuery?
# Yes
# No
Options de ligne de commande¶
# Skip installation of dependencies
yo webapp --skip-install
# Skip cache
yo webapp --skip-cache
# Force overwrite files
yo webapp --force
# Use specific options
yo angular --coffee --compass
# Quiet mode
yo webapp --quiet
# Get help
yo webapp --help
Fichiers de configuration¶
# Yeoman stores configuration in .yo-rc.json
cat .yo-rc.json
# Example .yo-rc.json
{
"generator-webapp": {
"includeBootstrap": true,
"includeModernizr": true,
"includeSass": true
}
}
Sous-générateurs¶
# List available sub-generators
yo webapp --help
# Common sub-generators:
yo angular:controller myController
yo angular:service myService
yo angular:directive myDirective
yo angular:filter myFilter
yo angular:route myRoute
# Express sub-generators:
yo express:route users
yo express:model User
yo express:controller users
Création de générateurs personnalisés¶
Structure des groupes électrogènes¶
# Create generator directory
mkdir generator-myapp
cd generator-myapp
# Initialize package.json
npm init
# Install yeoman-generator
npm install --save yeoman-generator
# Create generator structure
mkdir -p generators/app
touch generators/app/index.js
Générateur de base¶
// generators/app/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// Constructor
constructor(args, opts) {
super(args, opts);
// Add option
this.option('babel', {
desc: 'Use Babel',
type: Boolean,
defaults: false
});
}
// Prompt user for input
async prompting() {
this.answers = await this.prompt([
{
type: 'input',
name: 'name',
message: 'Your project name',
default: this.appname
},
{
type: 'confirm',
name: 'cool',
message: 'Would you like to enable the Cool feature?'
}
]);
}
// Write files
writing() {
// Copy template files
this.fs.copy(
this.templatePath('index.html'),
this.destinationPath('public/index.html')
);
// Copy with template processing
this.fs.copyTpl(
this.templatePath('_package.json'),
this.destinationPath('package.json'),
{
title: this.answers.name,
babel: this.options.babel
}
);
}
// Install dependencies
install() {
this.npmInstall();
}
};
Paquet.json pour générateur¶
{
"name": "generator-myapp",
"version": "1.0.0",
"description": "My custom Yeoman generator",
"main": "generators/app/index.js",
"keywords": [
"yeoman-generator"
],
"dependencies": {
"yeoman-generator": "^4.0.0"
},
"files": [
"generators"
]
}
Répertoire des modèles¶
# Create templates directory
mkdir -p generators/app/templates
# Add template files
touch generators/app/templates/index.html
touch generators/app/templates/_package.json
touch generators/app/templates/README.md
Fichiers de modèles¶
<!-- generators/app/templates/index.html -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
</head>
<body>
<h1>Welcome to <%= title %>!</h1>
<% if (cool) { %>
<p>Cool feature is enabled!</p>
<% } %>
</body>
</html>
// generators/app/templates/_package.json
{
"name": "<%= title %>",
"version": "1.0.0",
"description": "Generated with my custom generator",
"scripts": {
"start": "node server.js"<% if (babel) { %>,
"build": "babel src -d lib"<% } %>
},
"dependencies": {<% if (babel) { %>
"@babel/core": "^7.0.0",
"@babel/cli": "^7.0.0"<% } %>
}
}
Développement des groupes électrogènes¶
Cycle de vie des générateurs¶
// generators/app/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
// 1. Initialization
initializing() {
this.log('Initializing generator...');
}
// 2. Prompting
async prompting() {
this.answers = await this.prompt([
{
type: 'input',
name: 'name',
message: 'Project name:'
}
]);
}
// 3. Configuring
configuring() {
this.config.set('name', this.answers.name);
this.config.save();
}
// 4. Default (custom methods)
default() {
this.log('Running default tasks...');
}
// 5. Writing
writing() {
this.fs.copy(
this.templatePath('**/*'),
this.destinationPath()
);
}
// 6. Conflicts (handled automatically)
// 7. Install
install() {
this.npmInstall();
}
// 8. End
end() {
this.log('Generator finished successfully!');
}
};
Mise en service avancée¶
async prompting() {
const prompts = [
{
type: 'input',
name: 'name',
message: 'Project name:',
default: this.appname,
validate: (input) => {
if (input.length === 0) {
return 'Project name cannot be empty';
}
return true;
}
},
{
type: 'list',
name: 'framework',
message: 'Choose a framework:',
choices: [
'React',
'Angular',
'Vue',
'Vanilla JS'
],
default: 'React'
},
{
type: 'checkbox',
name: 'features',
message: 'Select features:',
choices: [
{
name: 'Sass',
value: 'sass',
checked: true
},
{
name: 'TypeScript',
value: 'typescript',
checked: false
},
{
name: 'Testing',
value: 'testing',
checked: true
}
]
},
{
type: 'confirm',
name: 'installDeps',
message: 'Install dependencies now?',
default: true
}
];
this.answers = await this.prompt(prompts);
}
Opérations du système de fichiers¶
writing() {
// Copy single file
this.fs.copy(
this.templatePath('index.html'),
this.destinationPath('public/index.html')
);
// Copy with template processing
this.fs.copyTpl(
this.templatePath('_package.json'),
this.destinationPath('package.json'),
this.answers
);
// Copy entire directory
this.fs.copy(
this.templatePath('src/**/*'),
this.destinationPath('src/')
);
// Write file from string
this.fs.write(
this.destinationPath('README.md'),
`# ${this.answers.name}\n\nGenerated with Yeoman`
);
// Extend JSON file
this.fs.extendJSON(
this.destinationPath('package.json'),
{
scripts: {
test: 'jest'
}
}
);
// Delete file
this.fs.delete(this.destinationPath('unwanted-file.js'));
}
Génération de fichiers conditionnels¶
writing() {
// Base files
this.fs.copy(
this.templatePath('base/**/*'),
this.destinationPath()
);
// Framework-specific files
if (this.answers.framework === 'React') {
this.fs.copy(
this.templatePath('react/**/*'),
this.destinationPath()
);
} else if (this.answers.framework === 'Angular') {
this.fs.copy(
this.templatePath('angular/**/*'),
this.destinationPath()
);
}
// Feature-specific files
if (this.answers.features.includes('typescript')) {
this.fs.copy(
this.templatePath('tsconfig.json'),
this.destinationPath('tsconfig.json')
);
}
if (this.answers.features.includes('testing')) {
this.fs.copy(
this.templatePath('test/**/*'),
this.destinationPath('test/')
);
}
}
Sous-générateurs¶
Création de sous-générateurs¶
# Create sub-generator directory
mkdir -p generators/component
touch generators/component/index.js
mkdir -p generators/component/templates
Mise en œuvre des sous-générateurs¶
// generators/component/index.js
const Generator = require('yeoman-generator');
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
// Accept component name as argument
this.argument('name', {
type: String,
required: true,
desc: 'Component name'
});
}
async prompting() {
this.answers = await this.prompt([
{
type: 'confirm',
name: 'withStyles',
message: 'Include CSS file?',
default: true
},
{
type: 'confirm',
name: 'withTest',
message: 'Include test file?',
default: true
}
]);
}
writing() {
const componentName = this.options.name;
// Create component file
this.fs.copyTpl(
this.templatePath('component.js'),
this.destinationPath(`src/components/${componentName}.js`),
{
name: componentName,
className: componentName.charAt(0).toUpperCase() + componentName.slice(1)
}
);
// Create styles if requested
if (this.answers.withStyles) {
this.fs.copyTpl(
this.templatePath('component.css'),
this.destinationPath(`src/components/${componentName}.css`),
{ name: componentName }
);
}
// Create test if requested
if (this.answers.withTest) {
this.fs.copyTpl(
this.templatePath('component.test.js'),
this.destinationPath(`src/components/${componentName}.test.js`),
{ name: componentName }
);
}
}
};
Modèles de sous-générateur¶
// generators/component/templates/component.js
import React from 'react';<% if (withStyles) { %>
import './<%= name %>.css';<% } %>
const <%= className %> = () => {
return (
<div className="<%= name %>">
<h2><%= className %> Component</h2>
</div>
);
};
export default <%= className %>;
Utilisation de sous-générateurs¶
# Run sub-generator
yo myapp:component MyButton
# With options
yo myapp:component MyButton --with-styles --with-test
# List available sub-generators
yo myapp --help
Configuration¶
Configuration du générateur¶
// Store configuration
configuring() {
this.config.set({
framework: this.answers.framework,
features: this.answers.features,
version: '1.0.0'
});
this.config.save();
}
// Read configuration
default() {
const config = this.config.getAll();
this.log('Current config:', config);
const framework = this.config.get('framework');
this.log('Framework:', framework);
}
Configuration globale¶
Variables d'environnement¶
// Use environment variables
writing() {
const isDev = process.env.NODE_ENV === 'development';
this.fs.copyTpl(
this.templatePath('config.js'),
this.destinationPath('config.js'),
{
apiUrl: isDev ? 'http://localhost:3000' : 'https://api.example.com'
}
);
}
Options et arguments¶
constructor(args, opts) {
super(args, opts);
// Add option
this.option('skip-install', {
desc: 'Skip installation of dependencies',
type: Boolean,
defaults: false
});
// Add argument
this.argument('name', {
type: String,
required: false,
desc: 'Project name'
});
}
// Use options and arguments
writing() {
const projectName = this.options.name || 'my-project';
const skipInstall = this.options['skip-install'];
// Use in templates
this.fs.copyTpl(
this.templatePath('_package.json'),
this.destinationPath('package.json'),
{ name: projectName }
);
}
install() {
if (!this.options['skip-install']) {
this.npmInstall();
}
}
Modèles¶
Modèle Syntaxe¶
<!-- EJS template syntax -->
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<% if (includeBootstrap) { %>
<link rel="stylesheet" href="bootstrap.css">
<% } %>
</head>
<body>
<h1>Welcome to <%= appName %>!</h1>
<% features.forEach(function(feature) { %>
<p>Feature: <%= feature %></p>
<% }); %>
<% if (author) { %>
<footer>Created by <%= author %></footer>
<% } %>
</body>
</html>
Aides au modèle¶
// Custom template helpers
writing() {
const templateData = {
title: this.answers.title,
features: this.answers.features,
// Helper functions
capitalize: (str) => str.charAt(0).toUpperCase() + str.slice(1),
kebabCase: (str) => str.toLowerCase().replace(/\s+/g, '-'),
camelCase: (str) => str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : '')
};
this.fs.copyTpl(
this.templatePath('component.js'),
this.destinationPath('src/Component.js'),
templateData
);
}
Modèles conditionnels¶
// generators/app/templates/webpack.config.js
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
module: {
rules: [<% if (babel) { %>
{
test: /\.js$/,
exclude: /node_modules/,
use: 'babel-loader'
},<% } %><% if (sass) { %>
{
test: /\.scss$/,
use: ['style-loader', 'css-loader', 'sass-loader']
},<% } %>
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}<% if (devServer) { %>,
devServer: {
contentBase: './dist',
hot: true
}<% } %>
};
Système de fichiers¶
Opérations de fichiers¶
writing() {
// Check if file exists
if (this.fs.exists(this.destinationPath('package.json'))) {
this.log('Package.json already exists');
}
// Read file
const content = this.fs.read(this.templatePath('template.txt'));
// Write file
this.fs.write(
this.destinationPath('output.txt'),
content.replace('{{name}}', this.answers.name)
);
// Append to file
this.fs.append(
this.destinationPath('README.md'),
'\n## Generated with Yeoman'
);
// Copy with transformation
this.fs.copy(
this.templatePath('src/**/*.js'),
this.destinationPath('src/'),
{
process: (content) => {
return content.toString().replace(/OLD_NAME/g, this.answers.name);
}
}
);
}
Manipulation JSON¶
writing() {
// Read and modify package.json
const pkg = this.fs.readJSON(this.destinationPath('package.json'), {});
// Add dependencies
pkg.dependencies = pkg.dependencies || {};
pkg.dependencies.express = '^4.17.1';
if (this.answers.database === 'mongodb') {
pkg.dependencies.mongoose = '^5.12.0';
}
// Add scripts
pkg.scripts = pkg.scripts || {};
pkg.scripts.start = 'node server.js';
pkg.scripts.dev = 'nodemon server.js';
// Write back to file
this.fs.writeJSON(this.destinationPath('package.json'), pkg);
// Or use extendJSON for simpler cases
this.fs.extendJSON(this.destinationPath('package.json'), {
keywords: ['yeoman', 'generator'],
author: this.answers.author
});
}
Opérations de répertoire¶
writing() {
// Create directory
this.fs.copy(
this.templatePath('empty-dir/.gitkeep'),
this.destinationPath('logs/.gitkeep')
);
// Copy entire directory structure
this.fs.copy(
this.templatePath('project-template/**/*'),
this.destinationPath(),
{
globOptions: {
dot: true // Include hidden files
}
}
);
// Selective copying
const features = this.answers.features;
if (features.includes('auth')) {
this.fs.copy(
this.templatePath('auth/**/*'),
this.destinationPath('src/auth/')
);
}
if (features.includes('database')) {
this.fs.copy(
this.templatePath('models/**/*'),
this.destinationPath('src/models/')
);
}
}
Interaction utilisateur¶
Demandes avancées¶
async prompting() {
const prompts = [
{
type: 'input',
name: 'name',
message: 'Project name:',
default: this.appname,
filter: (input) => input.toLowerCase().replace(/\s+/g, '-'),
validate: (input) => {
if (!/^[a-z0-9-]+$/.test(input)) {
return 'Project name must contain only lowercase letters, numbers, and hyphens';
}
return true;
}
},
{
type: 'list',
name: 'framework',
message: 'Choose a framework:',
choices: [
{ name: 'React', value: 'react' },
{ name: 'Angular', value: 'angular' },
{ name: 'Vue.js', value: 'vue' },
{ name: 'Vanilla JavaScript', value: 'vanilla' }
],
default: 'react'
},
{
type: 'checkbox',
name: 'features',
message: 'Select additional features:',
choices: [
{ name: 'TypeScript', value: 'typescript', checked: true },
{ name: 'Sass/SCSS', value: 'sass', checked: true },
{ name: 'ESLint', value: 'eslint', checked: true },
{ name: 'Prettier', value: 'prettier', checked: true },
{ name: 'Jest Testing', value: 'jest', checked: false },
{ name: 'Storybook', value: 'storybook', checked: false }
]
},
{
type: 'confirm',
name: 'installDeps',
message: 'Install dependencies automatically?',
default: true
}
];
// Conditional prompts
const answers = await this.prompt(prompts);
if (answers.framework === 'react') {
const reactPrompts = await this.prompt([
{
type: 'list',
name: 'reactVersion',
message: 'React version:',
choices: ['17', '18'],
default: '18'
},
{
type: 'confirm',
name: 'useHooks',
message: 'Use React Hooks?',
default: true
}
]);
Object.assign(answers, reactPrompts);
}
this.answers = answers;
}
Prompts dynamiques¶
async prompting() {
// First set of prompts
const basicAnswers = await this.prompt([
{
type: 'list',
name: 'projectType',
message: 'What type of project?',
choices: ['web-app', 'api', 'library', 'cli-tool']
}
]);
let additionalPrompts = [];
// Dynamic prompts based on project type
switch (basicAnswers.projectType) {
case 'web-app':
additionalPrompts = [
{
type: 'list',
name: 'frontend',
message: 'Frontend framework:',
choices: ['React', 'Vue', 'Angular', 'Svelte']
},
{
type: 'confirm',
name: 'pwa',
message: 'Make it a Progressive Web App?',
default: false
}
];
break;
case 'api':
additionalPrompts = [
{
type: 'list',
name: 'backend',
message: 'Backend framework:',
choices: ['Express', 'Koa', 'Fastify', 'NestJS']
},
{
type: 'list',
name: 'database',
message: 'Database:',
choices: ['MongoDB', 'PostgreSQL', 'MySQL', 'SQLite']
}
];
break;
case 'library':
additionalPrompts = [
{
type: 'checkbox',
name: 'targets',
message: 'Build targets:',
choices: ['CommonJS', 'ES Modules', 'UMD', 'Browser']
}
];
break;
}
const additionalAnswers = await this.prompt(additionalPrompts);
this.answers = { ...basicAnswers, ...additionalAnswers };
}
Indications de progrès¶
writing() {
this.log('Generating project files...');
// Show progress
const files = [
'package.json',
'README.md',
'src/index.js',
'src/components/App.js',
'public/index.html'
];
files.forEach((file, index) => {
this.log(`[${index + 1}/${files.length}] Creating ${file}`);
// Simulate file creation
this.fs.copyTpl(
this.templatePath(file),
this.destinationPath(file),
this.answers
);
});
this.log('✓ All files generated successfully!');
}
Groupes électrogènes d'essai¶
Configuration de l'essai¶
# Install testing dependencies
npm install --save-dev yeoman-test yeoman-assert mocha
# Create test directory
mkdir test
touch test/app.js
Essais de base¶
// test/app.js
const path = require('path');
const assert = require('yeoman-assert');
const helpers = require('yeoman-test');
describe('generator-myapp:app', () => {
beforeEach(() => {
return helpers
.run(path.join(__dirname, '../generators/app'))
.withPrompts({
name: 'test-project',
framework: 'react',
features: ['typescript', 'sass']
});
});
it('creates files', () => {
assert.file([
'package.json',
'README.md',
'src/index.js',
'public/index.html'
]);
});
it('fills package.json with correct information', () => {
assert.fileContent('package.json', '"name": "test-project"');
assert.jsonFileContent('package.json', {
dependencies: {
react: expect.any(String)
}
});
});
it('creates TypeScript config when selected', () => {
assert.file('tsconfig.json');
assert.fileContent('tsconfig.json', '"compilerOptions"');
});
it('creates Sass files when selected', () => {
assert.file('src/styles/main.scss');
});
});
Essais avancés¶
// test/sub-generator.js
describe('generator-myapp:component', () => {
it('creates component files', () => {
return helpers
.run(path.join(__dirname, '../generators/component'))
.withArguments(['MyButton'])
.withPrompts({
withStyles: true,
withTest: true
})
.then(() => {
assert.file([
'src/components/MyButton.js',
'src/components/MyButton.css',
'src/components/MyButton.test.js'
]);
assert.fileContent(
'src/components/MyButton.js',
'const MyButton = () => {'
);
});
});
it('skips optional files when not requested', () => {
return helpers
.run(path.join(__dirname, '../generators/component'))
.withArguments(['SimpleButton'])
.withPrompts({
withStyles: false,
withTest: false
})
.then(() => {
assert.file('src/components/SimpleButton.js');
assert.noFile([
'src/components/SimpleButton.css',
'src/components/SimpleButton.test.js'
]);
});
});
});
Essai avec options¶
describe('generator options', () => {
it('skips installation when --skip-install is passed', () => {
return helpers
.run(path.join(__dirname, '../generators/app'))
.withOptions({ 'skip-install': true })
.withPrompts({ name: 'test-app' })
.then(() => {
// Verify that npm install was not called
// This would require mocking or checking for specific behavior
assert.file('package.json');
});
});
it('uses Babel when --babel option is passed', () => {
return helpers
.run(path.join(__dirname, '../generators/app'))
.withOptions({ babel: true })
.withPrompts({ name: 'babel-app' })
.then(() => {
assert.file('.babelrc');
assert.jsonFileContent('package.json', {
devDependencies: {
'@babel/core': expect.any(String)
}
});
});
});
});
Services d'essai¶
// test/helpers.js
const helpers = require('yeoman-test');
const path = require('path');
// Helper to run generator with common setup
exports.runGenerator = (prompts = {}, options = {}) => {
return helpers
.run(path.join(__dirname, '../generators/app'))
.withPrompts({
name: 'test-project',
framework: 'react',
...prompts
})
.withOptions(options);
};
// Helper to check package.json dependencies
exports.assertDependency = (dep, version) => {
assert.jsonFileContent('package.json', {
dependencies: {
[dep]: version || expect.any(String)
}
});
};
Groupes électrogènes¶
Préparation à la publication¶
{
"name": "generator-myapp",
"version": "1.0.0",
"description": "Yeoman generator for my awesome app",
"homepage": "https://github.com/username/generator-myapp",
"author": {
"name": "Your Name",
"email": "your.email@example.com",
"url": "https://yourwebsite.com"
},
"files": [
"generators"
],
"main": "generators/index.js",
"keywords": [
"yeoman-generator",
"scaffold",
"framework",
"boilerplate"
],
"dependencies": {
"yeoman-generator": "^4.0.0",
"chalk": "^4.0.0",
"yosay": "^2.0.0"
},
"devDependencies": {
"yeoman-test": "^6.0.0",
"yeoman-assert": "^3.1.0",
"mocha": "^8.0.0"
},
"engines": {
"node": ">=10.0.0"
},
"license": "MIT"
}
Documentation¶
# generator-myapp
> Yeoman generator for creating awesome web applications
## Installation
First, install [Yeoman](http://yeoman.io) and generator-myapp using [npm](https://www.npmjs.com/).
```bash
npm installation -g yo
npm installation -g générateur-myapp
Usage¶
Generate your new project:
Options¶
--skip-install- Skip automatic installation of dependencies--babel- Include Babel configuration--typescript- Use TypeScript instead of JavaScript
Sub-generators¶
Component¶
Generate a new component:
Service¶
Generate a new service:
License¶
MIT © Your Name
### Édition à npm
```bash
# Test your generator locally
npm link
yo myapp
# Run tests
npm test
# Publish to npm
npm publish
# Update version and publish
npm version patch
npm publish
Intégration GitHub¶
# .github/workflows/test.yml
name: Test
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [12, 14, 16]
steps:
- uses: actions/checkout@v2
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v2
with:
node-version: ${{ matrix.node-version }}
- run: npm ci
- run: npm test
Caractéristiques avancées¶
Composabilité¶
// Compose with other generators
module.exports = class extends Generator {
default() {
// Compose with another generator
this.composeWith(require.resolve('generator-node/generators/app'), {
name: this.answers.name,
description: this.answers.description
});
// Compose with multiple generators
this.composeWith([
{
Generator: require('generator-eslint'),
path: require.resolve('generator-eslint')
},
{
Generator: require('generator-jest'),
path: require.resolve('generator-jest')
}
]);
}
};
Système de fichiers mémoire¶
// Work with in-memory file system
writing() {
// Create file in memory
this.fs.write(
this.destinationPath('temp-config.json'),
JSON.stringify(this.answers, null, 2)
);
// Read and process
const config = this.fs.readJSON(this.destinationPath('temp-config.json'));
// Use config to generate other files
this.fs.copyTpl(
this.templatePath('app.js'),
this.destinationPath('src/app.js'),
config
);
// Delete temporary file
this.fs.delete(this.destinationPath('temp-config.json'));
}
Règlement des conflits coutumiers¶
// Handle file conflicts
writing() {
this.fs.copy(
this.templatePath('important-file.js'),
this.destinationPath('src/important-file.js'),
{
process: (content, filename) => {
if (this.fs.exists(filename)) {
// Custom merge logic
const existing = this.fs.read(filename);
return this.mergeFiles(existing, content.toString());
}
return content;
}
}
);
}
mergeFiles(existing, newContent) {
// Custom merge logic
const existingLines = existing.split('\n');
const newLines = newContent.split('\n');
// Merge imports, keep existing functions, add new ones
// ... custom merge logic
return mergedContent;
}
Intégration¶
Intégration IDE¶
// .vscode/settings.json for generator development
{
"files.associations": {
"*.ejs": "html"
},
"emmet.includeLanguages": {
"ejs": "html"
}
}
Intégration des outils de construction¶
// gulpfile.js for generator development
const gulp = require('gulp');
const mocha = require('gulp-mocha');
gulp.task('test', () => {
return gulp.src('test/**/*.js')
.pipe(mocha());
});
gulp.task('watch', () => {
gulp.watch(['generators/**/*', 'test/**/*'], gulp.series('test'));
});
Intégration CI/CD¶
// scripts/test-generator.js
const helpers = require('yeoman-test');
const path = require('path');
async function testAllCombinations() {
const frameworks = ['react', 'angular', 'vue'];
const features = [
['typescript'],
['sass'],
['typescript', 'sass'],
[]
];
for (const framework of frameworks) {
for (const featureSet of features) {
console.log(`Testing ${framework} with features: ${featureSet.join(', ')}`);
await helpers
.run(path.join(__dirname, '../generators/app'))
.withPrompts({
framework,
features: featureSet
});
console.log('✓ Test passed');
}
}
}
testAllCombinations().catch(console.error);
Dépannage¶
Questions communes¶
# Generator not found
npm list -g --depth=0 | grep generator
npm install -g generator-myapp
# Permission errors
sudo npm install -g yo
sudo chown -R $(whoami) ~/.npm
# Template errors
yo myapp --debug
# Clear Yeoman cache
yo --clear-cache
Déboguement¶
// Add debug logging
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.log('Generator started with args:', args);
this.log('Options:', opts);
}
prompting() {
this.log('Current working directory:', process.cwd());
this.log('Destination root:', this.destinationRoot());
this.log('Template path:', this.templatePath());
}
};
Gestion des erreurs¶
// Graceful error handling
async prompting() {
try {
this.answers = await this.prompt(this.prompts);
} catch (error) {
this.log.error('Prompting failed:', error.message);
throw error;
}
}
writing() {
try {
this.fs.copyTpl(
this.templatePath('template.js'),
this.destinationPath('output.js'),
this.answers
);
} catch (error) {
this.log.error('Template processing failed:', error.message);
this.log('Template data:', this.answers);
throw error;
}
}
Meilleures pratiques¶
Conception du générateur¶
- ** Responsabilité unique**: Chaque générateur devrait avoir un objectif clair et ciblé
- Composabilité: Conception de générateurs pour bien fonctionner avec les autres
- Expérience utilisateur: Fournir des instructions claires et des par défaut utiles
- Manipulation d'erreur: Poignez les erreurs gracieusement avec les messages utiles
- Tests: Écrire des tests complets pour tous les scénarios
Code Organisation¶
generator-myapp/
├── generators/
│ ├── app/
│ │ ├── index.js
│ │ └── templates/
│ ├── component/
│ │ ├── index.js
│ │ └── templates/
│ └── service/
│ ├── index.js
│ └── templates/
├── test/
│ ├── app.js
│ ├── component.js
│ └── service.js
├── package.json
└── README.md
Rendement¶
- Template Caching: Modèles Cache fréquemment utilisés
- Dépendances minimales: Ne comprennent que les dépendances nécessaires
- Exploitation efficace des dossiers : Utiliser les opérations par lots lorsque c'est possible
- Rétroaction sur les progrès: montrer les progrès des opérations à long terme
Entretien¶
- Version sémantique: Utiliser une numérotation de version appropriée
- Changelog : tenir un journal de changement détaillé
- Documentation: Conserver la documentation à jour
- Compatibilité vers le bas: Maintenir la compatibilité dans la mesure du possible
Résumé¶
Yeoman est un outil d'échafaudage puissant qui aide les développeurs à rapidement bootstrap projets avec les meilleures pratiques et la structure cohérente. Les principales caractéristiques sont les suivantes :
- Écosystà ̈me général : Milliers de groupes électrogènes communautaires disponibles
- ** Personnalisable**: Créer des générateurs personnalisés pour des besoins spécifiques
- Template Engine: Templating EJS pour la génération de fichiers dynamiques
- Prompts interactifs: Capacités d'interaction utilisateur riches
- Composabilité: Combiner plusieurs générateurs
- Testing Support: utilitaires de test intégrés
- Système de fichiers API: Capacités de manipulation de fichiers puissantes
- Sous-générateurs: Créer des composants ciblés et réutilisables
Yeoman excelle dans l'élimination du temps d'installation de la plaque de chaudière et l'assurance d'une structure de projet cohérente entre les équipes et les projets. Alors que des outils modernes comme Create React App et Vue CLI ont repris certains cas d'utilisation, Yeoman reste précieux pour les besoins complexes, multi-cadres ou hautement personnalisés d'échafaudage de projet.