diff --git a/index.html b/index.html index aab1fb7..7bb2393 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,5 @@ +
- +
           __ \    _ \   _` |   __|
@@ -196,14 +261,18 @@
         
-
-
-
- -
-
-
- +
+
+
+ +
+
+
+ +
+
+
+
diff --git a/lib/app-router.js b/lib/app-router.js index 45d631f..d99b7fd 100644 --- a/lib/app-router.js +++ b/lib/app-router.js @@ -49,6 +49,8 @@ customElements.define('app-router', class AppRouter extends HTMLElement { window.addEventListener('load', () => { if (Pear.config.link.indexOf('pear://pulse') === 0) { this.load(Pear.config.link.slice(12)).catch(console.error) + } else { + this.load('/') } }) } diff --git a/lib/devtools.js b/lib/devtools.js new file mode 100644 index 0000000..a935e19 --- /dev/null +++ b/lib/devtools.js @@ -0,0 +1,278 @@ +/* eslint-env browser */ +import http from 'http' +import { WebSocketServer } from 'ws' +import { Session } from '@holepunchto/pear-inspect' +import b4a from 'b4a' + +customElements.define('developer-tooling', class extends HTMLElement { + router = null + + 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

+

First install the pear-inspect module into the application

+
npm install pear-inspect
+

Then add the following JavaScript code to the application, at the top:

+ +
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'))
+}
+ +

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 + 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() + }) + + const devtoolsHttpServer = http.createServer() + const devToolsWsServer = new WebSocketServer({ noServer: true }) + + devtoolsHttpServer.listen(9229, () => console.log('[devtoolsHttpServer] running on port 9229')) + 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:9229/${sessionId}`, + devtoolsFrontendUrlCompat: `devtools://devtools/bundled/inspector.html?experiments=true&v8only=true&ws=127.0.0.1:9229/${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}` + })) + + 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() + }) + } + + 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 = '' + } + + 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 +} diff --git a/package.json b/package.json index 7e911e7..7a53994 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ } }, "dependencies": { - "redhat-overpass-font": "^1.0.0" + "@holepunchto/pear-inspect": "^3.0.1", + "b4a": "^1.6.4", + "bare-path": "^2.1.0", + "redhat-overpass-font": "^1.0.0", + "ws": "^8.16.0" } }