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
andpublisher
: VS Code uses<publisher>.<name>
as a unique ID for the extension. For example, the Hello World sample has the IDvscode-samples.helloworld-sample
. VS Code uses the ID to uniquely identify your extension.
main
: The extension entry point.
activationEvents
andcontributes
: Activation Events and Contribution Points.
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
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:
Β