Skip to content

Commit

Permalink
feat(devtools): add devtools plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Nov 4, 2020
1 parent a821ec5 commit 894d50d
Show file tree
Hide file tree
Showing 3 changed files with 282 additions and 34 deletions.
312 changes: 280 additions & 32 deletions src/devtools.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
import { App, setupDevtoolsPlugin } from '@vue/devtools-api'
import {
App,
CustomInspectorNode,
CustomInspectorNodeTag,
CustomInspectorState,
setupDevtoolsPlugin,
TimelineEvent,
} from '@vue/devtools-api'
import { watch } from 'vue'
import { RouterMatcher } from './matcher'
import { RouteRecordMatcher } from './matcher/pathMatcher'
import { PathParser } from './matcher/pathParserRanker'
import { Router } from './router'
import { RouteLocationNormalized } from './types'

function formatRouteLocation(
routeLocation: RouteLocationNormalized,
tooltip?: string
) {
const copy = {
...routeLocation,
// remove variables that can contain vue instances
matched: routeLocation.matched.map(
({ instances, children, aliasOf, ...rest }) => rest
),
}

return {
_custom: {
type: null,
readOnly: true,
display: routeLocation.fullPath,
tooltip,
value: copy,
},
}
}

function formatDisplay(display: string) {
return {
_custom: {
display,
},
}
}

export function addDevtools(app: App, router: Router, matcher: RouterMatcher) {
// Take over router.beforeEach and afterEach

export function addDevtools(app: App, router: Router) {
setupDevtoolsPlugin(
{
id: 'Router',
Expand All @@ -11,53 +56,256 @@ export function addDevtools(app: App, router: Router) {
api => {
api.on.inspectComponent((payload, ctx) => {
if (payload.instanceData) {
const stateType = 'extra properties (test)'
payload.instanceData.state.push({
type: stateType,
key: 'foo',
value: 'bar',
editable: false,
})

payload.instanceData.state.push({
type: stateType,
key: 'time',
type: 'Routing',
key: '$route',
editable: false,
value: {
_custom: {
type: null,
readOnly: true,
display: `${router.currentRoute.value.fullPath}s`,
tooltip: 'Current Route',
value: router.currentRoute.value,
},
},
value: formatRouteLocation(
router.currentRoute.value,
'Current Route'
),
})
}
})

watch(router.currentRoute, () => {
// @ts-ignore
api.notifyComponentUpdate()
})

const navigationsLayerId = 'router:navigations'

api.addTimelineLayer({
id: 'router:navigations',
id: navigationsLayerId,
label: 'Router Navigations',
color: 0x92a2bf,
color: 0x40a8c4,
})

router.afterEach((from, to) => {
// @ts-ignore
api.notifyComponentUpdate()
// const errorsLayerId = 'router:errors'
// api.addTimelineLayer({
// id: errorsLayerId,
// label: 'Router Errors',
// color: 0xea5455,
// })

router.onError(error => {
api.addTimelineEvent({
layerId: navigationsLayerId,
event: {
// @ts-ignore
logType: 'error',
time: Date.now(),
data: { error },
},
})
})

console.log('adding devtools to timeline')
router.beforeEach((to, from) => {
const data: TimelineEvent<any, any>['data'] = {
guard: formatDisplay('beforEach'),
from: formatRouteLocation(
from,
'Current Location during this navigation'
),
to: formatRouteLocation(to, 'Target location'),
}

console.log('adding to timeline')
api.addTimelineEvent({
layerId: 'router:navigations',
layerId: navigationsLayerId,
event: {
time: Date.now(),
data: {
info: 'afterEach',
from,
to,
meta: {},
data,
},
})
})

router.afterEach((to, from, failure) => {
const data: TimelineEvent<any, any>['data'] = {
guard: formatDisplay('afterEach'),
}

if (failure) {
data.failure = {
_custom: {
type: Error,
readOnly: true,
display: failure ? failure.message : '',
tooltip: 'Navigation Failure',
value: failure,
},
meta: { foo: 'meta?' },
}
data.status = formatDisplay('❌')
} else {
data.status = formatDisplay('✅')
}

// we set here to have the right order
data.from = formatRouteLocation(
from,
'Current Location during this navigation'
)
data.to = formatRouteLocation(to, 'Target location')

api.addTimelineEvent({
layerId: navigationsLayerId,
event: {
time: Date.now(),
data,
// @ts-ignore
logType: failure ? 'warning' : 'default',
meta: {},
},
})
})

const routerInspectorId = 'hahaha router-inspector'

api.addInspector({
id: routerInspectorId,
label: 'Routes',
icon: 'book',
treeFilterPlaceholder: 'Filter routes',
})

api.on.getInspectorTree(payload => {
if (payload.app === app && payload.inspectorId === routerInspectorId) {
const routes = matcher.getRoutes().filter(route => !route.parent)
payload.rootNodes = routes.map(formatRouteRecordForInspector)
}
})

api.on.getInspectorState(payload => {
if (payload.app === app && payload.inspectorId === routerInspectorId) {
const routes = matcher.getRoutes()
const route = routes.find(
route => route.record.path === payload.nodeId
)

if (route) {
payload.state = {
options: formatRouteRecordMatcherForStateInspector(route),
}
}
}
})
}
)
}

function modifierForKey(key: PathParser['keys'][number]) {
if (key.optional) {
return key.repeatable ? '*' : '?'
} else {
return key.repeatable ? '+' : ''
}
}

function formatRouteRecordMatcherForStateInspector(
route: RouteRecordMatcher
): CustomInspectorState[string] {
const { record } = route
const fields: CustomInspectorState[string] = [
{ editable: false, key: 'path', value: record.path },
]

if (record.name != null)
fields.push({
editable: false,
key: 'name',
value: record.name,
})

fields.push({ editable: false, key: 'regexp', value: route.re })

if (route.keys.length)
fields.push({
editable: false,
key: 'keys',
value: {
_custom: {
type: null,
readOnly: true,
display: route.keys
.map(key => `${key.name}${modifierForKey(key)}`)
.join(' '),
tooltip: 'Param keys',
value: route.keys,
},
},
})

if (record.redirect != null)
fields.push({
editable: false,
key: 'redirect',
value: record.redirect,
})

if (route.alias.length)
fields.push({
editable: false,
key: 'aliases',
value: route.alias,
})

fields.push({
key: 'score',
editable: false,
value: {
_custom: {
type: null,
readOnly: true,
display: route.score.map(score => score.join(', ')).join(' | '),
tooltip: 'Score used to sort routes',
value: route.score,
},
},
})

return fields
}

function formatRouteRecordForInspector(
route: RouteRecordMatcher
): CustomInspectorNode {
const tags: CustomInspectorNodeTag[] = []

const { record } = route

if (record.name != null) {
tags.push({
label: String(record.name),
textColor: 0,
backgroundColor: 0x00bcd4,
})
}

if (record.aliasOf) {
tags.push({
label: 'alias',
textColor: 0,
backgroundColor: 0xff984f,
})
}

if (record.redirect) {
tags.push({
label:
'redirect: ' +
(typeof record.redirect === 'string' ? record.redirect : 'Object'),
textColor: 0xffffff,
backgroundColor: 0x666666,
})
}

return {
id: record.path,
label: record.path,
tags,
// @ts-ignore
children: route.children.map(formatRouteRecordForInspector),
}
}
2 changes: 1 addition & 1 deletion src/matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
import { warn } from '../warning'
import { assign, noop } from '../utils'

interface RouterMatcher {
export interface RouterMatcher {
addRoute: (record: RouteRecordRaw, parent?: RouteRecordMatcher) => () => void
removeRoute: {
(matcher: RouteRecordMatcher): void
Expand Down
2 changes: 1 addition & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1106,7 +1106,7 @@ export function createRouter(options: RouterOptions): Router {
}

if (__DEV__) {
addDevtools(app, router)
addDevtools(app, router, matcher)
}
},
}
Expand Down

0 comments on commit 894d50d

Please sign in to comment.