Aller au contenu

Générateur de générateur Nx

Ajoute un générateur Nx à un projet TypeScript, pour vous aider à automatiser les tâches répétitives telles que la génération de composants ou l’application de structures de projet particulières.

Vous pouvez générer un générateur de deux manières :

Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-generator
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-generator --dry-run
ParamètreTypePar défautDescription
project Requisstring-Projet TypeScript auquel ajouter le générateur. Nous recommandons d'utiliser le générateur ts#nx-plugin pour le créer.
name Requisstring-Nom du générateur
description string-Une description de votre générateur
directory string-Le répertoire dans le dossier source du projet de plugin où ajouter le générateur (par défaut : <name>)
preferInstallDependencies booleantrueIndique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir sur false pour différer l'installation lors de l'exécution de plusieurs générateurs en lot (une installation s'exécute quand même si nécessaire pour que les générateurs suivants puissent calculer le graphe de projet Nx) ; installer une seule fois à la fin.

Le générateur créera les fichiers de projet suivants dans le project donné :

  • Répertoiresrc/<name>/
    • schema.json Schéma pour l’entrée de votre générateur
    • schema.d.ts Types TypeScript pour votre schéma
    • generator.ts Implémentation de générateur stub
    • generator.spec.ts Tests pour votre générateur
    • README.md Documentation pour votre générateur
  • generators.json Configuration Nx pour définir vos générateurs
  • package.json Créé ou mis à jour pour ajouter une entrée “generators”
  • tsconfig.json Mis à jour pour utiliser CommonJS

Modification du projet

Ce générateur mettra à jour le project sélectionné pour utiliser CommonJS, car les générateurs Nx ne prennent en charge que CommonJS pour le moment (consultez ce problème GitHub pour la prise en charge ESM).

Sélectionnez votre projet nx-plugin local lors de l’exécution du générateur ts#nx-generator, et spécifiez un nom et éventuellement un répertoire et une description.

Le fichier schema.json définit les options que votre générateur accepte. Il suit le format JSON Schema avec des extensions spécifiques à Nx.

Un fichier schema.json a la structure de base suivante :

{
"$schema": "https://json-schema.org/schema",
"$id": "YourGeneratorName",
"title": "Your Generator Title",
"description": "Description of what your generator does",
"type": "object",
"properties": {
// Your generator options go here
},
"required": ["requiredOption1", "requiredOption2"]
}

Voici un exemple simple avec quelques options de base :

{
"$schema": "https://json-schema.org/schema",
"$id": "ComponentGenerator",
"title": "Create a Component",
"description": "Creates a new React component",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Component name",
"x-priority": "important"
},
"directory": {
"type": "string",
"description": "Directory where the component will be created",
"default": "src/components"
},
"withTests": {
"type": "boolean",
"description": "Whether to generate test files",
"default": true
}
},
"required": ["name"]
}

Vous pouvez personnaliser les invites affichées lors de l’exécution de votre générateur via la CLI en ajoutant la propriété x-prompt :

"name": {
"type": "string",
"description": "Component name",
"x-prompt": "What is the name of your component?"
}

Pour les options booléennes, vous pouvez utiliser une invite oui/non :

"withTests": {
"type": "boolean",
"description": "Whether to generate test files",
"x-prompt": "Would you like to generate test files?"
}

Pour les options avec un ensemble fixe de choix, utilisez enum afin que les utilisateurs puissent sélectionner l’une des options.

"style": {
"type": "string",
"description": "The styling approach to use",
"enum": ["css", "scss", "styled-components", "none"],
"default": "css"
}

Un modèle courant consiste à permettre aux utilisateurs de sélectionner parmi les projets existants dans l’espace de travail :

"project": {
"type": "string",
"description": "The project to add the component to",
"x-prompt": "Which project would you like to add the component to?",
"x-dropdown": "projects"
}

La propriété x-dropdown: "projects" indique à Nx de remplir le menu déroulant avec tous les projets de l’espace de travail.

Vous pouvez configurer des options pour qu’elles soient passées comme arguments positionnels lors de l’exécution du générateur depuis la ligne de commande :

"name": {
"type": "string",
"description": "Component name",
"x-priority": "important",
"$default": {
"$source": "argv",
"index": 0
}
}

Cela permet aux utilisateurs d’exécuter votre générateur comme nx g your-generator my-component au lieu de nx g your-generator --name=my-component.

Utilisez la propriété x-priority pour indiquer quelles options sont les plus importantes :

"name": {
"type": "string",
"description": "Component name",
"x-priority": "important"
}

Les options peuvent avoir des priorités de "important" ou "internal". Cela aide Nx à ordonner les propriétés dans l’extension Nx VSCode et la CLI Nx.

Vous pouvez fournir des valeurs par défaut pour les options :

"directory": {
"type": "string",
"description": "Directory where the component will be created",
"default": "src/components"
}

Pour plus de détails sur les schémas, consultez la documentation des options de générateur Nx.

Avec schema.json, le générateur crée un fichier schema.d.ts qui fournit des types TypeScript pour les options de votre générateur :

export interface YourGeneratorSchema {
name: string;
directory?: string;
withTests?: boolean;
}

Cette interface est utilisée dans l’implémentation de votre générateur pour fournir la sécurité des types et la complétion de code :

import { YourGeneratorSchema } from './schema';
export default async function (tree: Tree, options: YourGeneratorSchema) {
// TypeScript knows the types of all your options
const { name, directory = 'src/components', withTests = true } = options;
// ...
}

Après avoir créé le nouveau générateur comme ci-dessus, vous pouvez écrire votre implémentation dans generator.ts.

Un générateur est une fonction qui mute un système de fichiers virtuel (le Tree), en lisant et en écrivant des fichiers pour effectuer les modifications souhaitées. Les modifications du Tree ne sont écrites sur le disque qu’une fois que le générateur a fini de s’exécuter, sauf s’il est exécuté en mode “dry-run”. Un générateur vide ressemble à ceci :

export const myGenerator = async (tree: Tree, options: MyGeneratorSchema) => {
// Use the tree to apply changes
};
export default myGenerator;

Voici quelques opérations courantes que vous pourriez vouloir effectuer dans votre générateur :

// Read a file
const content = tree.read('path/to/file.ts', 'utf-8');
// Write a file
tree.write('path/to/new-file.ts', 'export const hello = "world";');
// Check if a file exists
if (tree.exists('path/to/file.ts')) {
// Do something
}

Vous pouvez générer des fichiers avec l’utilitaire generateFiles de @nx/devkit. Cela vous permet de définir des modèles avec la syntaxe EJS, et de substituer des variables.

import { generateFiles, joinPathFragments } from '@nx/devkit';
// Generate files from templates
generateFiles(
tree,
joinPathFragments(import.meta.dirname, 'files'), // Template directory
'path/to/output', // Output directory
{
// Variables to replace in templates
name: options.name,
nameCamelCase: camelCase(options.name),
nameKebabCase: kebabCase(options.name),
// Add more variables as needed
},
);

Vous pouvez utiliser GritQL pour rechercher et transformer de manière déclarative le code source dans vos générateurs. GritQL prend en charge plusieurs langages, notamment TypeScript, JavaScript, Python, HCL (Terraform), et plus encore — vous pouvez donc utiliser la même syntaxe de motif dans toute votre pile.

Le Nx Plugin for AWS expose deux assistants :

  • applyGritQL(tree, filePath, pattern) — applique un motif de réécriture GritQL à un fichier et renvoie Promise<boolean> indiquant si des modifications ont été apportées
  • matchGritQL(tree, filePath, pattern) — vérifie si un motif GritQL correspond quelque part dans un fichier et renvoie Promise<boolean>
import { applyGritQL, matchGritQL } from '@aws/nx-plugin/sdk/utils/ast';
// Replace a function call
await applyGritQL(
tree,
'src/app.ts',
'`console.log($msg)` => `logger.info($msg)`',
);
// Add an element to an array only if not already present
await applyGritQL(
tree,
'src/plugins.ts',
'`plugins: [$items]` => `plugins: [$items, myPlugin()]` where { $items <: not contains `myPlugin` }',
);
// Check if a pattern exists before making changes
if (!(await matchGritQL(tree, filePath, '`import { Auth } from "./auth"`'))) {
// Add the import
}

Les motifs GritQL fonctionnent également sur des fichiers non-TypeScript. Préfixez votre motif avec language <name> pour cibler d’autres langages :

// Python: replace print statements with logging calls
await applyGritQL(
tree,
'src/handler.py',
'language python\n`print($msg)` => `logger.info($msg)`',
);

Les motifs GritQL utilisent des extraits de code délimités par des backticks avec des $metavariables comme jokers. Utilisez => pour les réécritures et les clauses where pour les conditions.

import { addDependenciesToPackageJson } from '@nx/devkit';
// Add dependencies to package.json
addDependenciesToPackageJson(
tree,
{
'new-dependency': '^1.0.0',
},
{
'new-dev-dependency': '^2.0.0',
},
);
import { formatFilesInSubtree } from '@aws/nx-plugin/sdk/utils/format';
// Format all files that were modified
await formatFilesInSubtree(tree, 'optional/path/to/format');
import { readJson, updateJson } from '@nx/devkit';
// Read a JSON file
const packageJson = readJson(tree, 'package.json');
// Update a JSON file
updateJson(tree, 'tsconfig.json', (json) => {
json.compilerOptions = {
...json.compilerOptions,
strict: true,
};
return json;
});

Vous pouvez importer des générateurs du Nx Plugin for AWS, et les étendre ou les composer comme vous le souhaitez, par exemple vous pourriez souhaiter créer un générateur qui s’appuie sur un projet TypeScript :

import { tsProjectGenerator } from '@aws/nx-plugin/sdk/ts';
export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
const callback = await tsProjectGenerator(tree, { ... });
// Extend the TypeScript project generator here
// Return the callback to ensure dependencies are installed.
// You can wrap the callback if you wish to perform additional operations in the generator callback.
return callback;
};

Vous pouvez utiliser et étendre les générateurs que nous utilisons pour les clients et hooks TypeScript de manière similaire à ce qui précède :

import { openApiTsClientGenerator } from '@aws/nx-plugin/sdk/open-api';
export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
await openApiTsClientGenerator(tree, { ... });
// Add additional files here
};

Nous exposons également une méthode qui vous permet de construire une structure de données qui peut être utilisée pour itérer sur les opérations dans une spécification OpenAPI et donc instrumenter votre propre génération de code, par exemple :

import { buildOpenApiCodeGenerationData } from '@aws/nx-plugin/sdk/open-api.js';
export const myGenerator = async (tree: Tree, schema: MyGeneratorSchema) => {
const data = await buildOpenApiCodeGenerationData(tree, 'path/to/spec.json');
generateFiles(
tree,
joinPathFragments(import.meta.dirname, 'files'), // Template directory
'path/to/output', // Output directory
data,
);
};

Ce qui vous permet ensuite d’écrire des modèles tels que :

files/my-operations.ts.template
export const myOperationNames = [
<%_ allOperations.forEach((op) => { _%>
'<%- op.name %>',
<%_ }); _%>
];

Consultez le code source sur GitHub pour des exemples de modèles plus complexes.

Vous pouvez exécuter votre générateur de deux manières :

Terminal window
pnpm nx g @my-project/nx-plugin:my-generator
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
Terminal window
pnpm nx g @my-project/nx-plugin:my-generator --dry-run

Les tests unitaires pour les générateurs sont simples à implémenter. Voici un modèle typique :

import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { yourGenerator } from './generator.js';
describe('your generator', () => {
let tree;
beforeEach(() => {
// Create an empty workspace tree
tree = createTreeWithEmptyWorkspace();
// Add any files that should already exist in the tree
tree.write(
'project.json',
JSON.stringify({
name: 'test-project',
sourceRoot: 'src',
}),
);
tree.write('src/existing-file.ts', 'export const existing = true;');
});
it('should generate expected files', async () => {
// Run the generator
await yourGenerator(tree, {
name: 'test',
// Add other required options
});
// Check that files were created
expect(tree.exists('src/test/file.ts')).toBeTruthy();
// Check file content
const content = tree.read('src/test/file.ts', 'utf-8');
expect(content).toContain('export const test');
// You can also use snapshots
expect(tree.read('src/test/file.ts', 'utf-8')).toMatchSnapshot();
});
it('should update existing files', async () => {
// Run the generator
await yourGenerator(tree, {
name: 'test',
// Add other required options
});
// Check that existing files were updated
const content = tree.read('src/existing-file.ts', 'utf-8');
expect(content).toContain('import { test } from');
});
it('should handle errors', async () => {
// Expect the generator to throw an error in certain conditions
await expect(
yourGenerator(tree, {
name: 'invalid',
// Add options that should cause an error
}),
).rejects.toThrow('Expected error message');
});
});

Points clés pour tester les générateurs :

  • Utilisez createTreeWithEmptyWorkspace() pour créer un système de fichiers virtuel
  • Configurez tous les fichiers prérequis avant d’exécuter le générateur
  • Testez à la fois la création de nouveaux fichiers et les mises à jour de fichiers existants
  • Utilisez des snapshots pour le contenu de fichiers complexes
  • Testez les conditions d’erreur pour vous assurer que votre générateur échoue gracieusement

Vous pouvez également utiliser ts#nx-generator pour échafauder un générateur dans @aws/nx-plugin.

Lorsque ce générateur est exécuté dans notre dépôt, il générera les fichiers suivants pour vous :

  • Répertoirepackages/nx-plugin/src/<name>/
    • schema.json Schéma pour l’entrée de votre générateur
    • schema.d.ts Types TypeScript pour votre schéma
    • generator.ts Implémentation du générateur
    • generator.spec.ts Tests pour votre générateur
  • Répertoiredocs/src/content/docs/guides/
    • <name>.mdx Page de documentation pour votre générateur
  • packages/nx-plugin/generators.json Mis à jour pour inclure votre générateur
  • packages/nx-plugin/sdk/<prefix>.ts Mis à jour pour exposer votre générateur depuis le SDK (pour les générateurs ts# et py#)

Vous pouvez ensuite commencer à implémenter votre générateur.