How to Build a Webview-Powered VS Code Extension with LWC

Building interactive extensions for Visual Studio Code (VS Code) is not always easy when you use the built-in functionality. To change this, you can use Webviews. They allow you to run local web apps within VS Code, and those web apps can also communicate with the IDE. This blog post will introduce you to VS Code extensions, how to build a web app for VS Code with Lightning Web Components (LWC), and provide a useful tip to speed up your LWC development in general.

What is a VS Code extension?

VS Code was built with extensibility as one of its main principles. A VS Code extension is a plugin that adds functionality to VS Code, extending or customizing it. You can create extensions to support a new programming language, execute automated tasks, display a user interface with which users can interact, and much more. The options are huge. You can find some popular VS Code extensions published by Salesforce in the Salesforce Extension Pack.

One way in which you can create VS Code extensions is using Webviews and the Webview API. You can think of a Webview like an iframe, in which you can render a user interface built with HTML, JavaScript and CSS. You can communicate the embedded user interface with the VS Code extension code passing messages on both directions, using the provided APIs.

One example of a Webview powered VS Code extension is a fun project that myself and Kotaro Nishino have recently been working on. We’ve called it LWC Builder. LWC Builder facilitates the creation of Lightning Web Components that are meant to run on the Salesforce platform. Its main advantage is that it allows you to select the component configuration options from a user interface. For example, the pages for which you want it to be available, or the properties to surface in app builder. This saves you from having to remember or look for the different options, syntax and tags needed in the metadata file. The extension also lets you preview the component files, and you can finally create them with just one click.

The user interface from which you can configure and preview the component files is a LWC OSS application embedded in a VS Code Webview. You can install the LWC Builder VS Code extension we came up with through its VSIX file.

The following video showcases some of the capabilities that LWC Builder has. We create a component that surfaces a property when it’s placed on homepages. We also make the component available for some object’s record pages. Finally, we preview the resulting component files before actually creating them.

How to create a VS Code extension

VS Code extensions can be written in JavaScript or TypeScript. You can use Yeoman and the VS Code extension generator node package. You can install it running npm install -g yo generator-code and then execute it running yo code. This will open a wizard, similar to the one that you get with create-lwc-app; it will gather your selected configuration options to create the basic project structure.

For instance, if you create a TypeScript extension, this is the project structure that gets created, being extension.ts the app entry point.

How to embed a LWC OSS app as a Webview

For LWC Builder, we divided the application into two very well differentiated parts: the LWC OSS application, that can run independently of VS Code, as a regular LWC OSS app, and the VS Code extension.

The VS Code extension imports the LWC OSS app via npm module resolution to embed it into a Webview. This allowed us to quickly iterate in the development process, publishing new versions of the LWC OSS app often and just updating the dependency on the VS Code extension package.json file to get the changes. You can take a different approach, but our development experience doing it this way was very smooth.

{
  "dependencies": {
    "lwc-builder-ui": "^0.1.20"
  }
  //...
}

To create a Webview, you have to instantiate a WebviewPanel that includes the user interface app files. In our case the compiled LWC OSS app files. Basically, what we do is to read the LWC OSS app compiled files from the node_modules folder (previously downloaded through npm install), and apply some needed transformations.

this.webviewPanel = vscode.window.createWebviewPanel(
      'lwcBuilder',
      'LWC Builder',
      vscode.ViewColumn.One,
      {
        // Enable scripts in the webview
        enableScripts: true,
      }
    );

    // Set webview panel content
    const pathToLwcDist = path.join(context.extensionPath, LWC_BUILDER_UI_PATH);
    const pathToHtml = path.join(pathToLwcDist, HTML_FILE);
    let html = fs.readFileSync(pathToHtml).toString();
    this.webviewPanel.webview.html = HtmlUtils.transformHtml(html, pathToLwcDist, webview);
);

The transformations that you have to apply are:

  • To transform HTML <script> and <link> tags, as VS Code uses a protocol that needs relative file paths to start by vscode-webview-resource, for instance, instead of having <script src=“index.js”>, you’ll need to use <script src=“vscode-webview-resource:0.app.js:index.js”>. Same applies for links.
  • To add a Content Security Policy meta tag, so that local scripts and stylesheets can be loaded.

How to run VS Code actions from your WebView

The embedded app and the VSCode extension can communicate passing messages. This is, the receiver can define a listener (subscribe to a message), that will execute when the publisher publishes a message. This works in both directions.

As an example, let’s take a look at how we pass a message from the LWC OSS app to the VS Code extension on LWC Builder. When the user has finished configuring the new component and clicks on “Create”, the LWC OSS app sends a message to the Webview, containing the configuration selected by the user.

onButtonClick() {
    // Send message to server
    const message = new LWCBuilderEvent('create_button_clicked', this.contents);
    this.vscode?.postMessage(message);
}

When this message is listened, the VS Code extension creates the corresponding LWC files using VS Code API. The message handler, or listener, needs to be attached to the Webview panel.

export class WebviewInstance {
  // ...
  this.webviewPanel.webview.onDidReceiveMessage( this.onDidReceiveMessageHandler, this, this.subscriptions );

  protected onDidReceiveMessageHandler(event: LWCBuilderEvent): void { 
     // Handle messages from the UI switch (event.type) {
       case 'create_button_clicked': 
         createLwcFolder(event.payload, this.lwcFolderUri);
       case 'error': 
         vscode.window.showErrorMessage(event.error); return; 
       default: 
         vscode.window.showInformationMessage(`Unknown event: ${event.type}`);
     } 
  }
  // ... 
}

This is a diagram that represents the information flow:

How to define the contribution points for launching the Webview

To be able to launch a VS Code extension you need to define at least one contribution point. Contribution points are the entry points through which we want to allow the users to execute the extension code. A typical contribution point is when the user selects a command in the command palette – but it can be almost anything: selecting a text in the code, clicking on a menu etc. Contribution points are defined in the package.json file. For LWC Builder we defined two contribution points: a command that’s available in the command palette, when you’re inside an SFDX project, and also an option when you right-click on the lwc folder menu.

{
  "contributes": {
    "commands": [
      {
        "command": "lwc-builder.openLWCBuilder",
        "title": "Open LWC Builder"
      }
    ],
    "menus": {
      "explorer/context": [
        {
          "command": "lwc-builder.openLWCBuilder",
          "title": "Open LWC Builder",
          "when": "explorerResourceIsFolder && resourceFilename == lwc && sfdx:project_opened"
        }
      ],
      "commandPalette": [
        {
          "command": "lwc-builder.openLWCBuilder",
          "when": "sfdx:project_opened"
        }
      ]
    }
  },
  // ...
}

Contribution points launch commands. You define commands in the extension entry file (extension.ts in our case). For LWC Builder, we created an openLWCBuilder command that instantiates the Webview. We bound the two contribution points to the same command.

const openLWCBuilderCommand = vscode.commands.registerCommand(
    'lwc-builder.openLWCBuilder',
    (uri: vscode.Uri) => {
      new WebviewInstance(context, uri);
    }
  );

context.subscriptions.push(openLWCBuilderCommand);

How to package and publish the VSIX file

Finally, packaging and publishing a VS Code extension is straightforward. You just have to use vsce (“Visual Studio Code Extensions”), a command line tool built for this purpose. You can install it by running npm install -g vsce. Then, you can generate the installable .vsix file running vsce package, and optionally publish it to the VS Code marketplace running vsce publish.

Conclusions

VS Code extensions are cool, they help you customize and extend VS Code in an infinity of ways. Extensions can make your development experience faster, easier and even funnier! Creating VS Code extensions is not hard, and I have to say that working with VS Code documentation was awesome. It’s easy to follow, thorough and full of examples.

VS Code extensions can be greatly extended with Webview, embedding custom web applications easily. Initially, LWC Builder was a standalone web application built with LWC OSS. Thanks to Webviews, we were able to re-use what we had already built. Indeed, the LWC OSS app can still run in a standalone way if needed.

Building a VS Code Extension and a LWC OSS app was a great experience for us. We learnt a lot and now we hope that the extension can help other developers to be more productive. We are open to contributions. Don’t hesitate to help us to improve the LWC OSS app or the VS Code extension with your amazing ideas!

About the authors

Alba Rivas works as a Lead Developer Evangelist at Salesforce. She focuses on Lightning Web Components and Lightning adoption strategy. You can follow her on Twitter @AlbaSFDC.

Kotaro Nishino works as a Demo Engineer at Salesforce. He builds demos for customers in Japan and South Korea, and creates assets and tools enhancing productivities for Trailblazers. Record Clone is one of his recent works.