From a6bfbb32305c8e00169dfb0d29ca8f474e6f9beb Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Thu, 20 May 2021 18:27:09 +0200 Subject: [PATCH] dx: add devtools integration (#1942) (#1949) close #1942 Co-authored-by: Eduardo San Martin Morote Co-authored-by: Kia King Ishii --- .babelrc | 8 +- .eslintrc.json | 3 +- examples/classic/shopping-cart/api/shop.js | 24 +- .../shopping-cart/store/modules/cart.js | 26 +- .../shopping-cart/store/modules/nested.js | 9 + .../shopping-cart/store/modules/products.js | 7 +- package.json | 3 + rollup.config.js | 14 +- src/plugins/devtool.js | 263 ++++++++++++++- src/store-util.js | 278 ++++++++++++++++ src/store.js | 307 ++---------------- yarn.lock | 5 + 12 files changed, 617 insertions(+), 330 deletions(-) create mode 100644 examples/classic/shopping-cart/store/modules/nested.js create mode 100644 src/store-util.js diff --git a/.babelrc b/.babelrc index 1320b9a32..9d8eb6f14 100644 --- a/.babelrc +++ b/.babelrc @@ -1,3 +1,9 @@ { - "presets": ["@babel/preset-env"] + "presets": [ + ["@babel/preset-env", { + "exclude": [ + "transform-regenerator" + ] + }] + ] } diff --git a/.eslintrc.json b/.eslintrc.json index af0ad7581..fd2d71204 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -4,6 +4,7 @@ "plugin:vue-libs/recommended" ], "globals": { - "__DEV__": true + "__DEV__": true, + "__VUE_PROD_DEVTOOLS__": true } } diff --git a/examples/classic/shopping-cart/api/shop.js b/examples/classic/shopping-cart/api/shop.js index 708a5b77b..64f4e0dbb 100644 --- a/examples/classic/shopping-cart/api/shop.js +++ b/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) + }) +} diff --git a/examples/classic/shopping-cart/store/modules/cart.js b/examples/classic/shopping-cart/store/modules/cart.js index fcdd3b565..9fc7abd88 100644 --- a/examples/classic/shopping-cart/store/modules/cart.js +++ b/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 + } } diff --git a/examples/classic/shopping-cart/store/modules/nested.js b/examples/classic/shopping-cart/store/modules/nested.js new file mode 100644 index 000000000..e849e26a0 --- /dev/null +++ b/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) + } +} diff --git a/examples/classic/shopping-cart/store/modules/products.js b/examples/classic/shopping-cart/store/modules/products.js index 4dd0ebc18..a71bc360f 100644 --- a/examples/classic/shopping-cart/store/modules/products.js +++ b/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) } } diff --git a/package.json b/package.json index b7b358e42..ea805d907 100644 --- a/package.json +++ b/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", diff --git a/rollup.config.js b/rollup.config.js index 845ec6495..6382c2c9f 100644 --- a/rollup.config.js +++ b/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) { diff --git a/src/plugins/devtool.js b/src/plugins/devtool.js index 7473b73d8..f68bf6823 100644 --- a/src/plugins/devtool.js +++ b/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 + ) } diff --git a/src/store-util.js b/src/store-util.js new file mode 100644 index 000000000..cea5d8b02 --- /dev/null +++ b/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 } +} diff --git a/src/store.js b/src/store.js index 68113a5d2..3ae6898f9 100644 --- a/src/store.js +++ b/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 } -} diff --git a/yarn.lock b/yarn.lock index 0493e754d..1ce9991d3 100644 --- a/yarn.lock +++ b/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"