Forráskód Böngészése

dx: add devtools integration (#1942) (#1949)

close #1942

Co-authored-by: Eduardo San Martin Morote <posva13@gmail.com>
Co-authored-by: Kia King Ishii <kia.king.08@gmail.com>
Guillaume Chau 4 éve
szülő
commit
a6bfbb3230

+ 7 - 1
.babelrc

@@ -1,3 +1,9 @@
 {
-  "presets": ["@babel/preset-env"]
+  "presets": [
+    ["@babel/preset-env", {
+      "exclude": [
+        "transform-regenerator"
+      ]
+    }]
+  ]
 }

+ 2 - 1
.eslintrc.json

@@ -4,6 +4,7 @@
     "plugin:vue-libs/recommended"
   ],
   "globals": {
-    "__DEV__": true
+    "__DEV__": true,
+    "__VUE_PROD_DEVTOOLS__": true
   }
 }

+ 17 - 7
examples/classic/shopping-cart/api/shop.js

@@ -8,16 +8,26 @@ const _products = [
 ]
 
 export default {
-  getProducts (cb) {
-    setTimeout(() => cb(_products), 100)
+  async getProducts () {
+    await wait(100)
+    return _products
   },
 
-  buyProducts (products, cb, errorCb) {
-    setTimeout(() => {
+  async buyProducts (products) {
+    await wait(100)
+    if (
       // simulate random checkout failure.
       (Math.random() > 0.5 || navigator.webdriver)
-        ? cb()
-        : errorCb()
-    }, 100)
+    ) {
+      return
+    } else {
+      throw new Error('Checkout error')
+    }
   }
 }
+
+function wait (ms) {
+  return new Promise(resolve => {
+    setTimeout(resolve, ms)
+  })
+}

+ 15 - 11
examples/classic/shopping-cart/store/modules/cart.js

@@ -1,4 +1,5 @@
 import shop from '../../api/shop'
+import nested from './nested'
 
 // initial state
 // shape: [{ id, quantity }]
@@ -29,20 +30,20 @@ const getters = {
 
 // actions
 const actions = {
-  checkout ({ commit, state }, products) {
+  async checkout ({ commit, state }, products) {
     const savedCartItems = [...state.items]
     commit('setCheckoutStatus', null)
     // empty cart
     commit('setCartItems', { items: [] })
-    shop.buyProducts(
-      products,
-      () => commit('setCheckoutStatus', 'successful'),
-      () => {
-        commit('setCheckoutStatus', 'failed')
-        // rollback to the cart saved before sending the request
-        commit('setCartItems', { items: savedCartItems })
-      }
-    )
+    try {
+      await shop.buyProducts(products)
+      commit('setCheckoutStatus', 'successful')
+    } catch (e) {
+      console.error(e)
+      commit('setCheckoutStatus', 'failed')
+      // rollback to the cart saved before sending the request
+      commit('setCartItems', { items: savedCartItems })
+    }
   },
 
   addProductToCart ({ state, commit }, product) {
@@ -88,5 +89,8 @@ export default {
   state,
   getters,
   actions,
-  mutations
+  mutations,
+  modules: {
+    nested
+  }
 }

+ 9 - 0
examples/classic/shopping-cart/store/modules/nested.js

@@ -0,0 +1,9 @@
+export default {
+  namespaced: true,
+  state: () => ({
+    foo: 'bar'
+  }),
+  getters: {
+    twoBars: state => state.foo.repeat(2)
+  }
+}

+ 3 - 4
examples/classic/shopping-cart/store/modules/products.js

@@ -10,10 +10,9 @@ const getters = {}
 
 // actions
 const actions = {
-  getAllProducts ({ commit }) {
-    shop.getProducts(products => {
-      commit('setProducts', products)
-    })
+  async getAllProducts ({ commit }) {
+    const products = await shop.getProducts()
+    commit('setProducts', products)
   }
 }
 

+ 3 - 0
package.json

@@ -54,6 +54,9 @@
   "peerDependencies": {
     "vue": "^3.0.2"
   },
+  "dependencies": {
+    "@vue/devtools-api": "^6.0.0-beta.10"
+  },
   "devDependencies": {
     "@babel/core": "^7.12.10",
     "@babel/preset-env": "^7.12.11",

+ 11 - 3
rollup.config.js

@@ -25,6 +25,9 @@ function createEntries() {
 }
 
 function createEntry(config) {
+  const isGlobalBuild = config.format === 'iife'
+  const isBunderBuild = config.format !== 'iife' && !config.browser
+
   const c = {
     external: ['vue'],
     input: config.input,
@@ -44,15 +47,20 @@ function createEntry(config) {
     }
   }
 
-  if (config.format === 'iife' || config.format === 'umd') {
+  if (isGlobalBuild) {
     c.output.name = c.output.name || 'Vuex'
   }
 
+  if (!isGlobalBuild) {
+    c.external.push('@vue/devtools-api')
+  }
+
   c.plugins.push(replace({
     __VERSION__: pkg.version,
-    __DEV__: config.format !== 'iife' && !config.browser
+    __DEV__: isBunderBuild
       ? `(process.env.NODE_ENV !== 'production')`
-      : config.env !== 'production'
+      : config.env !== 'production',
+    __VUE_PROD_DEVTOOLS__: isBunderBuild ? '__VUE_PROD_DEVTOOLS__' : 'false'
   }))
 
   if (config.transpile !== false) {

+ 245 - 18
src/plugins/devtool.js

@@ -1,26 +1,253 @@
-const target = typeof window !== 'undefined'
-  ? window
-  : typeof global !== 'undefined'
-    ? global
-    : {}
-const devtoolHook = target.__VUE_DEVTOOLS_GLOBAL_HOOK__
+import { setupDevtoolsPlugin } from '@vue/devtools-api'
+import { makeLocalGetters } from '../store-util'
 
-export default function devtoolPlugin (store) {
-  if (!devtoolHook) return
+const LABEL_VUEX_BINDINGS = 'vuex bindings'
+const MUTATIONS_LAYER_ID = 'vuex:mutations'
+const ACTIONS_LAYER_ID = 'vuex:actions'
+const INSPECTOR_ID = 'vuex'
 
-  store._devtoolHook = devtoolHook
+let actionId = 0
 
-  devtoolHook.emit('vuex:init', store)
+export function addDevtools (app, store) {
+  setupDevtoolsPlugin(
+    {
+      id: 'org.vuejs.vuex',
+      app,
+      label: 'Vuex',
+      homepage: 'https://next.vuex.vuejs.org/',
+      logo: 'https://vuejs.org/images/icons/favicon-96x96.png',
+      packageName: 'vuex',
+      componentStateTypes: [LABEL_VUEX_BINDINGS]
+    },
+    (api) => {
+      api.addTimelineLayer({
+        id: MUTATIONS_LAYER_ID,
+        label: 'Vuex Mutations',
+        color: COLOR_LIME_500
+      })
 
-  devtoolHook.on('vuex:travel-to-state', targetState => {
-    store.replaceState(targetState)
+      api.addTimelineLayer({
+        id: ACTIONS_LAYER_ID,
+        label: 'Vuex Actions',
+        color: COLOR_LIME_500
+      })
+
+      api.addInspector({
+        id: INSPECTOR_ID,
+        label: 'Vuex',
+        icon: 'storage',
+        treeFilterPlaceholder: 'Filter stores...'
+      })
+
+      api.on.getInspectorTree((payload) => {
+        if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+          if (payload.filter) {
+            const nodes = []
+            flattenStoreForInspectorTree(nodes, store._modules.root, payload.filter, '')
+            payload.rootNodes = nodes
+          } else {
+            payload.rootNodes = [
+              formatStoreForInspectorTree(store._modules.root, '')
+            ]
+          }
+        }
+      })
+
+      api.on.getInspectorState((payload) => {
+        if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+          const modulePath = payload.nodeId
+          makeLocalGetters(store, modulePath)
+          payload.state = formatStoreForInspectorState(
+            getStoreModule(store._modules, modulePath),
+            store._makeLocalGettersCache,
+            modulePath
+          )
+        }
+      })
+
+      api.on.editInspectorState((payload) => {
+        if (payload.app === app && payload.inspectorId === INSPECTOR_ID) {
+          const modulePath = payload.nodeId
+          let path = payload.path
+          if (modulePath !== 'root') {
+            path = [...modulePath.split('/').filter(Boolean), ...path]
+          }
+          store._withCommit(() => {
+            payload.set(store._state.data, path, payload.state.value)
+          })
+        }
+      })
+
+      store.subscribe((mutation, state) => {
+        const data = {}
+
+        if (mutation.payload) {
+          data.payload = mutation.payload
+        }
+
+        data.state = state
+
+        api.notifyComponentUpdate()
+        api.sendInspectorTree(INSPECTOR_ID)
+        api.sendInspectorState(INSPECTOR_ID)
+
+        api.addTimelineEvent({
+          layerId: MUTATIONS_LAYER_ID,
+          event: {
+            time: Date.now(),
+            title: mutation.type,
+            data
+          }
+        })
+      })
+
+      store.subscribeAction({
+        before: (action, state) => {
+          const data = {}
+          if (action.payload) {
+            data.payload = action.payload
+          }
+          action._id = actionId++
+          action._time = Date.now()
+          data.state = state
+
+          api.addTimelineEvent({
+            layerId: ACTIONS_LAYER_ID,
+            event: {
+              time: action._time,
+              title: action.type,
+              groupId: action._id,
+              subtitle: 'start',
+              data
+            }
+          })
+        },
+        after: (action, state) => {
+          const data = {}
+          const duration = Date.now() - action._time
+          data.duration = {
+            _custom: {
+              type: 'duration',
+              display: `${duration}ms`,
+              tooltip: 'Action duration',
+              value: duration
+            }
+          }
+          if (action.payload) {
+            data.payload = action.payload
+          }
+          data.state = state
+
+          api.addTimelineEvent({
+            layerId: ACTIONS_LAYER_ID,
+            event: {
+              time: Date.now(),
+              title: action.type,
+              groupId: action._id,
+              subtitle: 'end',
+              data
+            }
+          })
+        }
+      })
+    }
+  )
+}
+
+// extracted from tailwind palette
+const COLOR_LIME_500 = 0x84cc16
+const COLOR_DARK = 0x666666
+const COLOR_WHITE = 0xffffff
+
+const TAG_NAMESPACED = {
+  label: 'namespaced',
+  textColor: COLOR_WHITE,
+  backgroundColor: COLOR_DARK
+}
+
+/**
+ * @param {string} path
+ */
+function extractNameFromPath (path) {
+  return path && path !== 'root' ? path.split('/').slice(-2, -1)[0] : 'Root'
+}
+
+/**
+ * @param {*} module
+ * @return {import('@vue/devtools-api').CustomInspectorNode}
+ */
+function formatStoreForInspectorTree (module, path) {
+  return {
+    id: path || 'root',
+    // all modules end with a `/`, we want the last segment only
+    // cart/ -> cart
+    // nested/cart/ -> cart
+    label: extractNameFromPath(path),
+    tags: module.namespaced ? [TAG_NAMESPACED] : [],
+    children: Object.keys(module._children).map((moduleName) =>
+      formatStoreForInspectorTree(
+        module._children[moduleName],
+        path + moduleName + '/'
+      )
+    )
+  }
+}
+
+/**
+ * @param {import('@vue/devtools-api').CustomInspectorNode[]} result
+ * @param {*} module
+ * @param {string} filter
+ * @param {string} path
+ */
+function flattenStoreForInspectorTree (result, module, filter, path) {
+  if (path.includes(filter)) {
+    result.push({
+      id: path || 'root',
+      label: path.endsWith('/') ? path.slice(0, path.length - 1) : path || 'Root',
+      tags: module.namespaced ? [TAG_NAMESPACED] : []
+    })
+  }
+  Object.keys(module._children).forEach(moduleName => {
+    flattenStoreForInspectorTree(result, module._children[moduleName], filter, path + moduleName + '/')
   })
+}
 
-  store.subscribe((mutation, state) => {
-    devtoolHook.emit('vuex:mutation', mutation, state)
-  }, { prepend: true })
+/**
+ * @param {*} module
+ * @return {import('@vue/devtools-api').CustomInspectorState}
+ */
+function formatStoreForInspectorState (module, getters, path) {
+  getters = path === 'root' ? getters : getters[path]
+  const gettersKeys = Object.keys(getters)
+  const storeState = {
+    state: Object.keys(module.state).map((key) => ({
+      key,
+      editable: true,
+      value: module.state[key]
+    }))
+  }
+
+  if (gettersKeys.length) {
+    storeState.getters = gettersKeys.map((key) => ({
+      key: key.endsWith('/') ? extractNameFromPath(key) : key,
+      editable: false,
+      value: getters[key]
+    }))
+  }
+
+  return storeState
+}
 
-  store.subscribeAction((action, state) => {
-    devtoolHook.emit('vuex:action', action, state)
-  }, { prepend: true })
+function getStoreModule (moduleMap, path) {
+  const names = path.split('/').filter((n) => n)
+  return names.reduce(
+    (module, moduleName, i) => {
+      const child = module[moduleName]
+      if (!child) {
+        throw new Error(`Missing module "${moduleName}" for path "${path}".`)
+      }
+      return i === names.length - 1 ? child : child._children
+    },
+    path === 'root' ? moduleMap : moduleMap.root._children
+  )
 }

+ 278 - 0
src/store-util.js

@@ -0,0 +1,278 @@
+import { reactive, watch } from 'vue'
+import { forEachValue, isObject, isPromise, assert, partial } from './util'
+
+export function genericSubscribe (fn, subs, options) {
+  if (subs.indexOf(fn) < 0) {
+    options && options.prepend
+      ? subs.unshift(fn)
+      : subs.push(fn)
+  }
+  return () => {
+    const i = subs.indexOf(fn)
+    if (i > -1) {
+      subs.splice(i, 1)
+    }
+  }
+}
+
+export function resetStore (store, hot) {
+  store._actions = Object.create(null)
+  store._mutations = Object.create(null)
+  store._wrappedGetters = Object.create(null)
+  store._modulesNamespaceMap = Object.create(null)
+  const state = store.state
+  // init all modules
+  installModule(store, state, [], store._modules.root, true)
+  // reset state
+  resetStoreState(store, state, hot)
+}
+
+export function resetStoreState (store, state, hot) {
+  const oldState = store._state
+
+  // bind store public getters
+  store.getters = {}
+  // reset local getters cache
+  store._makeLocalGettersCache = Object.create(null)
+  const wrappedGetters = store._wrappedGetters
+  const computedObj = {}
+  forEachValue(wrappedGetters, (fn, key) => {
+    // use computed to leverage its lazy-caching mechanism
+    // direct inline function use will lead to closure preserving oldState.
+    // using partial to return function with only arguments preserved in closure environment.
+    computedObj[key] = partial(fn, store)
+    Object.defineProperty(store.getters, key, {
+      // TODO: use `computed` when it's possible. at the moment we can't due to
+      // https://github.com/vuejs/vuex/pull/1883
+      get: () => computedObj[key](),
+      enumerable: true // for local getters
+    })
+  })
+
+  store._state = reactive({
+    data: state
+  })
+
+  // enable strict mode for new state
+  if (store.strict) {
+    enableStrictMode(store)
+  }
+
+  if (oldState) {
+    if (hot) {
+      // dispatch changes in all subscribed watchers
+      // to force getter re-evaluation for hot reloading.
+      store._withCommit(() => {
+        oldState.data = null
+      })
+    }
+  }
+}
+
+export function installModule (store, rootState, path, module, hot) {
+  const isRoot = !path.length
+  const namespace = store._modules.getNamespace(path)
+
+  // register in namespace map
+  if (module.namespaced) {
+    if (store._modulesNamespaceMap[namespace] && __DEV__) {
+      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
+    }
+    store._modulesNamespaceMap[namespace] = module
+  }
+
+  // set state
+  if (!isRoot && !hot) {
+    const parentState = getNestedState(rootState, path.slice(0, -1))
+    const moduleName = path[path.length - 1]
+    store._withCommit(() => {
+      if (__DEV__) {
+        if (moduleName in parentState) {
+          console.warn(
+            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
+          )
+        }
+      }
+      parentState[moduleName] = module.state
+    })
+  }
+
+  const local = module.context = makeLocalContext(store, namespace, path)
+
+  module.forEachMutation((mutation, key) => {
+    const namespacedType = namespace + key
+    registerMutation(store, namespacedType, mutation, local)
+  })
+
+  module.forEachAction((action, key) => {
+    const type = action.root ? key : namespace + key
+    const handler = action.handler || action
+    registerAction(store, type, handler, local)
+  })
+
+  module.forEachGetter((getter, key) => {
+    const namespacedType = namespace + key
+    registerGetter(store, namespacedType, getter, local)
+  })
+
+  module.forEachChild((child, key) => {
+    installModule(store, rootState, path.concat(key), child, hot)
+  })
+}
+
+/**
+ * make localized dispatch, commit, getters and state
+ * if there is no namespace, just use root ones
+ */
+function makeLocalContext (store, namespace, path) {
+  const noNamespace = namespace === ''
+
+  const local = {
+    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
+      const args = unifyObjectStyle(_type, _payload, _options)
+      const { payload, options } = args
+      let { type } = args
+
+      if (!options || !options.root) {
+        type = namespace + type
+        if (__DEV__ && !store._actions[type]) {
+          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
+          return
+        }
+      }
+
+      return store.dispatch(type, payload)
+    },
+
+    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
+      const args = unifyObjectStyle(_type, _payload, _options)
+      const { payload, options } = args
+      let { type } = args
+
+      if (!options || !options.root) {
+        type = namespace + type
+        if (__DEV__ && !store._mutations[type]) {
+          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
+          return
+        }
+      }
+
+      store.commit(type, payload, options)
+    }
+  }
+
+  // getters and state object must be gotten lazily
+  // because they will be changed by state update
+  Object.defineProperties(local, {
+    getters: {
+      get: noNamespace
+        ? () => store.getters
+        : () => makeLocalGetters(store, namespace)
+    },
+    state: {
+      get: () => getNestedState(store.state, path)
+    }
+  })
+
+  return local
+}
+
+export function makeLocalGetters (store, namespace) {
+  if (!store._makeLocalGettersCache[namespace]) {
+    const gettersProxy = {}
+    const splitPos = namespace.length
+    Object.keys(store.getters).forEach(type => {
+      // skip if the target getter is not match this namespace
+      if (type.slice(0, splitPos) !== namespace) return
+
+      // extract local getter type
+      const localType = type.slice(splitPos)
+
+      // Add a port to the getters proxy.
+      // Define as getter property because
+      // we do not want to evaluate the getters in this time.
+      Object.defineProperty(gettersProxy, localType, {
+        get: () => store.getters[type],
+        enumerable: true
+      })
+    })
+    store._makeLocalGettersCache[namespace] = gettersProxy
+  }
+
+  return store._makeLocalGettersCache[namespace]
+}
+
+function registerMutation (store, type, handler, local) {
+  const entry = store._mutations[type] || (store._mutations[type] = [])
+  entry.push(function wrappedMutationHandler (payload) {
+    handler.call(store, local.state, payload)
+  })
+}
+
+function registerAction (store, type, handler, local) {
+  const entry = store._actions[type] || (store._actions[type] = [])
+  entry.push(function wrappedActionHandler (payload) {
+    let res = handler.call(store, {
+      dispatch: local.dispatch,
+      commit: local.commit,
+      getters: local.getters,
+      state: local.state,
+      rootGetters: store.getters,
+      rootState: store.state
+    }, payload)
+    if (!isPromise(res)) {
+      res = Promise.resolve(res)
+    }
+    if (store._devtoolHook) {
+      return res.catch(err => {
+        store._devtoolHook.emit('vuex:error', err)
+        throw err
+      })
+    } else {
+      return res
+    }
+  })
+}
+
+function registerGetter (store, type, rawGetter, local) {
+  if (store._wrappedGetters[type]) {
+    if (__DEV__) {
+      console.error(`[vuex] duplicate getter key: ${type}`)
+    }
+    return
+  }
+  store._wrappedGetters[type] = function wrappedGetter (store) {
+    return rawGetter(
+      local.state, // local state
+      local.getters, // local getters
+      store.state, // root state
+      store.getters // root getters
+    )
+  }
+}
+
+function enableStrictMode (store) {
+  watch(() => store._state.data, () => {
+    if (__DEV__) {
+      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
+    }
+  }, { deep: true, flush: 'sync' })
+}
+
+export function getNestedState (state, path) {
+  return path.reduce((state, key) => state[key], state)
+}
+
+export function unifyObjectStyle (type, payload, options) {
+  if (isObject(type) && type.type) {
+    options = payload
+    payload = type
+    type = type.type
+  }
+
+  if (__DEV__) {
+    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
+  }
+
+  return { type, payload, options }
+}

+ 22 - 285
src/store.js

@@ -1,8 +1,16 @@
-import { reactive, watch } from 'vue'
+import { watch } from 'vue'
 import { storeKey } from './injectKey'
-import devtoolPlugin from './plugins/devtool'
+import { addDevtools } from './plugins/devtool'
 import ModuleCollection from './module/module-collection'
-import { forEachValue, isObject, isPromise, assert, partial } from './util'
+import { assert } from './util'
+import {
+  genericSubscribe,
+  getNestedState,
+  installModule,
+  resetStore,
+  resetStoreState,
+  unifyObjectStyle
+} from './store-util'
 
 export function createStore (options) {
   return new Store(options)
@@ -17,7 +25,8 @@ export class Store {
 
     const {
       plugins = [],
-      strict = false
+      strict = false,
+      devtools
     } = options
 
     // store internal state
@@ -30,6 +39,7 @@ export class Store {
     this._modulesNamespaceMap = Object.create(null)
     this._subscribers = []
     this._makeLocalGettersCache = Object.create(null)
+    this._devtools = devtools
 
     // bind commit and dispatch to self
     const store = this
@@ -57,16 +67,19 @@ export class Store {
 
     // apply plugins
     plugins.forEach(plugin => plugin(this))
-
-    const useDevtools = options.devtools !== undefined ? options.devtools : /* Vue.config.devtools */ true
-    if (useDevtools) {
-      devtoolPlugin(this)
-    }
   }
 
   install (app, injectKey) {
     app.provide(injectKey || storeKey, this)
     app.config.globalProperties.$store = this
+
+    const useDevtools = this._devtools !== undefined
+      ? this._devtools
+      : __DEV__ || __VUE_PROD_DEVTOOLS__
+
+    if (useDevtools) {
+      addDevtools(app, this)
+    }
   }
 
   get state () {
@@ -250,279 +263,3 @@ export class Store {
     this._committing = committing
   }
 }
-
-function genericSubscribe (fn, subs, options) {
-  if (subs.indexOf(fn) < 0) {
-    options && options.prepend
-      ? subs.unshift(fn)
-      : subs.push(fn)
-  }
-  return () => {
-    const i = subs.indexOf(fn)
-    if (i > -1) {
-      subs.splice(i, 1)
-    }
-  }
-}
-
-function resetStore (store, hot) {
-  store._actions = Object.create(null)
-  store._mutations = Object.create(null)
-  store._wrappedGetters = Object.create(null)
-  store._modulesNamespaceMap = Object.create(null)
-  const state = store.state
-  // init all modules
-  installModule(store, state, [], store._modules.root, true)
-  // reset state
-  resetStoreState(store, state, hot)
-}
-
-function resetStoreState (store, state, hot) {
-  const oldState = store._state
-
-  // bind store public getters
-  store.getters = {}
-  // reset local getters cache
-  store._makeLocalGettersCache = Object.create(null)
-  const wrappedGetters = store._wrappedGetters
-  const computedObj = {}
-  forEachValue(wrappedGetters, (fn, key) => {
-    // use computed to leverage its lazy-caching mechanism
-    // direct inline function use will lead to closure preserving oldState.
-    // using partial to return function with only arguments preserved in closure environment.
-    computedObj[key] = partial(fn, store)
-    Object.defineProperty(store.getters, key, {
-      // TODO: use `computed` when it's possible. at the moment we can't due to
-      // https://github.com/vuejs/vuex/pull/1883
-      get: () => computedObj[key](),
-      enumerable: true // for local getters
-    })
-  })
-
-  store._state = reactive({
-    data: state
-  })
-
-  // enable strict mode for new state
-  if (store.strict) {
-    enableStrictMode(store)
-  }
-
-  if (oldState) {
-    if (hot) {
-      // dispatch changes in all subscribed watchers
-      // to force getter re-evaluation for hot reloading.
-      store._withCommit(() => {
-        oldState.data = null
-      })
-    }
-  }
-}
-
-function installModule (store, rootState, path, module, hot) {
-  const isRoot = !path.length
-  const namespace = store._modules.getNamespace(path)
-
-  // register in namespace map
-  if (module.namespaced) {
-    if (store._modulesNamespaceMap[namespace] && __DEV__) {
-      console.error(`[vuex] duplicate namespace ${namespace} for the namespaced module ${path.join('/')}`)
-    }
-    store._modulesNamespaceMap[namespace] = module
-  }
-
-  // set state
-  if (!isRoot && !hot) {
-    const parentState = getNestedState(rootState, path.slice(0, -1))
-    const moduleName = path[path.length - 1]
-    store._withCommit(() => {
-      if (__DEV__) {
-        if (moduleName in parentState) {
-          console.warn(
-            `[vuex] state field "${moduleName}" was overridden by a module with the same name at "${path.join('.')}"`
-          )
-        }
-      }
-      parentState[moduleName] = module.state
-    })
-  }
-
-  const local = module.context = makeLocalContext(store, namespace, path)
-
-  module.forEachMutation((mutation, key) => {
-    const namespacedType = namespace + key
-    registerMutation(store, namespacedType, mutation, local)
-  })
-
-  module.forEachAction((action, key) => {
-    const type = action.root ? key : namespace + key
-    const handler = action.handler || action
-    registerAction(store, type, handler, local)
-  })
-
-  module.forEachGetter((getter, key) => {
-    const namespacedType = namespace + key
-    registerGetter(store, namespacedType, getter, local)
-  })
-
-  module.forEachChild((child, key) => {
-    installModule(store, rootState, path.concat(key), child, hot)
-  })
-}
-
-/**
- * make localized dispatch, commit, getters and state
- * if there is no namespace, just use root ones
- */
-function makeLocalContext (store, namespace, path) {
-  const noNamespace = namespace === ''
-
-  const local = {
-    dispatch: noNamespace ? store.dispatch : (_type, _payload, _options) => {
-      const args = unifyObjectStyle(_type, _payload, _options)
-      const { payload, options } = args
-      let { type } = args
-
-      if (!options || !options.root) {
-        type = namespace + type
-        if (__DEV__ && !store._actions[type]) {
-          console.error(`[vuex] unknown local action type: ${args.type}, global type: ${type}`)
-          return
-        }
-      }
-
-      return store.dispatch(type, payload)
-    },
-
-    commit: noNamespace ? store.commit : (_type, _payload, _options) => {
-      const args = unifyObjectStyle(_type, _payload, _options)
-      const { payload, options } = args
-      let { type } = args
-
-      if (!options || !options.root) {
-        type = namespace + type
-        if (__DEV__ && !store._mutations[type]) {
-          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
-          return
-        }
-      }
-
-      store.commit(type, payload, options)
-    }
-  }
-
-  // getters and state object must be gotten lazily
-  // because they will be changed by state update
-  Object.defineProperties(local, {
-    getters: {
-      get: noNamespace
-        ? () => store.getters
-        : () => makeLocalGetters(store, namespace)
-    },
-    state: {
-      get: () => getNestedState(store.state, path)
-    }
-  })
-
-  return local
-}
-
-function makeLocalGetters (store, namespace) {
-  if (!store._makeLocalGettersCache[namespace]) {
-    const gettersProxy = {}
-    const splitPos = namespace.length
-    Object.keys(store.getters).forEach(type => {
-      // skip if the target getter is not match this namespace
-      if (type.slice(0, splitPos) !== namespace) return
-
-      // extract local getter type
-      const localType = type.slice(splitPos)
-
-      // Add a port to the getters proxy.
-      // Define as getter property because
-      // we do not want to evaluate the getters in this time.
-      Object.defineProperty(gettersProxy, localType, {
-        get: () => store.getters[type],
-        enumerable: true
-      })
-    })
-    store._makeLocalGettersCache[namespace] = gettersProxy
-  }
-
-  return store._makeLocalGettersCache[namespace]
-}
-
-function registerMutation (store, type, handler, local) {
-  const entry = store._mutations[type] || (store._mutations[type] = [])
-  entry.push(function wrappedMutationHandler (payload) {
-    handler.call(store, local.state, payload)
-  })
-}
-
-function registerAction (store, type, handler, local) {
-  const entry = store._actions[type] || (store._actions[type] = [])
-  entry.push(function wrappedActionHandler (payload) {
-    let res = handler.call(store, {
-      dispatch: local.dispatch,
-      commit: local.commit,
-      getters: local.getters,
-      state: local.state,
-      rootGetters: store.getters,
-      rootState: store.state
-    }, payload)
-    if (!isPromise(res)) {
-      res = Promise.resolve(res)
-    }
-    if (store._devtoolHook) {
-      return res.catch(err => {
-        store._devtoolHook.emit('vuex:error', err)
-        throw err
-      })
-    } else {
-      return res
-    }
-  })
-}
-
-function registerGetter (store, type, rawGetter, local) {
-  if (store._wrappedGetters[type]) {
-    if (__DEV__) {
-      console.error(`[vuex] duplicate getter key: ${type}`)
-    }
-    return
-  }
-  store._wrappedGetters[type] = function wrappedGetter (store) {
-    return rawGetter(
-      local.state, // local state
-      local.getters, // local getters
-      store.state, // root state
-      store.getters // root getters
-    )
-  }
-}
-
-function enableStrictMode (store) {
-  watch(() => store._state.data, () => {
-    if (__DEV__) {
-      assert(store._committing, `do not mutate vuex store state outside mutation handlers.`)
-    }
-  }, { deep: true, flush: 'sync' })
-}
-
-function getNestedState (state, path) {
-  return path.reduce((state, key) => state[key], state)
-}
-
-function unifyObjectStyle (type, payload, options) {
-  if (isObject(type) && type.type) {
-    options = payload
-    payload = type
-    type = type.type
-  }
-
-  if (__DEV__) {
-    assert(typeof type === 'string', `expects string as the type, but found ${typeof type}.`)
-  }
-
-  return { type, payload, options }
-}

+ 5 - 0
yarn.lock

@@ -1619,6 +1619,11 @@
     "@vue/compiler-dom" "3.0.5"
     "@vue/shared" "3.0.5"
 
+"@vue/devtools-api@^6.0.0-beta.10":
+  version "6.0.0-beta.10"
+  resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.0.0-beta.10.tgz#f39da7618cee292e39c7274227c34163e30eb3ca"
+  integrity sha512-nktQYRnIFrh4DdXiCBjHnsHOMZXDIVcP9qlm/DMfxmjJMtpMGrSZCOKP8j7kDhObNHyqlicwoGLd+a4hf4x9ww==
+
 "@vue/reactivity@3.0.5":
   version "3.0.5"
   resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.5.tgz#e3789e4d523d845f9ae0b4d770e2b45594742fd2"