Parfois on a besoin d'un outil basique, en ligne de commande c'est plus simple à créer mais changer de langage pour créer un outil qu'on va utiliser occasionnellement ça le rend un peu trop couteux en effort… Pourquoi ne pas le faire en TypeScript ? Avec une plateforme moderne qui ne demande pas 2h de configuration avant de faire un "Hello world!" ?
deno init
Rentrons dans le vif du sujet : on va utiliser Deno. Deno c'est une plateforme "à la Node.js" pour exécuter du JavaScript, du TypeScript ou du WASM hors d'un navigateur mais en gardant nos habitudes du navigateur au maximum ! Et quand je dis "exécuter du TypeScript" c'est vraiment ça, pas de compilation/transpilation, pas de bidouille, TypeScript est depuis le début de Deno "first class citizen", il est traité par le moteur comme du JavaScript sans avoir à se prendre la tête !
Passons sur les promesses, allons dans le concret !
$ mkdir hello
$ cd ./hello
$ deno init
✅ Project initialized
Run these commands to get started
# Run the program
deno run main.ts
# Run the program and watch for file changes
deno task dev
# Run the tests
deno test
Voilà on a un projet Deno !
Je sais qu'on est habitué à mettre en place des projets avec des CLI qui prémâche la complexité. Regardons la "complexité" : 3 fichiers ont été créés main.ts, main_test.ts et deno.json.
// main.ts
export function add(a: number, b: number): number {
return a + b;
}
// Learn more at https://docs.deno.com/runtime/manual/examples/module_metadata#concepts
if (import.meta.main) {
console.log("Add 2 + 3 =", add(2, 3));
}
Rien de compliqué ici : on crée une fonction qui contient une logique métier (et est donc exporté pour les tests), un appel à cette fonction dans un console.log() conditionné avec import.meta.main.
Pour rappel
import.meta, c'est une partie des ESModules qui est souvent ignoré de la plupart des gens, mais qui permet de proposer des métadonnées de façon standard partagées partout dans l'application et aussi à la plateforme de fournir des informations.
// main_test.ts
import { assertEquals } from "@std/assert";
import { add } from "./main.ts";
Deno.test(function addTest() {
assertEquals(add(2, 3), 5);
});
Côté test on retrouve encore quelque chose de simple : un import d'une librairie (avec la même syntaxe import qu'on est habitué à utiliser et qui est standard/native dans les navigateurs), un import de notre fonction métier, un appel à Deno.test() pour créer un test.
// deno.json
{
"tasks": {
"dev": "deno run --watch main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1"
}
}
Et côté deno.json on retrouve plus ou moins ce qu'on retrouverait dans un package.json : des tasks qui sont équivalentes à des scripts en Node.js, des imports qui sont équivalents à des dependancies à une nuance forte prêt qu'ici on suit le format standard import-map qui est nativement supporté par les navigateurs.
La task "dev" est intéressante parce qu'elle montre qu'il y a nativement un mode "watch" sur les fichiers de sorte à relancer le programme sans n'avoir rien à faire.
Côté imports, on notera qu'on a besoin de préciser le dépôt (ici jsr (pour JavaScript Registry) qui est un effort d'unifier la gestion de dépendance à travers toutes les plateformes).
On notera l'absence de configuration pour TypeScript, ou même de configuration tout court en fait au final.
Lançons notre projet :
$ deno run main.ts
Add 2 + 3 = 5
Impressionnant ? Non, on est d'accord mais ça fonctionne.
Hello Deno!
Bon changeons un peu notre CLI pour qu'il dise bonjour !
// main.ts
export function hello(): string {
return "Hello Deno!";
}
if (import.meta.main) {
console.log(hello());
}
$ deno run main.ts
Hello Deno!
Si on ajoute un paramètre ?
// main.ts
export function hello([who = "Deno"]: string[]): string {
return `Hello ${who}!`;
}
if (import.meta.main) {
console.log(hello(Deno.args));
}
$ deno run main.ts
Hello Deno!
$ deno run main.ts World
Hello World!
Peu d'effort comme vous le voyez, Deno.args fourni simplement un tableau avec les arguments sans avoir à se préoccuper du chemin du script, du binaire Deno, ou autre comme on a souvent sur d'autres plateformes ! Vous commencez surement aussi à le sentir : tout ce qui n'est pas standard ECMAScript va venir de Deno.*, c'est volontaire pour éviter au maximum les globales qui pourraient casser la compatibilité avec le navigateur dans le futur, ça permet aussi de très facilement identifier ce qui provient de Deno.
Avec un peu plus de classe ?
Afficher un "Hello, world!" c'est la base, on peut faire mieux ! Donc je vous propose qu'on aille chercher un package NPM pour avoir un affichage un peu plus fancy !
Installons ce qu'il faut :
deno install npm:figlet
Vous noterez bien que j'ai écrit
npm:figletet pasfigletcomme on ferait avec npm, ici on a besoin de préciser quel registre on veut attaquer comme Deno permet de mixer les registres dans un même projet et qu'il va par défaut aller piocher dans le registre JSR. J'aurais pu aller chercher dans le JSR, mais je préfère vous montrer qu'on peut garder les packages NPM auxquels on est habitués avec Deno 🤓
Notre deno.json est maintenant :
{
"tasks": {
"dev": "deno run --watch main.ts"
},
"imports": {
"@std/assert": "jsr:@std/assert@1",
"figlet": "npm:figlet@^1.9.3"
}
}
Et du coup à l'usage ?
import figlet from "figlet";
export async function hello([who = "Deno"]: string[]): Promise<string> {
return await figlet(`Hello ${who}!`, { font: "Standard" });
}
if (import.meta.main) {
console.log(await hello(Deno.args));
}
Je force juste la police à utiliser pour le texte, mais il y a pas mal de paramétrage possible en vrai. Vous noterez je fais du top level await (await au premier niveau du fichier, pas à l'intérieur d'une fonction), élément supporté depuis à peu près toujours par Deno.
En sortie si on lance on a… 😰
┏ ⚠️ Deno requests read access to "/home/anthony/.cache/deno/npm/registry.npmjs.org/figlet/1.9.3/fonts/Standard.flf".
┠─ Requested by `Deno.readFile()` API.
┠─ To see a stack trace for this prompt, set the DENO_TRACE_PERMISSIONS environmental variable.
┠─ Learn more at: https://docs.deno.com/go/--allow-read
┠─ Run again with --allow-read to bypass this prompt.
┗ Allow? [y/n/A] (y = yes, allow; n = no, deny; A = allow all read permissions) >
Ça marche pas… En fait si, juste Deno est bien plus sécurisé que Node ou même Bun par défaut, il n'autorise rien aux codes qu'on lance pour limiter les risques d'accès à des éléments qu'on ne veut pas qu'il ait accès. Par défaut, un programme peut importer d'autres fichiers JS/TS, et afficher dans la console. C'est à peu près tout. Pour tout le reste, il faut lui donner des droits.
Note : je passe assez vite sur le système de sécurité de Deno, il n'est pas complexe, il est même plutôt simple au regard de sa capacité à être super fin. Juste je n'ai pas envie de passer trop de temps sur le sujet dans cet article et la documentation de Deno est vraiment bien !
Donc je change la task dev pour qu'on lui donne le droit en lecture (et pas écriture donc) :
{
"tasks": {
"dev": "deno run --allow-read --watch main.ts"
},
...
}
Je coupe le programme, je relance et là :
_ _ _ _ ____ _
| | | | ___| | | ___ | _ \ ___ _ __ ___ | |
| |_| |/ _ \ | |/ _ \ | | | |/ _ \ '_ \ / _ \| |
| _ | __/ | | (_) | | |_| | __/ | | | (_) |_|
|_| |_|\___|_|_|\___/ |____/ \___|_| |_|\___/(_)
Ce coup-ci c'est bon ! 😎
Et si on compilait ?
Un truc que je trouve top avec Deno c'est qu'on peut fabriquer un binaire all-in-one avec nos applications écrites en JavaScript ou TypeScript ou WASM.
Je change le deno.json pour avoir une nouvelle tâche :
{
"tasks": {
"dev": "deno run --allow-read --watch main.ts",
"compile": "deno compile --allow-read main.ts"
},
...
}
Puis on lance la tâche deno task compile :
Task compile deno compile --allow-read main.ts
Check file:///home/anthony/Projet/deno-cli-example/hello/main.ts
Compile file:///home/anthony/Projet/deno-cli-example/hello/main.ts to hello
Embedded Files
hello
├─┬ .deno_compile_node_modules (17.81MB - 17.76MB unique)
│ └─┬ localhost (17.81MB - 17.76MB unique)
│ ├── commander/14.0.0/* (204.18KB)
│ └── figlet/1.9.3/* (17.61MB - 17.56MB unique)
└── main.ts (712B)
Files: 17.81MB
Metadata: 1.45KB
Remote modules: 70B
Et là on peut le lancer :
$ ./hello
_ _ _ _ ____ _
| | | | ___| | | ___ | _ \ ___ _ __ ___ | |
| |_| |/ _ \ | |/ _ \ | | | |/ _ \ '_ \ / _ \| |
| _ | __/ | | (_) | | |_| | __/ | | | (_) |_|
|_| |_|\___|_|_|\___/ |____/ \___|_| |_|\___/(_)
$ ./hello World!
_ _ _ _ __ __ _ _ _ _
| | | | ___| | | ___ \ \ / /__ _ __| | __| | | |
| |_| |/ _ \ | |/ _ \ \ \ /\ / / _ \| '__| |/ _` | | |
| _ | __/ | | (_) | \ V V / (_) | | | | (_| |_|_|
|_| |_|\___|_|_|\___/ \_/\_/ \___/|_| |_|\__,_(_|_)
Ce fichier hello est compilé pour ma machine (Archlinux sur une architecture x86), mais je pourrais cross compiler pour Windows ou Mac ou du ARM. Ce fichier je peux ensuite le partager pour que n'importe qui puisse l'utiliser sans avoir à recompiler ou installer Deno ou même savoir qu'il y a du TypeScript sous le capot.
Par contre, je dois être honnête sur un point : c'est lourd. Le binaire hello fait 93Mo, juste pour afficher un "Hello World!", c'est pas très optimal… Mais ça dépend pour quel usage ! Si vous avez besoin d'un petit CLI que vous allez utiliser à l'occasion, c'est pas vraiment un souci.
Et dans le même temps, l'équipe de Deno doit travailler pour réduire cette taille petit à petit 🤞
Conclusion
On pourrait encore pousser plus loin, faire plein d'autres choses : des appels http, des lectures de fichier, des modifications de fichier, créer un petit serveur http, lire des variables d'environnements, manipuler l'entrée standard, etc. On peut faire beaucoup de choses assez simplement, et en vrai je pense qu'il n'y a que notre imagination comme limite ici !
J'aime beaucoup Deno, c'est une plateforme qui cherche à être au plus proche du standard ECMAScript pour facilité le partage de code entre navigateur et serveur sans avoir à passer par une compilation qui magiquement va rendre votre code compatible. Ici on est aligné sur le standard, ce qui ne l'est pas est facilement identifiable, c'est vraiment appréciable !
Crédit photo : Générée via Mistral AI avec le prompt suivant :
A vibrant, Studio Ghibli-inspired illustration of 'Deno Land': a magical valley where the mountains resemble stacked TypeScript (.ts) files with subtle code textures, and a sparkling river flows with glowing data streams. The sky glows with soft dawn colors, and floating islands are made of deno.json configurations. In the foreground, a wooden sign reads 'Welcome to Deno Land – Zero Config, Infinite Possibilities' in elegant letters. Anthropomorphic animals—a fox in a hoodie, a raccoon with glasses, and a cat reading a book—explore and picnic near a laptop displaying a Deno terminal. Trees have leaves shaped like JavaScript keywords (function, export, await), and the grass is dotted with tiny semicolon and curly brace flowers. The scene is warm, inviting, and whimsical, with soft lighting and a painterly, detailed style.