├── images ├── logo128.png ├── logo16.png ├── logo32.png └── logo48.png ├── .github └── FUNDING.yml ├── js ├── load.js ├── main.js ├── settings.js └── common.js ├── .gitignore ├── README.md ├── index.html ├── manifest.json ├── LICENSE ├── settings.html └── css └── style.css /images/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjihrig/node-v8-inspector/HEAD/images/logo128.png -------------------------------------------------------------------------------- /images/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjihrig/node-v8-inspector/HEAD/images/logo16.png -------------------------------------------------------------------------------- /images/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjihrig/node-v8-inspector/HEAD/images/logo32.png -------------------------------------------------------------------------------- /images/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cjihrig/node-v8-inspector/HEAD/images/logo48.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [cjihrig] 4 | patreon: cjihrig 5 | custom: https://www.paypal.me/cjihrig/5 6 | -------------------------------------------------------------------------------- /js/load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | setBrowserClickAction(); 4 | 5 | 6 | function launch (options) { 7 | launchDevTools(options) 8 | .catch(function errorHandler (err) { 9 | alert(`Could not launch debugger.\n${err.message}`); 10 | }); 11 | } 12 | 13 | 14 | chrome.runtime.onMessage.addListener(launch); 15 | 16 | 17 | chrome.browserAction.onClicked.addListener(function onClick () { 18 | loadOptions(launch); 19 | }); 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # OS X Garbage 36 | .DS_Store 37 | */.DS_Store 38 | */*/.DS_Store 39 | 40 | # Any zip files of releases 41 | *.zip 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![node-v8-inspector](https://github.com/continuationlabs/node-v8-inspector/raw/master/images/logo128.png) 2 | 3 | # node-v8-inspector 4 | 5 | Chrome extension for launching V8 Inspector for Node.js debugging. 6 | 7 | ## Usage 8 | 9 | 1. Install the `node-v8-inspector` extension from the [Chrome Web Store](https://chrome.google.com/webstore/detail/nodejs-v8-inspector/lfnddfpljnhbneopljflpombpnkfhggl). 10 | 2. Run a Node.js application with the `--inspect` command line flag. 11 | 3. Open the `node-v8-inspector` extension in Chrome. 12 | 4. Verify that `host` and `port` match your application's host and debug port. The default value is 9229, the same default used by Node.js. The host defaults to `localhost`. 13 | 5. Press `Launch V8 Inspector` button. 14 | 6. Debug your application. 15 | 7. Profit (optional). 16 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node.js V8 Inspector 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 | 13 |
14 |
15 | 16 | 17 |
18 |
19 |
20 | Launch V8 Inspector 21 |
22 |
23 |
24 |
25 |
26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.js V8 Inspector", 3 | "version": "0.12.0", 4 | "manifest_version": 2, 5 | "description": "Extension for launching V8 Inspector for Node.js debugging", 6 | "author": "Continuation Labs (http://continuation.io/)", 7 | "background": { 8 | "scripts": ["js/common.js", "js/load.js"], 9 | "persistent": false 10 | }, 11 | "browser_action": { 12 | "default_icon": "images/logo32.png" 13 | }, 14 | "icons": { 15 | "16": "images/logo16.png", 16 | "32": "images/logo32.png", 17 | "48": "images/logo48.png", 18 | "128": "images/logo128.png" 19 | }, 20 | "permissions": ["http://*/", "storage", "notifications"], 21 | "options_page": "settings.html", 22 | "commands": { 23 | "_execute_browser_action": { 24 | "suggested_key": { 25 | "windows": "Ctrl+Shift+8", 26 | "mac": "Command+Shift+8", 27 | "chromeos": "Ctrl+Shift+8", 28 | "linux": "Ctrl+Shift+8" 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /js/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.addEventListener('DOMContentLoaded', function onLoad() { 4 | loadOptions(function loadCb (options) { 5 | host.value = options.host; 6 | host.placeholder = options.host; 7 | port.value = options.port; 8 | port.placeholder = options.port; 9 | 10 | launch.addEventListener('click', function onClick() { 11 | const settings = { 12 | host: host.value, 13 | port: port.value, 14 | poll: options.poll 15 | }; 16 | 17 | if (options.poll === true) { 18 | // If polling mode is enabled, send the work to the background page. 19 | window.close(); 20 | chrome.runtime.sendMessage(settings); 21 | } else { 22 | error.style.display = 'none'; 23 | 24 | launchDevTools(settings) 25 | .catch(function errorHandler (err) { 26 | error.innerHTML = `Could not launch debugger
${err.message}`; 27 | error.style.display = 'block'; 28 | }); 29 | } 30 | }, false); 31 | }); 32 | }, false); 33 | -------------------------------------------------------------------------------- /js/settings.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | document.addEventListener('DOMContentLoaded', function onLoad () { 4 | let saveStatus = document.getElementById('saveStatus'); 5 | 6 | function clearSaveStatus () { 7 | saveStatus.textContent = ''; 8 | } 9 | 10 | loadOptions(function loadCb (options) { 11 | host.value = options.host; 12 | host.placeholder = options.host; 13 | port.value = options.port; 14 | port.placeholder = options.port; 15 | poll.checked = options.poll; 16 | defaults.checked = options.defaults; 17 | 18 | save.addEventListener('click', function onClick () { 19 | storeOptions({ 20 | host: host.value, 21 | port: port.value, 22 | poll: poll.checked, 23 | defaults: defaults.checked 24 | }, function storeCb () { 25 | setBrowserClickAction(); 26 | // Update status to let user know options were saved. 27 | saveStatus.textContent = 'Options saved.'; 28 | setTimeout(clearSaveStatus, 750); 29 | }); 30 | }, false); 31 | }); 32 | }, false); 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Continuation Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /settings.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node.js V8 Inspector Settings 5 | 6 | 7 | 8 |
9 |

Node.js V8 Inspector Settings

10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 |
22 |
23 | 24 |
25 |
26 |
27 | Save 28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | min-width: 300px; 7 | font-size: 18px; 8 | } 9 | 10 | form { 11 | display: flex; 12 | flex-direction: column; 13 | flex-wrap: nowrap; 14 | justify-content: flex-start; 15 | align-content: stretch; 16 | align-items: center; 17 | } 18 | 19 | label { 20 | font-size: 20px; 21 | } 22 | 23 | input[type=text], 24 | input[type=number] { 25 | display: block; 26 | margin: 0; 27 | font-family: sans-serif; 28 | font-size: 18px; 29 | appearance: none; 30 | box-shadow: none; 31 | border-radius: none; 32 | width: 75%; 33 | } 34 | 35 | input[type=text]:focus, 36 | input[type=number]:focus, 37 | input[type=checkbox]:focus { 38 | outline: none; 39 | } 40 | 41 | input[type=number]::-webkit-inner-spin-button, 42 | input[type=number]::-webkit-outer-spin-button { 43 | -webkit-appearance: none; 44 | margin: 0; 45 | } 46 | 47 | input[type=checkbox] { 48 | zoom: 2.5 49 | } 50 | 51 | label.form-label { 52 | margin-right: 15px; 53 | width: 25%; 54 | } 55 | 56 | div.button { 57 | background: #3498db; 58 | background-image: linear-gradient(to bottom, #3498db, #2980b9); 59 | box-shadow: 0px 1px 3px #666666; 60 | color: #ffffff; 61 | font-size: 20px; 62 | padding: 10px 20px 10px 20px; 63 | text-decoration: none; 64 | text-align: center; 65 | cursor: pointer; 66 | } 67 | 68 | div.button:hover { 69 | background: #3cb0fd; 70 | background-image: linear-gradient(to bottom, #3cb0fd, #3498db); 71 | text-decoration: none; 72 | } 73 | 74 | #error { 75 | color: #a94442; 76 | background-color: #f2dede; 77 | border-color: #ebcccc; 78 | padding: 15px; 79 | margin-bottom: 1rem; 80 | border: 1px solid transparent; 81 | border-radius: .25rem; 82 | display: none; 83 | flex: 1 1 auto; 84 | } 85 | 86 | .field { 87 | padding: 10px; 88 | border: solid 5px #c9c9c9; 89 | transition: border 0.3s; 90 | flex: 1 1 auto; 91 | } 92 | .field:focus, 93 | .field.focus { 94 | border: solid 5px #969696; 95 | } 96 | 97 | .flexRow { 98 | display: flex; 99 | flex-direction: row; 100 | flex-wrap: nowrap; 101 | justify-content: flex-start; 102 | align-content: stretch; 103 | align-items: center; 104 | order: 0; 105 | flex: 0 1 auto; 106 | align-self: auto; 107 | margin: 5px; 108 | } 109 | 110 | div.container { 111 | margin: 20px auto; 112 | border: 1px solid rgba(0, 0, 0, 0.15); 113 | padding: 10px; 114 | display: flex; 115 | flex-direction: column; 116 | flex-wrap: nowrap; 117 | justify-content: center; 118 | align-content: flex-start; 119 | align-items: stretch; 120 | max-width: 800px; 121 | } 122 | -------------------------------------------------------------------------------- /js/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const defaultHost = 'localhost'; 4 | const defaultPort = 9229; 5 | const defaultPoll = false; 6 | const launchDefaults = false; 7 | const notificationId = 'pollForDevTools'; 8 | const interval = 500; 9 | let openedInspectorTab = null; 10 | let notificationActive = false; 11 | let timeoutId; 12 | 13 | 14 | function storeOptions (options, callback) { 15 | const settings = Object.assign({ 16 | host: defaultHost, 17 | port: defaultPort, 18 | poll: defaultPoll, 19 | defaults: launchDefaults 20 | }, options); 21 | 22 | chrome.storage.sync.set(settings, callback); 23 | } 24 | 25 | 26 | function loadOptions (callback) { 27 | chrome.storage.sync.get({ 28 | host: defaultHost, 29 | port: defaultPort, 30 | poll: defaultPoll, 31 | defaults: launchDefaults 32 | }, callback); 33 | } 34 | 35 | 36 | function setBrowserClickAction () { 37 | loadOptions(function loadCb (options) { 38 | if (options.defaults === true) { 39 | chrome.browserAction.setPopup({ popup: '' }); 40 | } else { 41 | chrome.browserAction.setPopup({ popup: 'index.html' }); 42 | } 43 | }); 44 | } 45 | 46 | 47 | function parseJson (response) { 48 | if (response.status !== 200) { 49 | throw new Error(`Invalid configuration data at ${response.url}`); 50 | } 51 | 52 | return response.json(); 53 | } 54 | 55 | 56 | function openInspectorTab (data, options) { 57 | return new Promise((resolve, reject) => { 58 | // The replace is for older versions. For newer versions, it is a no-op. 59 | const devtoolsFrontendUrl = data[0].devtoolsFrontendUrl.replace( 60 | /^https:\/\/chrome-devtools-frontend\.appspot\.com/i, 61 | 'chrome-devtools://devtools/remote' 62 | ); 63 | 64 | const url = new URL(devtoolsFrontendUrl); 65 | const wsUrl = new URL(data[0].webSocketDebuggerUrl); 66 | 67 | // Update the WebSocket URL with the host and port options. Then, update 68 | // the DevTools URL with the new WebSocket URL. Also strip the protocol. 69 | wsUrl.hostname = options.host; 70 | wsUrl.port = options.port; 71 | url.searchParams.set('ws', wsUrl.toString().replace('ws://', '')); 72 | 73 | chrome.tabs.create({ 74 | // Without decoding 'ws', DevTools won't load the source files properly. 75 | url: decodeURIComponent(url.toString()) 76 | }, function onTabCreated(tab) { 77 | openedInspectorTab = tab; 78 | resolve(openedInspectorTab); 79 | }); 80 | }); 81 | } 82 | 83 | 84 | function closeInspectorTab () { 85 | if (openedInspectorTab) { 86 | // No callback, clear flag in chrome.tabs.onRemoved(). 87 | chrome.tabs.remove(openedInspectorTab.id); 88 | } 89 | } 90 | 91 | 92 | function addNotification () { 93 | if (!notificationActive) { 94 | chrome.notifications.create(notificationId, { 95 | type: 'basic', 96 | title: 'Node.js V8 Inspector', 97 | message: 'Polling for debug server. Dismiss to stop polling.', 98 | isClickable: true, 99 | requireInteraction: true, 100 | iconUrl: 'images/logo32.png' 101 | }, function onNotificationCreated () { 102 | notificationActive = true; 103 | }); 104 | } 105 | } 106 | 107 | 108 | function removeNotification () { 109 | if (notificationActive) { 110 | // No callback. Reset the flag in the onClosed listener. 111 | chrome.notifications.clear(notificationId); 112 | } 113 | } 114 | 115 | 116 | // Promise an openInspectorTab or reject if the devtools is not open. 117 | function openDevTools (options) { 118 | const jsonUrl = `http://${options.host}:${options.port}/json/list`; 119 | 120 | return fetch(jsonUrl) 121 | .then(function startedDebugging (response) { 122 | return parseJson(response) 123 | .then(function onJson (data) { 124 | // Server is active, do we need to openInspector? 125 | if (!openedInspectorTab) { 126 | return openInspectorTab(data, options); 127 | } else { 128 | return Promise.resolve(openInspectorTab); 129 | } 130 | }); 131 | }); 132 | } 133 | 134 | 135 | // Polling user experience loop: notification, poll, openInspector, repeat. 136 | function pollForDevTools (options) { 137 | // If already polling, do not start again. 138 | if (timeoutId) { 139 | return Promise.resolve('polling'); 140 | } 141 | 142 | let tabIdOpenedByPolling = null; 143 | 144 | timeoutId = setInterval(() => { 145 | // Fetch every time and open inspector only if it is not open. 146 | openDevTools(options) 147 | .then(function readyForDebugging (openInspectorTab) { 148 | // If the inspector was opened, remove the notification. 149 | if (openInspectorTab) { 150 | removeNotification(); 151 | // Remember which tab was opened. 152 | tabIdOpenedByPolling = openedInspectorTab.id 153 | } 154 | }) 155 | .catch(function noAnswer () { 156 | // Server is not active. Assume the session is over. 157 | // Forget out tabId first, so we don't stop polling. 158 | tabIdOpenedByPolling = null; 159 | closeInspectorTab(); 160 | // Tell the user we are ready for another session. 161 | addNotification(); 162 | }); 163 | }, interval); 164 | 165 | // If polling opened the inspector, stop polling on close. 166 | chrome.tabs.onRemoved.addListener(function onTabsRemoved (tabId) { 167 | if (tabIdOpenedByPolling && tabId === tabIdOpenedByPolling) { 168 | if (timeoutId) { 169 | clearInterval(timeoutId); 170 | timeoutId = null; 171 | } 172 | 173 | tabIdOpenedByPolling = null; 174 | } 175 | }); 176 | 177 | return Promise.resolve('polling'); 178 | } 179 | 180 | 181 | function launchDevTools (options) { 182 | if (options.poll) { 183 | return pollForDevTools(options); 184 | } 185 | 186 | return openDevTools(options); 187 | } 188 | 189 | 190 | chrome.tabs.onRemoved.addListener(function onTabsRemoved (tabId) { 191 | if (openedInspectorTab && tabId === openedInspectorTab.id) { 192 | openedInspectorTab = null; 193 | } 194 | }); 195 | 196 | 197 | chrome.notifications.onClosed.addListener(function onNotificationCleared () { 198 | notificationActive = false; 199 | }); 200 | 201 | 202 | // If the user is done debugging, stop polling. 203 | chrome.notifications.onClicked.addListener(function onNotificationClicked (id) { 204 | if (id === notificationId) { 205 | clearInterval(timeoutId); 206 | timeoutId = null; 207 | removeNotification(); 208 | } 209 | }); 210 | --------------------------------------------------------------------------------