Aller au contenu

Générateur de Migration Nx

Ajoute une Migration Nx à un Plugin Nx. Les migrations permettent à nx migrate de mettre à jour automatiquement les projets qui ont été générés par une version précédente de votre plugin lorsque vous effectuez un changement cassant — cibles renommées, configuration fournie modifiée, ou mise à jour de dépendance nécessitant des modifications de code associées.

Le générateur crée l’échafaudage de la migration, l’enregistre dans le migrations.json de votre plugin, et intègre le champ nx-migrations dans le package.json de votre plugin (en créant les deux s’ils n’existent pas encore).

Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-migration
Vous pouvez également effectuer une simulation pour voir quels fichiers seraient modifiés
Terminal window
pnpm nx g @aws/nx-plugin:ts#nx-migration --dry-run
ParamètreTypePar défautDescription
project Requisstring-Le projet Nx Plugin auquel ajouter la migration. Nous recommandons d'utiliser le générateur ts#nx-plugin pour le créer.
name Requisstring-Nom de la migration, en kebab-case (par exemple rename-foo-target)
description string-Une description de ce que fait la migration, affichée par nx migrate
kind deterministic | agentic | hybriddeterministicLe type de migration : deterministic (un codemod), agentic (un prompt appliqué par IA pour du code appartenant à l'utilisateur), ou hybrid (un codemod qui effectue la partie mécanique puis délègue à un agent)
preferInstallDependencies booleantrueIndique s'il faut privilégier l'installation des dépendances après l'exécution du générateur. Définir à false pour différer l'installation lors de l'enchaînement de plusieurs générateurs (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.

Les migrations se présentent sous trois formes, distinguées par les champs que l’entrée migrations.json contient. Passez --kind pour choisir (par défaut deterministic) :

  • Déterministe (implementation) — un codemod avec un avant/après exact, et celui à privilégier en premier. Il s’exécute sans surveillance, y compris en CI et dans les terminaux non interactifs, et s’applique de manière identique pour chaque utilisateur. Utilisez-le seul lorsque le changement a une forme que vous pouvez faire correspondre de manière fiable partout où elle apparaît, en signalant tout ce qu’il a ignoré via nextSteps.
  • Hybride (implementation + prompt) — les deux moitiés d’un seul changement. Utilisez-le lorsqu’un changement est cassant et ne peut pas être entièrement appliqué mécaniquement : le codemod fait autant qu’il peut correspondre en toute sécurité et retourne agentContext décrivant ce qu’il a changé et ce qu’il a ignoré ; Nx transmet ce contexte au prompt associé, qui dirige l’agent vers le reste. Le prompt est ce qui empêche un espace de travail d’être laissé cassé lorsque les modifications restantes couvrent trop de formes pour être codées.
  • Agentique (prompt) — un fichier d’instructions markdown appliqué par l’agent de codage local de l’utilisateur via le flux nx migrate agentique de Nx. Utilisez-le seul lorsqu’aucun avant/après mécanique ne tient du tout, car la modification correcte dépend de ce que l’utilisateur a construit. Lorsqu’aucun agent ne s’exécute (CI, aucun agent installé, consentement refusé), Nx présente le prompt comme des instructions manuelles — écrivez donc les prompts comme des étapes autonomes et actionnables par l’humain.

Le générateur crée les fichiers suivants dans le dossier source de votre projet de plugin, selon le kind choisi :

  • Répertoiresrc/migrations/latest/<name>/
    • migration.ts Implémentation du codemod (déterministe et hybride)
    • migration.spec.ts Tests pour votre migration (déterministe et hybride)
    • prompt.md Instructions pour l’agent/humain (agentique et hybride)
  • migrations.json Créé ou mis à jour pour enregistrer votre migration
  • package.json Créé ou mis à jour pour ajouter une entrée “nx-migrations”

Une nouvelle migration atterrit dans latest/. Regrouper les migrations par la version qui les livre maintient leur ordre visible d’un coup d’œil à mesure que la collection grandit — déplacez une migration dans un dossier v<x.y.z>/ (et mettez à jour ses chemins dans migrations.json) lorsque vous assignez sa version.

Elle enregistre la migration dans migrations.json avec les champs pour son type et sans version — voir Versionnage ci-dessous. La clé enregistrée est préfixée par le dossier dans lequel vit la migration (latest-<name>), de sorte que la réutilisation d’un nom pour un changement ultérieur ne peut pas écraser silencieusement une migration qui a déjà été livrée.

Une migration déterministe (ou hybride) est une fonction qui mute le système de fichiers virtuel (le Tree), retournant un MigrationReturnObject. Voici une migration hybride, qui retourne les deux :

import { type MigrationReturnObject, type Tree } from '@nx/devkit';
import { formatFilesInSubtree } from '@aws/nx-plugin/sdk/utils/format';
export default async function migration(
tree: Tree,
): Promise<MigrationReturnObject> {
const nextSteps: string[] = [];
const agentContext: string[] = [];
// Read and update the files your generators produce here.
await formatFilesInSubtree(tree);
return { nextSteps, agentContext };
}
  • nextSteps — les actions de suivi que Nx affiche pour l’utilisateur après l’exécution de nx migrate. Chaque entrée doit être quelque chose qu’ils doivent faire manuellement : réconcilier un fichier que vous avez ignoré (voir les garde-fous ci-dessous), ou terminer quelque chose au-delà de la portée de la migration, comme une installation pour rafraîchir un fichier de verrouillage. Ce n’est pas un journal de ce que vous avez changé — lorsqu’une transformation s’applique proprement, n’ajoutez rien, sinon les entrées qui nécessitent vraiment une action sont enfouies.
  • agentContext (hybride uniquement) — un résumé de ce que le codemod a changé, transmis au prompt associé lorsqu’il s’exécute sous le flux agentique de Nx afin que l’agent puisse se concentrer sur les parties appartenant à l’utilisateur.

Pour quelques exemples d’opérations que vous pouvez effectuer dans votre migration (générer des fichiers, modifier des fichiers existants en utilisant GritQL, lire et mettre à jour du JSON), consultez le guide du Générateur Nx.

Les migrations s’exécutent contre des espaces de travail que vous ne contrôlez pas, il est donc recommandé d’adhérer aux principes suivants (le squelette échafaudé les intègre) :

  • Transformez le code source avec GritQL. Utilisez les assistants GritQL (applyGritQL, matchGritQL) pour éditer le code source plutôt que des regex ou des remplacements de chaînes. GritQL correspond à l’AST, donc un seul motif fonctionne à travers le formatage, les espaces et les guillemets dans lesquels la copie d’un utilisateur peut avoir dérivé, là où la correspondance de texte manque silencieusement du code équivalent ou corrompt le fichier. Utilisez updateJson pour JSON et l’analyseur correspondant pour d’autres configurations structurées.
  • Faites correspondre le motif avant d’écrire. Si un fichier cible a divergé de la forme que vos générateurs produisent, ignorez-le et signalez-le via nextSteps, ou envisagez une migration hybride, plutôt que d’écraser les modifications de l’utilisateur. matchGritQL est un moyen pratique de faire cette vérification.
  • Signalez uniquement ce qui reste à faire. nextSteps est pour le travail que l’utilisateur doit reprendre, pas un enregistrement des modifications que vous avez effectuées — celles-ci sont déjà dans leur git diff.
  • Idempotent. Réexécuter la migration doit être une opération sans effet. Protégez les réécritures GritQL qui injectent du code avec une clause where { ... <: not contains ... } afin qu’une seconde exécution n’ajoute pas de doublon.
  • Formatez ce que vous écrivez. Terminez avec formatFilesInSubtree(tree) afin que les fichiers que votre migration a écrits soient correctement formatés.

Le migration.spec.ts échafaudé utilise createTreeUsingTsSolutionSetup() pour construire un espace de travail virtuel correspondant à la forme que notre preset génère. Testez que la migration fait passer un espace de travail de l’état “avant” à l’état cible, ignore (et signale) le code qu’elle ne reconnaît pas, et est idempotente :

import type { Tree } from '@nx/devkit';
import { createTreeUsingTsSolutionSetup } from '@aws/nx-plugin/sdk/utils/test';
import migration from './migration.js';
describe('my migration', () => {
let tree: Tree;
beforeEach(() => {
tree = createTreeUsingTsSolutionSetup();
});
it('should update the generated shape', async () => {
// Set up a tree with the "before" state
tree.write('project.json', JSON.stringify({ targets: { foo: {} } }));
// Run the migration
await migration(tree);
// Check that the tree matches the target state
const projectJson = JSON.parse(tree.read('project.json', 'utf-8'));
expect(projectJson.targets).toHaveProperty('bar');
expect(projectJson.targets).not.toHaveProperty('foo');
});
});

Les tests unitaires prouvent seulement que la migration fait ce que vous avez écrit qu’elle devait faire. Testez-la également contre un espace de travail généré par la version publiée de votre plugin — l’état à partir duquel vos utilisateurs effectuent la mise à niveau — et confirmez que le résultat correspond à ce que vos générateurs produisent aujourd’hui : inspectez le diff, générez les mêmes projets dans un nouvel espace de travail et comparez, vérifiez que l’espace de travail se construit toujours, puis réexécutez la migration pour confirmer qu’elle ne change rien. Répétez avec un espace de travail que vous avez d’abord personnalisé, pour vérifier que la migration laisse vos modifications intactes et les signale à la place.

Nx exécute une migration lorsque la version du plugin installé de l’utilisateur est inférieure à la version de la migration. Ce générateur n’écrit aucun champ version — assignez-le (ou tamponnez-le depuis votre processus de publication) lorsque vous coupez la version qui livre la migration, afin qu’elle s’exécute pour tous ceux qui effectuent une mise à niveau depuis une version antérieure.

Une fois que vous assignez les versions, un migrations.json enregistrant une migration de chaque type ressemble à ceci :

{
"$schema": "http://json-schema.org/schema",
"name": "my-plugin",
"generators": {
"v2.0.0-rename-foo-target": {
"version": "2.0.0",
"description": "Rename the foo target to bar",
"implementation": "./src/migrations/v2.0.0/rename-foo-target/migration"
},
"v3.0.0-migrate-custom-handlers": {
"version": "3.0.0",
"description": "Update custom handlers for the new API",
"prompt": "./src/migrations/v3.0.0/migrate-custom-handlers/prompt.md"
},
"v4.0.0-upgrade-framework": {
"version": "4.0.0",
"description": "Upgrade the framework and reconcile call sites",
"implementation": "./src/migrations/v4.0.0/upgrade-framework/migration",
"prompt": "./src/migrations/v4.0.0/upgrade-framework/prompt.md"
}
}
}

Les utilisateurs appliquent les migrations de votre plugin avec le flux nx migrate en deux étapes — générer les migrations en attente, installer, puis les exécuter :

Terminal window
pnpm nx migrate my-plugin@latest
pnpm nx migrate --run-migrations

Les migrations agentiques et hybrides sont appliquées par l’agent de codage local de l’utilisateur pendant nx migrate --run-migrations lorsqu’un agent est disponible.

Pour le flux complet du point de vue de l’utilisateur effectuant la mise à niveau — y compris des exemples de sortie pour les migrations déterministes et agentiques — voir Mise à Niveau de Votre Espace de Travail.