diff --git a/packages/@weex/plugins/debug/.gitignore b/packages/@weex/plugins/debug/.gitignore index bb987ae..87ae600 100644 --- a/packages/@weex/plugins/debug/.gitignore +++ b/packages/@weex/plugins/debug/.gitignore @@ -3,6 +3,7 @@ node_modules/ # coverage coverage/ +!frontend/src/assets/inspector/coverage/ package-lock.json diff --git a/packages/@weex/plugins/debug/.npmignore b/packages/@weex/plugins/debug/.npmignore index 1257691..a16c9c2 100644 --- a/packages/@weex/plugins/debug/.npmignore +++ b/packages/@weex/plugins/debug/.npmignore @@ -1,5 +1,6 @@ node_modules coverage/ +!frontend/public/assets/inspector/coverage/ .git/ -.vscode/ +.vscode/ \ No newline at end of file diff --git a/packages/@weex/plugins/debug/frontend/config/webpack.config.base.js b/packages/@weex/plugins/debug/frontend/config/webpack.config.base.js index f215df7..1f4ca68 100644 --- a/packages/@weex/plugins/debug/frontend/config/webpack.config.base.js +++ b/packages/@weex/plugins/debug/frontend/config/webpack.config.base.js @@ -54,10 +54,7 @@ let config = { from: 'src/assets', to: './assets' },{ - from: 'src/runtime.html', - to: './', - },{ - from: 'src/runtime', + from: 'runtime', to: './runtime', },{ from: 'node_modules/monaco-editor/min/vs', diff --git a/packages/@weex/plugins/debug/frontend/package.json b/packages/@weex/plugins/debug/frontend/package.json index 632bc84..eb9f7c9 100644 --- a/packages/@weex/plugins/debug/frontend/package.json +++ b/packages/@weex/plugins/debug/frontend/package.json @@ -89,7 +89,7 @@ "mocha": "~5.0.0", "monaco-editor": "^0.14.3", "ncp": "~2.0.0", - "node-sass": "~4.7.2", + "node-sass": "^4.7.2", "opn-cli": "~3.1.0", "optimize-css-assets-webpack-plugin": "~3.2.0", "postcss-loader": "~2.0.10", @@ -110,6 +110,6 @@ "vue-loader": "^15.4.2", "vue-template-compiler": "^2.5.17", "webpack": "~3.10.0", - "webpack-dev-server": "^3.1.11" + "webpack-dev-server": "^2.11.1" } } diff --git a/packages/@weex/plugins/debug/frontend/runtime/EventEmitter.js b/packages/@weex/plugins/debug/frontend/runtime/EventEmitter.js new file mode 100644 index 0000000..bc55fbb --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/runtime/EventEmitter.js @@ -0,0 +1,64 @@ +function EventEmitter() { + this._handlers = {}; +} +EventEmitter.prototype = { + constructor: EventEmitter, + off: function (method, handler) { + if (handler) { + for (var i = 0; i < this._handlers[method].length; i++) { + if (this._handlers[method][i] === handler) { + this._handlers[method].splice(i, 1); + i--; + } + } + } + else { + this._handlers[method] = []; + } + }, + once: function (method, handler) { + var self = this; + var fired = false; + + function g() { + self.off(method, g); + if (!fired) { + fired = true; + handler.apply(self, Array.prototype.slice.call(arguments)); + } + } + + this.on(method, g); + }, + on: function (method, handler) { + if (this._handlers[method]) { + this._handlers[method].push(handler); + } + else { + this._handlers[method] = [handler]; + } + }, + + _emit: function (method, args, context) { + var handlers = this._handlers[method]; + if (handlers && handlers.length > 0) { + handlers.forEach(function (handler) { + handler.apply(context, args) + }); + return true; + } + else { + return false; + } + }, + + emit: function (method) { + var context = {}; + var args = Array.prototype.slice.call(arguments, 1); + if (!this._emit(method, args, context)) { + this._emit('*', args, context) + } + this._emit('$finally', args, context); + return context; + } +}; \ No newline at end of file diff --git a/packages/@weex/plugins/debug/frontend/runtime/WebsocketClient.js b/packages/@weex/plugins/debug/frontend/runtime/WebsocketClient.js new file mode 100644 index 0000000..41cd4d5 --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/runtime/WebsocketClient.js @@ -0,0 +1,52 @@ +function WebsocketClient(url) { + this.connect(url); +} +WebsocketClient.prototype = { + constructor: WebsocketClient, + connect: function (url) { + var self = this; + self.isSocketReady = false; + self._sended = []; + self._received = []; + if (self.ws) { + self.ws.onopen = null; + self.ws.onmessage = null; + self.ws.onclose = null; + if (self.ws.readyState == WebSocket.OPEN) { + self.ws.close(); + } + } + var ws = new WebSocket(url); + self.ws = ws; + ws.onopen = function () { + self.isSocketReady = true; + self.emit('socketOpened'); + }; + ws.onmessage = function (e) { + var message = JSON.parse(e.data); + if (message.method) { + self.emit(message.method, message); + } + }; + ws.onclose = function () { + self.isSocketReady = false; + self.emit('socketClose'); + }; + + }, + send: function (data) { + var self = this; + if (self.isSocketReady) { + self.ws.send(JSON.stringify(data)); + } + else { + self.once('socketOpened', function () { + self.ws.send(JSON.stringify(data)) + }); + } + }, + close: function () { + this.ws && this.ws.close(); + } +}; +WebsocketClient.prototype.__proto__ = new EventEmitter(); diff --git a/packages/@weex/plugins/debug/frontend/runtime/index.js b/packages/@weex/plugins/debug/frontend/runtime/index.js new file mode 100644 index 0000000..ab8e001 --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/runtime/index.js @@ -0,0 +1,152 @@ +var workers = {}; +var instanceMaps = {}; +var RuntimeSocket +var BrowserChannelId +var cacheWeexEnv; +var cacheJsbundleImportMessage; +var cacheRegisterLoop = []; +var cacheSyncList = []; +var activeWorkerId; +var EntrySocket = new WebsocketClient('ws://' + location.host + '/page/entry'); + +EntrySocket.on('WxDebug.startDebugger', function (message) { + if (!RuntimeSocket) { + location.href = `http://${location.host}/runtime/runtime.html?channelId=${message.params}` + } + else if(RuntimeSocket && BrowserChannelId!==message.params){ + location.href = `http://${location.host}/runtime/runtime.html?channelId=${message.params}` + } +}) + +BrowserChannelId = new URLSearchParams(location.search).get('channelId'); + +if (BrowserChannelId) { + connect(BrowserChannelId) +} + +function connect(channelId) { + RuntimeSocket = new WebsocketClient('ws://' + window.location.host + '/debugProxy/runtime/' + channelId); + + RuntimeSocket.on('*', function (message) { + if (!message) return; + var domain = message.method.split('.')[0]; + if (domain === 'WxDebug') { + var instanceId; + if (message && message.params) { + instanceId = message.params && message.params.args && message.params.args[0]; + } + else { + instanceId = activeWorkerId + } + if (workers[instanceId]) { + workers[instanceId].postMessage(message); + } + } + }); + + RuntimeSocket.on('WxDebug.deviceDisconnect', function () { + location.href = `http://${location.host}/runtime/runtime.html` + }) + + RuntimeSocket.on('WxDebug.refresh', function () { + location.reload(); + }); + + RuntimeSocket.on('WxDebug.callJS', function (message) { + var instanceId = message.params.args[0]; + if (message.params.method === 'createInstanceContext') { + destroyJSRuntime(message) + message.channelId = BrowserChannelId; + message.method = 'WxDebug.initSandboxWorker'; + message.params.env = cacheWeexEnv; + message.params.syncList = cacheSyncList.splice(0, cacheSyncList.length); + initJSRuntime(message) + } + else if(message.params.method === 'createInstance') { + destroyJSRuntime(message) + message.channelId = BrowserChannelId; + message.method = 'WxDebug.initWorker'; + message.params.env = cacheWeexEnv; + initJSRuntime(message) + } + else if(message.params.method === 'importScript') { + if (workers[instanceId]) { + workers[instanceId].postMessage(message) + } + else { + cacheJsbundleImportMessage = message; + } + } + else if(message.params.method === 'destroyInstance') { + destroyJSRuntime(message); + } + else if (message.params.args && (message.params.method === 'registerComponents' || message.params.method === 'registerModules' || message.params.method === 'getJSFMVersion' || message.params.method === 'getJSFMVersion')) { + cacheRegisterLoop.push(message); + } + else { + if (message.params && message.params.args && message.params.args[0] && workers[message.params.args[0]]) { + workers[message.params.args[0]].postMessage(message); + } + else if (activeWorkerId && workers[activeWorkerId]) { + workers[activeWorkerId].postMessage(message); + } + } + }); + + RuntimeSocket.on('WxDebug.initJSRuntime', function (message) { + var logLevel = localStorage.getItem('logLevel'); + if (logLevel) { + message.params.env.WXEnvironment.logLevel = logLevel; + } + cacheWeexEnv = message.params.env; + cacheRegisterLoop = []; + }); +} + +function destroyJSRuntime(message) { + var instanceId = message.params.args[0]; + var workerjs = message.params.workerjs; + if (workerjs) { + instanceId = instanceMaps[workerjs] + } + if (workers[instanceId]) { + if (workers[instanceId].prev) { + activeWorkerId = workers[instanceId].prev; + } + else { + activeWorkerId = null; + } + workers[instanceId].terminate(); + delete workers[instanceId]; + } +} + +function initJSRuntime(message) { + var instanceId = activeWorkerId = message.params.args[0]; + instanceMaps[message.params.workerjs] = instanceId; + workers[instanceId] = new Worker(message.params.workerjs); + workers[instanceId]['prev'] = getPrevWorker(workers); + workers[instanceId].onmessage = function (message) { + message = message.data; + RuntimeSocket.send(message); + }; + cacheRegisterLoop.forEach(function(message) { + workers[instanceId].postMessage(message) + }) + if (cacheJsbundleImportMessage) { + workers[instanceId].postMessage(message); + } + workers[instanceId].postMessage(message); +} + +function getPrevWorker(workers) { + var lists = Object.keys(workers); + if (lists.length === 0) return null; + for(var i = lists.length - 2; i >=0; i--) { + if (workers[lists[lists.length - 2]]) { + return lists[lists.length - 2]; + } + } + return null; +} + diff --git a/packages/@weex/plugins/debug/frontend/src/runtime.html b/packages/@weex/plugins/debug/frontend/runtime/runtime.html similarity index 50% rename from packages/@weex/plugins/debug/frontend/src/runtime.html rename to packages/@weex/plugins/debug/frontend/runtime/runtime.html index c5af4c0..60f43e5 100644 --- a/packages/@weex/plugins/debug/frontend/src/runtime.html +++ b/packages/@weex/plugins/debug/frontend/runtime/runtime.html @@ -3,9 +3,9 @@ Weex Devtool - JS Debugger - - - + + +
diff --git a/packages/@weex/plugins/debug/frontend/src/assets/inspector/Runtime.js b/packages/@weex/plugins/debug/frontend/src/assets/inspector/Runtime.js new file mode 100644 index 0000000..f48f9e4 --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/src/assets/inspector/Runtime.js @@ -0,0 +1,1128 @@ +/* + * Copyright (C) 2014 Google Inc. All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Google Inc. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +// This gets all concatenated module descriptors in the release mode. +const allDescriptors = []; +let applicationDescriptor; +const _loadedScripts = {}; + +// FIXME: This is a workaround to force Closure compiler provide +// the standard ES6 runtime for all modules. This should be removed +// once Closure provides standard externs for Map et al. +for (const k of []) { // eslint-disable-line +} + +(function() { +const baseUrl = self.location ? self.location.origin + self.location.pathname : ''; +self._importScriptPathPrefix = baseUrl.substring(0, baseUrl.lastIndexOf('/') + 1); +})(); + +const REMOTE_MODULE_FALLBACK_REVISION = '@010ddcfda246975d194964ccf20038ebbdec6084'; + +/** + * @unrestricted + */ +var Runtime = class { // eslint-disable-line + /** + * @param {!Array.} descriptors + */ + constructor(descriptors) { + /** @type {!Array} */ + this._modules = []; + /** @type {!Object} */ + this._modulesMap = {}; + /** @type {!Array} */ + this._extensions = []; + /** @type {!Object} */ + this._cachedTypeClasses = {}; + /** @type {!Object} */ + this._descriptorsMap = {}; + + for (let i = 0; i < descriptors.length; ++i) + this._registerModule(descriptors[i]); + + Runtime._runtimeReadyPromiseCallback(); + } + + /** + * @param {string} url + * @return {!Promise.} + */ + static loadResourcePromise(url) { + return new Promise(load); + + /** + * @param {function(?)} fulfill + * @param {function(*)} reject + */ + function load(fulfill, reject) { + const xhr = new XMLHttpRequest(); + xhr.open('GET', url, true); + xhr.onreadystatechange = onreadystatechange; + + /** + * @param {Event} e + */ + function onreadystatechange(e) { + if (xhr.readyState !== XMLHttpRequest.DONE) + return; + + // DevTools Proxy server can mask 404s as 200s, check the body to be sure + const status = /^HTTP\/1.1 404/.test(e.target.response) ? 404 : xhr.status; + + if ([0, 200, 304].indexOf(status) === -1) // Testing harness file:/// results in 0. + reject(new Error('While loading from url ' + url + ' server responded with a status of ' + status)); + else + fulfill(e.target.response); + } + xhr.send(null); + } + } + + /** + * @param {string} url + * @return {!Promise.} + */ + static loadResourcePromiseWithFallback(url) { + return Runtime.loadResourcePromise(url).catch(err => { + const urlWithFallbackVersion = url.replace(/@[0-9a-f]{40}/, REMOTE_MODULE_FALLBACK_REVISION); + // TODO(phulce): mark fallbacks in module.json and modify build script instead + if (urlWithFallbackVersion === url || !url.includes('audits2_worker_module')) + throw err; + return Runtime.loadResourcePromise(urlWithFallbackVersion); + }); + } + + /** + * http://tools.ietf.org/html/rfc3986#section-5.2.4 + * @param {string} path + * @return {string} + */ + static normalizePath(path) { + if (path.indexOf('..') === -1 && path.indexOf('.') === -1) + return path; + + const normalizedSegments = []; + const segments = path.split('/'); + for (let i = 0; i < segments.length; i++) { + const segment = segments[i]; + if (segment === '.') + continue; + else if (segment === '..') + normalizedSegments.pop(); + else if (segment) + normalizedSegments.push(segment); + } + let normalizedPath = normalizedSegments.join('/'); + if (normalizedPath[normalizedPath.length - 1] === '/') + return normalizedPath; + if (path[0] === '/' && normalizedPath) + normalizedPath = '/' + normalizedPath; + if ((path[path.length - 1] === '/') || (segments[segments.length - 1] === '.') || + (segments[segments.length - 1] === '..')) + normalizedPath = normalizedPath + '/'; + + return normalizedPath; + } + + /** + * @param {!Array.} scriptNames + * @param {string=} base + * @return {!Promise.} + */ + static _loadScriptsPromise(scriptNames, base) { + /** @type {!Array>} */ + const promises = []; + /** @type {!Array} */ + const urls = []; + const sources = new Array(scriptNames.length); + let scriptToEval = 0; + for (let i = 0; i < scriptNames.length; ++i) { + const scriptName = scriptNames[i]; + let sourceURL = (base || self._importScriptPathPrefix) + scriptName; + + const schemaIndex = sourceURL.indexOf('://') + 3; + let pathIndex = sourceURL.indexOf('/', schemaIndex); + if (pathIndex === -1) + pathIndex = sourceURL.length; + sourceURL = sourceURL.substring(0, pathIndex) + Runtime.normalizePath(sourceURL.substring(pathIndex)); + + if (_loadedScripts[sourceURL]) + continue; + urls.push(sourceURL); + const loadResourcePromise = + base ? Runtime.loadResourcePromiseWithFallback(sourceURL) : Runtime.loadResourcePromise(sourceURL); + promises.push( + loadResourcePromise.then(scriptSourceLoaded.bind(null, i), scriptSourceLoaded.bind(null, i, undefined))); + } + return Promise.all(promises).then(undefined); + + /** + * @param {number} scriptNumber + * @param {string=} scriptSource + */ + function scriptSourceLoaded(scriptNumber, scriptSource) { + sources[scriptNumber] = scriptSource || ''; + // Eval scripts as fast as possible. + while (typeof sources[scriptToEval] !== 'undefined') { + evaluateScript(urls[scriptToEval], sources[scriptToEval]); + ++scriptToEval; + } + } + + /** + * @param {string} sourceURL + * @param {string=} scriptSource + */ + function evaluateScript(sourceURL, scriptSource) { + _loadedScripts[sourceURL] = true; + if (!scriptSource) { + // Do not reject, as this is normal in the hosted mode. + console.error('Empty response arrived for script \'' + sourceURL + '\''); + return; + } + self.eval(scriptSource + '\n//# sourceURL=' + sourceURL); + } + } + + /** + * @param {string} url + * @param {boolean} appendSourceURL + * @return {!Promise} + */ + static _loadResourceIntoCache(url, appendSourceURL) { + return Runtime.loadResourcePromise(url).then( + cacheResource.bind(this, url), cacheResource.bind(this, url, undefined)); + + /** + * @param {string} path + * @param {string=} content + */ + function cacheResource(path, content) { + if (!content) { + console.error('Failed to load resource: ' + path); + return; + } + const sourceURL = appendSourceURL ? Runtime.resolveSourceURL(path) : ''; + Runtime.cachedResources[path] = content + sourceURL; + } + } + + /** + * @return {!Promise} + */ + static async runtimeReady() { + return Runtime._runtimeReadyPromise; + } + + /** + * @param {string} appName + * @return {!Promise.} + */ + static async startApplication(appName) { + console.timeStamp('Runtime.startApplication'); + + const allDescriptorsByName = {}; + for (let i = 0; i < allDescriptors.length; ++i) { + const d = allDescriptors[i]; + allDescriptorsByName[d['name']] = d; + } + + if (!applicationDescriptor) { + let data = await Runtime.loadResourcePromise(appName + '.json'); + applicationDescriptor = JSON.parse(data); + let descriptor = applicationDescriptor; + while (descriptor.extends) { + data = await Runtime.loadResourcePromise(descriptor.extends + '.json'); + descriptor = JSON.parse(data); + applicationDescriptor.modules = descriptor.modules.concat(applicationDescriptor.modules); + } + } + + const configuration = applicationDescriptor.modules; + const moduleJSONPromises = []; + const coreModuleNames = []; + for (let i = 0; i < configuration.length; ++i) { + const descriptor = configuration[i]; + const name = descriptor['name']; + const moduleJSON = allDescriptorsByName[name]; + if (moduleJSON) + moduleJSONPromises.push(Promise.resolve(moduleJSON)); + else + moduleJSONPromises.push(Runtime.loadResourcePromise(name + '/module.json').then(JSON.parse.bind(JSON))); + if (descriptor['type'] === 'autostart') + coreModuleNames.push(name); + } + + const moduleDescriptors = await Promise.all(moduleJSONPromises); + + for (let i = 0; i < moduleDescriptors.length; ++i) { + moduleDescriptors[i].name = configuration[i]['name']; + moduleDescriptors[i].condition = configuration[i]['condition']; + moduleDescriptors[i].remote = configuration[i]['type'] === 'remote'; + } + self.runtime = new Runtime(moduleDescriptors); + if (coreModuleNames) + return /** @type {!Promise} */ (self.runtime._loadAutoStartModules(coreModuleNames)); + } + + /** + * @param {string} appName + * @return {!Promise.} + */ + static startWorker(appName) { + return Runtime.startApplication(appName).then(sendWorkerReady); + + function sendWorkerReady() { + self.postMessage('workerReady'); + } + } + + /** + * @param {string} name + * @return {?string} + */ + static queryParam(name) { + return Runtime._queryParamsObject[name] || null; + } + + /** + * @return {string} + */ + static queryParamsString() { + return location.search; + } + + /** + * @return {!Object} + */ + static _experimentsSetting() { + try { + return /** @type {!Object} */ ( + JSON.parse(self.localStorage && self.localStorage['experiments'] ? self.localStorage['experiments'] : '{}')); + } catch (e) { + console.error('Failed to parse localStorage[\'experiments\']'); + return {}; + } + } + + static _assert(value, message) { + if (value) + return; + Runtime._originalAssert.call(Runtime._console, value, message + ' ' + new Error().stack); + } + + /** + * @param {string} platform + */ + static setPlatform(platform) { + Runtime._platform = platform; + } + + /** + * @param {!Object} descriptor + * @return {boolean} + */ + static _isDescriptorEnabled(descriptor) { + const activatorExperiment = descriptor['experiment']; + if (activatorExperiment === '*') + return Runtime.experiments.supportEnabled(); + if (activatorExperiment && activatorExperiment.startsWith('!') && + Runtime.experiments.isEnabled(activatorExperiment.substring(1))) + return false; + if (activatorExperiment && !activatorExperiment.startsWith('!') && + !Runtime.experiments.isEnabled(activatorExperiment)) + return false; + const condition = descriptor['condition']; + if (condition && !condition.startsWith('!') && !Runtime.queryParam(condition)) + return false; + if (condition && condition.startsWith('!') && Runtime.queryParam(condition.substring(1))) + return false; + return true; + } + + /** + * @param {string} path + * @return {string} + */ + static resolveSourceURL(path) { + let sourceURL = self.location.href; + if (self.location.search) + sourceURL = sourceURL.replace(self.location.search, ''); + sourceURL = sourceURL.substring(0, sourceURL.lastIndexOf('/') + 1) + path; + return '\n/*# sourceURL=' + sourceURL + ' */'; + } + + useTestBase() { + Runtime._remoteBase = 'http://localhost:8000/inspector-sources/'; + if (Runtime.queryParam('debugFrontend')) + Runtime._remoteBase += 'debug/'; + } + + /** + * @param {!Runtime.ModuleDescriptor} descriptor + */ + _registerModule(descriptor) { + const module = new Runtime.Module(this, descriptor); + this._modules.push(module); + this._modulesMap[descriptor['name']] = module; + } + + /** + * @param {string} moduleName + * @return {!Promise.} + */ + loadModulePromise(moduleName) { + return this._modulesMap[moduleName]._loadPromise(); + } + + /** + * @param {!Array.} moduleNames + * @return {!Promise.>} + */ + _loadAutoStartModules(moduleNames) { + const promises = []; + for (let i = 0; i < moduleNames.length; ++i) + promises.push(this.loadModulePromise(moduleNames[i])); + return Promise.all(promises); + } + + /** + * @param {!Runtime.Extension} extension + * @param {?function(function(new:Object)):boolean} predicate + * @return {boolean} + */ + _checkExtensionApplicability(extension, predicate) { + if (!predicate) + return false; + const contextTypes = extension.descriptor().contextTypes; + if (!contextTypes) + return true; + for (let i = 0; i < contextTypes.length; ++i) { + const contextType = this._resolve(contextTypes[i]); + const isMatching = !!contextType && predicate(contextType); + if (isMatching) + return true; + } + return false; + } + + /** + * @param {!Runtime.Extension} extension + * @param {?Object} context + * @return {boolean} + */ + isExtensionApplicableToContext(extension, context) { + if (!context) + return true; + return this._checkExtensionApplicability(extension, isInstanceOf); + + /** + * @param {!Function} targetType + * @return {boolean} + */ + function isInstanceOf(targetType) { + return context instanceof targetType; + } + } + + /** + * @param {!Runtime.Extension} extension + * @param {!Set.=} currentContextTypes + * @return {boolean} + */ + isExtensionApplicableToContextTypes(extension, currentContextTypes) { + if (!extension.descriptor().contextTypes) + return true; + + return this._checkExtensionApplicability(extension, currentContextTypes ? isContextTypeKnown : null); + + /** + * @param {!Function} targetType + * @return {boolean} + */ + function isContextTypeKnown(targetType) { + return currentContextTypes.has(targetType); + } + } + + /** + * @param {*} type + * @param {?Object=} context + * @param {boolean=} sortByTitle + * @return {!Array.} + */ + extensions(type, context, sortByTitle) { + return this._extensions.filter(filter).sort(sortByTitle ? titleComparator : orderComparator); + + /** + * @param {!Runtime.Extension} extension + * @return {boolean} + */ + function filter(extension) { + if (extension._type !== type && extension._typeClass() !== type) + return false; + if (!extension.enabled()) + return false; + return !context || extension.isApplicable(context); + } + + /** + * @param {!Runtime.Extension} extension1 + * @param {!Runtime.Extension} extension2 + * @return {number} + */ + function orderComparator(extension1, extension2) { + const order1 = extension1.descriptor()['order'] || 0; + const order2 = extension2.descriptor()['order'] || 0; + return order1 - order2; + } + + /** + * @param {!Runtime.Extension} extension1 + * @param {!Runtime.Extension} extension2 + * @return {number} + */ + function titleComparator(extension1, extension2) { + const title1 = extension1.title() || ''; + const title2 = extension2.title() || ''; + return title1.localeCompare(title2); + } + } + + /** + * @param {*} type + * @param {?Object=} context + * @return {?Runtime.Extension} + */ + extension(type, context) { + return this.extensions(type, context)[0] || null; + } + + /** + * @param {*} type + * @param {?Object=} context + * @return {!Promise.>} + */ + allInstances(type, context) { + return Promise.all(this.extensions(type, context).map(extension => extension.instance())); + } + + /** + * @return {?function(new:Object)} + */ + _resolve(typeName) { + if (!this._cachedTypeClasses[typeName]) { + const path = typeName.split('.'); + let object = self; + for (let i = 0; object && (i < path.length); ++i) + object = object[path[i]]; + if (object) + this._cachedTypeClasses[typeName] = /** @type function(new:Object) */ (object); + } + return this._cachedTypeClasses[typeName] || null; + } + + /** + * @param {!Function} constructorFunction + * @return {!Object} + */ + sharedInstance(constructorFunction) { + if (Runtime._instanceSymbol in constructorFunction && + Object.getOwnPropertySymbols(constructorFunction).includes(Runtime._instanceSymbol)) + return constructorFunction[Runtime._instanceSymbol]; + + const instance = new constructorFunction(); + constructorFunction[Runtime._instanceSymbol] = instance; + return instance; + } +}; + +/** + * @type {!Object.} + */ +Runtime._queryParamsObject = { + __proto__: null +}; + +Runtime._instanceSymbol = Symbol('instance'); + +/** + * @type {!Object.} + */ +Runtime.cachedResources = { + __proto__: null +}; + + +Runtime._console = console; +Runtime._originalAssert = console.assert; + + +Runtime._platform = ''; + + +/** + * @unrestricted + */ +Runtime.ModuleDescriptor = class { + constructor() { + /** + * @type {string} + */ + this.name; + + /** + * @type {!Array.} + */ + this.extensions; + + /** + * @type {!Array.|undefined} + */ + this.dependencies; + + /** + * @type {!Array.} + */ + this.scripts; + + /** + * @type {string|undefined} + */ + this.condition; + + /** + * @type {boolean|undefined} + */ + this.remote; + } +}; + +/** + * @unrestricted + */ +Runtime.ExtensionDescriptor = class { + constructor() { + /** + * @type {string} + */ + this.type; + + /** + * @type {string|undefined} + */ + this.className; + + /** + * @type {string|undefined} + */ + this.factoryName; + + /** + * @type {!Array.|undefined} + */ + this.contextTypes; + } +}; + +/** + * @unrestricted + */ +Runtime.Module = class { + /** + * @param {!Runtime} manager + * @param {!Runtime.ModuleDescriptor} descriptor + */ + constructor(manager, descriptor) { + this._manager = manager; + this._descriptor = descriptor; + this._name = descriptor.name; + /** @type {!Array} */ + this._extensions = []; + + /** @type {!Map>} */ + this._extensionsByClassName = new Map(); + const extensions = /** @type {?Array.} */ (descriptor.extensions); + for (let i = 0; extensions && i < extensions.length; ++i) { + const extension = new Runtime.Extension(this, extensions[i]); + this._manager._extensions.push(extension); + this._extensions.push(extension); + } + this._loadedForTest = false; + } + + /** + * @return {string} + */ + name() { + return this._name; + } + + /** + * @return {boolean} + */ + enabled() { + return Runtime._isDescriptorEnabled(this._descriptor); + } + + /** + * @param {string} name + * @return {string} + */ + resource(name) { + const fullName = this._name + '/' + name; + const content = Runtime.cachedResources[fullName]; + if (!content) + throw new Error(fullName + ' not preloaded. Check module.json'); + return content; + } + + /** + * @return {!Promise.} + */ + _loadPromise() { + if (!this.enabled()) + return Promise.reject(new Error('Module ' + this._name + ' is not enabled')); + + if (this._pendingLoadPromise) + return this._pendingLoadPromise; + + const dependencies = this._descriptor.dependencies; + const dependencyPromises = []; + for (let i = 0; dependencies && i < dependencies.length; ++i) + dependencyPromises.push(this._manager._modulesMap[dependencies[i]]._loadPromise()); + + this._pendingLoadPromise = Promise.all(dependencyPromises) + .then(this._loadResources.bind(this)) + .then(this._loadScripts.bind(this)) + .then(() => this._loadedForTest = true); + + return this._pendingLoadPromise; + } + + /** + * @return {!Promise.} + * @this {Runtime.Module} + */ + _loadResources() { + const resources = this._descriptor['resources']; + if (!resources || !resources.length) + return Promise.resolve(); + const promises = []; + for (let i = 0; i < resources.length; ++i) { + const url = this._modularizeURL(resources[i]); + promises.push(Runtime._loadResourceIntoCache(url, true)); + } + return Promise.all(promises).then(undefined); + } + + /** + * @return {!Promise.} + */ + _loadScripts() { + if (!this._descriptor.scripts || !this._descriptor.scripts.length) + return Promise.resolve(); + + // Module namespaces. + // NOTE: Update scripts/special_case_namespaces.json if you add a special cased namespace. + // The namespace keyword confuses clang-format. + // clang-format off + const specialCases = { + 'sdk': 'SDK', + 'js_sdk': 'JSSDK', + 'browser_sdk': 'BrowserSDK', + 'ui': 'UI', + 'object_ui': 'ObjectUI', + 'javascript_metadata': 'JavaScriptMetadata', + 'perf_ui': 'PerfUI', + 'har_importer': 'HARImporter', + 'sdk_test_runner': 'SDKTestRunner', + 'cpu_profiler_test_runner': 'CPUProfilerTestRunner' + }; + const namespace = specialCases[this._name] || this._name.split('_').map(a => a.substring(0, 1).toUpperCase() + a.substring(1)).join(''); + self[namespace] = self[namespace] || {}; + // clang-format on + return Runtime._loadScriptsPromise(this._descriptor.scripts.map(this._modularizeURL, this), this._remoteBase()); + } + + /** + * @param {string} resourceName + */ + _modularizeURL(resourceName) { + return Runtime.normalizePath(this._name + '/' + resourceName); + } + + /** + * @return {string|undefined} + */ + _remoteBase() { + return !Runtime.queryParam('debugFrontend') && this._descriptor.remote && Runtime._remoteBase || undefined; + } + + /** + * @param {string} value + * @return {string} + */ + substituteURL(value) { + const base = this._remoteBase() || ''; + return value.replace(/@url\(([^\)]*?)\)/g, convertURL.bind(this)); + + function convertURL(match, url) { + return base + this._modularizeURL(url); + } + } +}; + + +/** + * @unrestricted + */ +Runtime.Extension = class { + /** + * @param {!Runtime.Module} module + * @param {!Runtime.ExtensionDescriptor} descriptor + */ + constructor(module, descriptor) { + this._module = module; + this._descriptor = descriptor; + + this._type = descriptor.type; + this._hasTypeClass = this._type.charAt(0) === '@'; + + /** + * @type {?string} + */ + this._className = descriptor.className || null; + this._factoryName = descriptor.factoryName || null; + } + + /** + * @return {!Object} + */ + descriptor() { + return this._descriptor; + } + + /** + * @return {!Runtime.Module} + */ + module() { + return this._module; + } + + /** + * @return {boolean} + */ + enabled() { + return this._module.enabled() && Runtime._isDescriptorEnabled(this.descriptor()); + } + + /** + * @return {?function(new:Object)} + */ + _typeClass() { + if (!this._hasTypeClass) + return null; + return this._module._manager._resolve(this._type.substring(1)); + } + + /** + * @param {?Object} context + * @return {boolean} + */ + isApplicable(context) { + return this._module._manager.isExtensionApplicableToContext(this, context); + } + + /** + * @return {!Promise.} + */ + instance() { + return this._module._loadPromise().then(this._createInstance.bind(this)); + } + + /** + * @return {boolean} + */ + canInstantiate() { + return !!(this._className || this._factoryName); + } + + /** + * @return {!Object} + */ + _createInstance() { + const className = this._className || this._factoryName; + if (!className) + throw new Error('Could not instantiate extension with no class'); + const constructorFunction = self.eval(/** @type {string} */ (className)); + if (!(constructorFunction instanceof Function)) + throw new Error('Could not instantiate: ' + className); + if (this._className) + return this._module._manager.sharedInstance(constructorFunction); + return new constructorFunction(this); + } + + /** + * @return {string} + */ + title() { + // FIXME: should be Common.UIString() but runtime is not l10n aware yet. + return this._descriptor['title-' + Runtime._platform] || this._descriptor['title']; + } + + /** + * @param {function(new:Object)} contextType + * @return {boolean} + */ + hasContextType(contextType) { + const contextTypes = this.descriptor().contextTypes; + if (!contextTypes) + return false; + for (let i = 0; i < contextTypes.length; ++i) { + if (contextType === this._module._manager._resolve(contextTypes[i])) + return true; + } + return false; + } +}; + +/** + * @unrestricted + */ +Runtime.ExperimentsSupport = class { + constructor() { + this._supportEnabled = Runtime.queryParam('experiments') !== null; + this._experiments = []; + this._experimentNames = {}; + this._enabledTransiently = {}; + } + + /** + * @return {!Array.} + */ + allConfigurableExperiments() { + const result = []; + for (let i = 0; i < this._experiments.length; i++) { + const experiment = this._experiments[i]; + if (!this._enabledTransiently[experiment.name]) + result.push(experiment); + } + return result; + } + + /** + * @return {boolean} + */ + supportEnabled() { + return this._supportEnabled; + } + + /** + * @param {!Object} value + */ + _setExperimentsSetting(value) { + if (!self.localStorage) + return; + self.localStorage['experiments'] = JSON.stringify(value); + } + + /** + * @param {string} experimentName + * @param {string} experimentTitle + * @param {boolean=} hidden + */ + register(experimentName, experimentTitle, hidden) { + Runtime._assert(!this._experimentNames[experimentName], 'Duplicate registration of experiment ' + experimentName); + this._experimentNames[experimentName] = true; + this._experiments.push(new Runtime.Experiment(this, experimentName, experimentTitle, !!hidden)); + } + + /** + * @param {string} experimentName + * @return {boolean} + */ + isEnabled(experimentName) { + this._checkExperiment(experimentName); + + if (this._enabledTransiently[experimentName]) + return true; + if (!this.supportEnabled()) + return false; + + return !!Runtime._experimentsSetting()[experimentName]; + } + + /** + * @param {string} experimentName + * @param {boolean} enabled + */ + setEnabled(experimentName, enabled) { + this._checkExperiment(experimentName); + const experimentsSetting = Runtime._experimentsSetting(); + experimentsSetting[experimentName] = enabled; + this._setExperimentsSetting(experimentsSetting); + } + + /** + * @param {!Array.} experimentNames + */ + setDefaultExperiments(experimentNames) { + for (let i = 0; i < experimentNames.length; ++i) { + this._checkExperiment(experimentNames[i]); + this._enabledTransiently[experimentNames[i]] = true; + } + } + + /** + * @param {string} experimentName + */ + enableForTest(experimentName) { + this._checkExperiment(experimentName); + this._enabledTransiently[experimentName] = true; + } + + clearForTest() { + this._experiments = []; + this._experimentNames = {}; + this._enabledTransiently = {}; + } + + cleanUpStaleExperiments() { + const experimentsSetting = Runtime._experimentsSetting(); + const cleanedUpExperimentSetting = {}; + for (let i = 0; i < this._experiments.length; ++i) { + const experimentName = this._experiments[i].name; + if (experimentsSetting[experimentName]) + cleanedUpExperimentSetting[experimentName] = true; + } + this._setExperimentsSetting(cleanedUpExperimentSetting); + } + + /** + * @param {string} experimentName + */ + _checkExperiment(experimentName) { + Runtime._assert(this._experimentNames[experimentName], 'Unknown experiment ' + experimentName); + } +}; + +/** + * @unrestricted + */ +Runtime.Experiment = class { + /** + * @param {!Runtime.ExperimentsSupport} experiments + * @param {string} name + * @param {string} title + * @param {boolean} hidden + */ + constructor(experiments, name, title, hidden) { + this.name = name; + this.title = title; + this.hidden = hidden; + this._experiments = experiments; + } + + /** + * @return {boolean} + */ + isEnabled() { + return this._experiments.isEnabled(this.name); + } + + /** + * @param {boolean} enabled + */ + setEnabled(enabled) { + this._experiments.setEnabled(this.name, enabled); + } +}; + +{ + (function parseQueryParameters() { + const queryParams = Runtime.queryParamsString(); + if (!queryParams) + return; + const params = queryParams.substring(1).split('&'); + for (let i = 0; i < params.length; ++i) { + const pair = params[i].split('='); + const name = pair.shift(); + Runtime._queryParamsObject[name] = pair.join('='); + } + })(); +} + +// This must be constructed after the query parameters have been parsed. +Runtime.experiments = new Runtime.ExperimentsSupport(); + +/** @type {Function} */ +Runtime._runtimeReadyPromiseCallback; +Runtime._runtimeReadyPromise = new Promise(fulfil => Runtime._runtimeReadyPromiseCallback = fulfil); +/** + * @type {?string} + */ +Runtime._remoteBase; +(function validateRemoteBase() { + if (location.href.startsWith('chrome-devtools://devtools/bundled/') && Runtime.queryParam('remoteBase')) { + const versionMatch = /\/serve_file\/(@[0-9a-zA-Z]+)\/?$/.exec(Runtime.queryParam('remoteBase')); + if (versionMatch) + Runtime._remoteBase = `${location.origin}/remote/serve_file/${versionMatch[1]}/`; + } +})(); + + +/** + * @interface + */ +function ServicePort() { +} + +ServicePort.prototype = { + /** + * @param {function(string)} messageHandler + * @param {function(string)} closeHandler + */ + setHandlers(messageHandler, closeHandler) {}, + + /** + * @param {string} message + * @return {!Promise} + */ + send(message) {}, + + /** + * @return {!Promise} + */ + close() {} +}; + +/** @type {!Runtime} */ +var runtime; // eslint-disable-line diff --git a/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/coverageListView.css b/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/coverageListView.css new file mode 100644 index 0000000..3b915e2 --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/coverageListView.css @@ -0,0 +1,61 @@ +.data-grid { + border: none; +} + +.data-grid td .url-outer { + width: 100%; + display: inline-flex; + justify-content: flex-start; +} + +.data-grid td .url-outer .filter-highlight { + font-weight: bold; +} + +.data-grid td .url-prefix { + overflow-x: hidden; + text-overflow: ellipsis; +} + +.data-grid td .url-suffix { + flex: none; +} + +.data-grid td .bar { + display: inline-block; + height: 8px; +} + +.data-grid .selected td .bar { + border-top: 1px white solid; + border-bottom: 1px white solid; +} + +.data-grid .selected td .bar:last-child { + border-right: 1px white solid; +} + +.data-grid .selected td .bar:first-child { + border-left: 1px white solid; +} + +.data-grid td .bar-container { +} + +.data-grid td .bar-unused-size { + background-color: #E57373; +} + +.data-grid td .bar-used-size { + background-color: #81C784; +} + +.data-grid td .percent-value { + color: #888; + width: 45px; + display: inline-block; +} + +.data-grid:focus tr.selected span.percent-value { + color: #eee; +} diff --git a/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/coverageView.css b/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/coverageView.css new file mode 100644 index 0000000..816ef2a --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/coverageView.css @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2016 The Chromium Authors. All rights reserved. + * Use of this source code is governed by a BSD-style license that can be + * found in the LICENSE file. + */ + +:host { + overflow: hidden; +} + +.coverage-toolbar-container { + display: flex; + border-bottom: 1px solid #ccc; + flex: 0 0; +} + +.coverage-toolbar { + display: inline-block; +} + +.coverage-toolbar-summary { + background-color: #eee; + border-top: 1px solid #ccc; + padding-left: 5px; + flex: 0 0 19px; + display: flex; + padding-right: 5px; +} + +.coverage-toolbar-summary .coverage-message { + padding-top: 2px; + padding-left: 1ex; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; +} + +.coverage-results { + overflow-y: auto; + display: flex; + flex: auto; +} + +.landing-page { + justify-content: center; + align-items: center; + padding: 20px; +} + +.landing-page .message { + white-space: pre-line; +} diff --git a/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/module.json b/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/module.json new file mode 100644 index 0000000..ca5239f --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/src/assets/inspector/coverage/module.json @@ -0,0 +1,62 @@ +{ + "extensions": [ + { + "type": "view", + "location": "drawer-view", + "id": "coverage", + "title": "Coverage", + "persistence": "closeable", + "className": "Coverage.CoverageView", + "order": 100 + }, + { + "type": "@SourceFrame.LineDecorator", + "className": "Coverage.CoverageView.LineDecorator", + "decoratorType": "coverage" + }, + { + "type": "action", + "actionId": "coverage.toggle-recording", + "iconClass": "largeicon-start-recording", + "toggledIconClass": "largeicon-stop-recording", + "toggleWithRedColor": true, + "className": "Coverage.CoverageView.ActionDelegate", + "category": "Performance", + "options": [ + { + "value": true, + "title": "Instrument coverage" + }, + { + "value": false, + "title": "Stop instrumenting coverage and show results" + } + ] + }, + { + "type": "action", + "actionId": "coverage.start-with-reload", + "iconClass": "largeicon-refresh", + "className": "Coverage.CoverageView.ActionDelegate", + "category": "Performance", + "title": "Start instrumenting coverage and reload page" + } + ], + "dependencies": [ + "sdk", + "ui", + "source_frame", + "sources", + "data_grid" + ], + "scripts": [ + "CoverageModel.js", + "CoverageListView.js", + "CoverageView.js", + "CoverageDecorationManager.js" + ], + "resources": [ + "coverageListView.css", + "coverageView.css" + ] +} diff --git a/packages/@weex/plugins/debug/frontend/src/assets/inspector/inspector.js b/packages/@weex/plugins/debug/frontend/src/assets/inspector/inspector.js new file mode 100644 index 0000000..c690b32 --- /dev/null +++ b/packages/@weex/plugins/debug/frontend/src/assets/inspector/inspector.js @@ -0,0 +1,4 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +Runtime.startApplication('inspector'); diff --git a/packages/@weex/plugins/debug/frontend/src/views/home/home.ts b/packages/@weex/plugins/debug/frontend/src/views/home/home.ts index 2d64ff4..3b61767 100644 --- a/packages/@weex/plugins/debug/frontend/src/views/home/home.ts +++ b/packages/@weex/plugins/debug/frontend/src/views/home/home.ts @@ -113,7 +113,6 @@ export class HomeComponent extends Vue { this.updateVersion(data.params.version) } else if (data.method === 'WxDebug.startDebugger') { this.$router.push({ path: `/client/weex/${data.params}?type=weex` }) - this.disconnect() } }) this.socket.on('close', (data) => { @@ -138,7 +137,7 @@ export class HomeComponent extends Vue { } disconnect () { - this.socket && this.socket.close() + this.socket && this.socket.connected && this.socket.close() this.cleanQRCode() this.$snotify.clear() } diff --git a/packages/@weex/plugins/debug/frontend/src/views/weex/weex.scss b/packages/@weex/plugins/debug/frontend/src/views/weex/weex.scss index b7420d5..9997e5d 100644 --- a/packages/@weex/plugins/debug/frontend/src/views/weex/weex.scss +++ b/packages/@weex/plugins/debug/frontend/src/views/weex/weex.scss @@ -188,7 +188,7 @@ &.navigation { flex: 1; input { - width: calc(100% - 40px); + width: calc(100% - 90px); height: 32px; border-radius: 3px; border: 1px solid #ced4da; @@ -213,7 +213,7 @@ &.navigation-button { flex: 1; .navigation-selection { - width: calc(100% - 80px); + width: calc(100% - 90px); margin-right: 10px; height: 30px; border: 1px solid #ddd; diff --git a/packages/@weex/plugins/debug/src/link/managers/runtime_manager.js b/packages/@weex/plugins/debug/src/link/managers/runtime_manager.js index 8d515b5..347cb9f 100644 --- a/packages/@weex/plugins/debug/src/link/managers/runtime_manager.js +++ b/packages/@weex/plugins/debug/src/link/managers/runtime_manager.js @@ -20,7 +20,7 @@ class RuntimeManager { for (const target of list) { const urlObj = URL.parse(target.url) if ( - urlObj.pathname === '/runtime.html' && + urlObj.pathname === '/runtime/runtime.html' && urlObj.port === config.port + '' ) { found = target diff --git a/packages/@weex/plugins/debug/src/server/headless.js b/packages/@weex/plugins/debug/src/server/headless.js index fcccb4b..0eeb835 100644 --- a/packages/@weex/plugins/debug/src/server/headless.js +++ b/packages/@weex/plugins/debug/src/server/headless.js @@ -12,8 +12,8 @@ exports.launchHeadless = async (host, remotePort) => { await page.setUserAgent( '5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/67.0.3396.99 Safari/537.36' ) - await page.goto(`http://${host}/runtime.html`) - logger.verbose(`Headless page goto http://${host}/runtime.html`) + await page.goto(`http://${host}/runtime/runtime.html`) + logger.verbose(`Headless page goto http://${host}/runtime/runtime.html`) } exports.closeHeadless = async () => {