À propos de node-ipc : pour une sortie de l'écosystème Node.js / NPM au profit de Deno

Version originale: 2022-03-20.
Dernière mise à jour: 2022-03-23T15:28:30-04:00.
Mots-clefs: nodejs, deno, npm, nix
3 minutes de lecture

node-ipc

Le « malware » : https://github.com/RIAEvangelist/node-ipc/blob/847047cf7f81ab08352038b2204f0e7633449580/dao/ssl-geospec.js, i.e. :

  • faire une requête à une API de géolocalisation IP avec une clef d’API hardcodée (désormais désactivée) ;
  • vérifier si le code du pays est le bon, e.g. (biélo)russie ;
  • récursivement réécrire des fichiers

Le problème : absence total de contrôle sur les dépendances et le code qu’ils exécutent

Pour faire une rétrospective rapide :

  • NPM1 supportait jusqu’à la version 7 les « hook scripts » : https://docs.npmjs.com/cli/v7/using-npm/scripts#hook-scripts, i.e. à chaque installation d’une dépendance, du code arbitraire pouvait s’éxécuter.
  • Les binaires Node.js jouissent d’une liberté absolue de faire tout ce qu’ils veulent sur le système2.

Les mauvais élèves-type

Empaqueter ce genre de choses est trop difficile

Dès que l’on veut faire un déploiement hors-ligne, sécurisé, avec un vrai traçage de la chaîne des dépendances, dépendre indirectement de ces choses, ce qui arrive trivialement dès qu’on utilise de la modernité comme Vite ou Web Test Runner, transforme l’expérience en une session de chasse à quel morceau de code s’amuse à faire l’hypothèse d’avoir Internet, un accès en écriture à node_modules, post-installation.

Recommençons à zéro: Deno

Deno offre une porte de sortie intéressante, on oublie :

  • package.json
  • node_modules
  • l’hypothèse que les APIs systèmes sont autorisés par défaut par l’utilisateur final

L’expérience est probablement trop prématuré encore pour certains projets, mais avec des choses comme https://esm.sh et https://alephjs.org/, on a quelque chose d’assez impressionnant sur le plan expérience développeur et expérience opérationnel3.

Un petit exemple avec Nix

Je ne crois pas avoir vu de tutoriels ou d’articles évidents sur comment faire du Nix et du Deno, puisque Deno enlève le modèle package.json, je propose une petite façon de faire très « bête » avec https://alephjs.org :

Étape 1 : penser ses dépendances

J’ai pris la voie de la facilité: fixed-output derivations, puisque je peux juste tout télécharger, puis vérifier que ma structure est bonne.

{ lib, stdenv, deno }:
{
  vendorDependencies = { projectName ? null, dependencySrc, vendorSha256 }: 
  let
    prefix = if projectName != null then "${projectName}-" else "";
  in
  let drv =
    stdenv.mkDerivation {
      name = "${prefix}deno-vendor-dependencies";

      outputHashMode = "recursive";
      outputHashAlgo = "sha256";
      outputHash = vendorSha256;

      buildInputs = [
        deno
      ];

      DENO_DIR = "$TMPDIR";

      src = dependencySrc;
      dontUnpack = true;

      buildPhase = ''
        deno vendor $src
      '';

      installPhase = ''
        mv vendor $out/
      '';
    };
  in
  drv;
}

C’est pas trop mal: 36 lignes, hein? On peut mieux faire certainement :).

Pour réinventer le package.json, pas très compliqué:

export * from 'https://deno.land/x/aleph/cli.ts';

export * from 'https://deno.land/x/aleph@v0.3.0-beta.19/types.d.ts';
export * from 'https://deno.land/x/aleph@v0.3.0-beta.19/framework/core/mod.ts';
export * from 'https://deno.land/x/aleph@v0.3.0-beta.19/framework/react/mod.ts';

export {default as React} from 'https://esm.sh/react@17.0.2';
export {default as ReactDOM} from 'https://esm.sh/react-dom@17.0.2';

Cela pourrait gagner à être simplifié, probablement.

Étape 2 : relier ses dépendances à son projet

C’est l’étape la plus difficile, Deno offre les import maps, c’est très malin.

Je regrette néanmoins l’absence de plusieurs imports maps pour pouvoir proposer des imports maps techniques, e.g. Nix les génère et des imports maps « utilisateur », e.g. mon code source et des alias propres.4

Le choix de la facilité encore:

{ pkgs ? import <nixpkgs> {}, lib ? pkgs.lib }:
let
  deno2nix = pkgs.callPackage ./nix/deno2nix.nix { };
  vendorDependencies = deno2nix.vendorDependencies {
    projectName = "example";
    dependencySrc = ./src/deps.ts;
    vendorSha256 = lib.fakeHash;
  };
in
{
  shell = pkgs.mkShell {
    buildInputs = [
      pkgs.deno
    ];

    shellHook = ''
      export IMPORT_MAP="${vendorDependencies}/import_map.json"
    '';
  };
}

Étape 3 : profiter

Le framework Aleph propose un binaire Aleph assez bizarrement installé qui reprend les mêmes problématiques que Node.js, hélas.5

Du coup, j’ai décidé de pin le CLI moi-même et de tricher avec une réimplementation légère en Bash que j’appelle aleph sans originalité:

#!/usr/bin/env bash
deno run \
  --no-remote \
  --allow-env=ALEPH_DEV,ALEPH_DEV_PORT \
  --allow-run=$(which deno) \
  --allow-read="/nix/store,$PWD" \
  --import-map=$IMPORT_MAP \
  https://deno.land/x/aleph/cli.ts \
  "$@"

Et voilà, je peux lancer mes déploiements et autres, de façon totalement sandboxé, et la seule chose accessible c’est mon working directory et le store Nix.


Je suis tombé assez amoureux de l’approche Deno et j’espère faire plus de choses, notamment explorer en profondeur de meilleures stratégies pour alimenter l’import map, gérer le working directory (plutôt, il faudrait viser les sources, etc.), enfin, sortir de la fixed output derivation avec les lockfiles que Deno peut produire.

Node.js n’a pas à perdurer, avec https://esm.sh et de l’huile de coude type Nix, on peut sortir presque complètement de la souffrance.


  1. Le package manager defacto sous Node.js↩︎

  2. Remarque: pour les langages interprétés du même genre, c’est pareil, e.g. Python.↩︎

  3. Ceux qui vont déployer, empaqueter, etc.↩︎

  4. D’ailleurs: les imports maps forment une spécification draft du W3C! c.f. https://wicg.github.io/import-maps/ ; bon poussée par Google :D↩︎

  5. Bon, on paye le tribut une fois en vrai.↩︎