├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── assets ├── chrome-web-store-btn.png ├── pptr-ide-extension.gif └── screenshots │ ├── $0-usage.gif │ ├── screen-1.png │ └── screen-2.png ├── package-lock.json ├── package.json ├── src ├── background.ts ├── devtools │ ├── devtools.html │ ├── devtools.ts │ ├── idePanel │ │ ├── components │ │ │ ├── ActionBar.tsx │ │ │ ├── AddScriptDialog.tsx │ │ │ ├── Editor.tsx │ │ │ ├── EditorTabs.tsx │ │ │ ├── IDEContext.ts │ │ │ ├── ScriptSelect.tsx │ │ │ ├── ScriptSettingDialog.tsx │ │ │ └── ThemeSwitch.tsx │ │ ├── extensionReducer.ts │ │ ├── idePanel.html │ │ ├── idePanel.scss │ │ ├── idePanel.tsx │ │ ├── pptr.png │ │ ├── typedefs │ │ │ ├── declarations.d.ts │ │ │ └── puppeteer.d.ts │ │ └── utils │ │ │ └── getElementSelector.ts │ └── sandbox │ │ ├── lib │ │ ├── executeScript.ts │ │ └── messageTransport.ts │ │ ├── sandbox.html │ │ └── sandbox.ts ├── manifest.json └── pptr.png ├── tsconfig.json └── webpack.config.js /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | docs/ 3 | dist/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/gts/", 3 | "rules": { 4 | "node/no-extraneous-require": [2, { 5 | "allowModules": ["terser-webpack-plugin"] 6 | }], 7 | "@typescript-eslint/no-explicit-any": 0, 8 | "@typescript-eslint/no-empty-interface": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Setup Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '16.9.1' 21 | - name: Install dependencies 22 | run: npm install 23 | - name: Lint 24 | run: npm run lint 25 | - name: Compile 26 | run: npm run compile 27 | - name: Generate Dist 28 | run: npm run dist -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | lint: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Setup Node 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: '16.9.1' 21 | - name: Install dependencies 22 | run: npm install 23 | - name: Lint 24 | run: npm run lint -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | .pnpm-debug.log* 9 | 10 | # Diagnostic reports (https://nodejs.org/api/report.html) 11 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | build/ 45 | 46 | # Snowpack dependency directory (https://snowpack.dev/) 47 | web_modules/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | .env.production 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | .parcel-cache 81 | 82 | # Next.js build output 83 | .next 84 | out 85 | 86 | # Nuxt.js build / generate output 87 | .nuxt 88 | dist 89 | 90 | # Gatsby files 91 | .cache/ 92 | # Comment in the public line in if your project uses Gatsby and not Next.js 93 | # https://nextjs.org/blog/next-9-1#public-directory-support 94 | # public 95 | 96 | # vuepress build output 97 | .vuepress/dist 98 | 99 | # Serverless directories 100 | .serverless/ 101 | 102 | # FuseBox cache 103 | .fusebox/ 104 | 105 | # DynamoDB Local files 106 | .dynamodb/ 107 | 108 | # TernJS port file 109 | .tern-port 110 | 111 | # Stores VSCode versions used for testing VSCode extensions 112 | .vscode-test 113 | 114 | # yarn v2 115 | .yarn/cache 116 | .yarn/unplugged 117 | .yarn/build-state.yml 118 | .yarn/install-state.gz 119 | .pnp.* 120 | 121 | .DS_Store 122 | 123 | .vscode -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ...require('gts/.prettierrc.json') 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Gajanan Patil 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # Puppeteer IDE Extension 4 | 5 | ![lint](https://github.com/gajananpp/puppeteer-ide-extension/actions/workflows/lint.yml/badge.svg) 6 | ![build](https://github.com/gajananpp/puppeteer-ide-extension/actions/workflows/build.yml/badge.svg) 7 | 8 | A standalone extension to write and execute puppeteer scripts from browser's developer tools. 9 | 10 | [Installation](#installation) • 11 | [Usage](#usage) • 12 | [Screenshots](#screenshots) • 13 | [Build From Source](#build-from-source) • 14 | [Privacy](#privacy) • 15 | [Todo](#todo) • 16 | [FAQs](#faqs) 17 | 18 | Demo GIF 19 | 20 | 21 |
22 | 23 | 24 | 25 | ## Installation 26 | This extension is published on chrome web store. 27 | 28 | [![Add from Chrome web store](assets/chrome-web-store-btn.png)](https://chrome.google.com/webstore/detail/puppeteer-ide/ilehdekjacappgghkgmmlbhgbnlkgoid) 29 | 30 | ## Usage 31 | 32 | This extension will add an extra tab named "Puppeteer IDE" in browser's developer tools from where you can write and execute puppeteer scripts. 33 | 34 | Use [page](https://pptr.dev/#?product=Puppeteer&version=v13.0.0&show=api-class-page) instance variable directly for the tab in which developer tools is opened. 35 | 36 | On clicking `Execute` button, the script will be executed on the inspected tab. 37 | 38 | The script will be auto saved as it is being edited. 39 | 40 | ## Screenshots 41 | 42 | Using `$0` :- 43 | ![Using $0 to get selector](assets/screenshots/$0-usage.gif) 44 | 45 | Dark theme :- 46 | ![Dark theme](assets/screenshots/screen-1.png) 47 | 48 | 49 | Light theme :- 50 | ![Light theme](assets/screenshots/screen-2.png) 51 | 52 | 53 | ## Build From Source 54 | 55 | To build extension from source :- 56 | ``` 57 | git clone https://github.com/gajananpp/puppeteer-ide-extension 58 | 59 | cd puppeteer-ide-extension 60 | 61 | npm install 62 | 63 | npm run dist 64 | ``` 65 | This will output extension in dist folder which you can load in your browser by following this [steps](https://developer.chrome.com/docs/extensions/mv3/getstarted/#:~:text=The%20directory%20holding%20the%20manifest%20file%20can%20be%20added%20as%20an%20extension%20in%20developer%20mode%20in%20its%20current%20state.). 66 | 67 | ## Privacy 68 | This extension is standalone. **It doesn't make any external api calls**. You can inspect network of page/extension and source code in this repo. 69 | 70 | ## Todo 71 | 72 | - [x] Add multi tab/script support. 73 | - [x] Add theme switch. 74 | - [x] Print unhandled errors in console tab of inspected window. 75 | - [x] Suggesting xPath of currently selected element when `$0` typed in editor. 76 | - [ ] Binding keyboard shortcut with script for execution without devtools opened. 77 | - [ ] Adjustable delay in execution. 78 | - [ ] Show used/available chrome storage space. 79 | 80 | 81 | ## FAQs 82 | 83 | **Q: Does this extension have any external dependency ?** 84 |
85 | No. This extension internally uses [chrome.debugger](https://developer.chrome.com/docs/extensions/reference/debugger/) api and is standalone, so there is no requirement of starting browser with remote debugging cli flag or having nodejs or any other service running. 86 | 87 |
88 | 89 | **Q: On which browsers can this extension be installed ?** 90 |
91 | This extension only works with chrome and other chromium based browsers like edge, brave etc. 92 | 93 |
94 | 95 | **Q: Execution stops abruptly when page navigates ?** 96 |
97 | Some other extensions may cause this issue, especially 3rd party extensions which are added by desktop applications. One particular extension is `Adobe Acrobat` which is added by Adobe's desktop application. 98 | You can disable this extension and try again executing. 99 | 100 |
101 | 102 | **Q: From where can this extension be installed ?** 103 |
104 | This extension is published on chrome web store. Click on below button to view it in chrome web store. 105 | 106 | [![Add from Chrome web store](assets/chrome-web-store-btn.png)](https://chrome.google.com/webstore/detail/puppeteer-ide/ilehdekjacappgghkgmmlbhgbnlkgoid) 107 | 108 |
109 | 110 | **Q: How can be puppeteer script executed in extension ?** 111 |
112 | Check out [puppeteer-extension-transport](https://github.com/gajananpp/puppeteer-extension-transport) package. 113 | 114 |
115 | -------------------------------------------------------------------------------- /assets/chrome-web-store-btn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajananpp/puppeteer-ide-extension/ffe2955ef02a1178e4c6f5236eadd0d7a4ccf196/assets/chrome-web-store-btn.png -------------------------------------------------------------------------------- /assets/pptr-ide-extension.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajananpp/puppeteer-ide-extension/ffe2955ef02a1178e4c6f5236eadd0d7a4ccf196/assets/pptr-ide-extension.gif -------------------------------------------------------------------------------- /assets/screenshots/$0-usage.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajananpp/puppeteer-ide-extension/ffe2955ef02a1178e4c6f5236eadd0d7a4ccf196/assets/screenshots/$0-usage.gif -------------------------------------------------------------------------------- /assets/screenshots/screen-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajananpp/puppeteer-ide-extension/ffe2955ef02a1178e4c6f5236eadd0d7a4ccf196/assets/screenshots/screen-1.png -------------------------------------------------------------------------------- /assets/screenshots/screen-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gajananpp/puppeteer-ide-extension/ffe2955ef02a1178e4c6f5236eadd0d7a4ccf196/assets/screenshots/screen-2.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "puppeteer-ide-extension", 3 | "version": "0.0.8", 4 | "description": "Develop, test and execute puppeteer scripts in browser's developer tools", 5 | "files": [ 6 | "build/src" 7 | ], 8 | "author": { 9 | "name": "Gajanan Patil" 10 | }, 11 | "homepage": "https://github.com/gajananpp/puppeteer-ide-extension/blob/main/README.md", 12 | "repository": { 13 | "url": "https://github.com/gajananpp/puppeteer-ide-extension" 14 | }, 15 | "license": "MIT", 16 | "keywords": [ 17 | "puppeteer", 18 | "debugger", 19 | "ide", 20 | "extension", 21 | "automation" 22 | ], 23 | "scripts": { 24 | "test": "echo \"Error: no test specified\" && exit 1", 25 | "prelint": "npm run fix", 26 | "lint": "gts lint", 27 | "clean": "gts clean", 28 | "compile": "webpack --config webpack.config.js", 29 | "postcompile": "npm run cp-assets", 30 | "dist": "cross-env NODE_ENV=production webpack --config webpack.config.js", 31 | "postdist": "npm run cp-assets", 32 | "cp-assets": "cpx \"src/**/*.{html,json,png}\" dist", 33 | "fix": "gts fix", 34 | "prepare": "npm run compile", 35 | "pretest": "npm run compile", 36 | "posttest": "npm run lint" 37 | }, 38 | "devDependencies": { 39 | "@types/chrome": "^0.0.168", 40 | "@types/node": "^14.11.2", 41 | "@types/puppeteer-core": "^5.4.0", 42 | "@types/react": "^17.0.37", 43 | "@types/react-dom": "^17.0.11", 44 | "browserify": "^17.0.0", 45 | "cpx": "^1.5.0", 46 | "cross-env": "^7.0.3", 47 | "css-loader": "^6.5.1", 48 | "gts": "^3.1.0", 49 | "sass": "^1.44.0", 50 | "sass-loader": "^12.4.0", 51 | "style-loader": "^3.3.1", 52 | "ts-loader": "^9.2.6", 53 | "typescript": "^4.5.4", 54 | "webpack": "^5.65.0", 55 | "webpack-cli": "^4.9.1", 56 | "webpack-merge": "^5.8.0" 57 | }, 58 | "dependencies": { 59 | "bootstrap": "^5.1.3", 60 | "monaco-editor": "^0.30.1", 61 | "puppeteer-core": "^13.0.0", 62 | "puppeteer-extension-transport": "^0.0.6", 63 | "react": "^17.0.2", 64 | "react-bootstrap": "^2.1.0", 65 | "react-dom": "^17.0.2", 66 | "react-icons": "^4.3.1", 67 | "react-select": "^5.2.2" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/background.ts: -------------------------------------------------------------------------------- 1 | import {ExtensionDebuggerTransport} from 'puppeteer-extension-transport'; 2 | 3 | export interface ExecutionCommand { 4 | type: 'startExecution' | 'stopExecution'; 5 | tabId: number; 6 | } 7 | 8 | export interface ExecutionEvent { 9 | type: 'executionStarted' | 'executionStopped'; 10 | tabId: number; 11 | } 12 | 13 | export interface CDPCommand { 14 | type: 'cdpCommand'; 15 | command: string; 16 | } 17 | 18 | export interface CDPEvent { 19 | type: 'cdpEvent'; 20 | data: string; 21 | } 22 | 23 | export interface ConnectionEvent { 24 | type: 'connected'; 25 | } 26 | 27 | export interface ConsoleCommand { 28 | type: 'console'; 29 | level: 'log' | 'error'; 30 | args: string; 31 | } 32 | 33 | export type Message = 34 | | ExecutionCommand 35 | | CDPCommand 36 | | ConsoleCommand 37 | | CDPEvent 38 | | ExecutionEvent 39 | | ConnectionEvent; 40 | 41 | interface Connections { 42 | /** key is the stringified tabId */ 43 | [key: string]: chrome.runtime.Port; 44 | } 45 | 46 | const connections: Connections = {}; 47 | 48 | chrome.runtime.onConnect.addListener(port => { 49 | connections[port.name] = port; 50 | 51 | const connectedEvent: ConnectionEvent = {type: 'connected'}; 52 | port.postMessage(connectedEvent); 53 | 54 | port.onMessage.addListener((message: Message) => { 55 | message.type === 'startExecution' 56 | ? DebuggerHandler.create(message.tabId) 57 | : null; 58 | }); 59 | port.onDisconnect.addListener(port => delete connections[port.name]); 60 | }); 61 | 62 | class DebuggerHandler { 63 | private static _debuggerHandler: DebuggerHandler; 64 | 65 | static isExecuting = false; 66 | 67 | transport: ExtensionDebuggerTransport; 68 | tabId: number; 69 | commands: {command: any; response: any}[]; 70 | events: any[]; 71 | 72 | /** 73 | * Starts debugger session, executes incoming cdp commands on target tab 74 | * and emits events/responses back to command sender 75 | * @param tabId - id of the target tab 76 | */ 77 | static async create(tabId: number) { 78 | if (this.isExecuting && this._debuggerHandler) { 79 | this._debuggerHandler._registerListeners(); 80 | return this._debuggerHandler; 81 | } else { 82 | const transport = await ExtensionDebuggerTransport.create(tabId); 83 | this._debuggerHandler = new DebuggerHandler(tabId, transport); 84 | return this._debuggerHandler; 85 | } 86 | } 87 | 88 | constructor(tabId: number, transport: ExtensionDebuggerTransport) { 89 | DebuggerHandler.isExecuting = true; 90 | this.tabId = tabId; 91 | this.transport = transport; 92 | this.transport.delay = 0.05 * 1000; 93 | this.commands = []; 94 | this.events = []; 95 | 96 | this._registerListeners(); 97 | } 98 | 99 | private _registerListeners() { 100 | const port = connections[this.tabId]; 101 | 102 | this.transport.onmessage = message => { 103 | const cdpEvent: CDPEvent = { 104 | type: 'cdpEvent', 105 | data: message, 106 | }; 107 | // send response/instrumentation event back 108 | port?.postMessage(cdpEvent); 109 | const parsedEvent = JSON.parse(message); 110 | if (parsedEvent.id) { 111 | // event is a response if contains `id` property which corresponds to a command 112 | const cmdIdx = this.commands.findIndex( 113 | commandObj => commandObj.command.id === parsedEvent.id 114 | ); 115 | cmdIdx !== -1 ? (this.commands[cmdIdx].response = parsedEvent) : null; 116 | } else { 117 | this.events.push(parsedEvent); 118 | } 119 | }; 120 | 121 | this.transport.onclose = () => { 122 | DebuggerHandler.isExecuting = false; 123 | this._unregisterListeners(); 124 | const executionEvent: ExecutionEvent = { 125 | type: 'executionStopped', 126 | tabId: this.tabId, 127 | }; 128 | port?.postMessage(executionEvent); 129 | console.log('CDP LOGS'); 130 | console.log({ 131 | commands: this.commands, 132 | events: this.events, 133 | }); 134 | }; 135 | 136 | this._incomingMessageHandler = this._incomingMessageHandler.bind(this); 137 | port?.onMessage.addListener(this._incomingMessageHandler); 138 | const executionEvent: ExecutionEvent = { 139 | type: 'executionStarted', 140 | tabId: this.tabId, 141 | }; 142 | port?.postMessage(executionEvent); 143 | } 144 | 145 | private _unregisterListeners() { 146 | this.transport.onmessage = undefined; 147 | this.transport.onclose = undefined; 148 | connections[this.tabId]?.onMessage.removeListener( 149 | this._incomingMessageHandler 150 | ); 151 | } 152 | 153 | private _incomingMessageHandler(message: Message) { 154 | if (message.type === 'cdpCommand') { 155 | // pass command to chrome.debugger 156 | this.transport.send(message.command); 157 | this.commands.push({ 158 | command: JSON.parse(message.command), 159 | response: {}, 160 | }); 161 | } else if (message.type === 'stopExecution') { 162 | this.transport.close(); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /src/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/devtools/devtools.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds Puppeteer IDE panel in devtools window. 3 | */ 4 | function addIDEPanel() { 5 | chrome.devtools.panels.create( 6 | 'Puppeteer IDE', 7 | 'devtools/idePanel/pptr.png', 8 | 'devtools/idePanel/idePanel.html', 9 | () => {} 10 | ); 11 | } 12 | 13 | addIDEPanel(); 14 | -------------------------------------------------------------------------------- /src/devtools/idePanel/components/ActionBar.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useState} from 'react'; 2 | import Nav from 'react-bootstrap/Nav'; 3 | import Navbar from 'react-bootstrap/Navbar'; 4 | import Container from 'react-bootstrap/Container'; 5 | import OverlayTrigger from 'react-bootstrap/OverlayTrigger'; 6 | import Tooltip from 'react-bootstrap/Tooltip'; 7 | import {FaPlay, FaPlus, FaCog, FaStop} from 'react-icons/fa'; 8 | 9 | import {IDEContext} from './IDEContext'; 10 | import {Script} from '../extensionReducer'; 11 | import {AddScriptDialog} from './AddScriptDialog'; 12 | import {ThemeSwitch} from './ThemeSwitch'; 13 | import {ScriptSettingDialog} from './ScriptSettingDialog'; 14 | import {ScriptSelect} from './ScriptSelect'; 15 | 16 | const CTRL_KEY = navigator.userAgent.includes('Windows') ? 'Ctrl' : '⌘'; 17 | // const SHIFT_KEY = navigator.userAgent.includes('Windows') ? 'Shift' : '⇧'; 18 | 19 | interface ActionBarProps { 20 | /** Execution triggerer */ 21 | execute: () => void; 22 | /** Execution stop triggerer */ 23 | stop: () => void; 24 | /** Puppeteer scripts */ 25 | scripts: Script[]; 26 | /** Current active tab */ 27 | activeTab?: { 28 | /** ID of the script opened in active tab */ 29 | scriptId: number; 30 | }; 31 | /** Execution status */ 32 | isExecuting: boolean; 33 | } 34 | 35 | export const ActionBar = (props: ActionBarProps) => { 36 | const {theme} = useContext(IDEContext); 37 | 38 | const [showAddScript, setShowAddScript] = useState(false); 39 | const openAddScriptDialog = () => setShowAddScript(true); 40 | const closeAddScriptDialog = () => setShowAddScript(false); 41 | 42 | const [scriptSetting, setScriptSetting] = useState({ 43 | show: false, 44 | script: props.scripts[0], 45 | }); 46 | const openScriptSettingDialog = () => { 47 | const script = props.scripts.find( 48 | script => script.id === props.activeTab?.scriptId 49 | ); 50 | if (props.activeTab && script) { 51 | setScriptSetting({ 52 | show: true, 53 | script: script, 54 | }); 55 | } 56 | }; 57 | const closeScriptSettingDialog = () => { 58 | setScriptSetting({ 59 | ...scriptSetting, 60 | show: false, 61 | }); 62 | }; 63 | 64 | // register shortcuts 65 | useEffect(() => { 66 | const shortcuts = (evt: KeyboardEvent) => { 67 | if (evt.ctrlKey || evt.metaKey) { 68 | switch (evt.key) { 69 | case '1': 70 | openAddScriptDialog(); 71 | break; 72 | 73 | case '2': 74 | props.isExecuting ? props.stop() : props.execute(); 75 | break; 76 | 77 | case '3': 78 | openScriptSettingDialog(); 79 | break; 80 | } 81 | } 82 | }; 83 | document.addEventListener('keydown', shortcuts); 84 | return () => document.removeEventListener('keydown', shortcuts); 85 | }, [props]); 86 | 87 | const NavLinkWrapper = (props: { 88 | children: (JSX.Element | string)[]; 89 | title: JSX.Element | string; 90 | }) => { 91 | return ( 92 | {props.title}} 95 | > 96 | 97 | {props.children} 98 | 99 | 100 | ); 101 | }; 102 | 103 | return ( 104 | 110 | 111 | 135 | 150 | 151 | 155 | {scriptSetting.script ? ( 156 | 161 | ) : null} 162 | 163 | ); 164 | }; 165 | -------------------------------------------------------------------------------- /src/devtools/idePanel/components/AddScriptDialog.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState} from 'react'; 2 | import Form from 'react-bootstrap/Form'; 3 | import Modal from 'react-bootstrap/Modal'; 4 | import Button from 'react-bootstrap/Button'; 5 | 6 | import {initialScript} from '../idePanel'; 7 | import {IDEContext} from './IDEContext'; 8 | 9 | interface AddScriptDialogProps { 10 | show: boolean; 11 | closeDialog: () => void; 12 | } 13 | 14 | export const AddScriptDialog = (props: AddScriptDialogProps) => { 15 | const [scriptTitle, setScriptTitle] = useState(''); 16 | 17 | const {dispatch, theme} = useContext(IDEContext); 18 | 19 | return ( 20 | 26 | 30 | Add Script 31 | 32 | 33 |
34 | setScriptTitle(evt.target.value)} 39 | /> 40 | 41 |
42 | 43 | 46 | 64 | 65 |
66 | ); 67 | }; 68 | -------------------------------------------------------------------------------- /src/devtools/idePanel/components/Editor.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useEffect, useRef, useState} from 'react'; 2 | import * as monaco from 'monaco-editor'; 3 | import {IDEContext} from './IDEContext'; 4 | 5 | interface EditorProps { 6 | /** On editor value change handler */ 7 | onChange: (value: string) => void; 8 | /** Monaco editor model */ 9 | model: monaco.editor.ITextModel; 10 | /** Custom actions */ 11 | actions: monaco.editor.IActionDescriptor[]; 12 | } 13 | 14 | (self as any).MonacoEnvironment = { 15 | getWorkerUrl: function (_moduleId: any, label: string) { 16 | return label === 'javascript' || label === 'typescript' 17 | ? 'ts.worker.js' 18 | : 'editor.worker.js'; 19 | }, 20 | }; 21 | 22 | /** 23 | * VS Code's monaco editor as a react component. 24 | * Click [here](https://github.com/microsoft/monaco-editor) for more info about monaco-editor 25 | * 26 | * @param props - {@link EditorProps} 27 | * @returns Editor Component 28 | */ 29 | export const Editor = (props: EditorProps) => { 30 | const editorContainer = useRef(null); 31 | const [editor, setEditor] = 32 | useState(null); 33 | 34 | const {theme} = useContext(IDEContext); 35 | 36 | useEffect(() => { 37 | if (editorContainer.current) { 38 | const editor = monaco.editor.create(editorContainer.current, { 39 | model: props.model, 40 | theme: theme === 'light' ? 'vs' : 'vs-dark', 41 | }); 42 | setEditor(editor); 43 | editor.onDidChangeModelContent(() => props.onChange(editor.getValue())); 44 | 45 | const windowResizeHandler = () => { 46 | editor.layout(); 47 | }; 48 | 49 | window.addEventListener('resize', windowResizeHandler); 50 | return () => { 51 | window.removeEventListener('resize', windowResizeHandler); 52 | editor.dispose(); 53 | }; 54 | } else { 55 | return () => {}; 56 | } 57 | }, []); 58 | 59 | useEffect(() => { 60 | props.actions.forEach(action => editor?.addAction(action)); 61 | }, [editor]); 62 | 63 | useEffect(() => { 64 | editor?.setModel(props.model); 65 | }, [editor, props.model]); 66 | 67 | useEffect(() => { 68 | monaco.editor.setTheme(theme === 'light' ? 'vs' : 'vs-dark'); 69 | }, [theme]); 70 | 71 | return
; 72 | }; 73 | -------------------------------------------------------------------------------- /src/devtools/idePanel/components/EditorTabs.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext, useState} from 'react'; 2 | import Nav from 'react-bootstrap/Nav'; 3 | import * as monaco from 'monaco-editor'; 4 | import Container from 'react-bootstrap/Container'; 5 | import {FaTimes} from 'react-icons/fa'; 6 | 7 | import {IDEContext} from './IDEContext'; 8 | import {Script} from '../extensionReducer'; 9 | 10 | export interface EditorTabsProps { 11 | /** Tabs in editor */ 12 | tabs: { 13 | /** ID of the script opened in given tab */ 14 | scriptId: number; 15 | /** Script's model */ 16 | model: monaco.editor.ITextModel; 17 | }[]; 18 | /** Helper to get script by id from stored scripts */ 19 | getScriptById: (scriptId: number) => Script | undefined; 20 | /** Index of the active tab */ 21 | activeTab: number; 22 | } 23 | 24 | export const EditorTabs = (props: EditorTabsProps) => { 25 | const {dispatch, theme} = useContext(IDEContext); 26 | 27 | const TabTitle = (tabTitleProps: {children: string; eventKey: number}) => { 28 | const isActive = props.activeTab === tabTitleProps.eventKey; 29 | const [closeIconStyle, setCloseIconStyle] = useState({ 30 | display: isActive ? 'inline' : 'none', 31 | }); 32 | 33 | return ( 34 | setCloseIconStyle({display: 'inline'})} 36 | onMouseLeave={() => { 37 | if (!isActive) setCloseIconStyle({display: 'none'}); 38 | }} 39 | className="ps-0 pe-0" 40 | eventKey={tabTitleProps.eventKey} 41 | > 42 | 46 | 47 | {tabTitleProps.children} 48 | 49 | { 52 | evt.stopPropagation(); 53 | dispatch({ 54 | type: 'removeTab', 55 | tabNumber: tabTitleProps.eventKey, 56 | }); 57 | }} 58 | > 59 | 60 | 61 | 62 | 63 | ); 64 | }; 65 | 66 | return ( 67 | 88 | ); 89 | }; 90 | -------------------------------------------------------------------------------- /src/devtools/idePanel/components/IDEContext.ts: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react'; 2 | import {ExtensionAction} from '../extensionReducer'; 3 | 4 | export interface IDEContextProps { 5 | /** port connected to service worker */ 6 | port: chrome.runtime.Port | null; 7 | /** port updater */ 8 | setPort: React.Dispatch>; 9 | /** extension state action dispatcher */ 10 | dispatch: (action: ExtensionAction) => void; 11 | /** ide theme */ 12 | theme: 'light' | 'dark'; 13 | } 14 | 15 | export const IDEContext = createContext({ 16 | port: null, 17 | setPort: () => {}, 18 | dispatch: (action: ExtensionAction) => action, 19 | theme: chrome.devtools?.panels?.themeName === 'default' ? 'light' : 'dark', 20 | }); 21 | -------------------------------------------------------------------------------- /src/devtools/idePanel/components/ScriptSelect.tsx: -------------------------------------------------------------------------------- 1 | import React, {useContext} from 'react'; 2 | import Select, {StylesConfig} from 'react-select'; 3 | import {Script} from '../extensionReducer'; 4 | import {IDEContext} from './IDEContext'; 5 | 6 | interface ScriptSelectProps { 7 | scripts: Script[]; 8 | activeTab?: { 9 | scriptId: number; 10 | }; 11 | } 12 | 13 | interface Option { 14 | label: string; 15 | value: number; 16 | id: number; 17 | } 18 | 19 | export const ScriptSelect = (props: ScriptSelectProps) => { 20 | const {theme, dispatch} = useContext(IDEContext); 21 | 22 | const bgColor = theme === 'light' ? '#ffffff' : '#3c3c3c'; 23 | const fontColor = theme === 'light' ? '#767676' : '#8d8d8e'; 24 | const inputFontColor = theme === 'light' ? '#616161' : '#cccccc'; 25 | 26 | const selectStyles: StylesConfig = { 27 | control: provided => ({ 28 | ...provided, 29 | backgroundColor: bgColor, 30 | borderColor: bgColor, 31 | }), 32 | indicatorSeparator: provided => ({ 33 | ...provided, 34 | backgroundColor: fontColor, 35 | }), 36 | dropdownIndicator: provided => ({ 37 | ...provided, 38 | color: fontColor, 39 | }), 40 | input: provided => ({ 41 | ...provided, 42 | color: inputFontColor, 43 | }), 44 | singleValue: provided => ({ 45 | ...provided, 46 | color: inputFontColor, 47 | }), 48 | menuList: provided => ({ 49 | ...provided, 50 | backgroundColor: bgColor, 51 | }), 52 | option: (provided, {isFocused, isSelected}) => ({ 53 | ...provided, 54 | color: isSelected || isFocused ? '#ffffff' : fontColor, 55 | backgroundColor: 56 | isSelected || isFocused ? '#017bcc' : provided.backgroundColor, 57 | }), 58 | container: provided => ({ 59 | ...provided, 60 | width: '20rem', 61 | }), 62 | placeholder: provided => ({ 63 | ...provided, 64 | color: theme === 'light' ? '#767676' : '#8d8d8e', 65 | }), 66 | }; 67 | 68 | const options = props.scripts.map((script, idx) => ({ 69 | value: idx, 70 | label: script.name, 71 | id: script.id, 72 | })); 73 | 74 | return ( 75 | element 961 | * matching selector, the method throws an error. 962 | * @param values Values of options to select. If the