Prérequis
Avant de commencer ce tutoriel, assurez-vous d'avoir :
- Bun 1.2+ installé (
curl -fsSL https://bun.sh/install | bash) - Une bonne maîtrise de TypeScript (interfaces, génériques, async/await)
- Une familiarité avec React (hooks, état, effets)
- Un éditeur de code — VS Code avec l'extension Bun est recommandé
- macOS 13+, Windows 10+, ou Ubuntu 22.04+
Ce que vous allez construire
Vous créerez NoteFlow — une application de prise de notes Markdown minimaliste pour bureau qui :
- Crée, édite et sauvegarde des notes sur le système de fichiers local
- Liste les notes dans une barre latérale avec changement en un clic
- Utilise un RPC typé pour que l'interface et le backend partagent une seule définition de type
- Se compile en moins de 20 Mo — contre plus de 200 Mo pour une application Electron équivalente
Pourquoi Electrobun ? La justification technique
Electron a révolutionné le développement d'applications bureau, mais il embarque un moteur Chromium complet avec chaque application. Une application "Hello World" Electron fait environ 140 Mo. Tauri a résolu le problème de taille en écrivant le backend en Rust — mais vous avez maintenant besoin de deux langages.
Electrobun adopte une approche différente :
| Fonctionnalité | Electron | Tauri | Electrobun |
|---|---|---|---|
| Runtime | Node.js + Chromium | Rust + WebView système | Bun + WebView système |
| Taille minimale | 140 Mo | 3 Mo | 12 Mo |
| Langage backend | JavaScript / TypeScript | Rust | TypeScript |
| Style IPC | Chaînes d'événements | Commandes Rust | RPC async typé |
| Mises à jour différentielles | Manuelles | Intégrées | Intégrées (patches de 4 Ko) |
| Courbe d'apprentissage | Faible | Élevée | Faible |
Étape 1 : Installer Bun
Electrobun nécessite Bun comme runtime. Si vous ne l'avez pas encore installé :
# macOS / Linux
curl -fsSL https://bun.sh/install | bash
# Windows (PowerShell)
powershell -c "irm bun.sh/install.ps1|iex"
# Vérification
bun --version # doit afficher 1.2.x ou supérieurÉtape 2 : Créer la structure du projet
Electrobun est livré avec un générateur de projet qui configure la bonne arborescence de dossiers :
bunx electrobun initRépondez aux questions :
- Nom du projet :
noteflow - Template :
react - Gestionnaire de paquets :
bun
Entrez ensuite dans le répertoire et installez les dépendances :
cd noteflow
bun installLa structure du projet ressemble à ceci :
noteflow/
├── electrobun.config.ts # Identité de l'app + cibles de build + canal de mise à jour
├── package.json
├── bun.lockb
├── src/
│ ├── main/ # Processus principal — tourne dans Bun, accès complet au système
│ │ └── index.ts
│ ├── shared/ # Types partagés entre le processus principal et la WebView
│ └── views/ # Interface WebView — un dossier par vue
│ └── mainview/
│ ├── index.html
│ ├── index.tsx
│ └── style.css
└── public/ # Assets statiques copiés dans le bundle
Étape 3 : Comprendre l'architecture à deux processus
Electrobun divise votre application en deux processus isolés :
Processus principal (src/main/) tourne dans Bun :
- Crée et gère les fenêtres natives (
BrowserWindow) - Lit et écrit dans le système de fichiers via les API natives de Bun
- Construit les menus natifs, la barre système, les boîtes de dialogue
- Exécute toutes les opérations privilégiées que la WebView ne peut pas faire
Processus WebView (src/views/) affiche votre interface en utilisant le moteur de navigateur natif du système :
- macOS : WebKit (le même moteur que Safari)
- Windows : Edge WebView2 (basé sur Chromium, fourni avec Windows 10+)
- Linux : WebKitGTK
Les deux processus communiquent via RPC typé — la fonctionnalité principale d'Electrobun.
Étape 4 : Définir le contrat RPC partagé
Créez src/shared/rpc.ts :
import { defineRpc } from "electrobun/bun";
// Appels de la WebView vers le processus principal
export const mainRpc = defineRpc({
listNotes: async (): Promise<string[]> => [],
readNote: async (filename: string): Promise<string> => "",
writeNote: async (filename: string, content: string): Promise<void> => {},
deleteNote: async (filename: string): Promise<void> => {},
openFilePicker: async (): Promise<string | null> => null,
});
// Appels du processus principal vers la WebView (événements push)
export const webviewRpc = defineRpc({
onNoteChanged: async (filename: string): Promise<void> => {},
});defineRpc inspecte les signatures de fonctions au moment du build. Quand la WebView appelle mainRpc.call.readNote("ma-note.md"), TypeScript applique exactement les types des paramètres et de la valeur de retour.
Étape 5 : Implémenter le processus principal
Remplacez src/main/index.ts par l'implémentation complète du backend :
import { BrowserWindow, app, dialog } from "electrobun/bun";
import { mainRpc } from "../shared/rpc";
import { join } from "path";
import { homedir } from "os";
import { mkdirSync } from "fs";
const NOTES_DIR = join(homedir(), ".noteflow", "notes");
mkdirSync(NOTES_DIR, { recursive: true });
// Enregistrer les handlers RPC avant l'ouverture de la fenêtre
mainRpc.handle({
async listNotes() {
const glob = new Bun.Glob("*.md");
const files: string[] = [];
for await (const file of glob.scan(NOTES_DIR)) {
files.push(file);
}
return files.sort();
},
async readNote(filename) {
const path = join(NOTES_DIR, filename);
const file = Bun.file(path);
if (!(await file.exists())) return "";
return file.text();
},
async writeNote(filename, content) {
await Bun.write(join(NOTES_DIR, filename), content);
},
async deleteNote(filename) {
await Bun.file(join(NOTES_DIR, filename)).delete();
},
async openFilePicker() {
const result = await dialog.showOpenDialog({
filters: [{ name: "Markdown", extensions: ["md", "txt"] }],
properties: ["openFile"],
});
return result.canceled ? null : result.filePaths[0];
},
});
app.on("ready", () => {
const win = new BrowserWindow({
title: "NoteFlow",
url: "views://mainview/index.html",
width: 1200,
height: 800,
minWidth: 800,
minHeight: 600,
styleMask: ["titled", "closable", "miniaturizable", "resizable"],
});
win.show();
});
app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
app.quit();
}
});Deux points importants :
- Les handlers RPC sont enregistrés avant
app.on("ready"). La WebView peut les appeler dès son chargement. dialog.showOpenDialogest un sélecteur de fichiers natif du système d'exploitation.
Étape 6 : Construire l'interface React
Remplacez src/views/mainview/index.tsx :
import React, { useState, useEffect, useCallback, useRef } from "react";
import { createRoot } from "react-dom/client";
import { mainRpc } from "../../shared/rpc";
type Note = { filename: string; title: string };
function noteTitle(filename: string) {
return filename.replace(/\.md$/, "").replace(/-/g, " ");
}
function App() {
const [notes, setNotes] = useState<Note[]>([]);
const [active, setActive] = useState<string | null>(null);
const [content, setContent] = useState("");
const [dirty, setDirty] = useState(false);
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
refresh();
}, []);
async function refresh() {
const files = await mainRpc.call.listNotes();
const noteList = files.map((f) => ({ filename: f, title: noteTitle(f) }));
setNotes(noteList);
if (noteList.length > 0 && active === null) {
await openNote(noteList[0].filename);
}
}
async function openNote(filename: string) {
if (dirty && active) await save(active, content);
const text = await mainRpc.call.readNote(filename);
setActive(filename);
setContent(text);
setDirty(false);
}
function handleChange(value: string) {
setContent(value);
setDirty(true);
if (saveTimer.current) clearTimeout(saveTimer.current);
saveTimer.current = setTimeout(() => {
if (active) save(active, value);
}, 1500);
}
const save = useCallback(async (filename: string, text: string) => {
await mainRpc.call.writeNote(filename, text);
setDirty(false);
}, []);
async function newNote() {
const timestamp = new Date().toISOString().split("T")[0];
const filename = `note-${timestamp}-${Date.now()}.md`;
await mainRpc.call.writeNote(filename, `# Nouvelle note\n\n`);
await refresh();
await openNote(filename);
}
async function deleteActive() {
if (!active || !confirm(`Supprimer "${noteTitle(active)}" ?`)) return;
await mainRpc.call.deleteNote(active);
setActive(null);
setContent("");
setDirty(false);
await refresh();
}
return (
<div className="app">
<aside className="sidebar">
<div className="sidebar-header">
<span className="brand">NoteFlow</span>
<button className="icon-btn" onClick={newNote} title="Nouvelle note">+</button>
</div>
<ul className="note-list">
{notes.map((n) => (
<li
key={n.filename}
className={n.filename === active ? "active" : ""}
onClick={() => openNote(n.filename)}
>
{n.title}
</li>
))}
</ul>
</aside>
<main className="editor-area">
{active ? (
<>
<div className="toolbar">
<span className="status">{dirty ? "Non sauvegardé…" : "Sauvegardé"}</span>
<button onClick={() => active && save(active, content)}>Sauvegarder</button>
<button className="danger" onClick={deleteActive}>Supprimer</button>
</div>
<textarea
className="editor"
value={content}
onChange={(e) => handleChange(e.target.value)}
onKeyDown={(e) => {
if ((e.ctrlKey || e.metaKey) && e.key === "s") {
e.preventDefault();
if (active) save(active, content);
}
}}
spellCheck={false}
/>
</>
) : (
<div className="empty-state">
<p>Cliquez sur + pour créer votre première note.</p>
</div>
)}
</main>
</div>
);
}
const el = document.getElementById("app");
if (el) createRoot(el).render(<App />);Patterns clés :
- Sauvegarde automatique après 1,5 seconde d'inactivité
Cmd+S/Ctrl+Sdéclenche une sauvegarde immédiate- Changer de note sauvegarde automatiquement la précédente avant le chargement
Étape 7 : Ajouter les styles CSS
Remplacez src/views/mainview/style.css par les styles de la version anglaise (les styles CSS sont identiques pour toutes les langues).
Étape 8 : Lancer en mode développement
Démarrez le serveur de développement :
bun startElectrobun lance le processus principal dans Bun et ouvre une fenêtre native affichant votre WebView. Les modifications des sources WebView rechargent automatiquement l'interface. Les changements du processus principal nécessitent un redémarrage.
Étape 9 : Ajouter un menu natif
Ajoutez un menu d'application macOS/Windows approprié dans src/main/index.ts :
import { Menu, MenuItem } from "electrobun/bun";
function buildMenu(win: BrowserWindow) {
const menu = new Menu();
const fileMenu = new MenuItem({
label: "Fichier",
submenu: [
new MenuItem({
label: "Nouvelle note",
accelerator: "CmdOrCtrl+N",
click: () => win.webContents.executeScript("window.__newNote?.()"),
}),
new MenuItem({ type: "separator" }),
new MenuItem({ role: "quit" }),
],
});
const editMenu = new MenuItem({
label: "Édition",
submenu: [
new MenuItem({ role: "undo" }),
new MenuItem({ role: "redo" }),
new MenuItem({ type: "separator" }),
new MenuItem({ role: "cut" }),
new MenuItem({ role: "copy" }),
new MenuItem({ role: "paste" }),
new MenuItem({ role: "selectAll" }),
],
});
menu.append(fileMenu);
menu.append(editMenu);
Menu.setApplicationMenu(menu);
}Étape 10 : Compiler pour la production
Quand votre application est prête à être distribuée :
bunx electrobun build --env=stableEmplacements de sortie :
- macOS :
dist/NoteFlow.app - Windows :
dist/NoteFlow-win-x64.exe - Linux :
dist/NoteFlow-linux-x64.AppImage
Le bundle macOS fera environ 14–64 Mo selon les dépendances — sans Chromium, sans Node.js, sans runtime Electron.
Étape 11 : Activer les mises à jour différentielles
Le système de mise à jour d'Electrobun génère des diffs binaires entre les versions. Une mise à jour typique ne dépasse pas 4–14 Ko au lieu d'une réinstallation complète.
Configurez electrobun.config.ts :
import { defineConfig } from "electrobun/config";
export default defineConfig({
app: {
name: "NoteFlow",
version: "1.0.0",
identifier: "tn.noqta.noteflow",
},
updates: {
provider: "github",
owner: "your-org",
repo: "noteflow",
channel: "stable",
},
build: {
targets: ["mac-arm64", "mac-x64", "win-x64", "linux-x64"],
},
});Vérifiez les mises à jour depuis le processus principal :
import { autoUpdater } from "electrobun/bun";
autoUpdater.on("update-downloaded", () => {
autoUpdater.quitAndInstall();
});
await autoUpdater.checkForUpdates();Tester votre implémentation
Après bun start, vérifiez ces points :
- La fenêtre s'ouvre avec le titre correct NoteFlow
- Cliquer sur + crée un nouveau fichier
.mddans~/.noteflow/notes/ - Taper dans l'éditeur sauvegarde automatiquement après 1,5 seconde
Cmd+S/Ctrl+Sdéclenche une sauvegarde immédiate- Changer de note sauvegarde automatiquement la précédente
- Après
bunx electrobun build, le bundle fait moins de 100 Mo
Résolution des problèmes
Fenêtre blanche au lancement
Vérifiez que src/views/mainview/index.html contient <div id="app"></div> et que index.tsx importe style.css. Lancez bun run build:webview séparément pour détecter les erreurs de bundle.
L'appel RPC reste en attente indéfiniment
Assurez-vous que mainRpc.handle(...) est appelé avant app.on("ready"). Si le handler est enregistré après l'ouverture de la fenêtre, le premier appel RPC de la WebView peut arriver avant que le handler existe.
Linux : WebView vide sur Ubuntu 22.04
Installez la dépendance WebKit : sudo apt install webkit2gtk-4.1. Sur Ubuntu 24.04+, cela est installé par défaut.
Le bundle de production est plus grand que prévu
Utilisez le flag --external dans electrobun.config.ts pour marquer les gros packages comme externes et ne bundler que ce dont vous avez besoin. Analysez avec bunx electrobun analyze.
Prochaines étapes
Une fois NoteFlow fonctionnel, voici des extensions naturelles à explorer :
- Aperçu Markdown — ajoutez un volet divisé avec la bibliothèque
markedpour le rendu HTML en temps réel - Barre système — utilisez l'API tray d'Electrobun pour garder NoteFlow accessible sans fenêtre
- Fenêtres multiples — créez un second
BrowserWindowpour un panneau de préférences - Synchronisation iCloud — pointez
NOTES_DIRvers un dossier iCloud Drive sur macOS pour une synchronisation transparente - Pipeline de release GitHub Actions — automatisez le build et la notarisation à chaque push sur
main
Conclusion
Vous avez construit une application bureau entièrement fonctionnelle en TypeScript pur — sans Rust, sans moteur de navigateur embarqué, et avec un bundle inférieur à 20 Mo. Le RPC typé d'Electrobun relie proprement les processus principal et WebView, et le système de mises à jour différentielles signifie que vos utilisateurs ne téléchargent jamais une réinstallation complète.
Electrobun est encore en train de mûrir, mais la v1 est prête pour la production pour les outils internes, les utilitaires de développement et les applications métier. Si vous êtes à l'aise avec TypeScript et souhaitez distribuer des logiciels de bureau natifs sans apprendre Rust, Electrobun est le chemin le plus ergonomique disponible en 2026.