https://lafor.ge/feed.xml

Deno : un successeur à nodeJS ?

2020-05-16

Je dois vous avouer que j'ai pris très récemment connaissance de ce projet.

Depuis qu'il est sorti en version 1.0.0 le 13 mai dernier, il a commencé à faire du bruit sur twitter et a fini par arriver à mes oreilles.

Je ne fais pas plus de nodeJS que ça mais j'ai récemment commencer à me mettre sur un projet en typescript, donc quand j'ai lu que deno pouvait comprendre de manière native le typescript. Je me suis que ça serait bien d'y jeter un petit coup d'oeil. Ce qui est chose faite 😀.

Le projet se prononce "Dino" pour faire le jeu de mot. Un bon nom de projet doit toujours avoir un jeu de mots 🤓.

Cet article est un peu tout à la fois: un pense-bête, un bilan de ce que j'ai pu comprendre de deno et un espace d'expérimentation.

Caractéristiques

Déjà, que promet Deno?

Deno n'est pas un langage de programmation mais un runtime. Il permet de manière native de faire tourner à la fois du typescript et du javascript.

Attention! Deno n'est pas du nodeJS amélioré, c'est une réécriture totale en Rust ❤️🦀 et utilise un nouvelle API de binding du moteur V8 sous la forme d'une lib appelée rusty_v8. Ceci implique que les modules de nodeJS comme http n'existe pas en tout cas pas sous la même forme que sous nodeJS.

Sa principale promesse est d'offrir un runtime sécurisé à la fois pour l'exécution du javascript mais aussi du typescript.

Une autre promesse est de s'affranchir des node_modules. En téléchageant une fois pour toute les dépendances pour les stocker dans un endroit unique sur le disque dur de l'utilisateur.

Deno permet aussi d'utiliser les API standards propres aux navigateurs internet modernes que sont fetch et l'objet window.

Il comporte aussi (pour le moment de manière instable ⚡️) des fonctionnalité comme les WebWorkers. Permettant de réaliser des opération en tâche de fond grâce à l'utilisation de threads séparés. Ceci étant permis grâce à l'architecture du projet se basant sur la célèbre et robuste lib rust tokio.

Il existe encore bien d'autre chose possible à réaliser mais pour le moment installons le Dino ^^.

Installation

J'écris ces lignes depuis un MacOS, je vais donc installer deno via homebrew mais il existe un nombre assez invraisemblable d'installer dino. Je vous laisse faire votre marché en fonction de votre OS et de votre préférence.

De mon côté, je vais donc éxécuter :

brew install dino

Si tout s'est bien passé vous devriez pouvoir faire :

# deno --version
deno 1.0.0
v8 8.4.300
typescript 3.9.2

Parfait c'est installé 🎉!

Mes premiers programmes

Deno est fait pour faire tourner à la fois du js et du ts, comme je préfère les langage typé. Allons y pour du ts.

Hello World

Je créé mon fichier hello.ts

// hello.ts
console.log("Hello from 🦕")

Que je lance via:

# deno run hello.ts 
Compile file:///lab/deno/playground/hello.ts
Hello from 🦕

Vous pouvez remarquer qu'il existe une phase de compilation, celle ci disparaît si vous relancez le deno run sans changer la source.

Le site de deno.land propose tout un tas d'études de cas et d'exemple de code. Nous allons en étudier certains.

Manipuler des fichiers

Il s'agit d'une réécriture de la commande UNIX cat qui a pour but de concaténer le contenu d'un ensemble de fichiers.

// cat.ts
for (let i = 0; i < Deno.args.length; i++) {
  let filename = Deno.args[i];
  let file = await Deno.open(filename);
  await Deno.copy(file, Deno.stdout);
  file.close();
}

Plusieurs choses ici, d'abord le Deno.args, Deno est un objet globalement injecté au runtime par deno avec tout ce qui est nécessaire pour gérer les I/O du programme et bien plus encore. Ici nous utilisons la propriété args qui nous permet de récupérer la liste des arguments passer au programme lors du run.

Ensuite nous avons le Deno.open qui permet d'ouvrir un fichier, à ceci près que tout appel de ce genre est asynchrone, il s'agit en fait d'une promesse d'ouverture du fichier. Pour récupérer le file descriptor il faut attendre que la promesse soit résolue ou rejeté par l'OS. C'est le boulot du mot clef await qui n'est pas spécifique à deno. Mais par contre de ce que je comprends l'intégralité du programme est considéré comme asynchrone. Et donc le async d'habitude nécessaire ne semble pas à avoir à être présent.

A la ligne suivante on remarque le Deno.stdout qui est un file descriptor pointant vers la sortie standard, écrire dans ce FD revient à écrire dans la console.

De la même manière le Deno.copy est une promesse de copy du FD du fichier dans stdout.

4 pauvres lignes et déjà une foule d'informations sur le fonctionnement de deno 😃.

Pour tester notre programme on va avoir besoin de fichier avec du contenu dedans. Je suis pas inspiré pour les noms.

// fichier1.txt
contenu1
// fichier2.txt
contenu1

C'est parti on peut lancer tout ça !

# deno run cat.ts fichier1.txt fichier2.txt 
Compile file:///lab/deno/playground/cat.ts
error: Uncaught PermissionDenied: read access to "/Users/yguern/Documents/lab/deno/playground/fichier1.txt", run again with the --allow-read flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
    at async Object.open ($deno$/files.ts:37:15)
    at async file:///Users/yguern/Documents/lab/deno/playground/cat.ts:4:16

Ça compile et fail 💥. Notre dino 🦕 préféré nous explique ce qui ne va pas, nous n'avons pas les permissions de lire dans le système de fichier. Quand je vous avais dit que le runtime était sécurisé je vous avais pas menti, à moins de l'exprimer explicitement, un programme n'a pas la possibilité de manipuler le FS ni le réseau. Il est même possible et de révoquer des droits en plein milieu du runtime.

Bon notre programme ne vient pas des hackers russes 🐻, on va considéré qu'il est safe. Relançons avec les bonnes permissions.

# deno run --allow-read cat.ts fichier1.txt fichier2.txt
Compile file:///lab/deno/playground/cat.ts
contenu1contenu2

Mieux ✅

Jouer avec l'API fetch

Un des intérêts de faire du deno et pas du nodeJS est de pouvoir bosser directement avec la fetch API.

On va essayer de récupérer des Chuck Norris Jokes, pour ce nous devons faire un appel sur une API.

const res = await fetch("http://api.icndb.com/jokes/random")
const body = await res.json()
const enc = new TextEncoder();
const joke = enc.encode(body.value.joke)
await Deno.stdout.write(joke)

Pas grand chose à dire sur ce qui est fait c'est assez classique et ressemblant à ce qui pourrait exister sur navigateur.

Une petite nouveauté est le Deno.stdout.write qui permet d'écrire dans le file descriptor stdout. Celui-ci prend comme argument un Uint8Array d'où l'utilisation de TextEncoder.

Même combat que tout à l'heure, vous n'avez pas autorisez explicitement l'accès au réseau:

# deno run joke.ts                                      
error: Uncaught PermissionDenied: network access to "http://api.icndb.com/jokes/random", run again with the --allow-net flag
    at unwrapResponse ($deno$/ops/dispatch_json.ts:43:11)
    at Object.sendAsync ($deno$/ops/dispatch_json.ts:98:10)
    at async fetch ($deno$/web/fetch.ts:591:27)
    at async file:///Users/yguern/Documents/lab/deno/playground/joke.ts:1:13

Pour se faire le flag --allow-net est là pour ça:

# deno run --allow-net joke.ts
Chuck Norris can remember the future.

A vous les blagues les plus hilarantes 🃏.

Pour le moment le "scheme" file:// n'est pas supporté mais une PR est à l'étude.

Echo server

C'est le cas d'étude le plus simple lorsque que l'on débute dans le TCP, il renvoie à l'expéditeur le contenu de qui est envoyé.

// echo.ts
const port =  Number.parseInt(Deno.args[0]) ?? 8090

const listener = Deno.listen({
    port
})

console.log(`listen on 0.0.0.0:${port}`)
for await (const conn of listener) {
    Deno.copy(conn, conn)
}

Rien de transcendant ligne 1, on utilise le Deno.args s'il existe comme port sinon on utilise une valeur par défaut. Ceci grâce au nullish coalescing operator ou ?? qui est voie d'adoption sur les navigateurs.

Le Deno.listen démarre un serveur TCP sur le port indiqué. L'objet listener réalise l'interface AsyncIterable<T> ce qui lui permet de se comporter comme un générateur de promesses de connexions.

Ce que for await peut alors utiliser pour gérer les connexions entrantes. Chaque conn correspond au socket établi par l'utilisateur sur notre serveur.

Ensuite pour chaque connexion on copie la connexion vers elle même et le tour et joué. Pour cela on utilise la méthode déjà étudier de Deno.copy.

L'éxécution avec les bonnes permissions nous donne:

# deno run --allow-net echo.ts 8091
listen on 0.0.0.0:8091

Notre serveur nous annonce qu'il attend une connexion entrante.

Utilisons netcat

# nc localhost 8091
Salut Deno !
Salut Deno !

Comme prévu on a un echo de ce qu'on a tapé 👻.

Lancer un programme

A l'instar de nodeJS nous pouvons aussi lancer un programme depuis notre typescript.

// run.ts
// On démarre dans un sous processus echo
const p = Deno.run({
  cmd: ["echo", "hello 🦕"],
});
// On attend que echo finisse
await p.status();
# deno run --allow-run run.ts                           
hello 🦕

Les Web Workers

Les Web Workers permettent d'éxécuter des scripts sur des threads séparés.

Cela se décompose en deux parties:

Le worker

// worker.ts
self.onmessage = (event) => {
    console.log(event.data)
    self.close()
}

On utilise l'API standard des WebWorker, celle ci possède une méthode onmessage qui est appelée dès qu'un evènement survient.

Cet évènement est provoqué du côté du main.ts par l'appelle à la méthode postMessage du worker.

// main.ts
const worker = new Worker('./worker.ts', { type: "module" })
worker.postMessage("Hello 🦕")

Deux choses importantes ici. D'abord la définition du worker doit être dans un fichier séparé et ensuite il faut impérativement et explicitement indiquer le { type: "module" } sinon ça marchera pas 😛.

# deno run --allow-read main.ts
Hello 🦕

Le flag --allow-read est ici essentiel car on intéragie avec le système de fichier.

Le WebAssembly

Et oui on peut en faire nativement avec deno.

Je vais créér mon propre fichier .wasm, mais vous êtes libre de passer cette étape 😀.

Construire un .wasm via Rust

D'abord on se fait un petit projet en wasm via rust.

Si vous ne l'avez pas déjà installez cargo-generate.

cargo install cargo-generate

Il va vous permettre de générer un projet boilerplate de wasm-pack.

Ensuite installez wasm-pack lui même:

curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Un fois ceci fait vous pouvez générer votre projet. Personellement je l'ai appelé deno.

J'ai très légèrement modifié deno/src/lib.rs

// deno/src/lib.rs
...
#[wasm_bindgen]
pub fn answer() -> i32 {
    42
}

Puis faire un wasm-pack build pour générer le fichier deno/pkg/deno_bg.wasm. Vous pouvez essayer de l'ouvrir mais c'est du binaire.

Utiliser le .wasm

Donc maintenant qu'on a notre .wasm nous allons essayer d'appeller la méthode answer.

// main.ts
const wasm = await Deno.readFile('./deno/pkg/deno_bg.wasm');
const wasmModule = new WebAssembly.Module(wasm)
const wasmInstance = new WebAssembly.Instance(wasmModule)
console.log(wasmInstance.exports.answer())

Tout d'abord on récupère le buffer du binaire dans la variable wasm. Puis on en fait un module via WebAssembly.Module, puis on transforme ce module en instance par la méthode WebAssembly.Instance pour enfin pouvoir l'utiliser.

La variable wasmInstance expose une propriété exports qui contient tout ce qui est exposé vers l'extérieur par notre projet Rust. Dont notre méthode answer que l'on peut exécuter comme une fonction typescript normale.

On lit un fichier donc attention aux permissions.

# deno run --allow-read main.ts
42

L'exemple est idiot mais ça marche, la fusion deno/rust est fonctionnelle. 🦕 ❤️ 🦀.

Tester

Ouais mais non, moi j'aime bien quand je peux tester mon code 😅. Et justement deno propose une librairie de test déjà intégrée.

On va encore faire du trivial mais c'est juste pour le principe de fonctionnement.

Je déclare une fonction hello et je veux vérifier son comportement.

// main.ts
import { assertEquals } from "https://deno.land/std/testing/asserts.ts";

function hello() {
    return "Hello 🦕"
}

Deno.test("Should return rigth string", () => {
    assertEquals(hello(), "Hello 🦕")
})

La première ligne importe depuis la lib standard le système d'assertion.

Et ensuite il suffit de faire comme avec jest et déclarer son test.

Puis pour lancer les tests

# deno test module.ts
running 1 tests
test Should return rigth string ... ok (3ms)

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (3ms)

Et voilà ça test ✅!

Metions honnorables

Formattage du code

Si vous êtes du genre à indenter n'importe comment et à mettre les paranthèses n'importe où ou à ne pas respecter la élémentaire de ne jamais mettre de tabulations !!

Deno propose une méthode de formattage.

# deno fmt fichier_crado.ts

Ça effectue les corrections et réenregistre le fichier.

Bundler

Si une source externe est composé de nombreux fichiers vous pouvez demander à deno de créer un bundle js de l'intégralité de ces fichiers.

# deno bundle https://deno.land/std/examples/colors.ts colors.bundle.js
Bundling https://deno.land/std/examples/colors.ts
Emitting bundle to "colors.bundle.js"
8044 bytes emmited.

Vous pouvez alors faire.

deno run colors.bundle.js                                            
Hello world!

Pour exécuter ce bundle.

Et la liste est encore longue. Mais je vais m'arrêter là cet article est déjà beacoup trop long pour une première découverte.

Conclusion

Je suis complètement séduit par deno, j'ai hâte de voir ce que ça va donner dans un proche avenir.

Pour le moment je n'ai pas fait de réel projet avec, mais je compte m'y mettre et je vous referait un article pour donner mes impressions.

Merci de m'avoir lu et à la prochaine ❤️.

avatar

Auteur: Akanoa

Je découvre, j'apprends, je comprends et j'explique ce que j'ai compris dans ce blog.

Ce travail est sous licence CC BY-NC-SA 4.0.