From: Eduardo San Martin Morote Date: Wed, 2 Sep 2020 15:46:02 +0000 (+0200) Subject: feat(devtools): add devtools plugin X-Git-Tag: v4.0.0-rc.2~11 X-Git-Url: http://git.ipfire.org/?a=commitdiff_plain;h=894d50d351a40df95a3227840f5485f7e8b90432;p=thirdparty%2Fvuejs%2Frouter.git feat(devtools): add devtools plugin --- diff --git a/src/devtools.ts b/src/devtools.ts index 7f9629a4..0e86cdb2 100644 --- a/src/devtools.ts +++ b/src/devtools.ts @@ -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', @@ -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['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['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), + } +} diff --git a/src/matcher/index.ts b/src/matcher/index.ts index cb5cc8cd..13105ba0 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -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 diff --git a/src/router.ts b/src/router.ts index 9a73539d..0be24c5a 100644 --- a/src/router.ts +++ b/src/router.ts @@ -1106,7 +1106,7 @@ export function createRouter(options: RouterOptions): Router { } if (__DEV__) { - addDevtools(app, router) + addDevtools(app, router, matcher) } }, }