iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
iducationducation
IndexArticlesFormationsProfilOutilsBibliothech
N°014 — 2026
Navigation
01Index02Articles03Formations04Profil05Outils06Bibliothech
N°014 — 2026
Formations
TypeScript · Intermédiaire

Extensions VS Code

Utiliser, créer et publier une extension VS Code en TypeScript : commandes, configuration, vues, webviews, diagnostics, tests, bundling et publication sur le Marketplace et Open VSX.

VS Code APITypeScriptesbuildvsceOpen VSXGitHub Actions
01Utiliser et gérer ses extensions02Générer et lancer sa première extension03Commandes, réglages et activation04Barre d'état, vues et décorations05Webviews : un panneau HTML dans l'éditeur06Diagnostics, corrections rapides et complétion07Tester et bundler l'extension08Publier sur le Marketplace et Open VSX
Chapitre 5·35 min

Webviews : un panneau HTML dans l'éditeur

Une webview est une iframe que VS Code place dans un onglet ou une vue latérale. On y met ce qu'on veut : HTML, CSS, JavaScript, un graphique, une application React entière. C'est l'outil le plus puissant de l'API, et le plus facile à mal utiliser.

Quand (ne pas) utiliser une webview

La documentation officielle le dit sans détour : une webview est lourde, elle ne respecte pas d'elle-même le thème ni l'accessibilité, et elle donne une interface qui ne ressemble pas au reste de l'éditeur. Avant d'en créer une, vérifiez qu'aucune API native ne suffit.

BesoinSolution native
Une liste ou un arbreTreeView (chapitre 4)
Un choix parmi des optionsshowQuickPick
Une saisieshowInputBox
Une information au survolHoverProvider (chapitre 6)
Un document en lecture seuleUn éditeur de texte virtuel (TextDocumentContentProvider)
Un graphique, un formulaire riche, un aperçuWebview

Pour TODO Radar, le tableau de bord des statistiques (répartition par marqueur, fichiers les plus chargés) justifie une webview.

Ouvrir un panneau

src/stats-panel.ts
import * as vscode from 'vscode'
import { FileTodos } from './workspace-scan'
 
export class StatsPanel {
  private static current: StatsPanel | undefined
  private readonly panel: vscode.WebviewPanel
 
  static show(context: vscode.ExtensionContext, files: FileTodos[]) {
    if (StatsPanel.current) {
      StatsPanel.current.panel.reveal(vscode.ViewColumn.Beside)
      StatsPanel.current.update(files)
      return
    }
    StatsPanel.current = new StatsPanel(context, files)
  }
 
  private constructor(private readonly context: vscode.ExtensionContext, files: FileTodos[]) {
    this.panel = vscode.window.createWebviewPanel(
      'todoRadar.stats',
      'TODO Radar — Statistiques',
      vscode.ViewColumn.Beside,
      {
        enableScripts: true,
        localResourceRoots: [vscode.Uri.joinPath(context.extensionUri, 'media')],
      }
    )
 
    this.panel.webview.html = this.getHtml()
    this.panel.onDidDispose(() => (StatsPanel.current = undefined), null, context.subscriptions)
    this.update(files)
  }
 
  update(files: FileTodos[]) {
    // envoyé à la webview, détaillé plus bas
  }
 
  private getHtml(): string {
    return '' // détaillé plus bas
  }
}

Le motif static current garantit un seul panneau : relancer la commande ramène l'onglet existant au lieu d'en ouvrir un deuxième.

Les options de createWebviewPanel :

  • enableScripts autorise le JavaScript dans la page, désactivé par défaut ;
  • localResourceRoots liste les seuls dossiers dont la webview peut charger des fichiers. Par défaut, c'est tout le workspace et le dossier de l'extension ; on restreint à media/ ;
  • retainContextWhenHidden (non utilisé ici) garde la page en vie quand l'onglet passe en arrière-plan. Pratique, mais gourmand en mémoire. Préférez sauvegarder l'état, voir plus bas.

Le HTML et la Content Security Policy

Une webview ne peut pas charger ./media/stats.js directement : les chemins doivent être convertis en URI spéciales via asWebviewUri. Et toute webview qui exécute du script doit déclarer une Content Security Policy stricte.

src/stats-panel.ts
private getHtml(): string {
  const webview = this.panel.webview
  const media = (file: string) =>
    webview.asWebviewUri(vscode.Uri.joinPath(this.context.extensionUri, 'media', file))
 
  const nonce = crypto.randomUUID().replace(/-/g, '')
 
  return /* html */ `<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="Content-Security-Policy"
        content="default-src 'none';
                 style-src ${webview.cspSource};
                 img-src ${webview.cspSource} https:;
                 script-src 'nonce-${nonce}';">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <link href="${media('stats.css')}" rel="stylesheet">
  <title>Statistiques</title>
</head>
<body>
  <h1>Marqueurs du projet</h1>
  <div id="summary"></div>
  <ol id="files"></ol>
  <script nonce="${nonce}" src="${media('stats.js')}"></script>
</body>
</html>`
}

La politique dit : rien n'est autorisé par défaut, les styles et images viennent uniquement de l'extension, et seuls les scripts portant le nonce généré s'exécutent. Un texte de TODO contenant <script> injecté dans la page ne s'exécutera donc pas. C'est la même défense que sur un site web, et le même raisonnement que dans l'article sur la cybersécurité pour les développeurs : ne jamais faire confiance au contenu affiché.

crypto.randomUUID() est disponible globalement dans Node.js 19 et plus, que VS Code embarque depuis longtemps.

Suivre le thème de l'utilisateur

VS Code injecte dans chaque webview des variables CSS reprenant les couleurs du thème actif, et une classe sur <body> (vscode-light, vscode-dark, vscode-high-contrast). On s'en sert au lieu de couleurs codées en dur :

media/stats.css
body {
  font-family: var(--vscode-font-family);
  font-size: var(--vscode-font-size);
  color: var(--vscode-foreground);
  background: var(--vscode-editor-background);
  padding: 0 1rem;
}
 
.bar {
  height: 0.5rem;
  background: var(--vscode-charts-yellow);
  border-radius: 2px;
}
 
a {
  color: var(--vscode-textLink-foreground);
}
 
li {
  border-bottom: 1px solid var(--vscode-panel-border);
  padding: 0.5rem 0;
}

Le nom d'une variable reprend celui de la couleur du thème : editor.background devient --vscode-editor-background. Les outils de développement de la webview (commande Developer: Open Webview Developer Tools) montrent la liste complète sur l'élément html.

Communiquer entre l'extension et la webview

La webview est isolée : elle n'a pas accès à l'API vscode. Les deux côtés échangent des messages JSON.

Côté extension, on envoie les données et on écoute les demandes :

src/stats-panel.ts
update(files: FileTodos[]) {
  const byTag: Record<string, number> = {}
  for (const f of files) for (const m of f.matches) byTag[m.tag] = (byTag[m.tag] ?? 0) + 1
 
  const topFiles = files
    .map((f) => ({ path: vscode.workspace.asRelativePath(f.uri), uri: f.uri.toString(), count: f.matches.length }))
    .sort((a, b) => b.count - a.count)
    .slice(0, 10)
 
  this.panel.webview.postMessage({ type: 'stats', byTag, topFiles })
}

Et dans le constructeur :

this.panel.webview.onDidReceiveMessage(
  async (message: { type: string; uri?: string }) => {
    if (message.type === 'open' && message.uri) {
      await vscode.window.showTextDocument(vscode.Uri.parse(message.uri), {
        viewColumn: vscode.ViewColumn.One,
      })
    }
  },
  null,
  context.subscriptions
)

Côté webview, acquireVsCodeApi() renvoie l'objet qui envoie les messages. On ne peut l'appeler qu'une fois par page.

media/stats.js
const vscode = acquireVsCodeApi()
 
const summary = document.getElementById('summary')
const list = document.getElementById('files')
 
function render({ byTag, topFiles }) {
  summary.textContent = Object.entries(byTag)
    .map(([tag, count]) => `${tag} : ${count}`)
    .join(' · ')
 
  const max = Math.max(1, ...topFiles.map((f) => f.count))
  list.replaceChildren(
    ...topFiles.map((file) => {
      const li = document.createElement('li')
      const link = document.createElement('a')
      link.href = '#'
      link.textContent = `${file.path} (${file.count})`
      link.addEventListener('click', (e) => {
        e.preventDefault()
        vscode.postMessage({ type: 'open', uri: file.uri })
      })
      const bar = document.createElement('div')
      bar.className = 'bar'
      bar.style.width = `${(file.count / max) * 100}%`
      li.append(link, bar)
      return li
    })
  )
}
 
window.addEventListener('message', (event) => {
  if (event.data.type === 'stats') {
    render(event.data)
    vscode.setState(event.data)
  }
})
 
// Restaurer l'affichage si l'onglet a été masqué puis réaffiché
const previous = vscode.getState()
if (previous) render(previous)

On construit le DOM avec textContent plutôt qu'innerHTML : le texte des fichiers et des TODO ne sera jamais interprété comme du HTML. La CSP est la deuxième ligne de défense, pas la première.

setState et getState remplacent retainContextWhenHidden à moindre coût : quand l'onglet revient au premier plan, la page est recréée mais retrouve son dernier état instantanément.

Enregistrer la commande

src/extension.ts
context.subscriptions.push(
  vscode.commands.registerCommand('todo-radar.showStats', async () => {
    const files = await scanWorkspace()
    StatsPanel.show(context, files)
  })
)
package.json
{
  "command": "todo-radar.showStats",
  "title": "Afficher les statistiques",
  "category": "TODO Radar",
  "icon": "$(graph)"
}

On ajoute aussi cette commande dans view/title, à côté du bouton de rafraîchissement.

Aller plus loin avec un framework

Pour une interface riche, on écrit la webview avec React, Vue ou Svelte, bundlée séparément par esbuild ou Vite dans media/. Le principe reste le même : un fichier JS chargé avec un nonce, et des messages pour parler à l'extension. Si vous hésitez entre les deux premiers, la comparaison React vs Vue aide à trancher.

Pour les composants d'interface, la bibliothèque @vscode/webview-ui-toolkit a été archivée par Microsoft en 2025. Les projets actuels s'appuient sur les variables CSS ci-dessus, ou sur @vscode-elements/elements, une bibliothèque de Web Components maintenue par la communauté qui reproduit les contrôles natifs.

Une webview peut aussi vivre dans la barre latérale, sous la vue des marqueurs : on déclare la vue avec "type": "webview" dans contributes.views et on implémente un WebviewViewProvider au lieu de créer un panneau.


L'extension sait afficher. Le chapitre suivant la fait parler le langage de l'éditeur : diagnostics soulignés en jaune, corrections rapides, complétion et survol.

Précédent
Barre d'état, vues et décorations
Suivant
Diagnostics, corrections rapides et complétion

Développeur fullstack passionné. J'apprends en construisant et je documente tout — front, back, outils. Le code s'apprend mieux en public.

Naviguer

IndexTous les articlesFormationsProfilOutilsBibliothech

Ailleurs

GitHub RSS

Newsletter

Les articles, libs et découvertes. Une fois par semaine, pas plus.

© 2026 William LoreeConçu & codé à la main