|
|
|
import path from 'path' |
|
import type { CustomState } from '@vue/devtools-api' |
|
import { parseCircularAutoChunks, stringifyCircularAutoChunks } from './transfer' |
|
import { |
|
getCustomInstanceDetails, |
|
getCustomObjectDetails, |
|
getCustomRouterDetails, |
|
getCustomStoreDetails, |
|
getInstanceMap, |
|
isVueInstance, |
|
} from './backend' |
|
import { SharedData } from './shared-data' |
|
import { isChrome, target } from './env' |
|
|
|
function cached(fn) { |
|
const cache = Object.create(null) |
|
return function cachedFn(str) { |
|
const hit = cache[str] |
|
return hit || (cache[str] = fn(str)) |
|
} |
|
} |
|
|
|
const classifyRE = /(?:^|[-_/])(\w)/g |
|
export const classify = cached((str) => { |
|
|
|
|
|
|
|
|
|
return str && (`${str}`).replace(classifyRE, toUpper) |
|
}) |
|
|
|
const camelizeRE = /-(\w)/g |
|
export const camelize = cached((str) => { |
|
return str && str.replace(camelizeRE, toUpper) |
|
}) |
|
|
|
const kebabizeRE = /([a-z0-9])([A-Z])/g |
|
export const kebabize = cached((str) => { |
|
return str && str |
|
.replace(kebabizeRE, (_, lowerCaseCharacter, upperCaseLetter) => { |
|
return `${lowerCaseCharacter}-${upperCaseLetter}` |
|
}) |
|
.toLowerCase() |
|
}) |
|
|
|
function toUpper(_, c) { |
|
return c ? c.toUpperCase() : '' |
|
} |
|
|
|
export function getComponentDisplayName(originalName, style = 'class') { |
|
switch (style) { |
|
case 'class': |
|
return classify(originalName) |
|
case 'kebab': |
|
return kebabize(originalName) |
|
case 'original': |
|
default: |
|
return originalName |
|
} |
|
} |
|
|
|
export function inDoc(node) { |
|
if (!node) { |
|
return false |
|
} |
|
const doc = node.ownerDocument.documentElement |
|
const parent = node.parentNode |
|
return doc === node |
|
|| doc === parent |
|
|| !!(parent && parent.nodeType === 1 && (doc.contains(parent))) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
export const UNDEFINED = '__vue_devtool_undefined__' |
|
export const INFINITY = '__vue_devtool_infinity__' |
|
export const NEGATIVE_INFINITY = '__vue_devtool_negative_infinity__' |
|
export const NAN = '__vue_devtool_nan__' |
|
|
|
export const SPECIAL_TOKENS = { |
|
'true': true, |
|
'false': false, |
|
'undefined': UNDEFINED, |
|
'null': null, |
|
'-Infinity': NEGATIVE_INFINITY, |
|
'Infinity': INFINITY, |
|
'NaN': NAN, |
|
} |
|
|
|
export const MAX_STRING_SIZE = 10000 |
|
export const MAX_ARRAY_SIZE = 5000 |
|
|
|
export function specialTokenToString(value) { |
|
if (value === null) { |
|
return 'null' |
|
} |
|
else if (value === UNDEFINED) { |
|
return 'undefined' |
|
} |
|
else if (value === NAN) { |
|
return 'NaN' |
|
} |
|
else if (value === INFINITY) { |
|
return 'Infinity' |
|
} |
|
else if (value === NEGATIVE_INFINITY) { |
|
return '-Infinity' |
|
} |
|
return false |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EncodeCache { |
|
map: Map<any, any> |
|
|
|
constructor() { |
|
this.map = new Map() |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
cache<TResult, TData>(data: TData, factory: (data: TData) => TResult): TResult { |
|
const cached: TResult = this.map.get(data) |
|
if (cached) { |
|
return cached |
|
} |
|
else { |
|
const result = factory(data) |
|
this.map.set(data, result) |
|
return result |
|
} |
|
} |
|
|
|
clear() { |
|
this.map.clear() |
|
} |
|
} |
|
|
|
const encodeCache = new EncodeCache() |
|
|
|
class ReviveCache { |
|
map: Map<number, any> |
|
index: number |
|
size: number |
|
maxSize: number |
|
|
|
constructor(maxSize: number) { |
|
this.maxSize = maxSize |
|
this.map = new Map() |
|
this.index = 0 |
|
this.size = 0 |
|
} |
|
|
|
cache(value: any) { |
|
const currentIndex = this.index |
|
this.map.set(currentIndex, value) |
|
this.size++ |
|
if (this.size > this.maxSize) { |
|
this.map.delete(currentIndex - this.size) |
|
this.size-- |
|
} |
|
this.index++ |
|
return currentIndex |
|
} |
|
|
|
read(id: number) { |
|
return this.map.get(id) |
|
} |
|
} |
|
|
|
const reviveCache = new ReviveCache(1000) |
|
|
|
const replacers = { |
|
internal: replacerForInternal, |
|
user: replaceForUser, |
|
} |
|
|
|
export function stringify(data, target: keyof typeof replacers = 'internal') { |
|
|
|
encodeCache.clear() |
|
return stringifyCircularAutoChunks(data, replacers[target]) |
|
} |
|
|
|
function replacerForInternal(key) { |
|
|
|
const val = this[key] |
|
const type = typeof val |
|
if (Array.isArray(val)) { |
|
const l = val.length |
|
if (l > MAX_ARRAY_SIZE) { |
|
return { |
|
_isArray: true, |
|
length: l, |
|
items: val.slice(0, MAX_ARRAY_SIZE), |
|
} |
|
} |
|
return val |
|
} |
|
else if (typeof val === 'string') { |
|
if (val.length > MAX_STRING_SIZE) { |
|
return `${val.substring(0, MAX_STRING_SIZE)}... (${(val.length)} total length)` |
|
} |
|
else { |
|
return val |
|
} |
|
} |
|
else if (type === 'undefined') { |
|
return UNDEFINED |
|
} |
|
else if (val === Number.POSITIVE_INFINITY) { |
|
return INFINITY |
|
} |
|
else if (val === Number.NEGATIVE_INFINITY) { |
|
return NEGATIVE_INFINITY |
|
} |
|
else if (type === 'function') { |
|
return getCustomFunctionDetails(val) |
|
} |
|
else if (type === 'symbol') { |
|
return `[native Symbol ${Symbol.prototype.toString.call(val)}]` |
|
} |
|
else if (type === 'bigint') { |
|
return getCustomBigIntDetails(val) |
|
} |
|
else if (val !== null && type === 'object') { |
|
const proto = Object.prototype.toString.call(val) |
|
if (proto === '[object Map]') { |
|
return encodeCache.cache(val, () => getCustomMapDetails(val)) |
|
} |
|
else if (proto === '[object Set]') { |
|
return encodeCache.cache(val, () => getCustomSetDetails(val)) |
|
} |
|
else if (proto === '[object RegExp]') { |
|
|
|
return `[native RegExp ${RegExp.prototype.toString.call(val)}]` |
|
} |
|
else if (proto === '[object Date]') { |
|
return getCustomDateDetails(val) |
|
} |
|
else if (proto === '[object Error]') { |
|
return `[native Error ${val.message}<>${val.stack}]` |
|
} |
|
else if (val.state && val._vm) { |
|
return encodeCache.cache(val, () => getCustomStoreDetails(val)) |
|
} |
|
else if (val.constructor && val.constructor.name === 'VueRouter') { |
|
return encodeCache.cache(val, () => getCustomRouterDetails(val)) |
|
} |
|
else if (isVueInstance(val)) { |
|
return encodeCache.cache(val, () => getCustomInstanceDetails(val)) |
|
} |
|
else if (typeof val.render === 'function') { |
|
return encodeCache.cache(val, () => getCustomComponentDefinitionDetails(val)) |
|
} |
|
else if (val.constructor && val.constructor.name === 'VNode') { |
|
return `[native VNode <${val.tag}>]` |
|
} |
|
else if (typeof HTMLElement !== 'undefined' && val instanceof HTMLElement) { |
|
return encodeCache.cache(val, () => getCustomHTMLElementDetails(val)) |
|
} |
|
else if (val.constructor?.name === 'Store' && val._wrappedGetters) { |
|
return `[object Store]` |
|
} |
|
else if (val.currentRoute) { |
|
return `[object Router]` |
|
} |
|
const customDetails = getCustomObjectDetails(val, proto) |
|
if (customDetails != null) { |
|
return customDetails |
|
} |
|
} |
|
else if (Number.isNaN(val)) { |
|
return NAN |
|
} |
|
return sanitize(val) |
|
} |
|
|
|
|
|
function replaceForUser(key) { |
|
|
|
let val = this[key] |
|
const type = typeof val |
|
if (val?._custom && 'value' in val._custom) { |
|
val = val._custom.value |
|
} |
|
if (type !== 'object') { |
|
if (val === UNDEFINED) { |
|
return undefined |
|
} |
|
else if (val === INFINITY) { |
|
return Number.POSITIVE_INFINITY |
|
} |
|
else if (val === NEGATIVE_INFINITY) { |
|
return Number.NEGATIVE_INFINITY |
|
} |
|
else if (val === NAN) { |
|
return Number.NaN |
|
} |
|
return val |
|
} |
|
return sanitize(val) |
|
} |
|
|
|
export function getCustomMapDetails(val) { |
|
const list = [] |
|
val.forEach( |
|
(value, key) => list.push({ |
|
key, |
|
value, |
|
}), |
|
) |
|
return { |
|
_custom: { |
|
type: 'map', |
|
display: 'Map', |
|
value: list, |
|
readOnly: true, |
|
fields: { |
|
abstract: true, |
|
}, |
|
}, |
|
} |
|
} |
|
|
|
export function reviveMap(val) { |
|
const result = new Map() |
|
const list = val._custom.value |
|
for (let i = 0; i < list.length; i++) { |
|
const { key, value } = list[i] |
|
result.set(key, revive(value)) |
|
} |
|
return result |
|
} |
|
|
|
export function getCustomSetDetails(val) { |
|
const list = Array.from(val) |
|
return { |
|
_custom: { |
|
type: 'set', |
|
display: `Set[${list.length}]`, |
|
value: list, |
|
readOnly: true, |
|
}, |
|
} |
|
} |
|
|
|
export function reviveSet(val) { |
|
const result = new Set() |
|
const list = val._custom.value |
|
for (let i = 0; i < list.length; i++) { |
|
const value = list[i] |
|
result.add(revive(value)) |
|
} |
|
return result |
|
} |
|
|
|
export function getCustomBigIntDetails(val) { |
|
const stringifiedBigInt = BigInt.prototype.toString.call(val) |
|
return { |
|
_custom: { |
|
type: 'bigint', |
|
display: `BigInt(${stringifiedBigInt})`, |
|
value: stringifiedBigInt, |
|
}, |
|
} |
|
} |
|
|
|
export function getCustomDateDetails(val: Date) { |
|
const dateCopy = new Date(val.getTime()) |
|
dateCopy.setMinutes(dateCopy.getMinutes() - dateCopy.getTimezoneOffset()) |
|
|
|
const displayedTime = Date.prototype.toString.call(val) |
|
return { |
|
_custom: { |
|
type: 'date', |
|
display: displayedTime, |
|
value: dateCopy.toISOString().slice(0, -1), |
|
skipSerialize: true, |
|
}, |
|
} |
|
} |
|
|
|
|
|
|
|
export function basename(filename, ext) { |
|
filename = filename.replace(/\\/g, '/') |
|
if (filename.includes(`/index${ext}`)) { |
|
filename = filename.replace(`/index${ext}`, ext) |
|
} |
|
return path.basename( |
|
filename.replace(/^[a-z]:/i, ''), |
|
ext, |
|
) |
|
} |
|
|
|
export function getComponentName(options) { |
|
const name = options.displayName || options.name || options._componentTag |
|
if (name) { |
|
return name |
|
} |
|
const file = options.__file |
|
if (file) { |
|
return classify(basename(file, '.vue')) |
|
} |
|
} |
|
|
|
export function getCustomComponentDefinitionDetails(def) { |
|
let display = getComponentName(def) |
|
if (display) { |
|
if (def.name && def.__file) { |
|
display += ` <span>(${def.__file})</span>` |
|
} |
|
} |
|
else { |
|
display = '<i>Unknown Component</i>' |
|
} |
|
return { |
|
_custom: { |
|
type: 'component-definition', |
|
display, |
|
tooltip: 'Component definition', |
|
...def.__file |
|
? { |
|
file: def.__file, |
|
} |
|
: {}, |
|
}, |
|
} |
|
} |
|
|
|
export function getCustomFunctionDetails(func: Function): CustomState { |
|
let string = '' |
|
let matches = null |
|
try { |
|
string = Function.prototype.toString.call(func) |
|
matches = String.prototype.match.call(string, /\([\s\S]*?\)/) |
|
} |
|
catch (e) { |
|
|
|
} |
|
|
|
const match = matches && matches[0] |
|
const args = typeof match === 'string' |
|
? match |
|
: '(?)' |
|
const name = typeof func.name === 'string' ? func.name : '' |
|
return { |
|
_custom: { |
|
type: 'function', |
|
display: `<span style="opacity:.5;">function</span> ${escape(name)}${args}`, |
|
tooltip: string.trim() ? `<pre>${string}</pre>` : null, |
|
_reviveId: reviveCache.cache(func), |
|
}, |
|
} |
|
} |
|
|
|
export function getCustomHTMLElementDetails(value: HTMLElement): CustomState { |
|
try { |
|
return { |
|
_custom: { |
|
type: 'HTMLElement', |
|
display: `<span class="opacity-30"><</span><span class="text-blue-500">${value.tagName.toLowerCase()}</span><span class="opacity-30">></span>`, |
|
value: namedNodeMapToObject(value.attributes), |
|
actions: [ |
|
{ |
|
icon: 'input', |
|
tooltip: 'Log element to console', |
|
action: () => { |
|
|
|
console.log(value) |
|
}, |
|
}, |
|
], |
|
}, |
|
} |
|
} |
|
catch (e) { |
|
return { |
|
_custom: { |
|
type: 'HTMLElement', |
|
display: `<span class="text-blue-500">${String(value)}</span>`, |
|
}, |
|
} |
|
} |
|
} |
|
|
|
function namedNodeMapToObject(map: NamedNodeMap) { |
|
const result: any = {} |
|
const l = map.length |
|
for (let i = 0; i < l; i++) { |
|
const node = map.item(i) |
|
result[node.name] = node.value |
|
} |
|
return result |
|
} |
|
|
|
export function getCustomRefDetails(instance, key, ref) { |
|
let value |
|
if (Array.isArray(ref)) { |
|
value = ref.map(r => getCustomRefDetails(instance, key, r)).map(data => data.value) |
|
} |
|
else { |
|
let name |
|
if (ref._isVue) { |
|
name = getComponentName(ref.$options) |
|
} |
|
else { |
|
name = ref.tagName.toLowerCase() |
|
} |
|
|
|
value = { |
|
_custom: { |
|
display: `<${name}${ |
|
ref.id ? ` <span class="attr-title">id</span>="${ref.id}"` : '' |
|
}${ref.className ? ` <span class="attr-title">class</span>="${ref.className}"` : ''}>`, |
|
uid: instance.__VUE_DEVTOOLS_UID__, |
|
type: 'reference', |
|
}, |
|
} |
|
} |
|
return { |
|
type: '$refs', |
|
key, |
|
value, |
|
editable: false, |
|
} |
|
} |
|
|
|
export function parse(data: any, revive = false) { |
|
return revive |
|
? parseCircularAutoChunks(data, reviver) |
|
: parseCircularAutoChunks(data) |
|
} |
|
|
|
const specialTypeRE = /^\[native (\w+) (.*?)(?:<>[.\s]*)?\]$/ |
|
const symbolRE = /^\[native Symbol Symbol\((.*)\)\]$/ |
|
|
|
function reviver(key, val) { |
|
return revive(val) |
|
} |
|
|
|
export function revive(val) { |
|
if (val === UNDEFINED) { |
|
return undefined |
|
} |
|
else if (val === INFINITY) { |
|
return Number.POSITIVE_INFINITY |
|
} |
|
else if (val === NEGATIVE_INFINITY) { |
|
return Number.NEGATIVE_INFINITY |
|
} |
|
else if (val === NAN) { |
|
return Number.NaN |
|
} |
|
else if (val && val._custom) { |
|
const { _custom: custom }: CustomState = val |
|
if (custom.type === 'component') { |
|
return getInstanceMap().get(custom.id) |
|
} |
|
else if (custom.type === 'map') { |
|
return reviveMap(val) |
|
} |
|
else if (custom.type === 'set') { |
|
return reviveSet(val) |
|
} |
|
else if (custom.type === 'bigint') { |
|
return BigInt(custom.value) |
|
} |
|
else if (custom.type === 'date') { |
|
return new Date(custom.value) |
|
} |
|
else if (custom._reviveId) { |
|
return reviveCache.read(custom._reviveId) |
|
} |
|
else { |
|
return revive(custom.value) |
|
} |
|
} |
|
else if (symbolRE.test(val)) { |
|
const [, string] = symbolRE.exec(val) |
|
return Symbol.for(string) |
|
} |
|
else if (specialTypeRE.test(val)) { |
|
const [, type, string,, details] = specialTypeRE.exec(val) |
|
const result = new target[type](string) |
|
if (type === 'Error' && details) { |
|
result.stack = details |
|
} |
|
return result |
|
} |
|
else { |
|
return val |
|
} |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function sanitize(data) { |
|
if ( |
|
!isPrimitive(data) |
|
&& !Array.isArray(data) |
|
&& !isPlainObject(data) |
|
) { |
|
|
|
|
|
return Object.prototype.toString.call(data) |
|
} |
|
else { |
|
return data |
|
} |
|
} |
|
|
|
export function isPlainObject(obj) { |
|
return Object.prototype.toString.call(obj) === '[object Object]' |
|
} |
|
|
|
function isPrimitive(data) { |
|
if (data == null) { |
|
return true |
|
} |
|
const type = typeof data |
|
return ( |
|
type === 'string' |
|
|| type === 'number' |
|
|| type === 'boolean' |
|
) |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function searchDeepInObject(obj, searchTerm) { |
|
const seen = new Map() |
|
const result = internalSearchObject(obj, searchTerm.toLowerCase(), seen, 0) |
|
seen.clear() |
|
return result |
|
} |
|
|
|
const SEARCH_MAX_DEPTH = 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function internalSearchObject(obj, searchTerm, seen, depth) { |
|
if (depth > SEARCH_MAX_DEPTH) { |
|
return false |
|
} |
|
let match = false |
|
const keys = Object.keys(obj) |
|
let key, value |
|
for (let i = 0; i < keys.length; i++) { |
|
key = keys[i] |
|
value = obj[key] |
|
match = internalSearchCheck(searchTerm, key, value, seen, depth + 1) |
|
if (match) { |
|
break |
|
} |
|
} |
|
return match |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function internalSearchArray(array, searchTerm, seen, depth) { |
|
if (depth > SEARCH_MAX_DEPTH) { |
|
return false |
|
} |
|
let match = false |
|
let value |
|
for (let i = 0; i < array.length; i++) { |
|
value = array[i] |
|
match = internalSearchCheck(searchTerm, null, value, seen, depth + 1) |
|
if (match) { |
|
break |
|
} |
|
} |
|
return match |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function internalSearchCheck(searchTerm, key, value, seen, depth) { |
|
let match = false |
|
let result |
|
if (key === '_custom') { |
|
key = value.display |
|
value = value.value |
|
} |
|
(result = specialTokenToString(value)) && (value = result) |
|
if (key && compare(key, searchTerm)) { |
|
match = true |
|
seen.set(value, true) |
|
} |
|
else if (seen.has(value)) { |
|
match = seen.get(value) |
|
} |
|
else if (Array.isArray(value)) { |
|
seen.set(value, null) |
|
match = internalSearchArray(value, searchTerm, seen, depth) |
|
seen.set(value, match) |
|
} |
|
else if (isPlainObject(value)) { |
|
seen.set(value, null) |
|
match = internalSearchObject(value, searchTerm, seen, depth) |
|
seen.set(value, match) |
|
} |
|
else if (compare(value, searchTerm)) { |
|
match = true |
|
seen.set(value, true) |
|
} |
|
return match |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
function compare(value, searchTerm) { |
|
return (`${value}`).toLowerCase().includes(searchTerm) |
|
} |
|
|
|
export function sortByKey(state) { |
|
return state && state.slice().sort((a, b) => { |
|
if (a.key < b.key) { |
|
return -1 |
|
} |
|
if (a.key > b.key) { |
|
return 1 |
|
} |
|
return 0 |
|
}) |
|
} |
|
|
|
export function simpleGet(object, path) { |
|
const sections = Array.isArray(path) ? path : path.split('.') |
|
for (let i = 0; i < sections.length; i++) { |
|
object = object[sections[i]] |
|
if (!object) { |
|
return undefined |
|
} |
|
} |
|
return object |
|
} |
|
|
|
export function focusInput(el) { |
|
el.focus() |
|
el.setSelectionRange(0, el.value.length) |
|
} |
|
|
|
export function openInEditor(file) { |
|
|
|
const fileName = file.replace(/\\/g, '\\\\') |
|
const src = `fetch('${SharedData.openInEditorHost}__open-in-editor?file=${encodeURI(file)}').then(response => { |
|
if (response.ok) { |
|
console.log('File ${fileName} opened in editor') |
|
} else { |
|
const msg = 'Opening component ${fileName} failed' |
|
const target = typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : {} |
|
if (target.__VUE_DEVTOOLS_TOAST__) { |
|
target.__VUE_DEVTOOLS_TOAST__(msg, 'error') |
|
} else { |
|
console.log('%c' + msg, 'color:red') |
|
} |
|
console.log('Check the setup of your project, see https://devtools.vuejs.org/guide/open-in-editor.html') |
|
} |
|
})` |
|
if (isChrome) { |
|
target.chrome.devtools.inspectedWindow.eval(src) |
|
} |
|
else { |
|
|
|
eval(src) |
|
} |
|
} |
|
|
|
const ESC = { |
|
'<': '<', |
|
'>': '>', |
|
'"': '"', |
|
'&': '&', |
|
} |
|
|
|
export function escape(s) { |
|
return s.replace(/[<>"&]/g, escapeChar) |
|
} |
|
|
|
function escapeChar(a) { |
|
return ESC[a] || a |
|
} |
|
|
|
export function copyToClipboard(state) { |
|
let text: string |
|
|
|
if (typeof state !== 'object') { |
|
text = String(state) |
|
} |
|
else { |
|
text = stringify(state, 'user') |
|
} |
|
|
|
|
|
if (typeof document === 'undefined') { |
|
return |
|
} |
|
const dummyTextArea = document.createElement('textarea') |
|
dummyTextArea.textContent = text |
|
document.body.appendChild(dummyTextArea) |
|
dummyTextArea.select() |
|
document.execCommand('copy') |
|
document.body.removeChild(dummyTextArea) |
|
} |
|
|
|
export function isEmptyObject(obj) { |
|
return obj === UNDEFINED || !obj || Object.keys(obj).length === 0 |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function chunk(array: unknown[], size: number): unknown[][] { |
|
return array.reduce((resultArray, item, index) => { |
|
const chunkIndex = Math.floor(index / size) |
|
|
|
if (!resultArray[chunkIndex]) { |
|
resultArray[chunkIndex] = [] |
|
} |
|
|
|
resultArray[chunkIndex].push(item) |
|
|
|
return resultArray |
|
}, []) as unknown[][] |
|
} |
|
|