Use React in your VSCode WebView with hot module replacement

December 17, 2021

For Front Matter and another extension, I am currently developing. I use the Visual Studio Code WebView API heavily as it provides fully customizable views for your panels or tabs. It allows any company and developer to create their own unique experiences.

One of the things I did for a long time was manually hitting the refresh button each time I updated the code. In case when you are working with a WebView, this experience is clumsy.

We are used to having Hot Module Replacement (HRM) for web projects, but unfortunately, this is not automatically available for VS Code extension development.

To improve the experience when working with WebViews in VS Code, I tried to make HMR work for create-react-app and webpack dev server. In this article, I will share the steps I had to take to make it work for CRA, but you will have to do a similar configuration in both cases.

Info In Front Matter, the React app is part of the solution and generates the bundle via a separate webpack config and makes use of the webpack dev server.

The approach

I choose to separate the project and use the CRA for the other project. You can create the React project anywhere you want, but a mono-repo approach might be appropriate as you will have to move some files during the packaging of your extension.

As a starting point, I took the WebView documenation.

WebView base HTML

In the sample, a getWebviewContent method in which the HTML is defined. To make React work in the WebView you have to change the code as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
const getWebviewContent = (context: ExtensionContext) => {
	const jsFile = "vscode.js";
	const cssFile = "vscode.css";
	const localServerUrl = "http://localhost:3000";

	let scriptUrl = null;
	let cssUrl = null;

	const isProduction = context.extensionMode === ExtensionMode.Production;
	if (isProduction) {
		scriptUrl = webView.asWebviewUri(Uri.file(join(context.extensionPath, 'dist', jsFile))).toString();
		scriptUrl = webView.asWebviewUri(Uri.file(join(context.extensionPath, 'dist', cssFile))).toString();
	} else {
		scriptUrl = `${localServerUrl}/${jsFile}`; 
	}

	return `<!DOCTYPE html>
	<html lang="en">
	<head>
		<meta charset="UTF-8">
		<meta name="viewport" content="width=device-width, initial-scale=1.0">
		${isProduction ? `<link href="${cssUrl}" rel="stylesheet">` : ''}
	</head>
	<body>
		<div id="root"></div>

		<script src="${scriptUrl}" />
	</body>
	</html>`;
}

In the method, a couple of things have changed:

  1. There is logic added when the extension is running in development or production mode;
  2. Some React HTML requirements have been added.

The production/development logic is required to ensure where the JS and CSS file gets loaded. In development, this will be from the localhost. During production, it loads from within the extension.

Webpack configuration

To simplify the JS and CSS references, I made some webpack changes. If you are using CRA, you best use the react-app-rewired dependency to override the webpack config.

My config-overrides.js looks as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
module.exports = function override(config, env) {
  // Define our own filename
  config.output.filename = 'vscode.js';

  // This way we only need to load one file for the webview
  config.optimization.splitChunks = {
    cacheGroups: {
        default: false
    }
  };
  config.optimization.runtimeChunk = false;

  // Makes sure the public path is set in JS so that files are correctly loaded
  config.output.publicPath = 'http://localhost:3000/';

  // Specifies the CSS file to output
  config.plugins = config.plugins.map(plugin => {
    if (plugin.constructor.name === "MiniCssExtractPlugin") {
      plugin.options.filename = `vscode.css`;
    }
    return plugin;
  });

  return config;
}

The most important line here is the config.output.publicPath, set to your local server. We need to provide it to make sure absolute paths are used for referencing files to load. As the code runs in the WebView, there is no actual HTML page, and the location of your webpage will be the VS Code instance: vscode-webview://.

If you do not provide the publicPath, only the first load runs fine. Once you update the code, you will notice that the hot-update files fail to fetch.

Failed to load the file update
Failed to load the file update

Looking at the URL from where the file loads, you’ll spot the vscode-webview:// path.

VS Code path
VS Code path

When providing the publicPath it works as expected.

Loading HMR changes correctly
Loading HMR changes correctly

What about packaging?

When you build your two solutions, you will have to ensure that the JS bundle and CSS file are copied/moved to the correct folder. In my case, I put these two files in the dist folder of the VS Code extension just before packaging it.

Important: Be aware it could be you’ll have to make other changes to your webpack configuration to make sure the assets are correctly loaded.

Comments

comments powered by Disqus