Table des matières
But de l’opération
L’adoption de préprocesseurs CSS tels que Sass/SCSS offrent aux développeurs une flexibilité et une puissance qui transcendent les capacités du CSS standard. Sass/SCSS, avec sa syntaxe avancée, permet de structurer de manière plus efficace le code CSS, rendant les feuilles de style plus lisibles, plus maintenables et plus faciles à gérer.
Dans cet article, je vais me pencher sur l’intégration de Sass/SCSS dans un projet Maven. Cette combinaison peut sembler inhabituelle au premier abord, mais elle ouvre la voie à une gestion plus rigoureuse et à une automatisation des processus de développement front-end dans les projets Java. En utilisant Sass/SCSS au lieu du CSS traditionnel, les développeurs bénéficient d’une capacité accrue à créer des designs complexes et réactifs, tout en maintenant l’ordre et l’efficacité grâce à la structure robuste de Maven.
Les alternatives
Il existe quelques alternatives pour mettre en place cela. La première est d’utiliser un plugin Maven reposant sur libsass. Il y en a plusieurs, plus ou moins maintenus ce qui est gênant lorsque l’on ne sait pas trop où l’on va. L’autre problème concernant libsass lui même est qu’il n’est plus maintenu et les développeurs conseillent d’utiliser Dart Sass qui est développé en Javascript et non plus en C/C++.
L’autre alternative est d’utiliser Node.js qui est nativement un interpréteur Javascript, il devient donc possible dès lors d’utiliser Sass directement, lui même étant en Javascript. Mais comment utiliser facilement Node.js sur un projet Java utilisant Maven ?
La réponse à cette question passe par l’utilisation du plugin Maven frontend-maven-plugin:
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
</plugin>
Ce plugin propose tout le nécessaire pour utiliser des outils liés à Node.js et qui pourraient vous aider dans la réalisation de votre application ou site web.
Le plugin front-maven-plugin
Que va faire ce plugin ? Il va télécharger Node.js et l’installer localement dans votre module Maven. Il va permettre également d’y télécharger npm et de lancer des scripts Javascript afin de réaliser des tâches de construction nécessaires à votre module.
Les goals disponibles
front-maven-plugin propose un grand nombre de goals Maven pour effectuer des opérations diverses avec Node.js. Voici liste liste des goals:
- install-node-and-npm
- install-node-and-pnpm
- install-node-and-yarn
- install-bun
- npm
- pnpm
- yarn
- gulp
- grunt
- bower
- bun
- jspm
- ember
- webpack
Les 4 premiers vont permettre de préparer l’environnement en installant des composants de base comme Node.js ou npm. Les autres vont permettre l’exécution de différents outils dont npm que je vais utiliser plus tard.
Paramétrage du plugin
Le paramétrage du plugin dans votre pom.xml est globalement assez simple: vous aurez différentes sections “execution” définissant les actions à réaliser avec Node.js. Dans mon cas, je vais utiliser le plugin pour installer Node.js, npm et exécuter un script de mon cru:
</properties>
<node.version>v18.12.0</node.version>
</properties>
<build>
<plugins>
<plugin>
<!-- Documentation : https://github.com/eirslett/frontend-maven-plugin -->
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.15.0</version>
<executions>
<execution>
<id>install node and npm</id>
<goals>
<goal>install-node-and-npm</goal>
</goals>
<phase>generate-resources</phase>
</execution>
<execution>
<id>npm install</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>install</arguments>
</configuration>
</execution>
<execution>
<id>build exec</id>
<goals>
<goal>npm</goal>
</goals>
<configuration>
<arguments>run build</arguments>
</configuration>
</execution>
</executions>
<configuration>
<nodeVersion>${node.version}</nodeVersion>
</configuration>
</plugin>
</plugins>
</build>
Les lignes 12 à 18 permettent l’installation de Node.js et npm.
Les lignes 19 à 27 vont lancer npm en lui passant l’argument “install”. Les dépendances décrites dans le fichier package.json que je vais décrire plus bas seront installées.
Enfin, les lignes 28 à 36 vont lancer le script “build.js”. Je détaillerai ce script plus bas dans cet article.
Avec une telle configuration, lorsque Maven va devoir générer les ressources, il téléchargera Node.js, npm, installera les dépendances et lancera le script build.js.
Le fichier package.json
Comme nous l’avons vu, la seconde action installera les dépendances nécessaires au script build.js. Pour cela, npm va regarder celles nécessaires dans son fichier package.json qui doit être placé dans le même répertoire que le pom.xml.
Voici le contenu du fichier:
{
"name": "node-projet",
"version": "1.0.0",
"scripts": {
"build": "node src/main/node/build.js --source=src/main/resources --target=target/generated-resources",
"watch": "node src/main/node/build.js --watch --source=src/main/resources --target=target/generated-resources"
},
"dependencies": {
"sass": "^1.69.7",
"shelljs": "^0.8.5",
"minimist": "^1.2.6"
}
}
Pour le moment, on ne va pas tenir compte de la partie “scripts” mais uniquement celle des “dependencies”. Les 2 dernières servent au fonctionnement du script. La première, sass, concerne bien évidemment l’outil sass utilisé et donc la version indiquée est importante, surtout si vous rencontrez des incompatibilités. La version indiquée ici est la version la plus à jour au moment de la rédaction de l’article.
Le script build.sh
Ce script est la goutte de potion magique de l’opération. Il va rechercher dans une arborescence fichiers .sass, .scss et .ts (Typescript) et convertir ceux-ci en fichier .css et .js dans un répertoire destination.
Si le tout est bien configuré, cela va donc permettre de transpiler ces fichiers sources à partir du répertoire des ressources de notre module Maven vers un sous-répertoire target de notre même module.
Voici le script:
// Execute some developments tools like sass or typescript
// generating some target files from a source folder using a
// recursive walk throught the directories.
var shell = require("shelljs");
var minimist = require('minimist');
var fs = require('fs');
var path = require('path');
const VERSION = "1.0.0";
// Retrieve the parameters
const argv = minimist(process.argv.slice(2));
checkArgs(argv);
// ---------------------------------------------------
// Check the parameters. If an error is found, display
// the help informations and stop the script.
//
// @param argv The parameters to check.
// ---------------------------------------------------
function checkArgs(argv) {
if (argv.help) {
help();
process.exit(0);
}
if (argv.version) {
version();
process.exit(0);
}
var hasErrors = false;
if (!argv.source) {
console.error("The source parameter is missing.");
hasErrors = true;
}
if (!argv.target) {
console.error("The target parameter is missing.");
hasErrors = true;
}
if (hasErrors) {
console.info("\nUse --help to show the help.");
process.exit(1);
}
}
// ---------------------------------------------------
// Show the help.
// ---------------------------------------------------
function help() {
console.info("Generate target files from a source folder using sass and typescript.\n");
console.info("The parameters are:");
console.info(" --help This help.");
console.info(" --source=<dir> The source folder,");
console.info(" --target=<dir> The target folder.");
console.info(" --version The script version.");
console.info(" --watch Watch the source directory.");
}
// ---------------------------------------------------
// Perform the given file.
// ---------------------------------------------------
function performFile(file) {
var ext = path.extname(file);
var filename = path.basename(file, ext);
var absoluteSourcePath = path.resolve(argv.source);
var sourceSubPath = path.dirname(path.resolve(file)).substring(absoluteSourcePath.length);
var targetWithSubPath = path.resolve(path.join(argv.target, sourceSubPath));
// Create the folders recursively
if (!fs.existsSync(targetWithSubPath)) {
console.log(`Creating path ${targetWithSubPath}`);
fs.mkdirSync(targetWithSubPath, { recursive: true });
}
if (ext == ".ts") {
const source = path.resolve(file);
const target = path.join(targetWithSubPath, filename + ".js");
if (fileMoreRecent(source, target)) {
console.log(`Executing tsc for ${source} => ${target}`);
var cmd = `tsc ${source} --outFile ${target}`;
shell.exec(cmd);
}
} else {
const source = path.resolve(file);
const target = `${targetWithSubPath}\\${filename}.css`;
if (fileMoreRecent(source, target)) {
console.log(`Executing sass for ${source} => ${target}`);
var cmd = `sass ${source} ${target}`;
shell.exec(cmd);
}
}
}
// ---------------------------------------------------
// Check if the source is more recent than the target
// file. If the target doesn't exist, always return
// true.
// ---------------------------------------------------
function fileMoreRecent(source, target) {
if (!fs.existsSync(target)) return true;
return (fs.statSync(source).mtime > fs.statSync(target).mtime);
}
// ---------------------------------------------------
// Walk recursively insside the source folder to
// the given "done" function.
// @param dir The source directory.
// @param done The callback function to call.
// ---------------------------------------------------
var walk = function(dir, done) {
var results = [];
fs.readdir(dir, function(err, list) {
if (err) return done(err);
var pending = list.length;
if (!pending) return done(null, results);
list.forEach(function(file) {
file = path.resolve(dir, file);
fs.stat(file, function(err, stat) {
if (stat && stat.isDirectory()) {
walk(file, function(err, res) {
results = results.concat(res);
if (!--pending) done(null, results);
});
} else {
var ext = path.extname(file);
if ((ext == '.ts' || ext == '.sass' || ext == '.scss') && !path.basename(file).startsWith("_")) {
results.push(file);
}
if (!--pending) done(null, results);
}
});
});
});
};
// Always walk inside the source folder to be sure the target are up-to-date
console.log(`Walking inside the source directory: ${argv.source}`);
walk(argv.source, function(err, results) {
if (err) throw err;
results.forEach(file => {
performFile(file);
});
});
if (argv.watch) {
// Watch the source directory
console.log(`Watching the source directory: ${argv.source}`);
fs.watch(argv.source, {recursive:true}, (eventType, file) => {
var ext = path.extname(file);
if ((ext == '.ts' || ext == '.sass' || ext == '.scss') && !path.basename(file).startsWith("_")) {
performFile(argv.source + "\\" + file);
}
});
} else {
console.log(`Done !`);
}
Ce script doit être placé dans src/main/node comme indiqué dans le package.json.
Il offre 2 modes de fonctionnement:
- A la construction (fonctionnement par défaut) utilisé lors de la compilation du module et du packaging,
- En mode scrutation (paramètre -watch) lors du développement permettant dès qu’un fichier est modifié qu’il soit transpilé en .css ou .js suivant le type de fichier source.
Inclusions/exclusions de resources
Tout cela devrait fonctionner mais il reste tout de même une chose importante à réaliser. En effet, Maven n’a pas connaissance des ressources nouvellement créées et ne sait pas également qu’il doit les prendre et exclure au contraire les ressources initiales (.sass, .scss, .ts). Pour être propre, nous devons les exclure du JAR qui sera généré. Pour effectuer ces inclusions et exclusions, il est nécessaire d’ajouter une section dans le build de notre pom.xml:
<build>
<resources>
<resource>
<directory>src/main/resources</directory>
<excludes>
<exclude>**/*.scss</exclude>
<exclude>**/*.sass</exclude>
<exclude>**/*.ts</exclude>
</excludes>
<includes>
<include>**/*.css</include>
<include>**/*.js</include>
</includes>
</resource>
<resource>
<directory>target/generated-resources</directory>
<includes>
<include>**/*.css</include>
<include>**/*.js</include>
</includes>
</resource>
</resources>
...
</build>
J’ai ajouté également l’inclusion des .css et .js (lignes 11 et 12) qui auraient été créés nativement dans src/main/resources.
Références
- frontend-maven-plugin: https://github.com/eirslett/frontend-maven-plugin
Aucun Commentaire! Soyez le premier.