diff --git a/shells/chrome/manifest.json b/shells/chrome/manifest.json index d19cb2702..54f5c31c2 100644 --- a/shells/chrome/manifest.json +++ b/shells/chrome/manifest.json @@ -38,7 +38,8 @@ { "matches": [""], "js": ["build/hook.js"], - "run_at": "document_start" + "run_at": "document_start", + "all_frames": true }, { "matches": [""], diff --git a/shells/chrome/src/backend.js b/shells/chrome/src/backend.js index df865d112..764076803 100644 --- a/shells/chrome/src/backend.js +++ b/shells/chrome/src/backend.js @@ -9,6 +9,7 @@ function handshake (e) { if (e.data.source === 'vue-devtools-proxy' && e.data.payload === 'init') { window.removeEventListener('message', handshake) + console.log('handshaked!') let listeners = [] const bridge = new Bridge({ listen (fn) { @@ -26,7 +27,7 @@ function handshake (e) { payload: data }, '*') } - }) + }, document.URL) bridge.on('shutdown', () => { listeners.forEach(l => { @@ -35,6 +36,7 @@ function handshake (e) { listeners = [] }) + console.log('init backend', bridge.frameURL) initBackend(bridge) } } diff --git a/shells/chrome/src/background.js b/shells/chrome/src/background.js index 5ef64c708..09c2df1af 100644 --- a/shells/chrome/src/background.js +++ b/shells/chrome/src/background.js @@ -4,27 +4,35 @@ const ports = {} chrome.runtime.onConnect.addListener(port => { - let tab + let tabId + let frameId, frameURL let name - if (isNumeric(port.name)) { - tab = port.name + let id + if (isNumeric(port.name.split('#')[0])) { + tabId = +port.name.split('#')[0] + frameURL = port.name.split('#')[1] + id = port.name name = 'devtools' - installProxy(+port.name) + installProxy(tabId, id) } else { - tab = port.sender.tab.id + tabId = port.sender.tab.id + frameId = port.sender.frameId + frameURL = port.sender.url + id = tabId + '#' + frameURL + console.log('heard of backend for tab#frame ', id) name = 'backend' } - if (!ports[tab]) { - ports[tab] = { + if (!ports[id]) { + ports[id] = { devtools: null, backend: null } } - ports[tab][name] = port + ports[id][name] = port - if (ports[tab].devtools && ports[tab].backend) { - doublePipe(tab, ports[tab].devtools, ports[tab].backend) + if (ports[id].devtools && ports[id].backend) { + doublePipe(id, ports[id].devtools, ports[id].backend) } }) @@ -32,14 +40,16 @@ function isNumeric (str) { return +str + '' === str } -function installProxy (tabId) { +function installProxy(tabId, portId) { chrome.tabs.executeScript(tabId, { - file: '/build/proxy.js' + file: '/build/proxy.js', + allFrames: true }, function (res) { if (!res) { - ports[tabId].devtools.postMessage('proxy-fail') + ports[portId].devtools.postMessage('proxy-fail') + console.log('proxy fails', portId) } else { - console.log('injected proxy to tab ' + tabId) + console.log('injected proxy to all frames of tab ' + tabId) } }) } diff --git a/shells/chrome/src/devtools-background.js b/shells/chrome/src/devtools-background.js index a6bfad35b..04082ebc8 100644 --- a/shells/chrome/src/devtools-background.js +++ b/shells/chrome/src/devtools-background.js @@ -17,7 +17,7 @@ function createPanelIfHasVue () { chrome.devtools.inspectedWindow.eval( '!!(window.__VUE_DEVTOOLS_GLOBAL_HOOK__.Vue)', function (hasVue) { - if (!hasVue || created) { + if (created) { //!hasVue || return } clearInterval(checkVueInterval) diff --git a/shells/chrome/src/devtools.js b/shells/chrome/src/devtools.js index ce42483b4..27e2a9995 100644 --- a/shells/chrome/src/devtools.js +++ b/shells/chrome/src/devtools.js @@ -1,41 +1,17 @@ // this script is called when the VueDevtools panel is activated. -import { initDevTools } from 'src/devtools' +import { initDevTools, registerFrame } from 'src/devtools' import Bridge from 'src/bridge' initDevTools({ - /** * Inject backend, connect to background, and send back the bridge. * * @param {Function} cb */ - connect (cb) { - // 1. inject backend code into page - injectScript(chrome.runtime.getURL('build/backend.js'), () => { - // 2. connect to background to setup proxy - const port = chrome.runtime.connect({ - name: '' + chrome.devtools.inspectedWindow.tabId - }) - let disconnected = false - port.onDisconnect.addListener(() => { - disconnected = true - }) - - const bridge = new Bridge({ - listen (fn) { - port.onMessage.addListener(fn) - }, - send (data) { - if (!disconnected) { - port.postMessage(data) - } - } - }) - // 3. send a proxy API to the panel - cb(bridge) - }) + connect(cb) { + cb() }, /** @@ -44,20 +20,37 @@ initDevTools({ * @param {Function} reloadFn */ - onReload (reloadFn) { + onReload(reloadFn) { chrome.devtools.network.onNavigated.addListener(reloadFn) } }) + +function handleRes (res) { + if (res.type === 'document') { + createPortForSubFrame(res.url) + } +} + +// Search for iframes... +// ...on devtool panel load +chrome.devtools.inspectedWindow.getResources(function (res) { + res.map(handleRes) +}) +// ...when they are added to the page afterwards +chrome.devtools.inspectedWindow.onResourceAdded.addListener(handleRes) + + /** * Inject a globally evaluated script, in the same context with the actual * user app. * * @param {String} scriptName + * @param {String} [frameURL] * @param {Function} cb */ -function injectScript (scriptName, cb) { +function injectScriptInFrame (scriptName, frameURL, cb) { const src = ` (function() { var script = document.constructor.prototype.createElement.call(document, 'script'); @@ -66,10 +59,41 @@ function injectScript (scriptName, cb) { script.parentNode.removeChild(script); })() ` - chrome.devtools.inspectedWindow.eval(src, function (res, err) { + chrome.devtools.inspectedWindow.eval(src, { frameURL: frameURL }, function (res, err) { if (err) { console.log(err) } cb() }) } + +function createPortForSubFrame(frameURL) { + console.log('Frame added:', frameURL) + // 1. inject backend code into frame + injectScriptInFrame(chrome.runtime.getURL('build/backend.js'), frameURL, () => { + // 2. connect to background to setup proxy + const port = chrome.runtime.connect({ + name: '' + chrome.devtools.inspectedWindow.tabId + '#' + frameURL, + }) + let disconnected = false + port.onDisconnect.addListener(() => { + disconnected = true + }) + + const bridge = new Bridge({ + listen(fn) { + port.onMessage.addListener(fn) + }, + send(data) { + if (!disconnected) { + port.postMessage(data) + } + } + }, port.name) + // 3. send the new frame along with the proxy API to the panel + registerFrame({ + url: frameURL, + bridge: bridge + }) + }) +} \ No newline at end of file diff --git a/shells/chrome/src/proxy.js b/shells/chrome/src/proxy.js index 1b6123987..6826c1842 100644 --- a/shells/chrome/src/proxy.js +++ b/shells/chrome/src/proxy.js @@ -3,30 +3,37 @@ // to the chrome runtime API. It serves as a proxy between the injected // backend and the Vue devtools panel. -const port = chrome.runtime.connect({ - name: 'content-script' -}) +// Install the proxy only once +if (!window.__VUE_DEVTOOL_PROXY_INSTALLED__) { + window.__VUE_DEVTOOL_PROXY_INSTALLED__ = true -port.onMessage.addListener(sendMessageToBackend) -window.addEventListener('message', sendMessageToDevtools) -port.onDisconnect.addListener(handleDisconnect) + const port = chrome.runtime.connect({ + name: 'content-script' + }) -sendMessageToBackend('init') + port.onMessage.addListener(sendMessageToBackend) + window.addEventListener('message', sendMessageToDevtools) + port.onDisconnect.addListener(handleDisconnect) -function sendMessageToBackend (payload) { - window.postMessage({ - source: 'vue-devtools-proxy', - payload: payload - }, '*') -} + console.log('proxy added for frame ', window.location.href) + sendMessageToBackend('init') -function sendMessageToDevtools (e) { - if (e.data && e.data.source === 'vue-devtools-backend') { - port.postMessage(e.data.payload) + function sendMessageToBackend(payload) { + window.postMessage({ + source: 'vue-devtools-proxy', + payload: payload + }, '*') + } + + function sendMessageToDevtools(e) { + if (e.data && e.data.source === 'vue-devtools-backend') { + port.postMessage(e.data.payload) + } + } + + function handleDisconnect() { + window.removeEventListener('message', sendMessageToDevtools) + sendMessageToBackend('shutdown') } -} -function handleDisconnect () { - window.removeEventListener('message', sendMessageToDevtools) - sendMessageToBackend('shutdown') } diff --git a/src/backend/index.js b/src/backend/index.js index f3ab29b9a..25d6a9265 100644 --- a/src/backend/index.js +++ b/src/backend/index.js @@ -5,7 +5,6 @@ import { highlight, unHighlight, getInstanceRect } from './highlighter' import { initVuexBackend } from './vuex' import { initEventsBackend } from './events' import { stringify, classify, camelize } from '../util' -import { getDocumentTarget, setDocumentTarget, getAllTargets } from './target-document' import path from 'path' // Use a custom basename functions instead of the shimed version @@ -42,6 +41,7 @@ export function initBackend (_bridge) { } function connect () { + console.log('connect') hook.currentTab = 'components' bridge.on('switch-tab', tab => { hook.currentTab = tab @@ -77,13 +77,6 @@ function connect () { flush() }) - bridge.on('change-target', (id) => { - const target = getAllTargets().find(t => t.id === id) - if (target) { - setDocumentTarget(target.doc) - scan() - } - }) bridge.on('refresh', scan) bridge.on('enter-instance', id => highlight(instanceMap.get(id))) bridge.on('leave-instance', unHighlight) @@ -111,10 +104,11 @@ function connect () { */ function scan () { + console.log('scan') rootInstances.length = 0 let inFragment = false let currentFragment = null - walk(getDocumentTarget(), function (node) { + walk(document, function (node) { if (inFragment) { if (node === currentFragment._fragmentEnd) { inFragment = false diff --git a/src/backend/target-document.js b/src/backend/target-document.js index ea568703e..bd98419c0 100644 --- a/src/backend/target-document.js +++ b/src/backend/target-document.js @@ -32,6 +32,7 @@ function packTarget (doc) { export function getAllTargets () { const targets = [packTarget(document)] + console.log('find targets', findTargetsInElement(targets[0])) return targets.concat(findTargetsInElement(targets[0])) } diff --git a/src/bridge.js b/src/bridge.js index f003ee7de..006aea419 100644 --- a/src/bridge.js +++ b/src/bridge.js @@ -1,13 +1,14 @@ import { EventEmitter } from 'events' export default class Bridge extends EventEmitter { - constructor (wall) { + constructor(wall, portName) { super() // Setting `this` to `self` here to fix an error in the Safari build: // ReferenceError: Cannot access uninitialized variable. // The error might be related to the webkit bug here: // https://bugs.webkit.org/show_bug.cgi?id=171543 const self = this + self.portName = portName self.setMaxListeners(Infinity) self.wall = wall wall.listen(message => { diff --git a/src/devtools/App.vue b/src/devtools/App.vue index ac64b1cca..8e462a3de 100644 --- a/src/devtools/App.vue +++ b/src/devtools/App.vue @@ -5,8 +5,8 @@
- {{ message }} - + {{ message }} + state.message, tab: state => state.tab, - newEventCount: state => state.events.newEventCount + newEventCount: state => state.events.newEventCount, + hasMultipleFrames: state => state.availableFrames.length > 1 }), methods: { switchTab (tab) { @@ -82,6 +83,7 @@ export default { const refreshIcon = this.$refs.refresh refreshIcon.style.animation = 'none' + console.log('refresh asked to', bridge.frameURL) bridge.send('refresh') bridge.once('flush', () => { refreshIcon.style.animation = 'rotate 1s' diff --git a/src/devtools/components/FrameSelector.vue b/src/devtools/components/FrameSelector.vue new file mode 100644 index 000000000..2980890c7 --- /dev/null +++ b/src/devtools/components/FrameSelector.vue @@ -0,0 +1,32 @@ + + + diff --git a/src/devtools/components/TargetSelector.vue b/src/devtools/components/TargetSelector.vue deleted file mode 100644 index defd452d3..000000000 --- a/src/devtools/components/TargetSelector.vue +++ /dev/null @@ -1,41 +0,0 @@ - - - diff --git a/src/devtools/index.js b/src/devtools/index.js index 574915f6e..1c6792a6b 100644 --- a/src/devtools/index.js +++ b/src/devtools/index.js @@ -1,7 +1,6 @@ import Vue from 'vue' import App from './App.vue' import store from './store' -import { parse } from '../util' // Capture and log devtool errors when running as actual extension // so that we can debug it by inspecting the background page. @@ -44,7 +43,9 @@ export function initDevTools (shell) { if (app) { app.$destroy() } - bridge.removeAllListeners() + if (bridge) + bridge.removeAllListeners() + store.dispatch('resetAvailableFrames') initApp(shell) }) } @@ -57,48 +58,7 @@ export function initDevTools (shell) { */ function initApp (shell) { - shell.connect(bridge => { - window.bridge = bridge - - bridge.once('ready', version => { - store.commit( - 'SHOW_MESSAGE', - 'Ready. Detected Vue ' + version + '.' - ) - bridge.send('vuex:toggle-recording', store.state.vuex.enabled) - bridge.send('events:toggle-recording', store.state.events.enabled) - }) - - bridge.once('proxy-fail', () => { - store.commit( - 'SHOW_MESSAGE', - 'Proxy injection failed.' - ) - }) - - bridge.on('flush', payload => { - store.commit('components/FLUSH', parse(payload)) - }) - - bridge.on('instance-details', details => { - store.commit('components/RECEIVE_INSTANCE_DETAILS', parse(details)) - }) - - bridge.on('vuex:init', snapshot => { - store.commit('vuex/INIT', snapshot) - }) - - bridge.on('vuex:mutation', payload => { - store.commit('vuex/RECEIVE_MUTATION', payload) - }) - - bridge.on('event:triggered', payload => { - store.commit('events/RECEIVE_EVENT', parse(payload)) - if (store.state.tab !== 'events') { - store.commit('events/INCREASE_NEW_EVENT_COUNT') - } - }) - + shell.connect(() => { app = new Vue({ store, render (h) { @@ -107,3 +67,15 @@ function initApp (shell) { }).$mount('#app') }) } + +/** + * Register a frame to become inspectable + * + * @param {object} frame + * @param {string} frame.url + * @param {Bridge} frame.bridge + */ +export function registerFrame(frame) { + console.log('Register frame', frame) + store.dispatch('registerFrame', frame) +} diff --git a/src/devtools/store/index.js b/src/devtools/store/index.js index c14ebb1bd..06e0467f1 100644 --- a/src/devtools/store/index.js +++ b/src/devtools/store/index.js @@ -3,13 +3,16 @@ import Vuex from 'vuex' import components from 'views/components/module' import vuex from 'views/vuex/module' import events from 'views/events/module' +import { parse } from '../../util' Vue.use(Vuex) const store = new Vuex.Store({ state: { message: '', - tab: 'components' + tab: 'components', + currentFrame: undefined, + availableFrames: [] }, mutations: { SHOW_MESSAGE (state, message) { @@ -18,10 +21,95 @@ const store = new Vuex.Store({ SWITCH_TAB (state, tab) { state.tab = tab }, + SET_CURRENT_FRAME (state, frame) { + state.currentFrame = frame + }, + ADD_FRAME (state, newFrame) { + state.availableFrames.push(newFrame) + }, + RESET_FRAMES (state) { + state.availableFrames = [] + }, RECEIVE_INSTANCE_DETAILS (state, instance) { state.message = 'Instance selected: ' + instance.name } }, + actions: { + resetAvailableFrames({ commit }) { + commit('SET_CURRENT_FRAME', undefined) + commit('RESET_FRAMES') + }, + + registerFrame({ commit, dispatch, state }, frame) { + commit('ADD_FRAME', frame) + if (state.availableFrames.length == 1) { + console.log('Selecting first frame by default') + dispatch('selectFrame', frame) + } + }, + + selectFrameByURL ({ dispatch, state }, frameURL) { + dispatch( + 'selectFrame', + state.availableFrames.find(f => f.url == frameURL) + ) + }, + + selectFrame ({ commit, state }, frame) { + if (!frame) { return } + commit('SET_CURRENT_FRAME', frame) + + // Cancel previous bridge + if (window.bridge) { + window.bridge.removeAllListeners() + } + + // Use the new frame bridge + let bridge = frame.bridge + window.bridge = bridge + + bridge.once('ready', version => { + store.commit( + 'SHOW_MESSAGE', + 'Ready. Detected Vue ' + version + '.' + ) + bridge.send('vuex:toggle-recording', store.state.vuex.enabled) + bridge.send('events:toggle-recording', store.state.events.enabled) + }) + + bridge.once('proxy-fail', () => { + store.commit( + 'SHOW_MESSAGE', + 'Proxy injection failed.' + ) + }) + + bridge.on('flush', payload => { + store.commit('components/FLUSH', parse(payload)) + }) + + bridge.on('instance-details', details => { + store.commit('components/RECEIVE_INSTANCE_DETAILS', parse(details)) + }) + + bridge.on('vuex:init', snapshot => { + store.commit('vuex/INIT', snapshot) + }) + + bridge.on('vuex:mutation', payload => { + store.commit('vuex/RECEIVE_MUTATION', payload) + }) + + bridge.on('event:triggered', payload => { + store.commit('events/RECEIVE_EVENT', parse(payload)) + if (store.state.tab !== 'events') { + store.commit('events/INCREASE_NEW_EVENT_COUNT') + } + }) + + console.log('Bridge in use is now:', bridge) + } + }, modules: { components, vuex,