Quokka Clone In 10 Minutes - VSCode Extention Tutorial
In this tutorial, we'll learn how to create a VSCode extension that will communicate with Node Inspector to collect execution data - basically a Quokka.js clone.
For those who don't know what Quokka.js is - it's a live scratchpad for Javascript/Typescript that allows you to see the results of code execution right in your editor.

The extension that we'll build will be called Wombat.js because wombats are cool and they poop cubes.

I'll be using Typescript because I like to have type annotations and smart code completion when I work with unfamiliar modules/libs.
Bootstrap New VSCode Extention
We'll start by creating our VSCode extension first.
VSCode provides a Yeoman generator to bootstrap new extensions.
-
Make sure you have Yeoman installed globally.
npm i -g yeoman -
Run vscode extention generator:
yo code .It will ask you a bunch of questions about your project. Answer them and it will create the file structure.
Here I will assume that you'll also choose Typescript as the preferred language.
-
Open the project in VSCode:
code <project name>Here you need to put your actual project name instead of
<project name>.Here I assume that you have a console command installed that allows you to launch VSCode from your terminal. If not - do this:
- Launch VSCode regular way.
- Open the Command Palette (
⇧⌘Pon Mac) and typeshell commandto find the Shell Command: Install 'code' command in PATH command.
-
Change command title in
package.json. It is located incontributes.commandsblock."contributes": { "commands": [ { "command": "extension.<your extension name>", "title": "Run wombat" } ] },
Get Data From Node Inspector
Since version 6.3 node provides a built-in inspector which API we are gonna use to get runtime information about our code.
-
Open the file
src/extention.tsand add the following imports:import * as path from "path"; import * as util from "util"; import * as inspector from "inspector"; -
Make the activation function asynchronous we'll need this to use promises and
async/awaitinstead of callback API thatinspectorprovides by default.export async function activate(context: vscode.ExtensionContext) { // ... -
Launch inspector:
export async function activate(context: vscode.ExtensionContext) { inspector.open(); // ...This is an equivalent of running
node --inspector somefile.js.Read more here
-
Start new client session. Add these lines after you activate the
inspector.const session = new inspector.Session(); session.connect(); -
Wrap
session.postintopromisify.const post = <any>util.promisify(session.post).bind(session);Unfortunately, we'll have to use type
anyhere. Usually, I avoid usinganyas much as I can, but here it will use the wrong type because of howsession.postfunction is typed.This is because
session.postis overloaded and has different arity for different actions it calls and the fact that by default typescript will pick the last function definition it finds.In the case with
post- it would be:post(method: "HeapProfiler.stopSampling", callback?: (err: Error | null, params: HeapProfiler.StopSamplingReturnType) => void): void;As you can see this type allows only
HeapProfiler.stopSamplingasmethod, and we want to use just regularstringtype.I didn't want to deal with complex type annotations in this tutorial. Alternatively, you could create your custom
promisifyfunction that will overcome that limitation. -
Make the
registedCommandcallback - asynchronous.To do this search for
vscode.commands.registerCommandand addasyncbefore callback definition.let disposable = vscode.commands.registerCommand( "extension.wombat", async () => { // ... -
Our app will need to get the text from the currently open file, so get the
activeTextEditorinstance.const activeEditor = vscode!.window!.activeTextEditor; if (!activeEditor) { return; }We instantly stop the execution if we can't get the editor instance.
Here is the second time I cut the corners with Typescript here.
See those
!.- it tells Typescript that there definitely, 100% certainly, I swear by god is a be a value in the previous field.It is called non-null assertion operator. It's important that it is not the same as
?.that is optional chaining operator (that might be familiar to Ruby, C# and probably other language users) and is not currently available neither in Typescript nor in Javascript. -
Get
documentandfileNamefor later use:const document = activeEditor!.document; const fileName = path.basename(document.uri.toString()) -
Compile the script from the editor:
const { scriptId } = await post("Runtime.compileScript", { expression: document.getText(), sourceURL: fileName, persistScript: true });Here we use
document.getText()to obtain the source code. We getscriptIdthat we will need in the next step. -
Run the script:
await post("Runtime.runScript", { scriptId }); -
Get all the variables in global namespace:
const data = await post("Runtime.globalLexicalScopeNames", { executionContextId: 1 });Here I've hardcoded the
executionContextId. Alternatively, you could get it by subscribing toRuntime.executionContextCreatedevent.It will return an array with a list of
var,letorconstdefinitions available in the global namespace.In the next steps, we'll get their values.
Of course, this is not how Quokka.js does that, but for this tutorial, it's just enough.
-
Map through the variable names and get their values:
data.names.map(async (expression: string) => { const { result: { value } } = await post("Runtime.evaluate", { expression, contextId: 1 }); })We do it by executing variable names as expressions in the same context we run our script.
-
Get variable locations. Add this to
mapfunction.const { result } = await post("Debugger.searchInContent", { scriptId, query: expression });
Display Info In UI
Now we need to display this information somehow.
I wanted to find out how did Quokka.js do it.
It was surprisingly cumbersome to find the API that allows you to display info on top of the text in VSCode because it wasn't mentioned in the API section among the capabilities. So I had to jump directly to the references section.
It was mentioned in the API docs as decorators, which I find a bit non-intuitive name. I was looking for something like "overlay API" - of course with zero success.
Back to the tutorial:
-
Define the
addDecorationWithTextfunction:const addDecorationWithText = ( contentText: string, line: number, column: number, activeEditor: vscode.TextEditor ) => { const decorationType = vscode.window.createTextEditorDecorationType({ after: { contentText, margin: "20px" } }); const range = new vscode.Range( new vscode.Position(line, column), new vscode.Position(line, column) ); activeEditor.setDecorations(decorationType, [{ range }]); };Due to how those decorations work we need to create a separate
decorationTypefor each case because we want to have differentcontextTextvalues.Then we define range - in our case, it's just one line so start and end sections of this range are the same.
And finally, we apply created a decoration to the
activeEditor. -
Time to use
addDecorationWithText. Call this function inside of the namesmapwe've defined earlier:addDecorationWithText( `${value}`, result[0].lineNumber, result[0].lineContent.length, activeEditor );Add it at the end of that function.
Launch The Extention
Time to check how our extension works.
-
Press
F5orDebug -> Start Debugging.It will launch a new VSCode window in debug mode.
-
Open some simple Javascript file:
const x = 10; let y = x; let z = 2 + 2; let foo = 'bar'; const test = 42;This is the file that I've used.
-
Run the
wombatcommand.Open
Command Pallete(⇧⌘Pon Mac) and typewombat. Then press enter.
Publishing Your Extention
Publishing VSCode extensions is done using vsce tool.
There is a guide in VSCode docs that explains how to do that.
You'll need to obtain your personal publishedID and then just run vsce publish.
Final Words
Of course, this extension we've made is very basic and is missing 99.9999999% functionality that is needed to use it for real.
In reality, I believe you would have to also get the AST tree of the script to know exact locations of all the variables you want to track.
Also, you would have to have some state management to store values along with the script execution.
I believe that you also would have to pause the execution in the beginning and then execute the script step by step, recording current state on every iteration.
But all that wasn't the point of this tutorial. I wanted to play around with the available APIs and make a fun project.
The source code is available on github. The extension itself is available in VSCode marketplace. Ping me on telegram if you have any questions.