Deno : un successeur Ă  nodeJS ?

Lecture 9 min. ‱


Table des matiĂšres

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 ❀.