UNPKG

console-remote-client

Version:

Remote JavaScript Console.Re client for Browsers and Node.js

964 lines (897 loc) 24.7 kB
/* globals CONSOLE_VERSION, location, prompt, performance, navigator, CSSRule, localStorage, consoleDefault */ /* eslint-env es6 */ /* eslint-disable no-console */ const io = require('socket.io-client') const { printStackTrace } = require('./printStackTrace') const { matchMedia, VISIBILITY, IEArraysPolyfill, isArray } = require('./polyfills') const { kindOf } = require('./kindof') const { XBBCODE } = require('./xbbcode') const DEFAULT_CONSOLERE_SERVER = 'https://console.re' const DEFAULT_CONSOLERE_CHANNEL = 'default' if (!window.console) window.console = {} window.matchMedia = window.matchMedia || matchMedia IEArraysPolyfill() var name = 'toServerRe' var consoleReOptions = { redirectDefaultConsoleToRemote: false, disableDefaultConsoleOutput: false, channel: DEFAULT_CONSOLERE_CHANNEL, server: DEFAULT_CONSOLERE_SERVER } var consolereScriptTag = document.getElementById('consolerescript') || null if (consolereScriptTag) { var options = consolereScriptTag.getAttribute('data-options') if (options && options.indexOf('redirectDefaultConsoleToRemote') !== -1) { consoleReOptions['redirectDefaultConsoleToRemote'] = true } if (options && options.indexOf('disableDefaultConsoleOutput') !== -1) { consoleReOptions['disableDefaultConsoleOutput'] = true } consoleReOptions.channel = consolereScriptTag.getAttribute('data-channel') || consoleReOptions.channel consoleReOptions.server = consolereScriptTag.getAttribute('data-server') || consoleReOptions.server } var isPrivate = consoleReOptions.server !== DEFAULT_CONSOLERE_SERVER var isLocal = window.location.port || /localhost|[0-9]{2,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}|::1|\.local|local|runjs\.co|\.dev|console.re|^$/gi.test( window.location.hostname ) if (!window.location.origin) window.location.origin = window.location.protocol + '//' + window.location.host function getCaller(d) { d = d || 7 var t = printStackTrace(), c = t[d], p if (c !== undefined) { p = c.match(/^.*([\/<][^\/>]*>?):(\d*):(\d*)?$/) if (p === null) p = c.match(/^.*([\/<][^\/>]*>?):(\d*)?$/) } else { // IE 7-9 p[1] = '' p[2] = '0' p[3] = '0' } return { file: p ? p[1] : '', line: p ? p[2] : '0', col: p ? p[3] : '0' } } function getWindowSize() { var w = document.width || window.outerWidth || document.documentElement.clientWidth, h = document.height || window.outerHeight || document.documentElement.clientHeight return 'Window Size: [number]' + w + 'px[/number] by [number]' + h + 'px[/number]' } function getOtherTypes(t) { var e, o = '' try { e = eval(t) if (e === true) { o = '[booltrue]true[/booltrue]' } else if (e === false) { o = '[boolfalse]false[/boolfalse]' } else if (!isNaN(parseFloat(e)) && isFinite(e)) { o = '[number]' + e + '[/number]' } else if (typeof e === 'number') { o = '[number][Number][/number]' } else if (typeof e === 'string') { o = '"String"' } else if (typeof e === 'function') { o = '[Function]' } else if (e.nodeType) { o = '<' + e.nodeName + ' Element>' } else if (typeof e === 'object') { o = '{Object}' if (isArray(e)) o = '[Array]' } else { o = '[color=red]undefined[/color]' } } catch (err) { o = '[color=red]' + err + '[/color]' } return o } function getType(t) { var o = '' if (typeof t !== 'string') return getOtherTypes(t) try { var obj = JSON.parse(t) if (typeof obj === 'object') { o = '{Object}' if (isArray(obj)) o = '[Array]' } else { o = getOtherTypes(t) } } catch (err) { o = getOtherTypes(t) } return o } function replaceWithNum(s) { var st = '' + s return st.replace(/([0-9]+)(px|em||)/g, '[number]$1$2[/number]') } function getSize(targetElement) { var w, h if (targetElement) { w = getStyle(targetElement, 'width') h = getStyle(targetElement, 'height') return '[number]' + w + '[/number]' + ' by ' + '[number]' + h + '[/number]' } return '' } function getStyle(targetElement, styleProp) { if (targetElement && targetElement.currentStyle) return targetElement.currentStyle[styleProp] else if (window.getComputedStyle) return document.defaultView.getComputedStyle(targetElement, null).getPropertyValue(styleProp) else return '' } var truncate = function (n, useWordBoundary) { if (this.length <= n) { return this } var subString = this.substr(0, n - 1) return (useWordBoundary ? subString.substr(0, subString.lastIndexOf(' ')) : subString) + '...' } function stringify(obj, cmd, prop) { if (typeof obj === 'undefined') return '___undefined___' if (typeof obj !== 'object') return obj var cache = [], k_map = [], i, d_node = {}, nid = '', nclass = '', ps, s = JSON.stringify(obj, function (k, v) { if (typeof v === 'undefined') { return '___undefined___' } if (!v) return v if (v.nodeType) { if (v.id) nid = v.id if (v.className) nclass = v.className if (cmd === 'size') { return '[tag]<' + v.nodeName + '>[/tag] ' + getSize(v) } else if (cmd === 'css') { if (isArray(prop)) { prop.forEach(function (p) { d_node[p] = replaceWithNum(getStyle(v, p)) }) return d_node } else if (prop) { ps = ' ' + prop + ':' + getStyle(v, prop) + ';' if (nid) nid = " [attr]id=[/attr][string]'" + nid + "'[/string]" if (nclass) nclass = " [attr]class=[/attr][string]'" + nclass + "'[/string]" return '[tag]<' + v.nodeName + '' + nid + '' + nclass + '>[/tag]' + replaceWithNum(ps) } } else { d_node.element = v.nodeName if (nid) d_node.id = nid if (nclass) d_node['class'] = nclass d_node.visible = VISIBILITY().isVisible(v) d_node.size = getSize(v) d_node.html = v.outerHTML } return d_node } if (v.window && v.window === v.window.window) return '{Window Object}' if (typeof v === 'function') return '[Function]' if (typeof v === 'object' && v !== null) { var t_array = Array.prototype.slice.call(v) if (v.length && t_array.length === v.length) v = t_array i = cache.indexOf(v) if (i !== -1) { return '[ Circular {' + (k_map[i] || 'root') + '} ]' } cache.push(v) k_map.push(k) } return v }) return s } var root = (function () { var socket var caller = [] var cache = [] var gcount = [] var now_labels = {} var gtimer = [] var or_change = false var api = { client: true, server: true, loaded: false } var levels = [ 'trace', 'debug', 'info', 'log', 'warn', 'error', 'size', 'test', 'assert', 'count', 'css', 'media', 'time', 'timeEnd', 'now', 'type', 'mark', 'command' ] function emit(level, args, cmd, cal) { caller = cal || getCaller() if ((!args.length || levels.indexOf(level) === -1) && level !== 'command') return if (api.client && consoleReOptions['disableDefaultConsoleOutput'] === false) { logConsole.apply(null, arguments) } if (api.server) { logIo.apply(null, arguments) } if (!socket) { api.connectIO() } } function logConsole(level, args) { level = level === 'trace' ? 'debug' : level args = isArray(args) ? args : [args] if (level !== 'command') { args.forEach(function (arg) { if (typeof arg === 'string' && arg !== '') { if (arg.indexOf('consoleRe [') === 0) return var p = XBBCODE.process({ text: arg }) if (!p.error) arg = p.html } outputToDefaultConsole(arg, level) }) } } function outputToDefaultConsole(arg, level) { if (!arg || arg === '' || arg === '-') return var consl = {} if (consoleReOptions['redirectDefaultConsoleToRemote'] && window.consoleDefault) { consl = consoleDefault[level] ? consoleDefault[level] : consoleDefault.log } else { consl = console[level] ? console[level] : console.log } try { consl('consoleRe [' + level + ']', arg) } catch (e) { void 0 } } function logIo(level, args, cmd) { var data, counter, timer, t_end, last, command = '' cmd = cmd || '' if (cmd === 'css') { last = args[args.length - 1] if (isArray(last) || 'string' === typeof last) args.pop() else cmd = '' } else if (cmd === 'count') { counter = args.toString() if (isNaN(gcount[counter])) { gcount[counter] = 1 } else { gcount[counter]++ } args.push(gcount[counter]) } else if (cmd === 'time') { timer = args.toString() gtimer[timer] = new Date().getTime() args.push('[white]started[/white]') } else if (cmd === 'timeEnd') { timer = args.toString() t_end = new Date().getTime() - gtimer[timer] if (!isNaN(t_end)) { args.push('[white]ends[/white] in [number]' + t_end + '[/number] ms') } else { args.push('[white]not started[/white]') } } else if (cmd === 'now') { var nowAt = args.toString() var pNow = perfNow() var timeNow var lastCall if (now_labels[nowAt]) { timeNow = pNow - now_labels[nowAt] lastCall = ' [color=gray]from last call[/color]' } else { timeNow = pNow lastCall = ' [color=gray]from navigation start[/color]' } now_labels[nowAt] = pNow nowAt = nowAt !== '-' ? lastCall + '[color=gray] at `' + nowAt + '` [/color]' : lastCall args = [] args.push('[white]performance now is[/white] [number]' + timeNow.toFixed(2) + '[/number] ms' + nowAt) } if (cmd !== '' && isArray(args)) logConsole(cmd, args) for (var i = 0; i < args.length; i++) { args[i] = stringify(args[i], cmd, last) } if (typeof args === 'object' && !args.length) data = args else { if (level === 'command') command = cmd data = { command: command, channel: consoleReOptions.channel, browser: browser, level: level, args: args, caller: caller } } if (socket) { if (cache.length) { sendCached(cache) } socket.emit(name, data) } else { cache.push([level, data]) } } for (var i = 0, l; i < levels.length; i++) { l = levels[i] api[l] = logLevel(l) } function logLevel(level) { return function () { api._dispatch(level, [].slice.call(arguments)) } } function onConnect() { if (socket) { var connectMessage = 'consoleRe [info] connected to server `' + consoleReOptions.server + '` channel `' + consoleReOptions.channel + '`' if (window.consoleDefault && consoleDefault.log) { consoleDefault.log(connectMessage) } else { console.log(connectMessage) } socket.emit('channel', consoleReOptions.channel) } sendCached(cache) } function sendCached(c) { var ced = c.shift() api._dispatch('command', '', 'autoclear') api._dispatch('command', '', 'automark') while (ced) { logIo.apply(null, ced) ced = c.shift() } } api.connectIO = function (r) { if (!isLocal && !isPrivate) { console.warn( 'consoleRe [warning] seems like you trying to use console remote on public, production server. Console.Re is for light, local development only, for production you can install and use self hosted solution https://console.re/#self-hosted-server' ) return } if (io) { socket = io.connect(consoleReOptions.server, { withCredentials: false, extraHeaders: { 'x-consolere': 'true' }, reconnectionDelay: 500, reconnectionDelayMax: 'Infinity' }) socket.on('connect', onConnect) socket.on('connect_failed', function () { var connectionError = 'consoleRe [error] connection failed to server `' + consoleReOptions.server + '`' if (window.consoleDefault && consoleDefault.error) { consoleDefault.error(connectionError) } else { console.error(connectionError) } }) } else if (!r) { api.connectIO(true) } } api.size = api.s = function (s) { if (!s || typeof s === 'undefined' || s === 'window') { api._dispatch('size', [getWindowSize()]) } else { api._dispatch('size', [].slice.call(arguments), 'size') } } api.count = api.c = function () { api._dispatch('count', [].slice.call(arguments), 'count') } api.time = api.ti = function () { api._dispatch('time', [].slice.call(arguments), 'time') } api.timeEnd = api.te = function () { api._dispatch('time', [].slice.call(arguments), 'timeEnd') } api.trace = api.tr = function () { var t = printStackTrace(), out = [], a = [].slice.call(arguments) for (i = 0; t.length > i; i++) { if (!/console.re.js/gi.test(t[i])) out.push(t[i]) } api._dispatch('trace', [a.toString(), out]) } api.error = api.e = function () { var a = [].slice.call(arguments), type = '', out = [] a.forEach(function (t) { type = '[color=red]' + t + '[/color]' out.push(type) }) api._dispatch('error', out) } api.css = api.cs = function () { api._dispatch('css', [].slice.call(arguments), 'css') } api.test = api.ts = function () { var a = [].slice.call(arguments), type = '', out = [] a.forEach(function (t) { var nameOut type = kindOf(t) if (/|Function|Object|Array|Element|/gi.test(type)) { type = '[color=#BBB519]' + type + '[/color]' } try { nameOut = truncate.apply(t.toString(), [30, false]) } catch (e) { nameOut = t } out.push('[color=#BC9044]' + nameOut + '[/color]' + '[color=gray] is [/color]' + type) }) api._dispatch('test', out) } api.type = api.t = function () { var a = [].slice.call(arguments), type = '', out = [] a.forEach(function (t) { type = '[color=#BBB519]' + kindOf(t) + '[/color]' out.push('type is ' + type) }) api._dispatch('type', out) } api.assert = api.a = function () { var a = [].slice.call(arguments), out = [] a.forEach(function (t, i) { if (typeof t !== 'string') { if (!t) { if (typeof a[i + 1] === 'string') { out.push('[color=red]' + a[i + 1] + '[/color]') } else { out.push('[color=red]Assertion Failure[/color]') } } } }) if (out.length) api._dispatch('assert', out) } api._dispatch = function (level, args, cmd, cal) { emit(level, args, cmd, cal) } api.media = api.mq = function (a, cal) { var mq_list = [], out = [], m = [], s_type = false, s_query = true, o_t = 'landscape', o_r = window.orientation || 0, timer_cal if (a === 'type') { s_type = true s_query = false } else if (a === 'all-types' || a === 'all') { s_query = s_type = true } if (a === 'watch') { var _timerVar timer_cal = getCaller(5) if (window.addEventListener) { window.addEventListener( 'resize', function () { watchMediaQuery(timer_cal) }, false ) window.addEventListener( 'orientationchange', function () { if (o_r !== window.orientation) or_change = true watchMediaQuery(timer_cal) }, false ) } } createMQList() m = mqChange() if (m.length) { if (m.length === 1) out.push(mqChange()[0]) else out.push(mqChange()) } else { out.push('[yellow]No Media Query Rules[/yellow]') } if (a === 'w') { out.push(getWindowSize()) if (or_change) { if (Math.abs(window.orientation) !== 90) o_t = 'portrait' out.push('Orientation: [yellow]' + o_t + '[/yellow]') } api._dispatch('media', out, '', cal) } else { api._dispatch('media', out) } function watchMediaQuery(t) { if (window.matchMedia) { clearTimeout(_timerVar) _timerVar = setTimeout(function () { api.media('w', t) }, 500) } } function inList(media) { for (var i = mq_list.length - 1; i >= 0; i--) { if (mq_list[i].media === media) { return true } } return false } function createMQList() { var mq = getMediaQueries(), i if (s_query) { for (i = mq.length - 1; i >= 0; i--) { if (!inList(mq[i])) { mq_list.push(window.matchMedia(mq[i])) } } } if (s_type) { var links = document.getElementsByTagName('link') for (i = links.length - 1; i >= 0; i--) { if (links[i].media) { mq_list.push(window.matchMedia(links[i].media)) } } } } function getMediaQueries() { var s = document.styleSheets, r, i, j, mq = [] for (i = s.length - 1; i >= 0; i--) { try { r = s[i].cssRules if (r) { for (j = 0; j < r.length; j++) { if (r[j].type === CSSRule.MEDIA_RULE) { mq.push(r[j].media.mediaText) } } } } catch (e) { console.error(e) } } return mq } function mqChange() { var q = [] for (var i in mq_list) { if (mq_list[i].matches) { q.push(replaceWithNum(mq_list[i].media)) } } return q } } api.now = api.n = function (label) { if (typeof label == 'undefined') label = '-' api._dispatch('now', label, 'now') } api.clear = api.cl = function () { api._dispatch('command', '', 'clear') } api.mark = api.m = function () { var args = [].slice.call(arguments) var strg = args.length === 0 ? '' : args api._dispatch('command', strg, 'mark') } api.l = api.log api.i = api.info api.d = api.debug api.w = api.warn api.redirectDefaultLog = function () { if ((window.consoleDefault && consoleDefault.log) || (!isLocal && !isPrivate)) { return } if (consoleReOptions['redirectDefaultConsoleToRemote']) { window.consoleDefault = {} var redirectMethods = ['log', 'info', 'warn', 'error', 'debug', 'time', 'timeEnd'] redirectMethods.forEach(function (method) { window.consoleDefault[method] = console[method] console[method] = logLevel(method) }) } } api.redirectDefaultLog() return api })() var perfNow = function () { var getNanoSeconds, hrtime, loadTime, moduleLoadTime, nodeLoadTime, upTime if (typeof performance !== 'undefined' && performance !== null && performance.now) { return performance.now() } else if (typeof process !== 'undefined' && process !== null && process.hrtime) { hrtime = process.hrtime getNanoSeconds = function () { var hr hr = hrtime() return hr[0] * 1e9 + hr[1] } moduleLoadTime = getNanoSeconds() upTime = process.uptime() * 1e9 nodeLoadTime = moduleLoadTime - upTime return (getNanoSeconds() - nodeLoadTime) / 1e6 } else if (Date.now) { loadTime = Date.now() return Date.now() - loadTime } else { loadTime = new Date().getTime() return new Date().getTime() - loadTime } } var BrowserDetect = { searchString: function (data) { var name = '' for (var i = 0; i < data.length; i++) { var d_string = data[i].str var d_prop = data[i].prop this.versionSearchString = data[i].vsearch || data[i].name if (d_string && d_string.indexOf(data[i].substr) !== -1) { name = data[i].name break } else if (d_prop) { name = data[i].name break } } return name }, searchVersion: function (dString) { var i = dString.indexOf(this.versionSearchString) if (i === -1) return false return parseFloat(dString.substr(i + this.versionSearchString.length + 1)) }, dataBrowser: [ { str: navigator.userAgent, substr: 'Edg', vsearch: 'Edg', name: { f: 'Edge', s: 'EG' } }, { str: navigator.userAgent, substr: 'Edge', vsearch: 'Edge', name: { f: 'Edge', s: 'EG' } }, { str: navigator.userAgent, substr: 'OPR', vsearch: 'OPR', name: { f: 'Opera', s: 'OP' } }, { str: navigator.userAgent, substr: 'Chrome', vsearch: 'Chrome', name: { f: 'Chrome', s: 'CR' } }, { str: navigator.userAgent, substr: 'OmniWeb', vsearch: 'OmniWeb', name: { f: 'OmniWeb', s: 'OW' } }, { str: navigator.vendor, substr: 'Apple', name: { f: 'Safari', s: 'SF' }, vsearch: 'Version' }, { prop: window.opera, name: { f: 'Opera', s: 'OP' }, vsearch: 'Version' }, { str: navigator.vendor, substr: 'iCab', name: { f: 'iCab', s: 'iC' } }, { str: navigator.vendor, substr: 'KDE', name: { f: 'Konqueror', s: 'KDE' } }, { str: navigator.userAgent, substr: 'Firefox', name: { f: 'Firefox', s: 'FF' }, vsearch: 'Firefox' }, { str: navigator.vendor, substr: 'Camino', name: { f: 'Camino', s: 'CM' } }, { str: navigator.userAgent, substr: 'Netscape', name: { f: 'Netscape', s: 'NS' } }, { str: navigator.userAgent, substr: 'MSIE', name: { f: 'Explorer', s: 'IE' }, vsearch: 'MSIE' }, { str: navigator.userAgent, substr: 'Trident', name: { f: 'Explorer', s: 'IE' }, vsearch: 'rv' }, { str: navigator.userAgent, substr: 'Mozilla', name: { f: 'Netscape', s: 'NS' }, vsearch: 'Mozilla' } ], dataOS: [ { str: navigator.platform, substr: 'Win', name: 'Win' }, { str: navigator.platform, substr: 'Mac', name: 'Mac' }, { str: navigator.userAgent, substr: 'iPhone', name: 'iOS' }, { str: navigator.userAgent, substr: 'iPad', name: 'iOS' }, { str: navigator.userAgent, substr: 'Android', name: 'Android' }, { str: navigator.platform, substr: 'Linux', name: 'Linux' } ], init: function () { return { browser: this.searchString(this.dataBrowser) || 'An unknown browser', version: this.searchVersion(navigator.userAgent) || this.searchVersion(navigator.appVersion) || '', OS: this.searchString(this.dataOS) || 'an unknown OS' } } } var browser = BrowserDetect.init() function handleError(msg, url, num) { if (!url && msg.indexOf('Script error') === 0 && num === 0) return var r = new RegExp(window.location.origin, 'g') url = url.replace(r, '') if (console.re) { console.re.error('[color=red]' + msg + '[/color] in [i]' + url + '[/i] Line: [b]' + num + '[/b]') } } window.onerror = handleError window.ConsoleRe = true window.ConsoleReConnectorVersion = CONSOLE_VERSION root.connect = function (options) { if (options != null) { if (options.redirectDefaultConsoleToRemote != null) { consoleReOptions.redirectDefaultConsoleToRemote = options.redirectDefaultConsoleToRemote } if (options.disableDefaultConsoleOutput !== null) { consoleReOptions.disableDefaultConsoleOutput = options.disableDefaultConsoleOutput } if (options.channel != null) { consoleReOptions.channel = options.channel } if (options.server != null) { consoleReOptions.server = options.server } } isPrivate = consoleReOptions.server !== DEFAULT_CONSOLERE_SERVER root.redirectDefaultLog() root.connectIO() return root } export default root