├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── assets └── webview.png ├── package-lock.json ├── package.json ├── src ├── extension.ts └── webview │ ├── App.tsx │ ├── index.tsx │ └── styles.css ├── tsconfig.json ├── vsc-extension-quickstart.md └── webpack ├── extension.config.js └── webview.config.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "sourceType": "module" 7 | }, 8 | "plugins": [ 9 | "@typescript-eslint" 10 | ], 11 | "rules": { 12 | "@typescript-eslint/naming-convention": "warn", 13 | "@typescript-eslint/semi": "warn", 14 | "curly": "warn", 15 | "eqeqeq": "warn", 16 | "no-throw-literal": "warn", 17 | "semi": "off" 18 | }, 19 | "ignorePatterns": [ 20 | "out", 21 | "dist", 22 | "**/*.d.ts" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": ["dbaeumer.vscode-eslint", "amodio.tsl-problem-matcher"] 5 | } 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": [ 13 | "--extensionDevelopmentPath=${workspaceFolder}" 14 | ], 15 | "outFiles": [ 16 | "${workspaceFolder}/dist/**/*.js" 17 | ], 18 | }, 19 | { 20 | "name": "Extension Tests", 21 | "type": "extensionHost", 22 | "request": "launch", 23 | "args": [ 24 | "--extensionDevelopmentPath=${workspaceFolder}", 25 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 26 | ], 27 | "outFiles": [ 28 | "${workspaceFolder}/out/**/*.js", 29 | "${workspaceFolder}/dist/**/*.js" 30 | ], 31 | "preLaunchTask": "tasks: watch-tests" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false, // set this to true to hide the "out" folder with the compiled JS files 5 | "dist": false // set this to true to hide the "dist" folder with the compiled JS files 6 | }, 7 | "search.exclude": { 8 | "out": true, // set this to false to include "out" folder in search results 9 | "dist": true // set this to false to include "dist" folder in search results 10 | }, 11 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 12 | "typescript.tsc.autoDetect": "off" 13 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$ts-webpack-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never", 13 | "group": "watchers" 14 | }, 15 | "group": { 16 | "kind": "build", 17 | "isDefault": true 18 | } 19 | }, 20 | { 21 | "type": "npm", 22 | "script": "watch-tests", 23 | "problemMatcher": "$tsc-watch", 24 | "isBackground": true, 25 | "presentation": { 26 | "reveal": "never", 27 | "group": "watchers" 28 | }, 29 | "group": "build" 30 | }, 31 | { 32 | "label": "tasks: watch-tests", 33 | "dependsOn": [ 34 | "npm: watch", 35 | "npm: watch-tests" 36 | ], 37 | "problemMatcher": [] 38 | } 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | node_modules/** 5 | src/** 6 | .gitignore 7 | .yarnrc 8 | vsc-extension-quickstart.md 9 | **/tsconfig.json 10 | **/.eslintrc.json 11 | **/*.map 12 | **/*.ts 13 | webpack/** -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-async-postmessaging" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [Unreleased] 8 | 9 | - Initial release -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Elio Struyf 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Visual Studio Code Extension - React Webview Starter 2 | 3 | This is a starter template for creating a Visual Studio Code extension with a React webview. 4 | 5 | ## Development 6 | 7 | - Clone this repository 8 | - Run `npm install` to install dependencies 9 | - Run `npm run watch` to start developing 10 | 11 | ## Usage 12 | 13 | The webview can be tested/opened by running the `React Webview: Open webview` command from the command palette. 14 | 15 | ![](./assets/webview.png) -------------------------------------------------------------------------------- /assets/webview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/estruyf/vscode-react-webview-template/a4f540e94b7812679714c532ed5cf651b26c5347/assets/webview.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-react-webview-starter", 3 | "displayName": "vscode-react-webview-starter", 4 | "description": "", 5 | "version": "0.0.1", 6 | "repository": { 7 | "url": "https://github.com/estruyf/vscode-react-webview-template", 8 | "type": "git" 9 | }, 10 | "engines": { 11 | "vscode": "^1.71.0" 12 | }, 13 | "categories": [ 14 | "Other" 15 | ], 16 | "activationEvents": [], 17 | "main": "./dist/extension.js", 18 | "contributes": { 19 | "commands": [ 20 | { 21 | "command": "vscode-react-webview-starter.openWebview", 22 | "title": "React Webview: Open webview" 23 | } 24 | ] 25 | }, 26 | "scripts": { 27 | "vscode:prepublish": "npm run package", 28 | "compile": "webpack", 29 | "watch": "npm-run-all --parallel watch:*", 30 | "watch:ext": "webpack --mode development --watch --config ./webpack/extension.config.js", 31 | "watch:wv": "webpack serve --mode development --config ./webpack/webview.config.js", 32 | "package": "npm run clean && npm-run-all --parallel package:*", 33 | "package:ext": "webpack --mode production --config ./webpack/extension.config.js", 34 | "package:wv": "webpack --mode production --config ./webpack/webview.config.js", 35 | "compile-tests": "tsc -p . --outDir out", 36 | "watch-tests": "tsc -p . -w --outDir out", 37 | "pretest": "npm run compile-tests && npm run compile && npm run lint", 38 | "lint": "eslint src --ext ts", 39 | "clean": "rimraf dist", 40 | "test": "node ./out/test/runTest.js" 41 | }, 42 | "devDependencies": { 43 | "@estruyf/vscode": "^1.1.0", 44 | "@types/glob": "^7.2.0", 45 | "@types/mocha": "^9.1.1", 46 | "@types/node": "16.x", 47 | "@types/react": "^19.0.1", 48 | "@types/react-dom": "^19.0.2", 49 | "@types/uuid": "^8.3.4", 50 | "@types/vscode": "^1.71.0", 51 | "@types/webpack": "^5.28.1", 52 | "@types/webpack-dev-server": "^4.7.2", 53 | "@typescript-eslint/eslint-plugin": "^5.31.0", 54 | "@typescript-eslint/parser": "^5.31.0", 55 | "@vscode/test-electron": "^2.1.5", 56 | "css-loader": "^6.7.1", 57 | "eslint": "^8.20.0", 58 | "glob": "^8.0.3", 59 | "mocha": "^10.0.0", 60 | "npm-run-all": "^4.1.5", 61 | "postcss-loader": "^7.0.1", 62 | "react": "^19.0.0", 63 | "react-dom": "^19.0.0", 64 | "rimraf": "^3.0.2", 65 | "style-loader": "^3.3.1", 66 | "ts-loader": "^9.4.1", 67 | "typescript": "^4.7.4", 68 | "webpack": "^5.97.1", 69 | "webpack-cli": "^5.1.4", 70 | "webpack-dev-server": "^5.2.0", 71 | "webpack-manifest-plugin": "^5.0.0" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | // The module 'vscode' contains the VS Code extensibility API 2 | // Import the module and reference it with the alias vscode in your code below 3 | import { join } from "path"; 4 | import * as vscode from "vscode"; 5 | import { ExtensionContext, ExtensionMode, Uri, Webview } from "vscode"; 6 | import { MessageHandlerData } from "@estruyf/vscode"; 7 | import { readFileSync } from "fs"; 8 | 9 | export function activate(context: vscode.ExtensionContext) { 10 | let disposable = vscode.commands.registerCommand( 11 | "vscode-react-webview-starter.openWebview", 12 | () => { 13 | const panel = vscode.window.createWebviewPanel( 14 | "react-webview", 15 | "React Webview", 16 | vscode.ViewColumn.One, 17 | { 18 | enableScripts: true, 19 | retainContextWhenHidden: true, 20 | } 21 | ); 22 | 23 | panel.webview.onDidReceiveMessage( 24 | (message) => { 25 | const { command, requestId, payload } = message; 26 | 27 | if (command === "GET_DATA") { 28 | // Do something with the payload 29 | 30 | // Send a response back to the webview 31 | panel.webview.postMessage({ 32 | command, 33 | requestId, // The requestId is used to identify the response 34 | payload: `Hello from the extension!`, 35 | } as MessageHandlerData); 36 | } else if (command === "GET_DATA_ERROR") { 37 | panel.webview.postMessage({ 38 | command, 39 | requestId, // The requestId is used to identify the response 40 | error: `Oops, something went wrong!`, 41 | } as MessageHandlerData); 42 | } else if (command === "POST_DATA") { 43 | vscode.window.showInformationMessage( 44 | `Received data from the webview: ${payload.msg}` 45 | ); 46 | } 47 | }, 48 | undefined, 49 | context.subscriptions 50 | ); 51 | 52 | panel.webview.html = getWebviewContent(context, panel.webview); 53 | } 54 | ); 55 | 56 | context.subscriptions.push(disposable); 57 | } 58 | 59 | // this method is called when your extension is deactivated 60 | export function deactivate() {} 61 | 62 | const getWebviewContent = (context: ExtensionContext, webview: Webview) => { 63 | const jsFile = "main.bundle.js"; 64 | const localServerUrl = "http://localhost:9000"; 65 | 66 | let scriptUrl = []; 67 | let cssUrl = null; 68 | 69 | const isProduction = context.extensionMode === ExtensionMode.Production; 70 | if (isProduction) { 71 | // Get the manifest file from the dist folder 72 | const manifest = readFileSync( 73 | join(context.extensionPath, "dist", "webview", "manifest.json"), 74 | "utf-8" 75 | ); 76 | const manifestJson = JSON.parse(manifest); 77 | for (const [key, value] of Object.entries(manifestJson)) { 78 | if (key.endsWith(".js")) { 79 | scriptUrl.push( 80 | webview 81 | .asWebviewUri( 82 | Uri.file(join(context.extensionPath, "dist", "webview", value)) 83 | ) 84 | .toString() 85 | ); 86 | } 87 | } 88 | } else { 89 | scriptUrl.push(`${localServerUrl}/${jsFile}`); 90 | } 91 | 92 | return ` 93 | 94 | 95 | 96 | 97 | ${isProduction ? `` : ""} 98 | 99 | 100 |
101 | 102 | ${scriptUrl.map((url) => ``).join("\n")} 103 | 104 | `; 105 | }; 106 | -------------------------------------------------------------------------------- /src/webview/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import { messageHandler } from '@estruyf/vscode/dist/client'; 3 | import "./styles.css"; 4 | 5 | export interface IAppProps {} 6 | 7 | export const App: React.FunctionComponent = ({ }: React.PropsWithChildren) => { 8 | const [message, setMessage] = React.useState(""); 9 | const [error, setError] = React.useState(""); 10 | 11 | const sendMessage = () => { 12 | messageHandler.send('POST_DATA', { msg: 'Hello from the webview' }); 13 | }; 14 | 15 | const requestData = () => { 16 | messageHandler.request('GET_DATA').then((msg) => { 17 | setMessage(msg); 18 | }); 19 | }; 20 | 21 | const requestWithErrorData = () => { 22 | messageHandler.request('GET_DATA_ERROR') 23 | .then((msg) => { 24 | setMessage(msg); 25 | }) 26 | .catch((err) => { 27 | setError(err); 28 | }); 29 | }; 30 | 31 | return ( 32 |
33 |

Hello from the React Webview Starter

34 | 35 |
36 | 39 | 40 | 43 | 44 | 47 |
48 | 49 | {message &&

Message from the extension: {message}

} 50 | 51 | {error &&

ERROR: {error}

} 52 |
53 | ); 54 | }; -------------------------------------------------------------------------------- /src/webview/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { createRoot } from 'react-dom/client'; 3 | import { App } from "./App"; 4 | 5 | declare const acquireVsCodeApi: () => { 6 | getState: () => T; 7 | setState: (data: T) => void; 8 | postMessage: (msg: unknown) => void; 9 | }; 10 | 11 | const elm = document.querySelector("#root"); 12 | if (elm) { 13 | const root = createRoot(elm); 14 | root.render(); 15 | } 16 | 17 | // Webpack HMR 18 | // @ts-expect-error 19 | if (import.meta.webpackHot) { 20 | // @ts-expect-error 21 | import.meta.webpackHot.accept() 22 | } -------------------------------------------------------------------------------- /src/webview/styles.css: -------------------------------------------------------------------------------- 1 | 2 | h1 { 3 | font-family: var(--vscode-editor-font-family); 4 | } 5 | 6 | .app { 7 | margin: 15px auto; 8 | max-width: 80rem; 9 | } 10 | 11 | .app__actions { 12 | display: flex; 13 | gap: 1rem; 14 | } 15 | 16 | .app__error { 17 | color: var(--vscode-editorError-foreground); 18 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "NodeNext", 4 | "moduleResolution": "nodenext", 5 | "target": "ES2020", 6 | "lib": [ 7 | "ES2020", 8 | "DOM", 9 | "esnext" 10 | ], 11 | "sourceMap": true, 12 | "rootDir": "src", 13 | "strict": true, 14 | "jsx": "react" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Setup 13 | 14 | * install the recommended extensions (amodio.tsl-problem-matcher and dbaeumer.vscode-eslint) 15 | 16 | 17 | ## Get up and running straight away 18 | 19 | * Press `F5` to open a new window with your extension loaded. 20 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 21 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 22 | * Find output from your extension in the debug console. 23 | 24 | ## Make changes 25 | 26 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 27 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 28 | 29 | 30 | ## Explore the API 31 | 32 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 33 | 34 | ## Run tests 35 | 36 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 37 | * Press `F5` to run the tests in a new window with your extension loaded. 38 | * See the output of the test result in the debug console. 39 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 40 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 41 | * You can create folders inside the `test` folder to structure your tests any way you want. 42 | 43 | ## Go further 44 | 45 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 46 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VS Code extension marketplace. 47 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 48 | -------------------------------------------------------------------------------- /webpack/extension.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | //@ts-check 8 | /** @typedef {import('webpack').Configuration} WebpackConfig **/ 9 | 10 | /** @type WebpackConfig */ 11 | const extensionConfig = { 12 | target: 'node', // VS Code extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 13 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 14 | 15 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 16 | output: { 17 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 18 | path: path.resolve(__dirname, '../dist'), 19 | filename: 'extension.js', 20 | libraryTarget: 'commonjs2' 21 | }, 22 | externals: { 23 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 24 | // modules added here also need to be added in the .vscodeignore file 25 | }, 26 | resolve: { 27 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 28 | extensions: ['.ts', '.js'] 29 | }, 30 | module: { 31 | rules: [ 32 | { 33 | test: /\.ts$/, 34 | exclude: /node_modules/, 35 | use: [ 36 | { 37 | loader: 'ts-loader' 38 | } 39 | ] 40 | } 41 | ] 42 | }, 43 | devtool: 'nosources-source-map', 44 | infrastructureLogging: { 45 | level: "log", // enables logging required for problem matchers 46 | }, 47 | }; 48 | module.exports = [ extensionConfig ]; -------------------------------------------------------------------------------- /webpack/webview.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | const WebpackManifestPlugin = require('webpack-manifest-plugin').WebpackManifestPlugin; 7 | 8 | const config = [ 9 | { 10 | name: 'webview', 11 | target: 'web', 12 | entry: './src/webview/index.tsx', 13 | output: { 14 | filename: '[name].bundle.js', 15 | path: path.resolve(__dirname, '../dist/webview') 16 | }, 17 | devtool: 'source-map', 18 | resolve: { 19 | extensions: ['.ts', '.js', '.tsx', '.jsx'] 20 | }, 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(ts|tsx)$/, 25 | exclude: /node_modules/, 26 | use: [{ 27 | loader: 'ts-loader' 28 | }] 29 | }, 30 | { 31 | test: /\.css$/, 32 | use: ['style-loader', 'css-loader', 'postcss-loader'] 33 | } 34 | ] 35 | }, 36 | performance: { 37 | hints: false 38 | }, 39 | plugins: [new WebpackManifestPlugin({ publicPath: "" })], 40 | devServer: { 41 | compress: true, 42 | port: 9000, 43 | hot: true, 44 | allowedHosts: "all", 45 | headers: { 46 | "Access-Control-Allow-Origin": "*", 47 | } 48 | } 49 | } 50 | ]; 51 | 52 | module.exports = (env, argv) => { 53 | for (const configItem of config) { 54 | configItem.mode = argv.mode; 55 | 56 | if (argv.mode === 'production') { 57 | configItem.devtool = "hidden-source-map"; 58 | 59 | configItem.optimization = { 60 | splitChunks: { 61 | chunks: 'all', 62 | }, 63 | }; 64 | } 65 | } 66 | 67 | return config; 68 | }; --------------------------------------------------------------------------------