Create a React Webview for a VSCode extension
πŸ†š

Create a React Webview for a VSCode extension

Tags
Node.js
React.js
VSCode
Published
Published May 11, 2023
Author

Context

Last week at R2Devops, I had the chance of writing a VSCode extension. Its purpose is to display a Side menu once a user opened a .gitlab-ci.yml file with the Public Marketplace of R2Devops (a catalog of ready-to-use CI/CD templates available). This article will describe the process of creating a VSCode extension that shows a React Webview.
Β 

Writing a VSCode extension

The first step to create a VSCode extension is to create the extension folder and run npm init to create the package.json file. Then we can use the VSCode Yeoman extension generator to create the skeleton of the extension.
npm install -g yo generator-code yo code

Anatomy

Here is the basic structure of the extension
. β”œβ”€β”€ .vscode β”‚ β”œβ”€β”€ launch.json // Config for launching and debugging the extension β”‚ └── tasks.json // Config for build task that compiles TypeScript β”œβ”€β”€ .gitignore // Ignore build output and node_modules β”œβ”€β”€ README.md // Readable description of your extension's functionality β”œβ”€β”€ src β”‚ └── extension.ts // Extension source code β”œβ”€β”€ package.json // Extension manifest β”œβ”€β”€ tsconfig.json // TypeScript configuration
Β 
Each VS Code extension must have a package.json as its Extension Manifest. The package.json contains a mix of Node.js fields such as scripts and devDependencies and VS Code specific fields such as publisher, activationEvents and contributes. You can find description of all VS Code specific fields in Extension Manifest Reference. Here are some most important fields:
  • name and publisher: VS Code uses <publisher>.<name> as a unique ID for the extension. For example, the Hello World sample has the ID vscode-samples.helloworld-sample. VS Code uses the ID to uniquely identify your extension.
  • main: The extension entry point.
  • engines.vscode: This specifies the minimum version of VS Code API that the extension depends on.
You can check the complete anatomy, on the vscode documentation. Now that we have our Extension skeleton, we can start the fun design part with React.

Create a webview with React

We created a classical React application with some modifications. As the webview can only takes one file in input, we should specify some options to React to build accordingly. You can find a sample repository that explain the whole process here. I choose to use the Webview UI Toolkit, a component library for building webview-based extensions to create extensions that have a consistent look and feel with the rest of the editor. The toolkit is built with the VSCode design language and automatically supports color themes. Additionally, I use the @vscode/codicons package for all icons. Now that we have our theme, we can simply define our webview, by defining a panel. A method render is called once the panel should render.
export class TemplatePickerPanel { public static currentPanel: TemplatePickerPanel | undefined; private readonly _panel: WebviewPanel; private _disposables: Disposable[] = []; private constructor(panel: WebviewPanel, extensionUri: Uri) { this._panel = panel; // Set an event listener to listen for when the panel is disposed (i.e. when the user closes // the panel or when the panel is closed programmatically) this._panel.onDidDispose(() => this.dispose(), null, this._disposables); // Set the HTML content for the webview panel this._panel.webview.html = this._getWebviewContent(this._panel.webview, extensionUri); // Set an event listener to listen for messages passed from the webview context this._setWebviewMessageListener(this._panel.webview); } public static render(extensionUri: Uri) { if (TemplatePickerPanel.currentPanel) { // If the webview panel already exists reveal it TemplatePickerPanel.currentPanel._panel.reveal(ViewColumn.Beside); } else { // If a webview panel does not already exist create and show a new one const panel = window.createWebviewPanel( // Panel view type "showTemplatePicker", // Panel title "Template Picker", // The editor column the panel should be displayed in ViewColumn.Beside, // Extra panel configurations { // Enable JavaScript in the webview enableScripts: true, retainContextWhenHidden: true, // Restrict the webview to only load resources from the `out` and `webview-ui/build` directories localResourceRoots: [ Uri.joinPath(extensionUri, "out"), Uri.joinPath(extensionUri, "webview-ui/build"), ], } ); const iconPath = getUri(panel.webview, extensionUri, ["webview-ui", "build", "logo128.png"]); panel.iconPath = iconPath; TemplatePickerPanel.currentPanel = new TemplatePickerPanel(panel, extensionUri); } }
Β 
Β 
The most important method is the _getWebViewContent. It returns the final Webview as HTML. Here is used the build output of React. We should also import all external assets manually. Here we import the JavaScript and the CSS compiled in two files.
private _getWebviewContent(webview: Webview, extensionUri: Uri) { // The CSS file from the React build output const stylesUri = getUri(webview, extensionUri, [ "webview-ui", "build", "static", "css", "main.css", ]); // The JS file from the React build output const scriptUri = getUri(webview, extensionUri, [ "webview-ui", "build", "static", "js", "main.js", ]); const nonce = getNonce(); // Inject only the authorized configuration // TODO: URL should match a pattern to avoid inserting junk URL and make request to other URLs. const configuration = getConfig(); // Tip: Install the es6-string-html VS Code extension to enable code highlighting below return /*html*/ ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no"> <meta name="theme-color" content="#000000"> <meta http-equiv="Content-Security-Policy" content="default-src 'self' ${configuration.apiUrl} ; style-src ${webview.cspSource}; script-src 'nonce-${nonce}';"> <link rel="stylesheet" type="text/css" href="${stylesUri}"> <title>Hello World</title> </head> <body> <noscript>You need to enable JavaScript to run this app.</noscript> <div id="root"></div> <script nonce="${nonce}" src="${scriptUri}"></script> </body> </html> `; }
The complete file could be found here.
Now that we have defined our WebView and the extension, we should exchange message between them :

Linking the both together

To link the webview and the extension, we used the VSCode API to send messages and show notifications based on events.
To link the webview with the extension, we need to send messages through the VSCode API. We can use the panel.webview.postMessage method to send messages from the extension to the webview. We can also use the window.addEventListener method to listen to messages from the webview in the extension.
Here's an example of how to send a message from the extension to the webview:
panel.webview.postMessage({ command: 'refresh' });
Β 
Here's an example of how to listen to messages from the webview in the extension:
window.addEventListener('message', event => { const message = event.data; if (message.command === 'refresh') { // Do something } });
Β 
In our case, the React application will send a message when clicking on the β€œAdd this job” button. The extension will retrieve this type of message and perform the needed operation on the file.
Β 

Call external API

notion image
What are Cross Origin Resource Sharing (CORS) Headers? (Source: bunny.net)
Β 
The major issue of this extension was the call of the external API. Indeed, the request failed with CORS issues from the extension, because VSCode does not add the CORS Origin header. So there are two options to solve the problem:
  • You may need to allow all Origin on the backend. This website
  • If you don’t have access to it, you can write a simple proxy to redirect incoming requests and avoid the problem. Here is the code of a simple proxy made with NodeJS.
Β 

Conclusion

In summary, we created a VSCode extension that displays the R2Devops Public Templates Marketplace in a Sidebar using a webview built with React and the Webview UI Toolkit. We used the VSCode API to link the webview and the extension and showed notifications based on events.
The final step, and not the easiest, was to find a name. The choice remains for Templates Picker !
Β 
πŸ‘ Don’t hesitate to check the extension on the vscode-store !
And below you can find the final project hosted on GitLab:
Β