/* eslint-env browser */ import http from 'http' import { WebSocketServer } from 'ws' import { Session } from 'pear-inspect' import b4a from 'b4a' customElements.define('developer-tooling', class extends HTMLElement { router = null port = 9229 constructor () { super() this.template = document.createElement('template') this.template.innerHTML = `

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) {
  const { Inspector } = await import('pear-inspect')
  const inpector = await new Inspector()
  const key = await inpector.enable()
  console.log('Debug with', 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

Apps

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

` this.root = this.attachShadow({ mode: 'open' }) this.root.appendChild(this.template.content.cloneNode(true)) 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.appsElem = this.root.querySelector('#apps') this.noAppsElem = this.root.querySelector('#no-apps') this.apps = new Map() this.addKeyInputElem.addEventListener('keypress', e => { this.addKeyErrorElem.textContent = '' }) this.addKeyFormElem.addEventListener('submit', e => { e.preventDefault() const inspectorKey = this.addKeyInputElem.value this.addApp(inspectorKey) }) const shouldLoadApp = Pear.config.linkData?.startsWith('devtools/') if (shouldLoadApp) { const id = Pear.config.linkData.split('/').pop() this.addApp(id) } this.initServer() } renderApps () { this.appsElem.replaceChildren(...[...this.apps].map(([sessionId, app]) => { const div = document.createElement('div') div.innerHTML = `
devtools://devtools/bundled/js_app.html?experiments=true&v8only=true&ws=127.0.0.1:${this.port}/${sessionId}
${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') } } addApp (inspectorKey) { 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.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() } initServer () { const devtoolsHttpServer = http.createServer() const devToolsWsServer = new WebSocketServer({ noServer: true }) devtoolsHttpServer.listen(this.port, () => console.log(`[devtoolsHttpServer] running on port ${this.port}`)) devtoolsHttpServer.on('request', (req, res) => { if (req.url !== '/json/list') { res.writeHead(404) res.end() return } 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:${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:${this.port}/${sessionId}` })) res.writeHead(200, { 'Content-Type': 'application/json; charset=UTF-8', 'Cache-Control': 'no-cache', 'Content-Length': JSON.stringify(targets).length }) res.end(JSON.stringify(targets)) }) 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) if (!sessionIdExists) return socket.destroy() devToolsWsServer.handleUpgrade(request, socket, head, ws => devToolsWsServer.emit('connection', ws, request)) }) devToolsWsServer.on('connection', (devtoolsSocket, request) => { const sessionId = request.url.substr(1) const app = this.apps.get(sessionId) if (!app) return devtoolsSocket.destroy() const { inspectorSession } = app const onMessage = msg => { const { pearInspectMethod } = msg const isACDPMessage = !pearInspectMethod if (isACDPMessage) return devtoolsSocket.send(JSON.stringify(msg)) } inspectorSession.connect() inspectorSession.on('message', onMessage) devtoolsSocket.on('message', msg => inspectorSession.post(JSON.parse(msg))) devtoolsSocket.on('close', () => { inspectorSession.disconnect() inspectorSession.off('message', onMessage) app.connected = false this.renderApps() }) app.connected = true this.renderApps() }) } load () { this.style.display = '' } unload () { this.style.display = 'none' } }) // Can't use `uuid` module for some reason as it results in a throw with `crypto` when importing function generateUuid () { let result, i, j result = '' for (j = 0; j < 32; j++) { if (j === 8 || j === 12 || j === 16 || j === 20) { result = result + '-' } i = Math.floor(Math.random() * 16).toString(16) result = result + i } return result }