diff --git a/.node-version b/.node-version new file mode 100644 index 000000000..b6a7d89c6 --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +16 diff --git a/README.md b/README.md index 67aba0e80..34f949660 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ To enable this feature, follow [this guide](./docs/open-in-editor.md). ### Manual Installation -This is only necessary when you want to build the extension yourself from source to get not-yet-released features. +This is only necessary when you want to build the extension with the source repo to get not-yet-released features. **Make sure you are using Node 6+ and NPM 3+** @@ -32,10 +32,10 @@ This is only necessary when you want to build the extension yourself from source 2. `cd vue-devtools` the newly created folder 2. run `yarn install` 3. then run `yarn run build` -4. Open the Chrome extension page (currently under Menu > More Tools > Extensions) +4. Open the Chrome extension page (currently under `Menu` > `More Tools` > `Extensions`) 5. Check "developer mode" on the top-right corner 6. Click the "load unpacked" button on the left, and choose the folder: `vue-devtools/packages/shell-chrome/` -7. Alternatilvely to step 3, you can also use `yarn dev:chrome` to build & watch the unpacked extension +7. Alternatively to step 3, you can also use `yarn dev:chrome` to build & watch the unpacked extension ### Development diff --git a/package.json b/package.json index 220acb779..37b45e619 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vue-devtools", - "version": "5.3.3", + "version": "5.3.4", "description": "devtools for Vue.js!", "private": true, "workspaces": [ diff --git a/packages/shell-chrome/manifest.json b/packages/shell-chrome/manifest.json index 905bee0c1..a836ed580 100644 --- a/packages/shell-chrome/manifest.json +++ b/packages/shell-chrome/manifest.json @@ -1,15 +1,15 @@ { - "name": "Vue.js devtools", - "version": "5.3.3", - "version_name": "5.3.3", - "description": "Chrome and Firefox DevTools extension for debugging Vue.js applications.", - "manifest_version": 2, + "name": "Vue.js devtools (v5)", + "version": "5.3.4", + "version_name": "5.3.4", + "description": "This version is provided for legacy users and will not be updated in the future.", + "manifest_version": 3, "icons": { "16": "icons/16.png", "48": "icons/48.png", "128": "icons/128.png" }, - "browser_action": { + "action": { "default_icon": { "16": "icons/16-gray.png", "48": "icons/48-gray.png", @@ -19,42 +19,38 @@ "default_popup": "popups/not-found.html" }, "web_accessible_resources": [ - "devtools.html", - "devtools-background.html", - "build/backend.js" + { + "resources": [ + "devtools.html", + "devtools-background.html", + "build/backend.js", + "build/proxy.js", + "build/hook-exec.js", + "build/detector-exec.js" + ], + "matches": [""], + "extension_ids": [] + } ], "devtools_page": "devtools-background.html", "background": { - "scripts": [ - "build/background.js" - ], - "persistent": false + "service_worker": "build/service-worker.js" }, - "permissions": [ - "http://*/*", - "https://*/*", - "file:///*", - "contextMenus", - "storage" - ], + "permissions": ["storage", "scripting"], + "host_permissions": [""], "content_scripts": [ { - "matches": [ - "" - ], - "js": [ - "build/hook.js" - ], + "matches": [""], + "js": ["build/hook.js"], "run_at": "document_start" }, { - "matches": [ - "" - ], - "js": [ - "build/detector.js" - ], + "matches": [""], + "js": ["build/detector.js"], "run_at": "document_idle" } - ] -} \ No newline at end of file + ], + "content_security_policy": { + "extension_pages": "script-src 'self'; object-src 'self'" + } +} diff --git a/packages/shell-chrome/src/detector-exec.js b/packages/shell-chrome/src/detector-exec.js new file mode 100644 index 000000000..113e6d718 --- /dev/null +++ b/packages/shell-chrome/src/detector-exec.js @@ -0,0 +1,85 @@ +import { installToast } from '@back/toast' + +function sendMessage (message) { + window.postMessage({ + key: '_vue-devtools-send-message', + message, + }) +} + +function detect () { + let delay = 1000 + let detectRemainingTries = 10 + + function runDetect () { + // Method 1: Check Nuxt.js + const nuxtDetected = !!(window.__NUXT__ || window.$nuxt) + + if (nuxtDetected) { + let Vue + + if (window.$nuxt) { + Vue = window.$nuxt.$root && window.$nuxt.$root.constructor + } + + sendMessage({ + 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) { + sendMessage({ + 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 + } + sendMessage({ + 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) { + detect() + installToast(window) +} diff --git a/packages/shell-chrome/src/detector.js b/packages/shell-chrome/src/detector.js index ae79e75fb..124897daf 100644 --- a/packages/shell-chrome/src/detector.js +++ b/packages/shell-chrome/src/detector.js @@ -1,71 +1,12 @@ -import { installToast } from '@back/toast' -import { isFirefox } from '@utils/env' - -window.addEventListener('message', e => { - if (e.source === window && e.data.vueDetected) { - chrome.runtime.sendMessage(e.data) +window.addEventListener('message', function (event) { + if (event.data.key === '_vue-devtools-send-message') { + chrome.runtime.sendMessage(event.data.message) } -}) - -function detect (win) { - setTimeout(() => { - // Method 1: Check Nuxt.js - const nuxtDetected = Boolean(window.__NUXT__ || window.$nuxt) - - if (nuxtDetected) { - let Vue - - if (window.$nuxt) { - Vue = window.$nuxt.$root.constructor - } - - win.postMessage({ - devtoolsEnabled: Vue && Vue.config.devtools, - vueDetected: true, - nuxtDetected: true - }, '*') - - return - } +}, false) - // Method 2: 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 - }, '*') - } - }, 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) - } +const script = document.createElement('script') +script.src = chrome.runtime.getURL('build/detector-exec.js') +script.onload = () => { + script.remove() } +;(document.head || document.documentElement).appendChild(script) diff --git a/packages/shell-chrome/src/devtools-background.js b/packages/shell-chrome/src/devtools-background.js index 17b87ec98..4437dadc4 100644 --- a/packages/shell-chrome/src/devtools-background.js +++ b/packages/shell-chrome/src/devtools-background.js @@ -46,7 +46,7 @@ chrome.runtime.onMessage.addListener(request => { if (request === 'vue-panel-load') { onPanelLoad() } else if (request.vueToast) { - toast(request.vueToast.message, request.vueToast.type) + toast(request.vueToast) } else if (request.vueContextMenu) { onContextMenu(request.vueContextMenu) } @@ -65,10 +65,10 @@ function onContextMenu ({ id }) { if (typeof res !== 'undefined' && res) { panelAction(() => { chrome.runtime.sendMessage('vue-get-context-menu-target') - }, 'Open Vue devtools to see component details') + }, 'open-devtools') } else { pendingAction = null - toast('No Vue component was found', 'warn') + toast('component-not-found') } }) } @@ -113,7 +113,16 @@ function onPanelHidden () { // Toasts -function toast (message, type = 'normal') { +const toastMessages = { + 'open-devtools': { message: 'Open Vue devtools to see component details', type: 'normal' }, + 'component-not-found': { message: 'No Vue component was found', type: 'warn' } +} + +function toast (id) { + if (!Object.keys().includes(id)) return + + const { message, type } = toastMessages[id] + const src = `(function() { __VUE_DEVTOOLS_TOAST__(\`${message}\`, '${type}'); })()` diff --git a/packages/shell-chrome/src/hook-exec.js b/packages/shell-chrome/src/hook-exec.js new file mode 100644 index 000000000..7549d0f16 --- /dev/null +++ b/packages/shell-chrome/src/hook-exec.js @@ -0,0 +1,3 @@ +import { installHook } from '@back/hook' + +installHook(window) diff --git a/packages/shell-chrome/src/hook.js b/packages/shell-chrome/src/hook.js index 722b53cc5..16269d7e3 100644 --- a/packages/shell-chrome/src/hook.js +++ b/packages/shell-chrome/src/hook.js @@ -1,18 +1,6 @@ -// This script is injected into every page. -import { installHook } from '@back/hook' -import { isFirefox } from '@utils/env' - -// inject the hook -if (document instanceof HTMLDocument) { - const source = ';(' + installHook.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) - } +const script = document.createElement('script') +script.src = chrome.runtime.getURL('build/hook-exec.js') +script.onload = () => { + script.remove() } +;(document.head || document.documentElement).appendChild(script) diff --git a/packages/shell-chrome/src/background.js b/packages/shell-chrome/src/service-worker.js similarity index 51% rename from packages/shell-chrome/src/background.js rename to packages/shell-chrome/src/service-worker.js index 04ad5f80e..a36f20b2b 100644 --- a/packages/shell-chrome/src/background.js +++ b/packages/shell-chrome/src/service-worker.js @@ -18,7 +18,7 @@ chrome.runtime.onConnect.addListener(port => { if (!ports[tab]) { ports[tab] = { devtools: null, - backend: null + backend: null, } } ports[tab][name] = port @@ -33,13 +33,17 @@ function isNumeric (str) { } function installProxy (tabId) { - chrome.tabs.executeScript(tabId, { - file: '/build/proxy.js' + chrome.scripting.executeScript({ + target: { tabId }, + files: ['build/proxy.js'], }, function (res) { if (!res) { ports[tabId].devtools.postMessage('proxy-fail') } else { - console.log('injected proxy to tab ' + tabId) + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.log('injected proxy to tab ' + tabId) + } } }) } @@ -48,76 +52,77 @@ 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) } - console.log('devtools -> backend', message) + 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) } - console.log('backend -> devtools', message) + if (process.env.NODE_ENV !== 'production') { + // eslint-disable-next-line no-console + console.log('%cbackend -> devtools', 'color:#888;', message) + } one.postMessage(message) } function shutdown () { - console.log('tab ' + id + ' disconnected.') + 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 - updateContextMenuItem() } one.onDisconnect.addListener(shutdown) two.onDisconnect.addListener(shutdown) - console.log('tab ' + id + ' connected.') - updateContextMenuItem() + 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({ + chrome.action.setIcon({ tabId: sender.tab.id, path: { - 16: `icons/16${suffix}.png`, - 48: `icons/48${suffix}.png`, - 128: `icons/128${suffix}.png` - } + 16: chrome.runtime.getURL(`icons/16${suffix}.png`), + 48: chrome.runtime.getURL(`icons/48${suffix}.png`), + 128: chrome.runtime.getURL(`icons/128${suffix}.png`), + }, + }, () => { + // noop }) - chrome.browserAction.setPopup({ + chrome.action.setPopup({ tabId: sender.tab.id, - popup: req.devtoolsEnabled ? `popups/enabled${suffix}.html` : `popups/disabled${suffix}.html` + popup: chrome.runtime.getURL(req.devtoolsEnabled ? `popups/enabled${suffix}.html` : `popups/disabled${suffix}.html`), + }, () => { + // noop }) } -}) - -// Right-click inspect context menu entry -let activeTabId -chrome.tabs.onActivated.addListener(({ tabId }) => { - activeTabId = tabId - updateContextMenuItem() -}) -function updateContextMenuItem () { - chrome.contextMenus.removeAll(() => { - if (ports[activeTabId]) { - chrome.contextMenus.create({ - id: 'vue-inspect-instance', - title: 'Inspect Vue component', - contexts: ['all'] + 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, }) - } - }) -} - -chrome.contextMenus.onClicked.addListener((info, tab) => { - chrome.runtime.sendMessage({ - vueContextMenu: { - id: info.menuItemId - } - }) + }) + } }) diff --git a/packages/shell-chrome/webpack.config.js b/packages/shell-chrome/webpack.config.js index c99a162bd..8870b2189 100644 --- a/packages/shell-chrome/webpack.config.js +++ b/packages/shell-chrome/webpack.config.js @@ -4,12 +4,14 @@ const { createConfig } = require('@vue-devtools/build-tools') module.exports = createConfig({ entry: { hook: './src/hook.js', + 'hook-exec': './src/hook-exec.js', devtools: './src/devtools.js', - background: './src/background.js', + 'service-worker': './src/service-worker.js', 'devtools-background': './src/devtools-background.js', backend: './src/backend.js', proxy: './src/proxy.js', - detector: './src/detector.js' + detector: './src/detector.js', + 'detector-exec': './src/detector-exec.js', }, output: { path: path.join(__dirname, 'build'), diff --git a/packages/shell-electron/package.json b/packages/shell-electron/package.json index 74b463f9a..8dc74457a 100644 --- a/packages/shell-electron/package.json +++ b/packages/shell-electron/package.json @@ -1,6 +1,6 @@ { "name": "@vue/devtools", - "version": "5.3.3", + "version": "5.3.4", "description": "StandAlone vue-devtools", "repository": { "url": "https://github.com/vuejs/vue-devtools.git",