Merge pull request #29 from holepunchto/inspector-changes

Inspector changes
This commit is contained in:
David Mark Clements
2024-02-01 17:26:25 +01:00
committed by GitHub
3 changed files with 199 additions and 80 deletions

View File

@@ -272,6 +272,9 @@
<section> <section>
<div class="col"><developer-tooling></devtools></div> <div class="col"><developer-tooling></devtools></div>
</section> </section>
<script>
process.env.WS_NO_BUFFER_UTIL = false // Squashing an error from the 'ws' module
</script>
<script type='module' src='./lib/devtools.js'></script> <script type='module' src='./lib/devtools.js'></script>
</app-router> </app-router>
<script type='module' src='./lib/app-router.js'></script> <script type='module' src='./lib/app-router.js'></script>

View File

@@ -1,11 +1,13 @@
/* eslint-env browser */ /* eslint-env browser */
import http from 'http' import http from 'http'
import { WebSocketServer } from 'ws' import { WebSocketServer } from 'ws'
import { Session } from '@holepunchto/pear-inspect' import { Session } from 'pear-inspect'
import b4a from 'b4a' import b4a from 'b4a'
import { spawn } from 'child_process'
customElements.define('developer-tooling', class extends HTMLElement { customElements.define('developer-tooling', class extends HTMLElement {
router = null router = null
port = 9342
constructor () { constructor () {
super() super()
@@ -17,33 +19,57 @@ customElements.define('developer-tooling', class extends HTMLElement {
width: 100%; width: 100%;
} }
#add-key-error { #add-key-error,
#change-port-error {
color: red; color: red;
} }
#no-apps.hidden { #server-message:hover #change-port-show {
display: inline-block;
}
#change-port-show {
display: none;
}
.hidden {
display: none; display: none;
} }
.app { .app {
display: flex; display: flex;
align-items: center; align-items: center;
padding: 0.25rem;
padding-top: 0.1rem;
padding-bottom: 0.15rem;
} }
.app .title { .app .title {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.app:hover .copy,
.app:hover .open-in-chrome,
.app:hover .remove { .app:hover .remove {
display: block; display: block;
} }
.app .copy,
.app .open-in-chrome {
margin-right: 10px;
}
.app .copy,
.app .open-in-chrome,
.app .remove { .app .remove {
cursor: pointer;
padding-right: 10px;
margin-left: calc(-1rem - 10px);
width: 1rem;
display: none; 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 { #remote-inspect-explain {
float:left; float:left;
max-width:55%; max-width:55%;
@@ -52,6 +78,10 @@ customElements.define('developer-tooling', class extends HTMLElement {
float:right; float:right;
max-width:40%; max-width:40%;
} }
#server-message {
font-size: 0.8rem;
margin-top: 30px;
}
h2 { margin: 0 } h2 { margin: 0 }
p { p {
@@ -93,25 +123,25 @@ customElements.define('developer-tooling', class extends HTMLElement {
<div> <div>
<h1>Developer Tooling</h1> <h1>Developer Tooling</h1>
<div> <div>
<div id=remote-inspect-explain> <div id="remote-inspect-explain">
<h2>Remotely inspect Pear applications.</h2> <h2>Remotely inspect Pear applications.</h2>
<p> Some application setup is required to enable remote debugging </p> <p> Some application setup is required to enable remote debugging </p>
<p> Install the <code>pear-inspect</code> module into the application </p> <p> Install the <code>pear-inspect</code> module into the application </p>
<pre><code>npm install pear-inspect</code></pre> <pre><code>npm install pear-inspect</code></pre>
<p> Add the following code to the application, before any other code: </p> <p> Add the following code to the application, before any other code: </p>
<pre><code>if (Pear.config.dev) { <pre><code>if (Pear.config.dev) {
const { Inspector } = await import('pear-inspect') const { Inspector } = await import('pear-inspect')
const inpector = await new Inspector() const inpector = await new Inspector()
const key = await inpector.enable() const key = await inpector.enable()
console.log('Pear Inspector key:', key.toString('hex')) console.log('Debug with pear://runtime/devtools/'
+ key.toString('hex'))
}</code></pre> }</code></pre>
<p>When the application is opened in development mode the inspection key will be logged.</p> <p>When the application is opened in development mode the inspection key will be logged.</p>
<p>Paste the logged key into the input and use a compatible inspect protocol tool, such as chrome://inspect to view the remote target</p> <p>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.</p>
</div> </div>
<div id=remote-inspect> <div id="remote-inspect">
<div> <div>
<form id="add-key-form"> <form id="add-key-form">
<input id="add-key-input" type="text" placeholder="Paste Pear Inspector Key Here"/> <input id="add-key-input" type="text" placeholder="Paste Pear Inspector Key Here"/>
@@ -121,6 +151,22 @@ customElements.define('developer-tooling', class extends HTMLElement {
<h2>Apps</h2> <h2>Apps</h2>
<h3 id="no-apps">No apps added. Add an inspect key to start debugging.</h3> <h3 id="no-apps">No apps added. Add an inspect key to start debugging.</h3>
<div id="apps"></div> <div id="apps"></div>
<div id="server-message" class="hidden">
Pear DevTools connection running on
<div>
<span id="server-location">localhost</span>
<span id="change-port-show" class="button">
Change port
</span>
<div>
<form id="change-port-form" class="hidden">
<input id="change-port-input" type="number" placeholder="New port" />
</form>
<p id="change-port-error"></p>
</div>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -131,18 +177,92 @@ customElements.define('developer-tooling', class extends HTMLElement {
this.addKeyFormElem = this.root.querySelector('#add-key-form') this.addKeyFormElem = this.root.querySelector('#add-key-form')
this.addKeyInputElem = this.root.querySelector('#add-key-input') this.addKeyInputElem = this.root.querySelector('#add-key-input')
this.addKeyErrorElem = this.root.querySelector('#add-key-error') 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.appsElem = this.root.querySelector('#apps')
this.noAppsElem = this.root.querySelector('#no-apps') this.noAppsElem = this.root.querySelector('#no-apps')
this.changePortElem = this.root.querySelector('#change-port')
this.apps = new Map() this.apps = new Map()
this.addKeyInputElem.addEventListener('keypress', e => { this.addKeyInputElem.addEventListener('keydown', e => {
this.addKeyErrorElem.textContent = '' this.addKeyErrorElem.textContent = ''
}) })
this.addKeyFormElem.addEventListener('submit', e => { this.addKeyFormElem.addEventListener('submit', e => {
e.preventDefault() e.preventDefault()
const inspectorKey = this.addKeyInputElem.value const inspectorKey = this.addKeyInputElem.value
if (inspectorKey.length !== 64) { this.addApp(inspectorKey)
})
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 = `
<div class="app">
<div class="title">${app.title} (${app.url})</div>
<div class="button copy">Copy URL</div>
<div class="button open-in-chrome">Open in Chrome</div>
<div class="button remove">✕</div>
</div>
`
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' this.addKeyErrorElem.textContent = 'Key needs to be 64 characters long'
return return
} }
@@ -158,25 +278,33 @@ customElements.define('developer-tooling', class extends HTMLElement {
inspectorSession.on('close', () => { inspectorSession.on('close', () => {
this.apps.delete(sessionId) this.apps.delete(sessionId)
this.renderApps() this.render()
}) })
inspectorSession.on('info', ({ filename }) => { inspectorSession.on('info', ({ filename }) => {
app.url = filename app.url = filename
app.title = filename.split('/').pop() app.title = filename.split('/').pop()
this.apps.set(sessionId, app) this.apps.set(sessionId, app)
this.renderApps() this.render()
}) })
this.addKeyInputElem.value = '' this.addKeyInputElem.value = ''
this.addKeyErrorElem.textContent = '' this.addKeyErrorElem.textContent = ''
this.renderApps() this.render()
}) }
const devtoolsHttpServer = http.createServer() initServer () {
this.devtoolsHttpServer?.close()
this.devtoolsHttpServer = http.createServer()
const devToolsWsServer = new WebSocketServer({ noServer: true }) const devToolsWsServer = new WebSocketServer({ noServer: true })
devtoolsHttpServer.listen(9229, () => console.log('[devtoolsHttpServer] running on port 9229')) this.devtoolsHttpServer.listen(this.port, () => {
devtoolsHttpServer.on('request', (req, res) => { 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') { if (req.url !== '/json/list') {
res.writeHead(404) res.writeHead(404)
res.end() res.end()
@@ -185,14 +313,14 @@ customElements.define('developer-tooling', class extends HTMLElement {
const targets = [...this.apps].map(([sessionId, app]) => ({ const targets = [...this.apps].map(([sessionId, app]) => ({
description: 'node.js instance', // `Pear app: ${app.name}`, 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}`, 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:9229/${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', faviconUrl: 'https://nodejs.org/static/images/favicons/favicon.ico',
id: sessionId, id: sessionId,
title: app.title, title: app.title,
type: 'node', type: 'node',
url: `file://${app.url}`, url: `file://${app.url}`,
webSocketDebuggerUrl: `ws://127.0.0.1:9229/${sessionId}` webSocketDebuggerUrl: `ws://127.0.0.1:${this.port}/${sessionId}`
})) }))
res.writeHead(200, { res.writeHead(200, {
@@ -202,7 +330,7 @@ customElements.define('developer-tooling', class extends HTMLElement {
}) })
res.end(JSON.stringify(targets)) 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}`) console.log(`[devtoolsHttpServer] UPGRADE. url=${request.url}`)
const sessionId = request.url.substr(1) const sessionId = request.url.substr(1)
const sessionIdExists = this.apps.has(sessionId) const sessionIdExists = this.apps.has(sessionId)
@@ -231,39 +359,14 @@ customElements.define('developer-tooling', class extends HTMLElement {
inspectorSession.disconnect() inspectorSession.disconnect()
inspectorSession.off('message', onMessage) inspectorSession.off('message', onMessage)
app.connected = false app.connected = false
this.renderApps() this.render()
}) })
app.connected = true app.connected = true
this.renderApps() this.render()
}) })
} }
renderApps () {
this.appsElem.replaceChildren(...[...this.apps].map(([sessionId, app]) => {
const div = document.createElement('div')
div.innerHTML = `
<div class="app">
<div class="remove" data-session-id="${sessionId}">✕</div>
<div class="title">${app.title} (${app.url})</div>
</div>
`
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 () { load () {
this.style.display = '' this.style.display = ''
} }
@@ -286,3 +389,16 @@ function generateUuid () {
} }
return result 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)
}

View File

@@ -10,9 +10,9 @@
} }
}, },
"dependencies": { "dependencies": {
"@holepunchto/pear-inspect": "^3.0.1",
"b4a": "^1.6.4", "b4a": "^1.6.4",
"bare-path": "^2.1.0", "bare-path": "^2.1.0",
"pear-inspect": "^1.0.3",
"redhat-overpass-font": "^1.0.0", "redhat-overpass-font": "^1.0.0",
"ws": "^8.16.0" "ws": "^8.16.0"
} }