Skip to content

Commit

Permalink
feat: separate firefox shell with manifest v2
Browse files Browse the repository at this point in the history
  • Loading branch information
Akryum committed Feb 6, 2024
1 parent aa6ffe0 commit 9ad80cc
Show file tree
Hide file tree
Showing 14 changed files with 608 additions and 2 deletions.
2 changes: 1 addition & 1 deletion extension-zips.js
Expand Up @@ -31,7 +31,7 @@ function bytesToSize(bytes) {

(async () => {
await writeZip('devtools-chrome.zip', 'shell-chrome')
await writeZip('devtools-firefox.zip', 'shell-chrome')
await writeZip('devtools-firefox.zip', 'shell-firefox')

async function writeZip(fileName, packageDir) {
// create a file to stream archive data to.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -28,7 +28,7 @@
"build": "lerna run build",
"build:watch": "lerna run build --scope @vue-devtools/app-backend* --scope @vue-devtools/shared-* --scope @vue/devtools-api && lerna run build:watch --stream --no-sort --concurrency 99",
"lint": "eslint .",
"run:firefox": "web-ext run -s packages/shell-chrome -a dist -i src",
"run:firefox": "web-ext run -s packages/shell-firefox -a dist -i src -u http://localhost:8090/target.html",
"zip": "node ./extension-zips.js",
"sign:firefox": "node ./sign-firefox.js",
"release": "npm run test && node release.js && npm run build && npm run zip && npm run pub",
Expand Down
3 changes: 3 additions & 0 deletions packages/shell-firefox/.gitignore
@@ -0,0 +1,3 @@
icons/
popups/
*.html
7 changes: 7 additions & 0 deletions packages/shell-firefox/copy.sh
@@ -0,0 +1,7 @@
#!/bin/bash

rm -rf icons

cp -r ../shell-chrome/icons .
cp -r ../shell-chrome/popups .
cp ../shell-chrome/*.html .
58 changes: 58 additions & 0 deletions packages/shell-firefox/manifest.json
@@ -0,0 +1,58 @@
{
"name": "Vue.js devtools",
"version": "6.5.1",
"version_name": "6.5.1",
"description": "Browser DevTools extension for debugging Vue.js applications.",
"manifest_version": 2,
"icons": {
"16": "icons/16.png",
"48": "icons/48.png",
"128": "icons/128.png"
},
"browser_action": {
"default_icon": {
"16": "icons/16-gray.png",
"48": "icons/48-gray.png",
"128": "icons/128-gray.png"
},
"default_title": "Vue Devtools",
"default_popup": "popups/not-found.html"
},
"web_accessible_resources": [
"devtools.html",
"devtools-background.html",
"build/backend.js"
],
"devtools_page": "devtools-background.html",
"background": {
"scripts": [
"build/background.js"
],
"persistent": true
},
"permissions": [
"<all_urls>",
"storage"
],
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"build/hook.js"
],
"run_at": "document_start"
},
{
"matches": [
"<all_urls>"
],
"js": [
"build/detector.js"
],
"run_at": "document_idle"
}
],
"content_security_policy": "script-src 'self'; object-src 'self'"
}
18 changes: 18 additions & 0 deletions packages/shell-firefox/package.json
@@ -0,0 +1,18 @@
{
"name": "@vue-devtools/shell-firefox",
"version": "0.0.0",
"scripts": {
"build": "rimraf ./build && ./copy.sh && cross-env NODE_ENV=production webpack --progress"
},
"dependencies": {
"@vue-devtools/app-backend-core": "^0.0.0",
"@vue-devtools/app-frontend": "^0.0.0",
"@vue-devtools/shared-utils": "^0.0.0"
},
"devDependencies": {
"@vue-devtools/build-tools": "^0.0.0",
"rimraf": "^3.0.2",
"webpack": "^5.35.1",
"webpack-cli": "^4.6.0"
}
}
55 changes: 55 additions & 0 deletions packages/shell-firefox/src/backend.js
@@ -0,0 +1,55 @@
// this is injected to the app page when the panel is activated.

import { initBackend } from '@back'
import { Bridge } from '@vue-devtools/shared-utils'

window.addEventListener('message', handshake)

function sendListening() {
window.postMessage({
source: 'vue-devtools-backend-injection',
payload: 'listening',
}, '*')
}
sendListening()

function handshake(e) {
if (e.data.source === 'vue-devtools-proxy' && e.data.payload === 'init') {
window.removeEventListener('message', handshake)

let listeners = []
const bridge = new Bridge({
listen(fn) {
const listener = (evt) => {
if (evt.data.source === 'vue-devtools-proxy' && evt.data.payload) {
fn(evt.data.payload)
}
}
window.addEventListener('message', listener)
listeners.push(listener)
},
send(data) {
// if (process.env.NODE_ENV !== 'production') {
// console.log('[chrome] backend -> devtools', data)
// }
window.postMessage({
source: 'vue-devtools-backend',
payload: data,
}, '*')
},
})

bridge.on('shutdown', () => {
listeners.forEach((l) => {
window.removeEventListener('message', l)
})
listeners = []
window.addEventListener('message', handshake)
})

initBackend(bridge)
}
else {
sendListening()
}
}
125 changes: 125 additions & 0 deletions packages/shell-firefox/src/background.js
@@ -0,0 +1,125 @@
// the background script runs all the time and serves as a central message
// hub for each vue devtools (panel + proxy + backend) instance.

const ports = {}

chrome.runtime.onConnect.addListener((port) => {
let tab
let name
if (isNumeric(port.name)) {
tab = port.name
name = 'devtools'
installProxy(+port.name)
}
else {
tab = port.sender.tab.id
name = 'backend'
}

if (!ports[tab]) {
ports[tab] = {
devtools: null,
backend: null,
}
}
ports[tab][name] = port

if (ports[tab].devtools && ports[tab].backend) {
doublePipe(tab, ports[tab].devtools, ports[tab].backend)
}
})

function isNumeric(str) {
return `${+str}` === str
}

function installProxy(tabId) {
chrome.tabs.executeScript(tabId, {
file: '/build/proxy.js',
}, (res) => {
if (!res) {
ports[tabId].devtools.postMessage('proxy-fail')
}
else {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`injected proxy to tab ${tabId}`)
}
}
})
}

function doublePipe(id, one, two) {
one.onMessage.addListener(lOne)
function lOne(message) {
if (message.event === 'log') {
// eslint-disable-next-line no-console
return console.log(`tab ${id}`, message.payload)
}
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log('%cdevtools -> backend', 'color:#888;', message)
}
two.postMessage(message)
}
two.onMessage.addListener(lTwo)
function lTwo(message) {
if (message.event === 'log') {
// eslint-disable-next-line no-console
return console.log(`tab ${id}`, message.payload)
}
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log('%cbackend -> devtools', 'color:#888;', message)
}
one.postMessage(message)
}
function shutdown() {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`tab ${id} disconnected.`)
}
one.onMessage.removeListener(lOne)
two.onMessage.removeListener(lTwo)
one.disconnect()
two.disconnect()
ports[id] = null
}
one.onDisconnect.addListener(shutdown)
two.onDisconnect.addListener(shutdown)
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.log(`tab ${id} connected.`)
}
}

chrome.runtime.onMessage.addListener((req, sender) => {
if (sender.tab && req.vueDetected) {
const suffix = req.nuxtDetected ? '.nuxt' : ''

chrome.browserAction.setIcon({
tabId: sender.tab.id,
path: {
16: `icons/16${suffix}.png`,
48: `icons/48${suffix}.png`,
128: `icons/128${suffix}.png`,
},
})
chrome.browserAction.setPopup({
tabId: sender.tab.id,
popup: req.devtoolsEnabled ? `popups/enabled${suffix}.html` : `popups/disabled${suffix}.html`,
})
}

if (req.action === 'vue-take-screenshot' && sender.envType === 'devtools_child') {
browser.tabs.captureVisibleTab({
format: 'png',
}).then((dataUrl) => {
browser.runtime.sendMessage({
action: 'vue-screenshot-result',
id: req.id,
dataUrl,
})
})
}
})
100 changes: 100 additions & 0 deletions packages/shell-firefox/src/detector.js
@@ -0,0 +1,100 @@
import { installToast } from '@back/toast'
import { isFirefox } from '@vue-devtools/shared-utils'

window.addEventListener('message', (e) => {
if (e.source === window && e.data.vueDetected) {
chrome.runtime.sendMessage(e.data)
}
})

function detect(win) {
let delay = 1000
let detectRemainingTries = 10

function runDetect() {
// Method 1: Check Nuxt
const nuxtDetected = !!(window.__NUXT__ || window.$nuxt)

if (nuxtDetected) {
let Vue

if (window.$nuxt) {
Vue = window.$nuxt.$root && window.$nuxt.$root.constructor
}

win.postMessage({
devtoolsEnabled: (/* Vue 2 */ Vue && Vue.config.devtools)
|| (/* Vue 3.2.14+ */ window.__VUE_DEVTOOLS_GLOBAL_HOOK__ && window.__VUE_DEVTOOLS_GLOBAL_HOOK__.enabled),
vueDetected: true,
nuxtDetected: true,
}, '*')

return
}

// Method 2: Check Vue 3
const vueDetected = !!(window.__VUE__)
if (vueDetected) {
win.postMessage({
devtoolsEnabled: /* Vue 3.2.14+ */ window.__VUE_DEVTOOLS_GLOBAL_HOOK__ && window.__VUE_DEVTOOLS_GLOBAL_HOOK__.enabled,
vueDetected: true,
}, '*')

return
}

// Method 3: Scan all elements inside document
const all = document.querySelectorAll('*')
let el
for (let i = 0; i < all.length; i++) {
if (all[i].__vue__) {
el = all[i]
break
}
}
if (el) {
let Vue = Object.getPrototypeOf(el.__vue__).constructor
while (Vue.super) {
Vue = Vue.super
}
win.postMessage({
devtoolsEnabled: Vue.config.devtools,
vueDetected: true,
}, '*')
return
}

if (detectRemainingTries > 0) {
detectRemainingTries--
setTimeout(() => {
runDetect()
}, delay)
delay *= 5
}
}

setTimeout(() => {
runDetect()
}, 100)
}

// inject the hook
if (document instanceof HTMLDocument) {
installScript(detect)
installScript(installToast)
}

function installScript(fn) {
const source = `;(${fn.toString()})(window)`

if (isFirefox) {
// eslint-disable-next-line no-eval
window.eval(source) // in Firefox, this evaluates on the content window
}
else {
const script = document.createElement('script')
script.textContent = source
document.documentElement.appendChild(script)
script.parentNode.removeChild(script)
}
}

0 comments on commit 9ad80cc

Please sign in to comment.