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.
+
+
+
`
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"
}