Recast s'impose comme un outil incontournable pour la transformation de code JavaScript via l'analyse de l'Arbre de Syntaxe Abstraite (AST). Contrairement à d'autres outils qui réécrivent entièrement le fichier, Recast se distingue par sa capacité à préserver le formatage d'origine, en ne réimprimant que les sections modifiées. Cette caractéristique en fait un allié de choix pour concevoir des plugins Webpack ou Rollup performants et respectueux du style de code enitial.
Pourquoi adopter Recast pour vos transformations ?
Dans l'écosystème moderne du développement Web, la manipulation du code source durant l'étape de compilation est fréquente. Recast offre plusieurs avantages techniques :
- Impression non destructive : Il conserve l'indentation, les guillemets et les points-virgules originaux des segments de code non touchés.
- Flexibilité des parseurs : Compatible avec Esprima, Acorn, ou encore @babel/parser pour supporter TypeScript et les propositions ECMAScript récentes.
- Gestion native des Source Maps : Génération simplifiée des fichiers de correspondance pour faciliter le débogage après transformation.
Configuration initiale
Pour intégrer Recast à votre environnement, installez le paquet principal ainsi que le parseur Babel pour une compatibilité étendue :
npm install recast @babel/parser
L'utilisation de base repose sur deux fonctions principales : parse et print.
const { parse, print } = require("recast");
const babelParser = require("@babel/parser");
const source = `const valeur = 10;`;
// Analyse avec un parseur spécifique
const ast = parse(source, {
parser: {
parse: (code) => babelParser.parse(code, {
sourceType: "module"
})
}
});
Modification de l'AST : Exemple pratique
Imaginons que nous souhaitions transformer une déclaration de fonction classique en une expression de fonction assignée à une constante. Voici comment manipuler les nœuds avec le module types.
const recast = require("recast");
const b = recast.types.builders;
const codeSource = `function calculerSurface(r) {
return 3.14 * r * r;
}`;
const arbre = recast.parse(codeSource);
// Cibler la fonction au début du programme
const fonctionInitiale = arbre.program.body[0];
// Construction de la nouvelle structure : const calculerSurface = function(r) { ... }
arbre.program.body[0] = b.variableDeclaration("const", [
b.variableDeclarator(
fonctionInitiale.id,
b.functionExpression(
null,
fonctionInitiale.params,
fonctionInitiale.body
)
)
]);
const resultat = recast.print(arbre).code;
console.log(resultat);
Le résultat produit respecte la structure demandée tout en conservant la logique interne du bloc de code original.
Implémentation dans un plugin Webpack
Dans Webpack, la transformation s'opère généralement au niveau des hooks de compilation. Voici un exemple minimaliste de plugin remplaçant les déclarations var par const.
const { parse, print, types } = require("recast");
class ModernizeCodePlugin {
apply(compiler) {
compiler.hooks.emit.tapAsync("ModernizeCodePlugin", (compilation, callback) => {
Object.keys(compilation.assets).forEach((filename) => {
if (!filename.endsWith(".js")) return;
const asset = compilation.assets[filename];
const ast = parse(asset.source());
types.visit(ast, {
visitVariableDeclaration(path) {
if (path.node.kind === "var") {
path.node.kind = "const";
}
this.traverse(path);
}
});
const output = print(ast).code;
compilation.assets[filename] = {
source: () => output,
size: () => output.length
};
});
callback();
});
}
}
module.exports = ModernizeCodePlugin;
Intégration avec Rollup
Rollup facilite la transformation via le hook transform. L'avantage ici est la gestion native des Source Maps par Rollup, que Recast peut alimenter directement.
import { parse, print, types } from "recast";
export default function myTransformPlugin() {
return {
name: 'recast-transform',
transform(code, id) {
if (!id.endsWith(".js")) return null;
const ast = parse(code);
types.visit(ast, {
visitIdentifier(path) {
// Exemple : Renommer 'oldApi' en 'newApi'
if (path.node.name === "oldApi") {
path.node.name = "newApi";
}
return false; // Arrête la visite pour ce nœud
}
});
const transformed = print(ast, { sourceMapName: id });
return {
code: transformed.code,
map: transformed.map
};
}
};
}
Conseils d'optimisation et bonnes pratiques
La manipulation d'AST peut être coûteuse en ressources sur de gros projets. Voici quelques pistes pour maintenir de bonnes performances :
- Ciblage précis : Évitez de parcourir l'intégralité de l'arbre si vous savez exactement où se situent les changements.
- Fusion des visites : Si vous avez plusieurs transformations à effectuer, regroupez-les dans un seul appel à
types.visitpour limiter les traversées de l'arbre. - Utilisation sélective du "Pretty Print" : Si le formatage d'origine n'est pas une priorité absolue,
recast.prettyPrintpeut être utilisé pour harmoniser le style de sortie selon des règles strictes (indentation, etc.).
Recast offre un équilibre idéal entre puisasnce de modification et respect du code source. Que ce soit pour du refactoring automatisé ou de l'optimisation de build, il permet d'automatiser des tâches complexes avec une grande précision technique.