If you’re reading this, you’re probably curious about creating your first Visual Studio Code (VS Code) extension. That was me not too long ago—comfortable with JavaScript, totally unsure how VS Code extensions actually run. It’s rewarding work, but the learning curve is full of little “gotchas” you won’t notice until something breaks.

This guide covers what I wish I’d known on day one: not just the happy path (scaffold → run → package), but the real-world bits like testing, packaging, talking to an external gRPC service, and why require(‘vscode’) sometimes explodes. 

Sidebar: What is grpc?

gRPC is a framework that lets different programs talk to each other over a network by calling functions as if they were local, even if they’re written in different languages. It uses Protocol Buffers (protobuf) to define the data and services, making communication fast and efficient.

1) The Mental Model: Where Your Code Actually Runs

A VS Code extension is not just a Node script. It runs inside a sandboxed Extension Host process alongside VS Code. The extension Host communicates with the main process using Remote Procedure Calls (R.P.C.) to manage tasks and share data between the two processes.

That has two important consequences:

  • VS Code APIs are only available inside the Extension Host. If you try to require('vscode') in a plain Node test, it fails. Use the VS Code test runner to load that environment.

  • Webviews are browser sandboxes. Inside a webview, there’s no Node require.
    I
    t’s just browser javascript. It communicates with your extension via postMessage and the use of the acquireVsCodeApi() bridge.

Keep this model in mind and a lot of weird errors make sense.  

2) Project Anatomy

You’ll typically have:

  • package.json: describes your extension, activation events, contributions, and entry point.

  • extension.js: the activation code: register commands, creates Webviews, talks to the VS Code API.

  • Support modules: e.g. grpcClient.js to talk to an external gRPC server.

  • Tests: either VS Code–hosted tests or plain Node/Mocha tests (more on this later).

Key tip: keep “VS Code world” (webviews, commands, context, workspace) separate from “external world” (gRPC, file I/O, business logic). It makes testing and maintenance much easier.

 

3) Scaffolding (Start Simple)

The quickest way to get started is with Yo Code

npm install -g yo generator-code
yo code

This gives you package.json, a minimal extension.js, and a debug config. Press F5 in VS Code to launch an Extension Development Host window and try the “Hello World” command.

Lesson learned: don’t over-engineer early. Prove the activation/command flow works first, then layer in real logic.

 

4) Webviews Without Tears

Webviews are browser islands. That means:

  • No Node APIs (no require, no fs, no path).

  • Use const vscode = acquireVsCodeApi() and vscode.postMessage(...) to send messages to the extension.

  • For dynamic HTML updates (e.g. rendering options user can select after they’re authenticated), attach event listeners after injecting HTML or use event delegation:

// Inside your webview script
document.getElementById('container').addEventListener('click', (e) => {
    if (e.target.matches('#save-options')) {
        // handle click
    }
});

 State persistence: use webview.setState() / getState() to remember UI state between close/reopen. On the extension side, repost the latest data on resolveWebviewView so the webview redraws properly.

 

5) Packaging (and Getting the Version You Want)

To build a .vsix you can install or share: 

npm install -g @vscode/vsce
vsce package

The .vsix filename is derived from your version in package.json. For example if you want to set your version to 3.5.0

{
    "name": "sample-vscode-client",
    "version": "3.5.0",
    "main": "src/main/js/extension.js"
}

Running vsce package packages the extension and creates your_extension_name-3.5.0.vsix.

 

6) Testing, The Trickiest Part: What to Run Where

This was the biggest rabbit hole. I think it’s worthwhile to first think through which parts of your extension you will be testing. If you are not testing any of the VS Code api, then building the tests are a lot easier.

You’ll likely need two styles of tests:

  1. VS Code–hosted tests (when you need vscode APIs)
    • Use @vscode/test-electron to spin up the Extension Host.
    • Your package.json script might look like:
"scripts": {
    "test": "node ./src/test/runTest.js"
}
    • runTest.js calls VS Code’s test runner, which loads your test files inside the Extension Host.

 2. Plain Node/Mocha tests (no VS Code API needed)

  • Perfect for pure modules like testing support modules (grpcClient.js).

Example: 

"scripts": {
    "test": "mocha 'src/test/**/*.test.js'"
}

Pro tip: start with plain Mocha tests for your non-VS Code logic. Only use the VS Code runner when you truly need extension APIs.

 

7) Git Hygiene & Project Size (what to .gitignore)

VS Code’s test runner stashes downloads in .vscode-test/ (hundreds of MB). Ignore it: 

.vscode-test/

Also consider ignoring build artifacts like *.vsix, dist/, etc., unless you intend to keep them in version control. 

Final Advice

  • Keep VS Code API code thin. Put heavy logic in plain modules you can test with Mocha.

  • Test at two levels (if needed). Plain Node test and VSCode hosted tests, if needed.

  • Expect async headaches. Use retry loops and generous timeouts where appropriate.

  • Ship small wins. Get a command working before introducing webviews and external services.

Building a VS Code extension isn’t “just another Node project.” It’s editor APIs, sandboxed processes, webviews acting like mini browsers, and sometimes external dependencies. Once you learn the rhythms, though, you gain a superpower: you can bend your editor to your workflow. Start small, iterate. Happy coding!