├── .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 | 
6 | 
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 |

19 |
20 |
21 |
22 |
23 |
24 |
25 | ## Installation
26 | This extension is published on chrome web store.
27 |
28 | [](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 | 
44 |
45 | Dark theme :-
46 | 
47 |
48 |
49 | Light theme :-
50 | 
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 | [](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 | 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 |