Skip to main content

In this article

10 fonctionnalités modernes du runtime Node.js à adopter en 2024

Écrit par
feature java dto

29 mai 2024

0 minutes de lecture
10 Node.js runtime features you SHOULD be using in 2024

L’univers des runtimes JavaScript côté serveur regorge d’innovations : Bun progresse grâce à sa compatibilité avec les API Node.js, tandis que le runtime Node.js offre une riche bibliothèque standard et de nombreuses fonctionnalités.

À l’aube de 2024, cet article est l’occasion de découvrir les dernières fonctionnalités du runtime Node.js. Se tenir à jour, ce n’est pas seulement « suivre l’évolution du secteur » : c’est tirer parti des API modernes pour écrire du code plus efficace, plus performant et plus sûr.

Cet article présente 10 fonctionnalités modernes du runtime Node.js que tous les développeurs devraient commencer à utiliser en 2024. Des API tout juste sorties aux fonctionnalités prometteuses des nouveaux venus comme Bun et Deno, nous faisons le tour du sujet.

Prérequis : version LTS de Node.js

Avant de découvrir ces fonctionnalités modernes, assurez-vous d’utiliser la version LTS (support à long terme) de Node.js. Au moment de la rédaction de cet article, la dernière version LTS de Node.js est la v20.14.0.

Pour vérifier votre version de Node.js, utilisez la commande suivante :

node --version

Si vous n’utilisez pas actuellement la version LTS, vous pouvez utiliser un gestionnaire de versions comme fnm ou nvm pour passer facilement d’une version de Node.js à une autre.

Quelles sont les nouveautés de Node.js 20 ?

Dans les sections qui suivent, nous allons découvrir quelques fonctionnalités introduites dans les versions récentes de Node.js. Certaines sont stables, d’autres sont encore expérimentales, et quelques-unes sont prises en charge depuis plus longtemps, mais vous ne les connaissez peut-être pas encore.

Nous aborderons les sujets suivants :

  1. Le test runner de Node.js

  2. Le mocking natif de Node.js

  3. Couverture de tests native de Node.js

  4. Le mode watch de Node.js

  5. Corepack dans Node.js

  6. Le chargeur .env de Node.js

  7. import.meta.file dans Node.js pour __dirname et __file

  8. Les promesses des timers natifs de Node.js

  9. Le module de permissions de Node.js

  10. Le module de politiques de Node.js

1. Le test runner natif de Node.js

Qu’utilisait-on avant l’intégration d’un test runner au runtime natif de Node.js ? Jusqu’à présent, vous avez probablement utilisé l’une des solutions populaires, comme node-tap, jest, mocha ou vitest. 

Voyons comment intégrer le test runner natif de Node.js à votre processus de développement. Pour commencer, importez le module de test de Node.js dans votre fichier de test, comme ci-dessous :

import { test } from 'node:test';

Voyons maintenant les différentes étapes pour utiliser le test runner de Node.js.

Exécuter un seul test avec node:test

Pour créer un test, utilisez la fonction test en lui transmettant le nom du test et une fonction de rappel. C’est dans cette fonction de rappel que vous définissez la logique du test.

import { test } from "node:test";
import assert from "node:assert";
import { add } from "../src/math.js";

test("should add two numbers", () => {
  const result = add(1, 2);
  assert.strictEqual(result, 3);
});

test("should fail to add strings", () => {
  assert.throws(() => {
    add("1", "2");
  });
});

Pour exécuter ce test, utilisez la commande node --test, suivie du nom de votre fichier de test :

node --test tests/math.test.js

Le test runner de Node.js peut détecter et exécuter automatiquement les fichiers de test de votre projet. Par convention, ces fichiers se terminent par .test.js, mais cette convention de nommage n’est pas obligatoire.

Si vous omettez l’argument positionnel du fichier de test, le test runner de Node.js applique des heuristiques et des motifs glob pour trouver les fichiers de test : par exemple, tous les fichiers d’un dossier test/ ou tests/, ou les fichiers dont le préfixe est test- ou le suffixe .test.

Par exemple, pour rechercher les fichiers de test avec un motif glob :

node --test '**/*.test.js'

Effectuer des assertions avec node:assert

Le test runner de Node.js prend en charge les assertions grâce au module intégré assert. Vous pouvez utiliser différentes méthodes, comme assert.strictEqual, pour vérifier vos tests.

import assert from 'node:assert';

test('Test 1', () => {
  assert.strictEqual(1 + 1, 2);
});

Suites de tests et hooks avec le test runner natif de Node.js

La fonction describe permet de regrouper des tests associés dans une suite. Vos tests sont ainsi mieux organisés et plus faciles à gérer.

import { test, describe } from "node:test";

describe('My Test Suite', () => {
  test('Test 1', () => {
    // Test 1 logic
  });

  test('Test 2', () => {
    // Test 2 logic
  });
});

Les hooks de test sont des fonctions spéciales exécutées avant ou après vos tests. Ils permettent de préparer ou de nettoyer les environnements de test.

test.beforeEach(() => {
  // Runs before each test
});

test.afterEach(() => {
  // Runs after each test
});

Vous pouvez également ignorer un test avec la fonction test.skip. C’est pratique si vous souhaitez désactiver temporairement un test en particulier.

test.skip('My skipped test', () => {
  // Test logic
});

De plus, le test runner de Node.js propose différents reporters, qui formatent et affichent les résultats des tests de plusieurs façons. Vous pouvez en spécifier un à l’aide de l’option --reporter.

node --test --test-reporter=tap

Faut-il abandonner Jest ?

Jest est un framework de test populaire dans la communauté Node.js, mais il présente certains inconvénients qui rendent le test runner natif de Node.js plus intéressant.

En installant Jest, même comme simple dépendance de développement, vous ajoutez 277 dépendances transitives régies par différentes licences, notamment MIT, Apache-2.0, CC-BY-4.0 et une licence inconnue. Le saviez-vous ?

Graphe des dépendances transitives de Jest.
  • Jest modifie les variables globales, ce qui peut entraîner des comportements inattendus dans vos tests.

  • L’opérateur instanceof ne fonctionne pas toujours comme prévu dans Jest.

  • Jest ajoute de nombreuses dépendances à votre projet. Il devient alors plus difficile de maintenir à jour les dépendances tierces, et vous devez gérer inutilement les problèmes de sécurité et d’autres préoccupations liés aux dépendances utilisées pendant le développement.

  • Jest peut être plus lent que le test runner natif de Node.js en raison de sa surcharge.

Le test runner natif de Node.js offre d’autres fonctionnalités intéressantes, comme l’exécution de sous-tests et de tests concurrents. Avec les sous-tests, chaque fonction de rappel test() reçoit un argument context, qui permet de créer des tests imbriqués via context.test. Les tests concurrents sont très utiles si vous savez les utiliser correctement et éviter les conditions de concurrence. Il suffit de transmettre un objet potentiel concurrency: true comme deuxième argument à la suite de tests describe().

Qu’est-ce qu’un exécuteur de tests ?

Un exécuteur de tests est un outil logiciel qui permet aux développeurs de gérer et d’exécuter des tests automatisés sur leur code. L’exécuteur de tests Node.js est un framework conçu pour fonctionner parfaitement avec Node.js et offrir un environnement complet pour écrire et exécuter des tests sur vos applications Node.js.

2. Le mocking natif de Node.js

Le mocking est une stratégie utilisée par les développeurs pour isoler le code à tester. Le runtime Node.js a introduit des fonctionnalités de mocking natives qu’il est essentiel de comprendre et d’utiliser efficacement.

Vous avez probablement déjà utilisé les fonctionnalités de mocking d’autres frameworks de test, comme jest.spyOn ou mockResolvedValueOncel. Elles sont utiles pour éviter d’exécuter le code réel dans vos tests, par exemple des requêtes HTTP ou des API du système de fichiers, et remplacer ces opérations par des stubs et des mocks que vous pourrez examiner ensuite.

Contrairement à d’autres fonctionnalités du runtime Node.js, comme le watch et la couverture, le mocking n’est pas marqué comme expérimental. Toutefois, cette nouvelle fonctionnalité, introduite uniquement dans Node.js 18, pourrait encore évoluer.

Mocking natif de Node.js avec import { mock } from 'node:test'

Voyons comment utiliser le mocking natif de Node.js dans un exemple concret. Le test runner et le mocking de modules sont désormais disponibles comme fonctionnalités stables dans Node.js 20 LTS.

Nous allons utiliser un module utilitaire, dotenv.js, qui charge des variables d’environnement depuis un fichier .env. Nous utiliserons également un fichier de test, dotenv.test.js, pour tester le module dotenv.js.

Voici notre propre module dotenv :

// dotenv.js
import fs from "node:fs/promises";

export async function loadEnv(path = ".env") {
  const rawDataEnv = await fs.readFile(path, "utf8");
  const env = {};
  rawDataEnv.split("\n").forEach((line) => {
    const [key, value] = line.split("=");
    env[key] = value;
  });

  return env;
}

Dans le fichier dotenv.js, nous avons une fonction asynchrone, loadEnv, qui lit un fichier à l’aide de la méthode fs.readFile, puis sépare son contenu en paires clé-valeur. Comme vous pouvez le voir, elle utilise l’API native du système de fichiers de Node.js, fs.

Voyons maintenant comment tester cette fonction avec la fonctionnalité de mocking native de Node.js.

// dotenv.test.js
import { describe, test, mock } from "node:test";
import assert from "node:assert";
import fs from "node:fs/promises";

import { loadEnv } from "../src/dotenv.js";

describe("dotenv test suite", () => {
  test("should load env file", async () => {
    const mockImplementation = async (path) => {
      return "PORT=3000\n";
    };
    const mockedReadFile = mock.method(fs, "readFile", mockImplementation);

    const env = await loadEnv(".env");

    assert.strictEqual(env.PORT, "3000");
    assert.strictEqual(mockedReadFile.mock.calls.length, 1);
  });
});

Dans le fichier de test, nous importons la méthode mock depuis node:test pour créer une implémentation simulée de fs.readFile. Cette implémentation renvoie la chaîne "PORT=3000\n", quel que soit le chemin de fichier transmis.

Nous appelons ensuite la fonction loadEnv et utilisons le module assert pour vérifier deux éléments :

  1. L’objet renvoyé possède une propriété PORT dont la valeur est "3000".

  2. La méthode fs.readFile a été appelée exactement une fois.

Grâce à la fonctionnalité de mocking native de Node.js, nous pouvons isoler efficacement la fonction loadEnv du système de fichiers et la tester indépendamment. Les fonctionnalités de mocking de Node.js 20 permettent également de simuler les timers.

Qu’est-ce que le mocking ?

Dans les tests logiciels, le mocking consiste à remplacer les fonctionnalités réelles de certains modules par des fonctionnalités artificielles. L’objectif principal est d’isoler l’unité de code testée de ses dépendances externes, afin que le test vérifie uniquement le fonctionnement de cette unité, et non celui de ses dépendances. Le mocking permet également de simuler différents scénarios, comme des erreurs provenant des dépendances, qui peuvent être difficiles à reproduire de manière fiable dans un environnement réel.

3. La couverture de tests native de Node.js

Qu’est-ce que la couverture des tests ?

La couverture des tests est une mesure utilisée lors des tests logiciels. Elle aide les développeurs à comprendre dans quelle mesure le code source d’une application est testé. C’est essentiel, car elle met en évidence les parties du code qui n’ont pas été testées et permet ainsi aux développeurs de repérer les faiblesses potentielles de leur logiciel.

Pourquoi la couverture de tests est-elle importante ? Elle contribue à garantir la qualité des logiciels en réduisant le nombre de bugs et en évitant les régressions. Elle vous renseigne également sur l’efficacité de vos tests et vous aide à créer une application plus robuste, fiable et sécurisée.

Utiliser la couverture de tests native de Node.js

Depuis la version 20, le runtime Node.js intègre des fonctionnalités natives de couverture de tests. Notez toutefois que la couverture de tests native de Node.js est actuellement considérée comme expérimentale. Elle est donc disponible, mais pourrait évoluer dans les prochaines versions.

Pour utiliser la couverture de tests native de Node.js, vous devez ajouter l’option de ligne de commande --experimental-test-coverage. Voici comment ajouter une entrée test:coverage au champ scripts de votre fichier package.json afin d’exécuter les tests de votre projet :

{
  "scripts": {
    "test": "node --test ./tests",
    "test:coverage": "node --experimental-test-coverage --test ./tests"
  }
}

Dans l’exemple ci-dessus, le script test:coverage utilise l’option --experimental-test-coverage pour générer des données de couverture pendant l’exécution des tests.

Après avoir exécuté npm run test:coverage, vous devriez obtenir un résultat semblable à celui-ci :

ℹ tests 7
ℹ suites 4
ℹ pass 5
ℹ fail 0
ℹ cancelled 0
ℹ skipped 1
ℹ todo 1
ℹ duration_ms 84.018917
ℹ start of coverage report
ℹ ---------------------------------------------------------------------
ℹ file                 | line % | branch % | funcs % | uncovered lines
ℹ ---------------------------------------------------------------------
ℹ src/dotenv.js        | 100.00 |   100.00 |  100.00 | 
ℹ src/math.js          | 100.00 |   100.00 |  100.00 | 
ℹ tests/dotenv.test.js | 100.00 |   100.00 |  100.00 | 
ℹ tests/math.test.js   |  94.64 |   100.00 |   91.67 | 24-26
ℹ ---------------------------------------------------------------------
ℹ all files            |  96.74 |   100.00 |   94.44 |
ℹ ---------------------------------------------------------------------
ℹ end of coverage report

Ce rapport indique le pourcentage d’instructions, de branches, de fonctions et de lignes couvertes par les tests.

La couverture de tests native de Node.js est un outil puissant qui peut vous aider à améliorer la qualité de vos applications Node.js. Bien qu’elle soit actuellement considérée comme expérimentale, elle peut vous fournir des informations précieuses sur la couverture de vos tests et orienter vos efforts de test. En comprenant cette fonctionnalité et en l’utilisant, vous pouvez vous assurer que votre code est robuste, fiable et sécurisé.

4. Le mode watch de Node.js

Le mode watch de Node.js est une fonctionnalité puissante pour les développeurs. Il permet de suivre en temps réel les modifications apportées aux fichiers Node.js et de réexécuter automatiquement les scripts.

Avant d’explorer les fonctionnalités watch natives de Node.js, il est important de mentionner nodemon, un outil populaire qui répondait à ce besoin dans les premières versions de Node.js. Nodemon est un utilitaire en ligne de commande (CLI) qui redémarre l’application Node.js dès qu’une modification est détectée dans le répertoire des fichiers.

npm install -g nodemon
nodemon

Cette fonctionnalité est particulièrement utile pendant le développement. Elle permet de gagner du temps et d’améliorer la productivité en évitant de redémarrer manuellement l’application à chaque modification d’un fichier.

Snyk Advisor affichant l’état de santé du package npm nodemon.

Avec les progrès de Node.js, le runtime intègre désormais une fonctionnalité qui permet d’obtenir le même résultat. Vous n’avez donc plus besoin d’installer de dépendances tierces supplémentaires dans vos projets, comme nodemon.

Avant de passer au tutoriel, notez que le mode watch natif de Node.js est encore expérimental et pourrait évoluer. Veillez à utiliser une version de Node.js qui prend en charge cette fonctionnalité.

Utiliser les fonctionnalités watch natives de Node.js 20

Node.js 20 intègre des fonctionnalités natives de surveillance des fichiers accessibles avec l’option de ligne de commande --watch. Cette fonctionnalité est simple à utiliser et peut même reconnaître les motifs glob pour répondre à des besoins de surveillance de fichiers plus complexes.

Pour utiliser la commande --watch, ajoutez-la à votre script Node.js dans la ligne de commande, comme ci-dessous :

node --watch app.js

Pour les motifs glob, vous pouvez utiliser l’option --watch avec un motif spécifique afin de surveiller plusieurs fichiers ou répertoires. C’est particulièrement utile lorsque vous souhaitez surveiller un groupe de fichiers correspondant à un motif précis :

node --watch 'lib/**/*.js' app.js

L’option --watch peut également être utilisée avec --test pour relancer les tests à chaque modification des fichiers de test :

node --watch --test '**/*.test.js'

Cette combinaison peut accélérer considérablement votre processus de développement piloté par les tests (TDD), en exécutant automatiquement vos tests à chaque modification.

Notez que, depuis Node.js 20, le mode watch est toujours considéré comme expérimental. La fonctionnalité est donc pleinement opérationnelle, mais elle pourrait être moins stable ou moins optimisée que les fonctionnalités qui ne sont pas expérimentales.

En pratique, vous pourriez rencontrer quelques bizarreries ou bugs avec l’option --watch.

5. Corepack de Node.js

Corepack de Node.js est une fonctionnalité intéressante qui mérite d’être explorée. Introduite dans Node.js 16, elle est toujours considérée comme expérimentale. C’est une raison de plus de découvrir ce qu’elle propose et comment l’utiliser dans vos projets JavaScript.

Qu’est-ce que Corepack ?

Corepack est un projet sans dépendance à l’exécution qui fait le lien entre les projets Node.js et les gestionnaires de paquets auxquels ils sont destinés. Une fois installé, il fournit un programme appelé corepack que les développeurs peuvent utiliser dans leurs projets pour s’assurer d’avoir le bon gestionnaire de paquets, sans avoir à se soucier de son installation globale.

Pourquoi utiliser Corepack ?

En tant que développeurs JavaScript, nous travaillons souvent sur plusieurs projets, chacun pouvant avoir son propre gestionnaire de paquets préféré. Vous connaissez la situation : un projet gère ses dépendances avec pnpm et un autre avec yarn, et vous devez donc jongler avec différentes versions des gestionnaires de paquets.

Cela peut entraîner des conflits et des incohérences. Corepack résout ce problème en permettant à chaque projet de spécifier et d’utiliser facilement son gestionnaire de paquets préféré.

De plus, Corepack isole votre projet du système global, ce qui lui permet de continuer à fonctionner même si des paquets globaux sont mis à jour ou supprimés. Votre projet gagne ainsi en cohérence et en fiabilité.

Installer et utiliser Corepack

L’installation de Corepack est très simple. Comme il est fourni avec Node.js depuis la version 16, il vous suffit d’installer ou de mettre à niveau Node.js vers cette version ou une version ultérieure.

Une fois l’installation effectuée, vous pouvez définir le gestionnaire de paquets de votre projet dans le fichier package.json, comme ceci :

{
  "packageManager": "yarn@2.4.1"
}

Vous pouvez ensuite utiliser Corepack dans votre projet comme suit :

corepack enable

Si vous saisissez yarn dans le répertoire du projet sans avoir installé Yarn, Corepack détectera et installera automatiquement la version adéquate.

Vous utiliserez ainsi la version 2.4.1 de Yarn pour installer les dépendances de votre projet, quelle que soit la version globale de Yarn installée sur le système.

Pour installer Yarn globalement ou utiliser une version spécifique, vous pouvez exécuter :

corepack install --global yarn@stable

Corepack : toujours une fonctionnalité expérimentale

Bien qu’il ait été introduit dans Node.js 16, Corepack est toujours considéré comme expérimental. Cela signifie que, même s’il est censé fonctionner correctement, il est encore en cours de développement et certains aspects de son comportement pourraient changer à l’avenir.

Cela dit, Corepack est facile à installer et à utiliser, et renforce la fiabilité de vos projets. C’est une fonctionnalité qui mérite vraiment d’être explorée et intégrée à votre flux de développement.

6. Chargeur .env de Node.js

La configuration des applications est essentielle. En tant que développeur Node.js, vous avez certainement eu besoin de gérer des identifiants d’API, des numéros de port serveur ou des configurations de base de données.

En tant que développeurs, nous avons besoin de pouvoir définir des paramètres différents selon les environnements, sans modifier le code source. Dans les applications Node.js, une méthode répandue consiste à utiliser des variables d’environnement stockées dans des fichiers .env.

Le paquet npm dotenv

Avant la prise en charge native des fichiers .env dans Node.js, les développeurs utilisaient principalement le paquet npm dotenv. Le paquet dotenv charge les variables d’environnement d’un fichier .env dans process.env, où elles sont ensuite accessibles dans toute l’application.

Voici un exemple d’utilisation classique du paquet dotenv :

require('dotenv').config();

console.log(process.env.MY_VARIABLE);

Cette méthode fonctionnait bien, mais elle nécessitait d’ajouter une dépendance à votre projet. Avec l’arrivée du chargeur natif .env, vous pouvez désormais charger vos variables d’environnement directement, sans paquet externe.

Prise en charge native du chargement des fichiers .env dans Node.js

Depuis Node.js 20, l’environnement d’exécution intègre une fonctionnalité permettant de charger les variables d’environnement à partir de fichiers .env. Bien qu’elle soit encore en cours de développement, cette fonctionnalité a déjà changé la donne pour les développeurs.

Pour charger un fichier .env, vous pouvez utiliser l’option CLI --env-file au démarrage de votre application Node.js. Cette option indique le chemin du fichier .env à charger.

node --env-file=./.env index.js

Les variables d’environnement du fichier .env indiqué seront chargées dans process.env. Elles seront ensuite accessibles dans votre application comme auparavant.

Charger plusieurs fichiers .env

Le chargeur .env de Node.js permet également de charger plusieurs fichiers .env. C’est utile lorsque vous avez différents ensembles de variables d’environnement selon les environnements (développement, test, production, par exemple).

Vous pouvez spécifier plusieurs options --env-file pour charger plusieurs fichiers. Ils sont chargés dans l’ordre indiqué, et les variables des fichiers suivants remplacent celles des précédents.

Voici un exemple :

node --env-file=./.env.default --env-file=./.env.development index.js

Dans cet exemple, ./.env.default contient les variables par défaut et ./.env.development celles propres au développement. Les variables présentes à la fois dans ./.env.development et ./.env.default seront remplacées par celles de ./.env.default.

La prise en charge native des fichiers .env dans Node.js représente une amélioration importante pour les développeurs Node.js. Elle simplifie la gestion de la configuration et évite d’avoir à ajouter un paquet. Utilisez l’option CLI --env-file dans vos applications Node.js pour profiter de cette simplicité.

7. Prise en charge de __dirname et __file avec import.meta dans Node.js

Si vous utilisez les conventions de modules CommonJS dans Node.js, vous avez l’habitude de travailler avec filename et __dirname pour obtenir le nom du répertoire et le chemin du fichier courant. Jusqu’à récemment, ces valeurs n’étaient pas facilement accessibles avec ESM, et vous deviez écrire le code suivant pour extraire __dirname :

import url from 'url'
import path from 'path'
const dirname = path.dirname(url.fileURLToPath(import.meta.url))

Ou, si vous êtes fan de Matteo Collina, vous avez peut-être choisi d’utiliser son paquet npm desm.

Matteo Collina, mainteneur de npm, présente un package qui fournit __dirname et __filename dans les projets ESM via les données de l’objet import.meta.

Node.js évolue constamment pour proposer aux développeurs des moyens plus efficaces de gérer les fichiers et les chemins. Node.js v20.11.0 et Node.js v21.2.0 ont introduit une évolution importante pour les développeurs Node.js : la prise en charge intégrée de import.meta.dirname et import.meta.filename.

Utiliser import.meta.filename et import.meta.dirname dans Node.js

Heureusement, l’arrivée de import.meta.filename et import.meta.dirname simplifie considérablement le processus. Voyons comment charger un fichier de configuration avec ces nouvelles fonctionnalités.

Imaginons qu’un fichier de configuration YAML à charger se trouve dans le même répertoire que votre fichier JavaScript. Voici comment procéder :

import fs from 'fs';

const { dirname: __dirname, filename: __filename } = import.meta;
const projectSetup = fs.readFileSync(`${__dirname}/setup.yml`, "utf8");

console.log(projectSetup);

Dans cet exemple, nous utilisons import.meta.dirname pour récupérer le nom du répertoire du fichier courant et l’affecter à la variable __dirname, par commodité et conformément aux conventions de codage CommonJS.

8. Promesses des minuteurs natifs de Node.js

Node.js, un environnement d’exécution JavaScript populaire basé sur le moteur JavaScript V8 de Chrome, cherche depuis toujours à simplifier la vie des développeurs grâce à des mises à jour régulières et à de nouvelles fonctionnalités.

Node.js a ajouté la prise en charge native des minuteurs avec une syntaxe basée sur les promesses dès Node.js v15, mais je reconnais que je ne les ai pas utilisés régulièrement.

Les minuteurs setTimeout() et setInterval() de JavaScript : petit rappel

Avant d’aborder les promesses natives des minuteurs, rappelons brièvement le fonctionnement des minuteurs JavaScript setTimeout() et setInterval().

L’API setTimeout() est une fonction JavaScript qui exécute une fonction ou un fragment de code donné à l’expiration du délai.

setTimeout(function(){ 
    console.log("Hello World!"); 
}, 3000);

Dans le code ci-dessus, « Hello World! » s’affiche dans la console au bout de 3 secondes (3 000 millisecondes).

De son côté, setInterval() exécute de façon répétée la fonction indiquée, en marquant un délai entre chaque appel.

setInterval(function(){ 
    console.log("Hello again!"); 
}, 2000);

Dans le code ci-dessus, « Hello again! » s’affiche dans la console toutes les 2 secondes (2 000 millisecondes).

L’ancienne méthode : envelopper setTimeout() dans une promesse

Auparavant, les développeurs devaient souvent envelopper artificiellement la fonction setTimeout() dans une promesse pour l’utiliser de manière asynchrone. Cela permettait d’utiliser setTimeout() avec async/await.

Voici comment procéder :

function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

async function demo() {
  console.log('Taking a break...');
  await sleep(2000);
  console.log('Two seconds later...');
}

demo();

Le code affichait « Taking a break... », attendait deux secondes, puis affichait « Two seconds later... ».

Cette méthode fonctionnait, mais elle ajoutait une complexité inutile au code.

Les promesses natives des minuteurs dans Node.js : une méthode plus simple

Avec les promesses natives des minuteurs dans Node.js, plus besoin d’envelopper setTimeout() dans une promesse. Vous pouvez utiliser directement setTimeout() avec async/await. Le code est ainsi plus clair, plus lisible et plus facile à maintenir. Voici comment utiliser les promesses natives des minuteurs dans Node.js :

const {
  setTimeout,
} = require('node:timers/promises');

setTimeout(2000, 'Two seconds later...').then((res) => {
  console.log(res);  
});

console.log('Taking a break...');

Dans le code ci-dessus, setTimeout() est importé depuis node:timers/promises. Nous l’utilisons ensuite directement avec async/await. Le code affiche « Taking a break... », attend deux secondes, puis affiche « Two seconds later... ».

Cela simplifie considérablement la programmation asynchrone et facilite la lecture, l’écriture et la maintenance du code.

9. Modèle de permissions de Node.js

Rafael Gonzaga, aujourd’hui membre du TSC de Node.js, a relancé les travaux sur le module de permissions de Node.js qui, comme dans Deno, définit des restrictions configurables sur les ressources au niveau du processus.

Face aux risques liés à la sécurité de la chaîne d’approvisionnement, aux paquets npm malveillants et à d’autres menaces, il est de plus en plus essentiel de gérer et de contrôler les ressources auxquelles vos applications Node.js peuvent accéder, pour des raisons de sécurité et de conformité.

À cet égard, Node.js a introduit une fonctionnalité expérimentale appelée module de permissions, qui permet de gérer les permissions d’accès aux ressources de vos applications Node.js. Cette fonctionnalité s’active à l’aide de l’option de ligne de commande --experimental-permission.

Modèle de permissions des ressources de Node.js

Le modèle de permissions de Node.js fournit une abstraction permettant de gérer l’accès à différentes ressources, comme les systèmes de fichiers, les réseaux, les variables d’environnement et les threads worker. Cette fonctionnalité est particulièrement utile pour limiter les ressources accessibles à une partie de votre application.

Voici quelques restrictions courantes que vous pouvez définir avec le modèle de permissions :

  • Lecture et écriture dans le système de fichiers avec --allow-fs-read=* et --allow-fs-write=*. Vous pouvez indiquer des répertoires et des chemins de fichiers précis, ainsi qu’autoriser plusieurs ressources en répétant les options.

  • Lancement de processus enfants avec --allow-child-process

  • Lancement de threads worker avec --allow-worker

Le modèle de permissions de Node.js fournit également une API d’exécution, process.permission.has(resource, value), permettant de vérifier des autorisations d’accès précises.

Si vous tentez d’accéder à une ressource non autorisée, par exemple de lire le fichier .env, vous obtiendrez une erreur ERR_ACCESS_DENIED :

> start:protected
> node --env-file=.env --experimental-permission server.js

node:internal/modules/cjs/loader:197
  const result = internalModuleStat(filename);
                 ^

Error: Access to this API has been restricted
    at stat (node:internal/modules/cjs/loader:197:18)
    at Module._findPath (node:internal/modules/cjs/loader:682:16)
    at resolveMainPath (node:internal/modules/run_main:28:23)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:135:24)
    at node:internal/main/run_main_module:28:49 {
  code: 'ERR_ACCESS_DENIED',
  permission: 'FileSystemRead',
  resource: '/Users/lirantal/repos/modern-nodejs-runtime-features-2024/server.js'
}

Node.js v21.6.1

Exemple de modèle de permissions dans Node.js

Imaginons que vous ayez une application Node.js qui gère l’envoi de fichiers. Vous souhaitez limiter cette partie de l’application à un répertoire précis où sont stockés les fichiers envoyés.

Activez la fonctionnalité expérimentale de permissions au démarrage de votre application Node.js avec l’option --experimental-permission.

node --experimental-permission ./app.js

Nous voulons également autoriser explicitement l’application à lire deux fichiers fiables, .env et setup.yml. Il faut donc modifier la commande précédente comme suit :

node --experimental-permission --allow-fs-write=/tmp/uploads --allow-fs-read=.env --allow-fs-read=setup.yml ./app.js

Ainsi, si l’application tente d’accéder à des ressources du système de fichiers en écriture en dehors du chemin d’envoi indiqué, elle s’arrêtera avec une erreur.

L’exemple de code suivant montre comment encapsuler l’accès à une ressource dans un bloc try/catch, ainsi que comment utiliser l’API d’exécution des permissions de Node.js pour vérifier l’accès sans déclencher d’exception d’erreur :

 const { dirname: __dirname, filename: __filename } = import.meta;
// @TODO to avoid the Node.js resource permission issue you should update
// the path to be `setup.yml` in the current directory and not `../setup.yml`.
// the outside path for setup.yml was only changed in the source code to
// show you how Node.js resource permission module will halt if trying to access
// something outside the current directory.
const filePath = `${__dirname}/../setup.yml`;
try {
  const projectSetup = fs.readFileSync(filePath, "utf8");
  // @TODO do something with projectSetup if you want to
} catch (error) {
  console.error(error.code);
}
// @TODO or consider using the permissions runtime API check:
if (!process.permission.has("read", filePath)) {
  console.error("no permissions to read file at", filePath);
}

Notez que la fonctionnalité de permissions de Node.js est toujours expérimentale et susceptible d’évoluer.

Pour en savoir plus sur les permissions et les conventions de sécurité adaptées à la production, découvrez comment créer des applications Node.js sécurisées dans ces articles de blog de Snyk :

Ces articles proposent un guide complet pour créer des images de conteneur sécurisées pour les applications web Node.js, un aspect essentiel du développement d’applications Node.js sécurisées.

10. Module de politique de Node.js

Le module de politique de Node.js est une fonctionnalité de sécurité conçue pour empêcher le chargement et l’exécution de code malveillant dans une application Node.js. Il ne permet pas de retracer l’origine du code chargé, mais constitue une solide protection contre les menaces potentielles.

Le module de politique utilise l’option CLI --experimental-policy pour activer le chargement de code fondé sur des politiques. Cette option prend en argument un fichier manifeste de politique au format JSON. Par exemple : --experimental-policy=policy.json.

Le fichier manifeste de politique contient les règles que Node.js respecte lors du chargement des modules. Il permet de contrôler efficacement la nature du code chargé dans votre application.

Implémenter le module de politique de Node.js : guide étape par étape

Voyons un exemple simple pour illustrer l’utilisation du module de politique de Node.js :

1. Créez un fichier de politique. Ce fichier JSON doit définir les politiques de votre application concernant le chargement des modules. Appelons-le policy.json. 

Par exemple :

    {
      "resources": {
        "./moduleA.js": {
          "integrity": "sha384-xxxxx"
        },
        "./moduleB.js": {
          "integrity": "sha384-yyyyy"
        }
      }
    }

Ce fichier de politique indique que moduleA.js et moduleB.js doivent présenter des valeurs d’intégrité spécifiques pour être chargés.

Cependant, générer le fichier de politique pour toutes vos dépendances directes et transitives n’est pas simple. Il y a quelques années, Bradley Meck a créé le package npm node-policy, qui fournit une interface CLI pour automatiser la génération de ce fichier.

2. Exécutez votre application Node.js avec l’option --experimental-policy :

  node --experimental-policy=policy.json app.js

Cette commande indique à Node.js de respecter les politiques définies dans policy.json lors du chargement des modules dans app.js.

3. Pour empêcher toute altération du fichier de politique, vous pouvez fournir une valeur d’intégrité pour le fichier lui-même à l’aide de l’option --policy-integrity :

    node --experimental-policy=policy.json --policy-integrity="sha384-zzzzz" app.js

Cette commande garantit l’intégrité du fichier de politique, même si celui-ci est modifié sur le disque.

Limites de la politique d’intégrité de Node.js 

L’environnement d’exécution Node.js ne dispose d’aucune fonctionnalité intégrée pour générer ou gérer le fichier de politique. Cela peut entraîner des difficultés, par exemple pour gérer différentes politiques selon que l’environnement est de production ou de développement, ou pour prendre en charge les imports de modules dynamiques.

Autre limite : si vous utilisez déjà un package npm malveillant dans son état actuel, il est trop tard pour générer un fichier de politique d’intégrité des modules.

Je vous conseille de suivre les évolutions dans ce domaine et d’adopter progressivement cette fonctionnalité.

Pour en savoir plus sur le module de politique de Node.js, consultez l’article présentant les politiques d’intégrité expérimentales de Node.js, qui propose un tutoriel détaillé, étape par étape, sur l’utilisation des politiques d’intégrité de Node.js.

Pour conclure

À l’issue de ce tour d’horizon des fonctionnalités modernes de l’environnement d’exécution Node.js à adopter en 2024, une chose est claire : elles sont conçues pour simplifier votre processus de développement, améliorer les performances de vos applications et renforcer leur sécurité. Loin d’être de simples tendances, ces fonctionnalités pourraient bien redéfinir notre approche du développement Node.js.

Renforcez la sécurité de Node.js avec Snyk

Si ces fonctionnalités Node.js peuvent considérablement améliorer votre processus de développement et les performances de vos applications, il reste essentiel de rester vigilant face aux menaces potentielles. Snyk peut vous accompagner dans cette démarche. Cet outil puissant vous aide à détecter et à corriger les vulnérabilités connues dans vos dépendances Node.js et à préserver la sécurité de votre environnement de développement.

Pour profiter de tout ce que Snyk a à offrir, inscrivez-vous gratuitement ici et commencez à développer des applications Node.js plus sécurisées.

Publié dans: