diff --git a/index.html b/index.html index 7bb2393..5776ef7 100644 --- a/index.html +++ b/index.html @@ -272,6 +272,9 @@
+ diff --git a/lib/devtools.js b/lib/devtools.js index d6b8df0..49f9741 100644 --- a/lib/devtools.js +++ b/lib/devtools.js @@ -1,11 +1,13 @@ /* eslint-env browser */ import http from 'http' import { WebSocketServer } from 'ws' -import { Session } from '@holepunchto/pear-inspect' +import { Session } from 'pear-inspect' import b4a from 'b4a' +import { spawn } from 'child_process' customElements.define('developer-tooling', class extends HTMLElement { router = null + port = 9342 constructor () { super() @@ -17,33 +19,57 @@ customElements.define('developer-tooling', class extends HTMLElement { width: 100%; } - #add-key-error { + #add-key-error, + #change-port-error { color: red; } - #no-apps.hidden { + #server-message:hover #change-port-show { + display: inline-block; + } + #change-port-show { + display: none; + } + + .hidden { display: none; } .app { display: flex; align-items: center; + padding: 0.25rem; + padding-top: 0.1rem; + padding-bottom: 0.15rem; } .app .title { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .app:hover .copy, + .app:hover .open-in-chrome, .app:hover .remove { display: block; } + .app .copy, + .app .open-in-chrome { + margin-right: 10px; + } + .app .copy, + .app .open-in-chrome, .app .remove { - cursor: pointer; - padding-right: 10px; - margin-left: calc(-1rem - 10px); - width: 1rem; display: none; } + .button { + cursor: pointer; + background: #3a4816; + color: #efeaea; + padding: 0 0.25rem; + font-family: 'overpass-mono'; + border-radius: 1px; + white-space: nowrap; + } #remote-inspect-explain { float:left; max-width:55%; @@ -52,9 +78,13 @@ customElements.define('developer-tooling', class extends HTMLElement { float:right; max-width:40%; } + #server-message { + font-size: 0.8rem; + margin-top: 30px; + } h2 { margin: 0 } - p { + p { margin-block-start: 0.75em; margin-block-end: 0.75em; } @@ -93,25 +123,25 @@ customElements.define('developer-tooling', class extends HTMLElement {

Developer Tooling

-
+

Remotely inspect Pear applications.

-

Some application setup is required to enable remote debugging

Install the pear-inspect module into the application

npm install pear-inspect

Add the following code to the application, before any other code:

-
if (Pear.config.dev) {
+
if (Pear.config.dev) {
   const { Inspector } = await import('pear-inspect')
   const inpector = await new Inspector()
   const key = await inpector.enable()
-  console.log('Pear Inspector key:', key.toString('hex'))
+  console.log('Debug with pear://runtime/devtools/'
+    + key.toString('hex'))
 }

When the application is opened in development mode the inspection key will be logged.

-

Paste the logged key into the input and use a compatible inspect protocol tool, such as chrome://inspect to view the remote target

-
-
+

Paste the logged key into the input to add it. Then open in Chrome or copy the URL to a compatible inspect tool to view the remote target.

+
+
@@ -121,8 +151,24 @@ customElements.define('developer-tooling', class extends HTMLElement {

Apps

No apps added. Add an inspect key to start debugging.

+
+
` this.root = this.attachShadow({ mode: 'open' }) @@ -131,52 +177,134 @@ customElements.define('developer-tooling', class extends HTMLElement { this.addKeyFormElem = this.root.querySelector('#add-key-form') this.addKeyInputElem = this.root.querySelector('#add-key-input') this.addKeyErrorElem = this.root.querySelector('#add-key-error') + this.changePortFormElem = this.root.querySelector('#change-port-form') + this.changePortInputElem = this.root.querySelector('#change-port-input') + this.changePortErrorElem = this.root.querySelector('#change-port-error') + this.changePortShowElem = this.root.querySelector('#change-port-show') this.appsElem = this.root.querySelector('#apps') this.noAppsElem = this.root.querySelector('#no-apps') + this.changePortElem = this.root.querySelector('#change-port') this.apps = new Map() - this.addKeyInputElem.addEventListener('keypress', e => { + this.addKeyInputElem.addEventListener('keydown', e => { this.addKeyErrorElem.textContent = '' }) - this.addKeyFormElem.addEventListener('submit', e => { e.preventDefault() const inspectorKey = this.addKeyInputElem.value - if (inspectorKey.length !== 64) { - this.addKeyErrorElem.textContent = 'Key needs to be 64 characters long' - return - } - - const sessionId = generateUuid() - const inspectorSession = new Session({ inspectorKey: b4a.from(inspectorKey, 'hex') }) - const app = { - inspectorKey, - title: '', - url: '', - inspectorSession - } - - inspectorSession.on('close', () => { - this.apps.delete(sessionId) - this.renderApps() - }) - inspectorSession.on('info', ({ filename }) => { - app.url = filename - app.title = filename.split('/').pop() - this.apps.set(sessionId, app) - this.renderApps() - }) - - this.addKeyInputElem.value = '' - this.addKeyErrorElem.textContent = '' - this.renderApps() + this.addApp(inspectorKey) }) - const devtoolsHttpServer = http.createServer() + this.changePortInputElem.addEventListener('keydown', e => { + const isEscape = e.key === 'Escape' + this.changePortErrorElem.textContent = '' + if (isEscape) { + this.changePortFormElem.classList.add('hidden') + this.changePortInputElem.value = '' + } + }) + this.changePortShowElem.addEventListener('click', () => { + this.changePortFormElem.classList.remove('hidden') + }) + this.changePortFormElem.addEventListener('submit', e => { + e.preventDefault() + this.port = Number(this.changePortInputElem.value) + this.initServer() + }) + + + const shouldLoadApp = Pear.config.linkData?.startsWith('devtools/') + if (shouldLoadApp) { + this.addApp(Pear.config.linkData) + } + + this.initServer() + } + + render () { + this.appsElem.replaceChildren(...[...this.apps].map(([sessionId, app]) => { + const div = document.createElement('div') + div.innerHTML = ` +
+
${app.title} (${app.url})
+
Copy URL
+
Open in Chrome
+
+
+ ` + div.querySelector('.copy').addEventListener('click', () => { + navigator.clipboard.writeText(`devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:${this.port}/${sessionId}`) + }) + div.querySelector('.open-in-chrome').addEventListener('click', () => { + openChrome(`devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:${this.port}/${sessionId}`) + }) + div.querySelector('.remove').addEventListener('click', () => { + this.apps.delete(sessionId) + this.render() + }) + + return div + })) + + if (this.apps.size > 0) { + this.noAppsElem.classList.add('hidden') + } else { + this.noAppsElem.classList.remove('hidden') + } + + if (this.port) { + this.root.querySelector('#server-location').textContent = `http://localhost:${this.port}` + this.root.querySelector('#server-message').classList.remove('hidden') + } + } + + addApp (inspectorKey) { + const isUrl = inspectorKey.includes('devtools/') + if (isUrl) inspectorKey = inspectorKey.split('devtools/').pop() + const isIncorrectLength = inspectorKey.length !== 64 + if (isIncorrectLength) { + this.addKeyErrorElem.textContent = 'Key needs to be 64 characters long' + return + } + + const sessionId = generateUuid() + const inspectorSession = new Session({ inspectorKey: b4a.from(inspectorKey, 'hex') }) + const app = { + inspectorKey, + title: '', + url: '', + inspectorSession + } + + inspectorSession.on('close', () => { + this.apps.delete(sessionId) + this.render() + }) + inspectorSession.on('info', ({ filename }) => { + app.url = filename + app.title = filename.split('/').pop() + this.apps.set(sessionId, app) + this.render() + }) + + this.addKeyInputElem.value = '' + this.addKeyErrorElem.textContent = '' + this.render() + } + + initServer () { + this.devtoolsHttpServer?.close() + + this.devtoolsHttpServer = http.createServer() const devToolsWsServer = new WebSocketServer({ noServer: true }) - devtoolsHttpServer.listen(9229, () => console.log('[devtoolsHttpServer] running on port 9229')) - devtoolsHttpServer.on('request', (req, res) => { + this.devtoolsHttpServer.listen(this.port, () => { + console.log(`[devtoolsHttpServer] running on port ${this.port}`) + this.render() + this.changePortFormElem.classList.add('hidden') + }) + this.devtoolsHttpServer.on('error', err => this.changePortErrorElem.textContent = err?.message) + this.devtoolsHttpServer.on('request', (req, res) => { if (req.url !== '/json/list') { res.writeHead(404) res.end() @@ -185,14 +313,14 @@ customElements.define('developer-tooling', class extends HTMLElement { const targets = [...this.apps].map(([sessionId, app]) => ({ description: 'node.js instance', // `Pear app: ${app.name}`, - devtoolsFrontendUrl: `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:9229/${sessionId}`, - devtoolsFrontendUrlCompat: `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/${sessionId}`, + devtoolsFrontendUrl: `devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:${this.port}/${sessionId}`, + devtoolsFrontendUrlCompat: `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:${this.port}/${sessionId}`, faviconUrl: 'https://nodejs.org/static/images/favicons/favicon.ico', id: sessionId, title: app.title, type: 'node', url: `file://${app.url}`, - webSocketDebuggerUrl: `ws://127.0.0.1:9229/${sessionId}` + webSocketDebuggerUrl: `ws://127.0.0.1:${this.port}/${sessionId}` })) res.writeHead(200, { @@ -202,7 +330,7 @@ customElements.define('developer-tooling', class extends HTMLElement { }) res.end(JSON.stringify(targets)) }) - devtoolsHttpServer.on('upgrade', (request, socket, head) => { + this.devtoolsHttpServer.on('upgrade', (request, socket, head) => { console.log(`[devtoolsHttpServer] UPGRADE. url=${request.url}`) const sessionId = request.url.substr(1) const sessionIdExists = this.apps.has(sessionId) @@ -231,39 +359,14 @@ customElements.define('developer-tooling', class extends HTMLElement { inspectorSession.disconnect() inspectorSession.off('message', onMessage) app.connected = false - this.renderApps() + this.render() }) app.connected = true - this.renderApps() + this.render() }) } - renderApps () { - this.appsElem.replaceChildren(...[...this.apps].map(([sessionId, app]) => { - const div = document.createElement('div') - div.innerHTML = ` -
-
-
${app.title} (${app.url})
-
- ` - div.querySelector('.remove').addEventListener('click', () => { - this.apps.delete(sessionId) - this.renderApps() - }) - - return div - })) - - if (this.apps.size > 0) { - this.noAppsElem.classList.add('hidden') - } else { - this.noAppsElem.classList.remove('hidden') - } - - } - load () { this.style.display = '' } @@ -286,3 +389,16 @@ function generateUuid () { } return result } + +function openChrome (url) { + const params = { + darwin: ['open', '-a', 'Google Chrome', url], + linux: ['google-chrome', url], + win32: ['start', 'chrome', url] + }[process.platform] + + if (!params) throw new Error('Cannot open Chrome') + + const [command, ...args] = params + spawn(command, args) +} diff --git a/package.json b/package.json index 7a53994..736deb3 100644 --- a/package.json +++ b/package.json @@ -10,9 +10,9 @@ } }, "dependencies": { - "@holepunchto/pear-inspect": "^3.0.1", "b4a": "^1.6.4", "bare-path": "^2.1.0", + "pear-inspect": "^1.0.3", "redhat-overpass-font": "^1.0.0", "ws": "^8.16.0" }