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 7·30 min

Tester et bundler l'extension

Une extension se teste à deux niveaux. La logique pure (le scanner) se teste comme n'importe quel module Node.js, en quelques millisecondes. L'intégration avec VS Code (commandes, diagnostics) demande de lancer un vrai VS Code, piloté par le framework de test officiel. Puis on réduit le paquet à un seul fichier JavaScript.

Tests unitaires : la logique sans VS Code

C'est le bénéfice d'avoir isolé scanner.ts au chapitre 2 : il n'importe pas vscode, donc il se teste avec le test runner intégré à Node.js, sans rien installer.

src/test/unit/scanner.test.ts
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { scanText, hasTicket } from '../../scanner'
 
test('détecte les trois marqueurs par défaut', () => {
  const source = [
    'const a = 1 // TODO: renommer',
    '// FIXME fuite mémoire',
    '/* HACK contournement IE */',
    'const todoList = []',
  ].join('\n')
 
  const matches = scanText(source)
  assert.deepEqual(matches.map((m) => m.tag), ['TODO', 'FIXME', 'HACK'])
  assert.equal(matches[0].text, 'renommer')
  assert.equal(matches[1].line, 1)
})
 
test('ignore les mots qui contiennent le marqueur', () => {
  assert.equal(scanText('const todoList = []; const TODOS = 3').length, 0)
})
 
test('reconnaît les références de ticket', () => {
  const [withHash, withJira, without] = scanText(
    '// TODO #142 pagination\n// TODO PROJ-87 cache\n// TODO nettoyer'
  )
  assert.equal(hasTicket(withHash), true)
  assert.equal(hasTicket(withJira), true)
  assert.equal(hasTicket(without), false)
})
package.json
"scripts": {
  "test:unit": "node --import tsx --test src/test/unit/*.test.ts"
}

Il faut tsx en dépendance de développement pour exécuter le TypeScript directement (npm install -D tsx). Ces tests tournent en une fraction de seconde : on les lance à chaque enregistrement.

Tests d'intégration dans un vrai VS Code

Pour vérifier qu'une commande existe, que les diagnostics apparaissent ou qu'un réglage est pris en compte, il faut l'API vscode. Elle n'existe qu'à l'intérieur de VS Code. @vscode/test-cli télécharge une instance de VS Code, y charge l'extension et exécute les tests Mocha dedans.

Le générateur a déjà installé @vscode/test-cli et @vscode/test-electron. La configuration :

.vscode-test.mjs
import { defineConfig } from '@vscode/test-cli'
 
export default defineConfig({
  files: 'out/test/integration/**/*.test.js',
  workspaceFolder: './test-fixtures',
  mocha: {
    ui: 'tdd',
    timeout: 20000,
  },
})

workspaceFolder ouvre un dossier de test qui contient des fichiers avec des marqueurs connus :

test-fixtures/sample.ts
// TODO #12 : avec ticket
// TODO : sans ticket
export const value = 42 // FIXME valeur en dur

Et un réglage qui active la règle des tickets pour ce dossier :

test-fixtures/.vscode/settings.json
{
  "todoRadar.requireTicket": true
}

Les tests :

src/test/integration/extension.test.ts
import * as assert from 'assert'
import * as vscode from 'vscode'
 
const EXTENSION_ID = 'iducation.todo-radar'
 
suite('TODO Radar', () => {
  suiteSetup(async () => {
    await vscode.extensions.getExtension(EXTENSION_ID)!.activate()
  })
 
  test('les commandes sont enregistrées', async () => {
    const commands = await vscode.commands.getCommands(true)
    for (const id of ['todo-radar.countInFile', 'todo-radar.refresh', 'todo-radar.showStats']) {
      assert.ok(commands.includes(id), `commande manquante : ${id}`)
    }
  })
 
  test('signale le TODO sans ticket', async () => {
    const [uri] = await vscode.workspace.findFiles('sample.ts')
    await vscode.window.showTextDocument(uri)
 
    // Laisser le temps au debounce et au calcul
    await new Promise((resolve) => setTimeout(resolve, 600))
 
    const diagnostics = vscode.languages
      .getDiagnostics(uri)
      .filter((d) => d.source === 'TODO Radar')
 
    assert.equal(diagnostics.length, 1)
    assert.equal(diagnostics[0].range.start.line, 1)
  })
})

EXTENSION_ID est publisher.name. On fixera le publisher au chapitre 8 ; tant qu'il manque, VS Code utilise undefined_publisher.

Les tests d'intégration sont compilés par tsc dans out/, séparément du bundle :

package.json
"scripts": {
  "compile-tests": "tsc -p . --outDir out",
  "pretest": "npm run compile-tests && npm run compile",
  "test": "vscode-test"
}

npm test télécharge VS Code au premier lancement (dans .vscode-test/, à ajouter au .gitignore), puis exécute la suite. Pour lancer les tests depuis l'éditeur avec points d'arrêt, installez l'extension Extension Test Runner : les tests apparaissent dans le panneau Testing.

Sur un serveur Linux sans écran, VS Code a besoin d'un affichage virtuel : préfixez la commande par xvfb-run -a. On s'en sert dans la CI du chapitre suivant.

Pourquoi bundler

Sans bundler, le paquet publié contient out/ et tout node_modules de production. VS Code doit charger des centaines de petits fichiers à l'activation. Avec esbuild, tout tient dans un dist/extension.js minifié.

L'écart est concret. Sur une extension qui dépend d'une seule librairie moyenne, on passe couramment de plusieurs milliers de fichiers à quelques-uns, et d'un temps d'activation de plusieurs centaines de millisecondes à quelques dizaines. Developer: Show Running Extensions permet de mesurer l'activation avant et après.

La configuration esbuild

Le générateur a créé esbuild.js. Version simplifiée pour comprendre chaque option :

esbuild.js
const esbuild = require('esbuild')
 
const production = process.argv.includes('--production')
const watch = process.argv.includes('--watch')
 
async function main() {
  const ctx = await esbuild.context({
    entryPoints: ['src/extension.ts'],
    bundle: true,
    format: 'cjs',
    platform: 'node',
    outfile: 'dist/extension.js',
    external: ['vscode'],
    minify: production,
    sourcemap: !production,
    sourcesContent: false,
    logLevel: 'info',
  })
 
  if (watch) {
    await ctx.watch()
  } else {
    await ctx.rebuild()
    await ctx.dispose()
  }
}
 
main().catch((error) => {
  console.error(error)
  process.exit(1)
})

Les trois options à ne pas toucher :

  • external: ['vscode'] : le module vscode n'existe pas sur npm, il est fourni par l'éditeur à l'exécution. Le bundler doit le laisser tel quel ;
  • format: 'cjs' : VS Code charge les extensions comme des modules CommonJS ;
  • platform: 'node' : les modules Node (path, crypto) restent des imports natifs.

esbuild ne vérifie pas les types, il les retire. C'est pour ça que le script package lance tsc --noEmit avant : sans lui, une erreur de type passerait jusqu'en production.

Pour une web extension (vscode.dev), on ajoute un second build avec platform: 'browser', format: 'cjs' et une sortie déclarée dans le champ browser du manifeste. Le code ne peut alors utiliser ni fs ni child_process : c'est une raison de plus d'avoir utilisé vscode.workspace.fs au chapitre 4.

Ce qui part dans le paquet

.vscodeignore liste ce qu'il ne faut pas publier. Avec un bundle, presque tout s'exclut :

.vscodeignore
.vscode/**
.vscode-test/**
src/**
out/**
test-fixtures/**
node_modules/**
esbuild.js
tsconfig.json
.vscode-test.mjs
**/*.map
**/*.ts
.gitignore

node_modules/** peut être exclu entièrement parce que tout a été intégré au bundle. Attention à l'exception : une dépendance avec un binaire natif (.node) ne se bundle pas. Elle doit rester dans node_modules et être réincluse avec une ligne !node_modules/ma-dependance/**.

Pour vérifier le contenu exact avant publication :

npx @vscode/vsce ls

La commande liste chaque fichier qui partira dans le paquet. Si vous y voyez src/ ou des fichiers .map, .vscodeignore est incomplet.


Testée, bundlée, légère : l'extension est prête à sortir de votre machine. Le dernier chapitre la publie sur le Marketplace de Microsoft et sur Open VSX, puis automatise les publications suivantes.

Précédent
Diagnostics, corrections rapides et complétion
Suivant
Publier sur le Marketplace et Open VSX

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