├── .gitignore ├── app ├── api │ └── index.ts ├── components │ ├── Home.tsx │ ├── ReceivedMessages.tsx │ ├── SendMessage.tsx │ ├── Message.tsx │ ├── About.tsx │ └── App.tsx ├── context │ └── MessageContext.tsx ├── index.css ├── index.tsx ├── global.d.ts ├── tsconfig.json └── routes │ ├── RouteWithSubRoutes.tsx │ └── config.tsx ├── .prettierrc ├── screenshot.gif ├── .vscode ├── extensions.json ├── tasks.json ├── settings.json └── launch.json ├── src ├── config │ └── index.ts ├── view │ ├── messages │ │ └── messageTypes.ts │ └── ViewLoader.ts └── extension.ts ├── .vscodeignore ├── CHANGELOG.md ├── test ├── extension.test.ts ├── runTest.ts └── index.ts ├── webpack.config.js ├── tsconfig.json ├── .eslintrc.json ├── README.md ├── vsc-extension-quickstart.md └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | .DS_Store 6 | .history 7 | -------------------------------------------------------------------------------- /app/api/index.ts: -------------------------------------------------------------------------------- 1 | export const apiUrl = `https://randomuser.me/api/?gender=${apiUserGender}`; 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "singleQuote": true, 4 | "arrowParens": "avoid" 5 | } 6 | -------------------------------------------------------------------------------- /screenshot.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hacker0limbo/vscode-webview-react-boilerplate/HEAD/screenshot.gif -------------------------------------------------------------------------------- /app/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const Home = () => { 4 | return

Home

; 5 | }; 6 | -------------------------------------------------------------------------------- /app/context/MessageContext.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const MessagesContext = React.createContext([]); 4 | -------------------------------------------------------------------------------- /app/index.css: -------------------------------------------------------------------------------- 1 | .navbar { 2 | padding: 0; 3 | } 4 | 5 | .navbar > li { 6 | display: inline-block; 7 | margin-right: 1rem; 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /app/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from './components/App'; 4 | import './index.css'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export const getAPIUserGender = () => { 4 | const gender = vscode.workspace.getConfiguration('webviewReact').get('userApiGender', 'male'); 5 | return gender; 6 | }; 7 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | src/** 5 | test/** 6 | .gitignore 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | .prettierrc 13 | screenshot.gif -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "vscode-webview-react-boilerplate" 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 -------------------------------------------------------------------------------- /app/global.d.ts: -------------------------------------------------------------------------------- 1 | type Message = import('../src/view/messages/messageTypes').Message; 2 | 3 | type VSCode = { 4 | postMessage(message: T): void; 5 | getState(): any; 6 | setState(state: any): void; 7 | }; 8 | 9 | declare const vscode: VSCode; 10 | 11 | declare const apiUserGender: string; 12 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "allowSyntheticDefaultImports": true, 5 | "moduleResolution": "node", 6 | "target": "ES5", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "experimentalDecorators": true, 10 | "lib": ["dom", "ES2015"], 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/view/messages/messageTypes.ts: -------------------------------------------------------------------------------- 1 | export type MessageType = 'RELOAD' | 'COMMON'; 2 | 3 | export interface Message { 4 | type: MessageType; 5 | payload?: any; 6 | } 7 | 8 | export interface CommonMessage extends Message { 9 | type: 'COMMON'; 10 | payload: string; 11 | } 12 | 13 | export interface ReloadMessage extends Message { 14 | type: 'RELOAD'; 15 | } 16 | -------------------------------------------------------------------------------- /test/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | import * as vscode from 'vscode'; 3 | 4 | describe('Extension Test Suite', () => { 5 | vscode.window.showInformationMessage('Start all tests.'); 6 | 7 | it('Sample test', () => { 8 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 9 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 10 | }); 11 | }); 12 | -------------------------------------------------------------------------------- /app/routes/RouteWithSubRoutes.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route } from 'react-router-dom'; 3 | import { RouteConfig } from './config'; 4 | 5 | export const RouteWithSubRoutes = (route: RouteConfig) => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /.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": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.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 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /app/components/ReceivedMessages.tsx: -------------------------------------------------------------------------------- 1 | import React, { useContext } from 'react'; 2 | import { MessagesContext } from '../context/MessageContext'; 3 | 4 | export const ReceivedMessages = () => { 5 | const receivedMessages = useContext(MessagesContext); 6 | 7 | return ( 8 |
9 |

Received Messages from Extension:

10 |
    11 | {receivedMessages.map((receivedMessage, i) => ( 12 |
  • {receivedMessage}
  • 13 | ))} 14 |
15 |
16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: path.join(__dirname, 'app', 'index.tsx'), 5 | resolve: { 6 | extensions: ['.ts', '.tsx', '.js', '.jsx', '.css'], 7 | }, 8 | devtool: 'inline-source-map', 9 | module: { 10 | rules: [ 11 | { 12 | test: /\.tsx?$/, 13 | use: 'ts-loader', 14 | exclude: '/node_modules/', 15 | }, 16 | { 17 | test: /\.css$/, 18 | use: ['style-loader', 'css-loader'], 19 | }, 20 | ], 21 | }, 22 | output: { 23 | filename: 'bundle.js', 24 | path: path.resolve(__dirname, 'out', 'app'), 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": ".", 11 | "strict": true /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test", 20 | "app" 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /app/components/SendMessage.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { CommonMessage } from '../../src/view/messages/messageTypes'; 3 | 4 | export const SendMessage = () => { 5 | const [message, setMessage] = useState(''); 6 | 7 | const handleMessageChange = (e: React.ChangeEvent) => { 8 | setMessage(e.target.value); 9 | }; 10 | 11 | const sendMessage = () => { 12 | vscode.postMessage({ 13 | type: 'COMMON', 14 | payload: message, 15 | }); 16 | }; 17 | 18 | return ( 19 |
20 |

Send Message to Extension:

21 | 22 | 23 |
24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | 3 | import { runTests } from 'vscode-test'; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, '../../'); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, './index'); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error('Failed to run tests'); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /app/components/Message.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Switch, Link } from 'react-router-dom'; 3 | import { RouteWithSubRoutes } from '../routes/RouteWithSubRoutes'; 4 | import { RouteConfigComponentProps } from '../routes/config'; 5 | 6 | export const Message: React.FC = ({ routes }) => { 7 | return ( 8 |
9 |

Message

10 |
    11 |
  • 12 | Received Messages 13 |
  • 14 |
  • 15 | Send Message 16 |
  • 17 |
18 | 19 | 20 | {routes!.map((route, i) => ( 21 | 22 | ))} 23 | 24 |
25 | ); 26 | }; 27 | -------------------------------------------------------------------------------- /.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 | "react-hooks" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/naming-convention": [ 14 | "warn", 15 | { 16 | "selector": "function", 17 | "format": ["PascalCase", "camelCase"] 18 | } 19 | ], 20 | "@typescript-eslint/semi": "warn", 21 | "curly": "warn", 22 | "eqeqeq": "warn", 23 | "no-throw-literal": "warn", 24 | "semi": "off", 25 | "react-hooks/rules-of-hooks": "error", 26 | "react-hooks/exhaustive-deps": "warn" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import { ViewLoader } from './view/ViewLoader'; 3 | import { CommonMessage } from './view/messages/messageTypes'; 4 | 5 | export function activate(context: vscode.ExtensionContext) { 6 | context.subscriptions.push( 7 | vscode.commands.registerCommand('webview.open', () => { 8 | ViewLoader.showWebview(context); 9 | }), 10 | 11 | vscode.commands.registerCommand('extension.sendMessage', () => { 12 | vscode.window 13 | .showInputBox({ 14 | prompt: 'Send message to Webview', 15 | }) 16 | .then(result => { 17 | result && 18 | ViewLoader.postMessageToWebview({ 19 | type: 'COMMON', 20 | payload: result, 21 | }); 22 | }); 23 | }) 24 | ); 25 | } 26 | 27 | export function deactivate() {} 28 | -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as Mocha from 'mocha'; 3 | import * as glob from 'glob'; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: 'bdd', 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname); 13 | 14 | return new Promise((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | -------------------------------------------------------------------------------- /app/routes/config.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Home } from '../components/Home'; 3 | import { About } from '../components/About'; 4 | import { Message } from '../components/Message'; 5 | import { ReceivedMessages } from '../components/ReceivedMessages'; 6 | import { SendMessage } from '../components/SendMessage'; 7 | 8 | export type RouteConfigComponentProps = Pick; 9 | 10 | export type RouteConfig = { 11 | path: string; 12 | component: React.ComponentType; 13 | exact?: boolean; 14 | routes?: RouteConfig[]; 15 | }; 16 | 17 | export const routes: RouteConfig[] = [ 18 | { 19 | path: '/', 20 | component: Home, 21 | exact: true 22 | }, 23 | { 24 | path: '/about', 25 | component: About, 26 | }, 27 | { 28 | path: '/message', 29 | component: Message, 30 | routes: [ 31 | { 32 | path: '/message/received', 33 | component: ReceivedMessages, 34 | }, 35 | { 36 | path: '/message/send', 37 | component: SendMessage, 38 | }, 39 | ], 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /.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}/out/src/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}", 19 | "sourceMaps": true 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "args": [ 26 | "--extensionDevelopmentPath=${workspaceFolder}", 27 | "--extensionTestsPath=${workspaceFolder}/out/test/index" 28 | ], 29 | "outFiles": [ 30 | "${workspaceFolder}/out/test/**/*.js" 31 | ], 32 | "preLaunchTask": "${defaultBuildTask}", 33 | "sourceMaps": true 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VSCode Webview React Boilerplate 2 | 3 | An awesome boilerplate for developing VSCode Extension Webview in `React`, `React Router` and `TypeScript`. 4 | 5 | ![screenshot](screenshot.gif) 6 | 7 | Project was inspired by: 8 | - [Reactception : extending a VS Code extension with Webviews and React](https://medium.com/younited-tech-blog/reactception-extending-vs-code-extension-with-webviews-and-react-12be2a5898fd) 9 | - [vscode-webview-react](https://github.com/rebornix/vscode-webview-react) 10 | - [VSCode Webview API](https://code.visualstudio.com/api/extension-guides/webview) 11 | 12 | ## Development 13 | 14 | Install dependencies first. 15 | 16 | ```bash 17 | $ npm install 18 | ``` 19 | 20 | After the install process you can press `F5` to "Start Debugging" (or: select in menu **"Debug" -> "Run Extension"**). A new Extension Development Host window will open in which you need to open command palette (`Ctrl/Cmd + Shift + P`) and select **"Webview React: Open Webview"** to open webview. 21 | 22 | Functionalities covered: 23 | - Router 24 | - Communication between Webview and Extension 25 | - Integrated configuration 26 | - Mock API Request -------------------------------------------------------------------------------- /app/components/About.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useCallback, useEffect } from 'react'; 2 | import { apiUrl } from '../api'; 3 | 4 | type UserInfo = { 5 | name: string; 6 | gender: string; 7 | email: string; 8 | }; 9 | 10 | export const About = () => { 11 | const [userInfo, setUserInfo] = useState({ 12 | name: '', 13 | gender: '', 14 | email: '', 15 | }); 16 | const [loading, setLoading] = useState(false); 17 | 18 | const fetchUser = useCallback(() => { 19 | setLoading(true); 20 | fetch(apiUrl) 21 | .then(res => res.json()) 22 | .then(({ results }) => { 23 | const user = results[0]; 24 | setLoading(false); 25 | setUserInfo({ 26 | name: `${user.name.first} ${user.name.last}`, 27 | gender: user.gender, 28 | email: user.email, 29 | }); 30 | }) 31 | .catch(err => { 32 | setLoading(false); 33 | }); 34 | }, []); 35 | 36 | useEffect(() => { 37 | fetchUser(); 38 | }, [fetchUser]); 39 | 40 | return ( 41 |
42 |

About

43 |

User Info

44 | {loading ? ( 45 |
Loading...
46 | ) : ( 47 |
    48 |
  • Name: {userInfo.name}
  • 49 |
  • Gender: {userInfo.gender}
  • 50 |
  • Email: {userInfo.email}
  • 51 |
52 | )} 53 | 54 |
55 | ); 56 | }; 57 | -------------------------------------------------------------------------------- /app/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useCallback } from 'react'; 2 | import { MemoryRouter as Router, Link, Switch } from 'react-router-dom'; 3 | import { routes } from '../routes/config'; 4 | import { RouteWithSubRoutes } from '../routes/RouteWithSubRoutes'; 5 | import { MessagesContext } from '../context/MessageContext'; 6 | import { CommonMessage, Message, ReloadMessage } from '../../src/view/messages/messageTypes'; 7 | 8 | export const App = () => { 9 | const [messagesFromExtension, setMessagesFromExtension] = useState([]); 10 | 11 | const handleMessagesFromExtension = useCallback( 12 | (event: MessageEvent) => { 13 | if (event.data.type === 'COMMON') { 14 | const message = event.data as CommonMessage; 15 | setMessagesFromExtension([...messagesFromExtension, message.payload]); 16 | } 17 | }, 18 | [messagesFromExtension] 19 | ); 20 | 21 | useEffect(() => { 22 | window.addEventListener('message', (event: MessageEvent) => { 23 | handleMessagesFromExtension(event); 24 | }); 25 | 26 | return () => { 27 | window.removeEventListener('message', handleMessagesFromExtension); 28 | }; 29 | }, [handleMessagesFromExtension]); 30 | 31 | const handleReloadWebview = () => { 32 | vscode.postMessage({ 33 | type: 'RELOAD', 34 | }); 35 | }; 36 | 37 | return ( 38 | 41 |
    42 |
  • 43 | Home 44 |
  • 45 |
  • 46 | About 47 |
  • 48 |
  • 49 | Message 50 |
  • 51 |
52 | 53 | 54 | 55 | 56 | {routes.map((route, i) => ( 57 | 58 | ))} 59 | 60 | 61 |
62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /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 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-webview-react-boilerplate", 3 | "displayName": "VSCode webview react boilerplate", 4 | "description": "Boilerplate for developing webview using react in VSCode extension", 5 | "version": "0.0.1", 6 | "engines": { 7 | "vscode": "^1.52.0" 8 | }, 9 | "categories": [ 10 | "Other" 11 | ], 12 | "keywords": [ 13 | "vscode", 14 | "vscode-extension", 15 | "vscode-extension-boilerplate", 16 | "boilerplate" 17 | ], 18 | "activationEvents": [ 19 | "onStartupFinished" 20 | ], 21 | "main": "./out/src/extension.js", 22 | "contributes": { 23 | "commands": [ 24 | { 25 | "command": "webview.open", 26 | "title": "Open Webview", 27 | "category": "Webview React" 28 | }, 29 | { 30 | "command": "extension.sendMessage", 31 | "title": "Send Message to Webview", 32 | "category": "Webview React" 33 | } 34 | ], 35 | "configuration": { 36 | "title": "Webview React", 37 | "properties": { 38 | "webviewReact.userApiGender": { 39 | "type": "string", 40 | "default": "male", 41 | "enum": [ 42 | "male", 43 | "female" 44 | ], 45 | "enumDescriptions": [ 46 | "Fetching user information with gender of male", 47 | "Fetching user information with gender of female" 48 | ] 49 | } 50 | } 51 | } 52 | }, 53 | "scripts": { 54 | "vscode:prepublish": "npm run compile", 55 | "compile": "npm-run-all compile:*", 56 | "compile:extension": "tsc -p ./", 57 | "compile:view": "webpack --mode development", 58 | "watch": "npm-run-all -p watch:*", 59 | "watch:extension": "tsc -watch -p ./", 60 | "watch:view": "webpack --watch --mode development", 61 | "pretest": "npm run compile && npm run lint", 62 | "lint": "eslint src --ext ts", 63 | "test": "node ./out/test/runTest.js" 64 | }, 65 | "devDependencies": { 66 | "@types/glob": "^7.1.3", 67 | "@types/mocha": "^8.0.4", 68 | "@types/node": "^12.11.7", 69 | "@types/react": "^17.0.1", 70 | "@types/react-dom": "^17.0.0", 71 | "@types/react-router-dom": "^5.1.7", 72 | "@types/vscode": "^1.52.0", 73 | "@typescript-eslint/eslint-plugin": "^4.9.0", 74 | "@typescript-eslint/parser": "^4.9.0", 75 | "css-loader": "^5.0.1", 76 | "eslint": "^7.15.0", 77 | "eslint-plugin-react-hooks": "^4.2.0", 78 | "glob": "^7.1.6", 79 | "mocha": "^8.1.3", 80 | "npm-run-all": "^4.1.5", 81 | "prettier": "^2.2.1", 82 | "style-loader": "^2.0.0", 83 | "ts-loader": "^8.0.15", 84 | "typescript": "^4.1.2", 85 | "vscode-test": "^1.4.1", 86 | "webpack": "^5.21.1", 87 | "webpack-cli": "^4.5.0" 88 | }, 89 | "dependencies": { 90 | "react": "^17.0.1", 91 | "react-dom": "^17.0.1", 92 | "react-router-dom": "^5.2.0" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/view/ViewLoader.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import { getAPIUserGender } from '../config'; 4 | import { Message, CommonMessage } from './messages/messageTypes'; 5 | 6 | export class ViewLoader { 7 | public static currentPanel?: vscode.WebviewPanel; 8 | 9 | private panel: vscode.WebviewPanel; 10 | private context: vscode.ExtensionContext; 11 | private disposables: vscode.Disposable[]; 12 | 13 | constructor(context: vscode.ExtensionContext) { 14 | this.context = context; 15 | this.disposables = []; 16 | 17 | this.panel = vscode.window.createWebviewPanel('reactApp', 'React App', vscode.ViewColumn.One, { 18 | enableScripts: true, 19 | retainContextWhenHidden: true, 20 | localResourceRoots: [vscode.Uri.file(path.join(this.context.extensionPath, 'out', 'app'))], 21 | }); 22 | 23 | // render webview 24 | this.renderWebview(); 25 | 26 | // listen messages from webview 27 | this.panel.webview.onDidReceiveMessage( 28 | (message: Message) => { 29 | if (message.type === 'RELOAD') { 30 | vscode.commands.executeCommand('workbench.action.webview.reloadWebviewAction'); 31 | } else if (message.type === 'COMMON') { 32 | const text = (message as CommonMessage).payload; 33 | vscode.window.showInformationMessage(`Received message from Webview: ${text}`); 34 | } 35 | }, 36 | null, 37 | this.disposables 38 | ); 39 | 40 | this.panel.onDidDispose( 41 | () => { 42 | this.dispose(); 43 | }, 44 | null, 45 | this.disposables 46 | ); 47 | } 48 | 49 | private renderWebview() { 50 | const html = this.render(); 51 | this.panel.webview.html = html; 52 | } 53 | 54 | static showWebview(context: vscode.ExtensionContext) { 55 | const cls = this; 56 | const column = vscode.window.activeTextEditor 57 | ? vscode.window.activeTextEditor.viewColumn 58 | : undefined; 59 | if (cls.currentPanel) { 60 | cls.currentPanel.reveal(column); 61 | } else { 62 | cls.currentPanel = new cls(context).panel; 63 | } 64 | } 65 | 66 | static postMessageToWebview(message: T) { 67 | // post message from extension to webview 68 | const cls = this; 69 | cls.currentPanel?.webview.postMessage(message); 70 | } 71 | 72 | public dispose() { 73 | ViewLoader.currentPanel = undefined; 74 | 75 | // Clean up our resources 76 | this.panel.dispose(); 77 | 78 | while (this.disposables.length) { 79 | const x = this.disposables.pop(); 80 | if (x) { 81 | x.dispose(); 82 | } 83 | } 84 | } 85 | 86 | render() { 87 | const bundleScriptPath = this.panel.webview.asWebviewUri( 88 | vscode.Uri.file(path.join(this.context.extensionPath, 'out', 'app', 'bundle.js')) 89 | ); 90 | 91 | const gender = getAPIUserGender(); 92 | 93 | return ` 94 | 95 | 96 | 97 | 98 | 99 | React App 100 | 101 | 102 | 103 |
104 | 108 | 111 | 112 | 113 | 114 | `; 115 | } 116 | } 117 | --------------------------------------------------------------------------------