Browse Source

Implement module `namespace` option (#420)

* refactoring: adding module tree model

* add namespace if module namepsace option is specified

* hot reload namespace

* localize getters, dispatch and commit if module is namespaced

* add error message for unknown local action/mutation

* namespace option only accepts string value

* add more test for local getters, dispatch and commit

* update docs for namespace options

* add a caveat for plugin developers

* add more tests

* add typings for module namespace feature

* use normal object for local getters to compat with root getters

* remove unused this._options

* use root dispatch and commit if there is no namespace
katashin 8 years ago
parent
commit
f84da30054

+ 75 - 24
docs/en/modules.md

@@ -81,42 +81,93 @@ const moduleA = {
 
 
 ### Namespacing
 ### Namespacing
 
 
-Note that actions, mutations and getters inside modules are still registered under the **global namespace** - this allows multiple modules to react to the same mutation/action type. You can namespace the module assets yourself to avoid name clashing by prefixing or suffixing their names. And you probably should if you are writing a reusable Vuex module that will be used in unknown environments. For example, we want to create a `todos` module:
+Note that actions, mutations and getters inside modules are still registered under the **global namespace** - this allows multiple modules to react to the same mutation/action type. You probably should namespace your Vuex module if you are writing a reusable one that will be used in unknown environments. To support namespacing for avoiding name clashing, Vuex provides `namespace` option. If you specify string value to `namespace` option, module assets types are prefixed by the given value:
 
 
 ``` js
 ``` js
-// types.js
+export default {
+  namespace: 'account/',
 
 
-// define names of getters, actions and mutations as constants
-// and they are prefixed by the module name `todos`
-export const DONE_COUNT = 'todos/DONE_COUNT'
-export const FETCH_ALL = 'todos/FETCH_ALL'
-export const TOGGLE_DONE = 'todos/TOGGLE_DONE'
+  // module assets
+  state: { ... }, // module state will not be changed by prefix option
+  getters: {
+    isAdmin () { ... } // -> getters['account/isAdmin']
+  },
+  actions: {
+    login () { ... } // -> dispatch('account/login')
+  },
+  mutations: {
+    login () { ... } // -> commit('account/login')
+  },
+
+  // nested modules
+  modules: {
+    // inherit the namespace from parent module
+    myPage: {
+      state: { ... },
+      getters: {
+        profile () { ... } // -> getters['account/profile']
+      }
+    },
+
+    // nest the namespace
+    posts: {
+      namespace: 'posts/',
+
+      state: { ... },
+      getters: {
+        popular () { ... } // -> getters['account/posts/popular']
+      }
+    }
+  }
+}
 ```
 ```
 
 
-``` js
-// modules/todos.js
-import * as types from '../types'
+Namespaced getters and actions will receive localized `getters`, `dispatch` and `commit`. In other words, you can use the module assets without writing prefix in the same module. If you want to use the global ones, `rootGetters` is passed to the 4th argument of getter functions and the property of the action context. In addition, `dispatch` and `commit` receives `root` option on their last argument.
 
 
-// define getters, actions and mutations using prefixed names
-const todosModule = {
-  state: { todos: [] },
+``` js
+export default {
+  namespace: 'prefix/',
 
 
   getters: {
   getters: {
-    [types.DONE_COUNT] (state) {
-      // ...
-    }
+    // `getters` is localized to this module's getters
+    // you can use rootGetters via 4th argument of getters
+    someGetter (state, getters, rootState, rootGetters) {
+      getters.someOtherGetter // -> 'prefix/someOtherGetter'
+      rootGetters.someOtherGetter // -> 'someOtherGetter'
+    },
+    someOtherGetter: state => { ... }
   },
   },
 
 
   actions: {
   actions: {
-    [types.FETCH_ALL] (context, payload) {
-      // ...
-    }
-  },
+    // dispatch and commit are also localized for this module
+    // they will accept `root` option for the root dispatch/commit
+    someAction ({ dispatch, commit, getters, rootGetters }) {
+      getters.someGetter // -> 'prefix/someGetter'
+      rootGetters.someGetter // -> 'someGetter'
+
+      dispatch('someOtherAction') // -> 'prefix/someOtherAction'
+      dispatch('someOtherAction', null, { root: true }) // -> 'someOtherAction'
+
+      commit('someMutation') // -> 'prefix/someMutation'
+      commit('someMutation', null, { root: true }) // -> 'someMutation'
+    },
+    someOtherAction (ctx, payload) { ... }
+  }
+}
+```
 
 
-  mutations: {
-    [types.TOGGLE_DONE] (state, payload) {
-      // ...
-    }
+#### Caveat for Plugin Developers
+
+You may care about unpredictable namespacing for your modules when you create a [plugin](plugins.md) that provides the modules and let users add them to a Vuex store. Your modules will be also namespaced if the plugin users add your modules under a namespaced module. To adapt this situation, you may need to receive a namespace value via your plugin option:
+
+``` js
+// get namespace value via plugin option
+// and returns Vuex plugin function
+export function createPlugin (options = {}) {
+  return function (store) {
+    // add namespace to plugin module's types
+    const namespace = options.namespace || ''
+    store.dispatch(namespace + 'pluginAction')
   }
   }
 }
 }
 ```
 ```

+ 145 - 98
src/index.js

@@ -1,7 +1,8 @@
 import devtoolPlugin from './plugins/devtool'
 import devtoolPlugin from './plugins/devtool'
 import applyMixin from './mixin'
 import applyMixin from './mixin'
 import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
 import { mapState, mapMutations, mapGetters, mapActions } from './helpers'
-import { isObject, isPromise, assert } from './util'
+import { forEachValue, isObject, isPromise, assert } from './util'
+import ModuleCollection from './module/module-collection'
 
 
 let Vue // bind on install
 let Vue // bind on install
 
 
@@ -17,12 +18,11 @@ class Store {
     } = options
     } = options
 
 
     // store internal state
     // store internal state
-    this._options = options
     this._committing = false
     this._committing = false
     this._actions = Object.create(null)
     this._actions = Object.create(null)
     this._mutations = Object.create(null)
     this._mutations = Object.create(null)
     this._wrappedGetters = Object.create(null)
     this._wrappedGetters = Object.create(null)
-    this._runtimeModules = Object.create(null)
+    this._modules = new ModuleCollection(options)
     this._subscribers = []
     this._subscribers = []
     this._watcherVM = new Vue()
     this._watcherVM = new Vue()
 
 
@@ -42,7 +42,7 @@ class Store {
     // init root module.
     // init root module.
     // this also recursively registers all sub-modules
     // this also recursively registers all sub-modules
     // and collects all module getters inside this._wrappedGetters
     // and collects all module getters inside this._wrappedGetters
-    installModule(this, state, [], options)
+    installModule(this, state, [], this._modules.root)
 
 
     // initialize the store vm, which is responsible for the reactivity
     // initialize the store vm, which is responsible for the reactivity
     // (also registers _wrappedGetters as computed properties)
     // (also registers _wrappedGetters as computed properties)
@@ -60,13 +60,14 @@ class Store {
     assert(false, `Use store.replaceState() to explicit replace store state.`)
     assert(false, `Use store.replaceState() to explicit replace store state.`)
   }
   }
 
 
-  commit (type, payload, options) {
+  commit (_type, _payload, _options) {
     // check object-style commit
     // check object-style commit
-    if (isObject(type) && type.type) {
-      options = payload
-      payload = type
-      type = type.type
-    }
+    const {
+      type,
+      payload,
+      options
+    } = unifyObjectStyle(_type, _payload, _options)
+
     const mutation = { type, payload }
     const mutation = { type, payload }
     const entry = this._mutations[type]
     const entry = this._mutations[type]
     if (!entry) {
     if (!entry) {
@@ -83,12 +84,13 @@ class Store {
     }
     }
   }
   }
 
 
-  dispatch (type, payload) {
+  dispatch (_type, _payload) {
     // check object-style dispatch
     // check object-style dispatch
-    if (isObject(type) && type.type) {
-      payload = type
-      type = type.type
-    }
+    const {
+      type,
+      payload
+    } = unifyObjectStyle(_type, _payload)
+
     const entry = this._actions[type]
     const entry = this._actions[type]
     if (!entry) {
     if (!entry) {
       console.error(`[vuex] unknown action type: ${type}`)
       console.error(`[vuex] unknown action type: ${type}`)
@@ -123,11 +125,11 @@ class Store {
     })
     })
   }
   }
 
 
-  registerModule (path, module) {
+  registerModule (path, rawModule) {
     if (typeof path === 'string') path = [path]
     if (typeof path === 'string') path = [path]
     assert(Array.isArray(path), `module path must be a string or an Array.`)
     assert(Array.isArray(path), `module path must be a string or an Array.`)
-    this._runtimeModules[path.join('.')] = module
-    installModule(this, this.state, path, module)
+    this._modules.register(path, rawModule)
+    installModule(this, this.state, path, this._modules.get(path))
     // reset store to update getters...
     // reset store to update getters...
     resetStoreVM(this, this.state)
     resetStoreVM(this, this.state)
   }
   }
@@ -135,7 +137,7 @@ class Store {
   unregisterModule (path) {
   unregisterModule (path) {
     if (typeof path === 'string') path = [path]
     if (typeof path === 'string') path = [path]
     assert(Array.isArray(path), `module path must be a string or an Array.`)
     assert(Array.isArray(path), `module path must be a string or an Array.`)
-    delete this._runtimeModules[path.join('.')]
+    this._modules.unregister(path)
     this._withCommit(() => {
     this._withCommit(() => {
       const parentState = getNestedState(this.state, path.slice(0, -1))
       const parentState = getNestedState(this.state, path.slice(0, -1))
       Vue.delete(parentState, path[path.length - 1])
       Vue.delete(parentState, path[path.length - 1])
@@ -144,7 +146,7 @@ class Store {
   }
   }
 
 
   hotUpdate (newOptions) {
   hotUpdate (newOptions) {
-    updateModule(this._options, newOptions)
+    this._modules.update(newOptions)
     resetStore(this)
     resetStore(this)
   }
   }
 
 
@@ -156,41 +158,13 @@ class Store {
   }
   }
 }
 }
 
 
-function updateModule (targetModule, newModule) {
-  if (newModule.actions) {
-    targetModule.actions = newModule.actions
-  }
-  if (newModule.mutations) {
-    targetModule.mutations = newModule.mutations
-  }
-  if (newModule.getters) {
-    targetModule.getters = newModule.getters
-  }
-  if (newModule.modules) {
-    for (const key in newModule.modules) {
-      if (!(targetModule.modules && targetModule.modules[key])) {
-        console.warn(
-          `[vuex] trying to add a new module '${key}' on hot reloading, ` +
-          'manual reload is needed'
-        )
-        return
-      }
-      updateModule(targetModule.modules[key], newModule.modules[key])
-    }
-  }
-}
-
 function resetStore (store) {
 function resetStore (store) {
   store._actions = Object.create(null)
   store._actions = Object.create(null)
   store._mutations = Object.create(null)
   store._mutations = Object.create(null)
   store._wrappedGetters = Object.create(null)
   store._wrappedGetters = Object.create(null)
   const state = store.state
   const state = store.state
-  // init root module
-  installModule(store, state, [], store._options, true)
-  // init all runtime modules
-  Object.keys(store._runtimeModules).forEach(key => {
-    installModule(store, state, key.split('.'), store._runtimeModules[key], true)
-  })
+  // init all modules
+  installModule(store, state, [], store._modules.root, true)
   // reset vm
   // reset vm
   resetStoreVM(store, state)
   resetStoreVM(store, state)
 }
 }
@@ -202,12 +176,12 @@ function resetStoreVM (store, state) {
   store.getters = {}
   store.getters = {}
   const wrappedGetters = store._wrappedGetters
   const wrappedGetters = store._wrappedGetters
   const computed = {}
   const computed = {}
-  Object.keys(wrappedGetters).forEach(key => {
-    const fn = wrappedGetters[key]
+  forEachValue(wrappedGetters, (fn, key) => {
     // use computed to leverage its lazy-caching mechanism
     // use computed to leverage its lazy-caching mechanism
     computed[key] = () => fn(store)
     computed[key] = () => fn(store)
     Object.defineProperty(store.getters, key, {
     Object.defineProperty(store.getters, key, {
-      get: () => store._vm[key]
+      get: () => store._vm[key],
+      enumerable: true // for local getters
     })
     })
   })
   })
 
 
@@ -239,62 +213,128 @@ function resetStoreVM (store, state) {
 
 
 function installModule (store, rootState, path, module, hot) {
 function installModule (store, rootState, path, module, hot) {
   const isRoot = !path.length
   const isRoot = !path.length
-  const {
-    state,
-    actions,
-    mutations,
-    getters,
-    modules
-  } = module
+  const namespace = store._modules.getNamespace(path)
 
 
   // set state
   // set state
   if (!isRoot && !hot) {
   if (!isRoot && !hot) {
     const parentState = getNestedState(rootState, path.slice(0, -1))
     const parentState = getNestedState(rootState, path.slice(0, -1))
     const moduleName = path[path.length - 1]
     const moduleName = path[path.length - 1]
     store._withCommit(() => {
     store._withCommit(() => {
-      Vue.set(parentState, moduleName, state || {})
+      Vue.set(parentState, moduleName, module.state)
     })
     })
   }
   }
 
 
-  if (mutations) {
-    Object.keys(mutations).forEach(key => {
-      registerMutation(store, key, mutations[key], path)
-    })
-  }
+  const local = makeLocalContext(store, namespace)
 
 
-  if (actions) {
-    Object.keys(actions).forEach(key => {
-      registerAction(store, key, actions[key], path)
-    })
-  }
+  module.forEachMutation((mutation, key) => {
+    const namespacedType = namespace + key
+    registerMutation(store, namespacedType, mutation, path)
+  })
+
+  module.forEachAction((action, key) => {
+    const namespacedType = namespace + key
+    registerAction(store, namespacedType, action, local, path)
+  })
+
+  module.forEachGetter((getter, key) => {
+    const namespacedType = namespace + key
+    registerGetter(store, namespacedType, getter, local, path)
+  })
 
 
-  if (getters) {
-    wrapGetters(store, getters, path)
+  module.forEachChild((child, key) => {
+    installModule(store, rootState, path.concat(key), child, hot)
+  })
+}
+
+/**
+ * make localized dispatch, commit and getters
+ * if there is no namespace, just use root ones
+ */
+function makeLocalContext (store, namespace) {
+  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 (!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 (!store._mutations[type]) {
+          console.error(`[vuex] unknown local mutation type: ${args.type}, global type: ${type}`)
+          return
+        }
+      }
+
+      store.commit(type, payload, options)
+    }
   }
   }
 
 
-  if (modules) {
-    Object.keys(modules).forEach(key => {
-      installModule(store, rootState, path.concat(key), modules[key], hot)
+  // getters object must be gotten lazily
+  // because store.getters will be changed by vm update
+  Object.defineProperty(local, 'getters', {
+    get: noNamespace ? () => store.getters : () => makeLocalGetters(store, namespace)
+  })
+
+  return local
+}
+
+function makeLocalGetters (store, 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
     })
     })
-  }
+  })
+
+  return gettersProxy
 }
 }
 
 
-function registerMutation (store, type, handler, path = []) {
+function registerMutation (store, type, handler, path) {
   const entry = store._mutations[type] || (store._mutations[type] = [])
   const entry = store._mutations[type] || (store._mutations[type] = [])
   entry.push(function wrappedMutationHandler (payload) {
   entry.push(function wrappedMutationHandler (payload) {
     handler(getNestedState(store.state, path), payload)
     handler(getNestedState(store.state, path), payload)
   })
   })
 }
 }
 
 
-function registerAction (store, type, handler, path = []) {
+function registerAction (store, type, handler, local, path) {
   const entry = store._actions[type] || (store._actions[type] = [])
   const entry = store._actions[type] || (store._actions[type] = [])
-  const { dispatch, commit } = store
   entry.push(function wrappedActionHandler (payload, cb) {
   entry.push(function wrappedActionHandler (payload, cb) {
     let res = handler({
     let res = handler({
-      dispatch,
-      commit,
-      getters: store.getters,
+      dispatch: local.dispatch,
+      commit: local.commit,
+      getters: local.getters,
       state: getNestedState(store.state, path),
       state: getNestedState(store.state, path),
+      rootGetters: store.getters,
       rootState: store.state
       rootState: store.state
     }, payload, cb)
     }, payload, cb)
     if (!isPromise(res)) {
     if (!isPromise(res)) {
@@ -311,21 +351,19 @@ function registerAction (store, type, handler, path = []) {
   })
   })
 }
 }
 
 
-function wrapGetters (store, moduleGetters, modulePath) {
-  Object.keys(moduleGetters).forEach(getterKey => {
-    const rawGetter = moduleGetters[getterKey]
-    if (store._wrappedGetters[getterKey]) {
-      console.error(`[vuex] duplicate getter key: ${getterKey}`)
-      return
-    }
-    store._wrappedGetters[getterKey] = function wrappedGetter (store) {
-      return rawGetter(
-        getNestedState(store.state, modulePath), // local state
-        store.getters, // getters
-        store.state // root state
-      )
-    }
-  })
+function registerGetter (store, type, rawGetter, local, path) {
+  if (store._wrappedGetters[type]) {
+    console.error(`[vuex] duplicate getter key: ${type}`)
+    return
+  }
+  store._wrappedGetters[type] = function wrappedGetter (store) {
+    return rawGetter(
+      getNestedState(store.state, path), // local state
+      local.getters, // local getters
+      store.state, // root state
+      store.getters // root getters
+    )
+  }
 }
 }
 
 
 function enableStrictMode (store) {
 function enableStrictMode (store) {
@@ -340,6 +378,15 @@ function getNestedState (state, path) {
     : state
     : state
 }
 }
 
 
+function unifyObjectStyle (type, payload, options) {
+  if (isObject(type) && type.type) {
+    options = payload
+    payload = type
+    type = type.type
+  }
+  return { type, payload, options }
+}
+
 function install (_Vue) {
 function install (_Vue) {
   if (Vue) {
   if (Vue) {
     console.error(
     console.error(

+ 74 - 0
src/module/module-collection.js

@@ -0,0 +1,74 @@
+import Module from './module'
+import { forEachValue } from '../util'
+
+export default class ModuleCollection {
+  constructor (rawRootModule) {
+    // register root module (Vuex.Store options)
+    this.root = new Module(rawRootModule, false)
+
+    // register all nested modules
+    if (rawRootModule.modules) {
+      forEachValue(rawRootModule.modules, (rawModule, key) => {
+        this.register([key], rawModule, false)
+      })
+    }
+  }
+
+  get (path) {
+    return path.reduce((module, key) => {
+      return module.getChild(key)
+    }, this.root)
+  }
+
+  getNamespace (path) {
+    let module = this.root
+    return path.reduce((namespace, key) => {
+      module = module.getChild(key)
+      return namespace + module.namespace
+    }, '')
+  }
+
+  update (rawRootModule) {
+    update(this.root, rawRootModule)
+  }
+
+  register (path, rawModule, runtime = true) {
+    const parent = this.get(path.slice(0, -1))
+    const newModule = new Module(rawModule, runtime)
+    parent.addChild(path[path.length - 1], newModule)
+
+    // register nested modules
+    if (rawModule.modules) {
+      forEachValue(rawModule.modules, (rawChildModule, key) => {
+        this.register(path.concat(key), rawChildModule, runtime)
+      })
+    }
+  }
+
+  unregister (path) {
+    const parent = this.get(path.slice(0, -1))
+    const key = path[path.length - 1]
+    if (!parent.getChild(key).runtime) return
+
+    parent.removeChild(key)
+  }
+}
+
+function update (targetModule, newModule) {
+  // update target module
+  targetModule.update(newModule)
+
+  // update nested modules
+  if (newModule.modules) {
+    for (const key in newModule.modules) {
+      if (!targetModule.getChild(key)) {
+        console.warn(
+          `[vuex] trying to add a new module '${key}' on hot reloading, ` +
+          'manual reload is needed'
+        )
+        return
+      }
+      update(targetModule.getChild(key), newModule.modules[key])
+    }
+  }
+}

+ 64 - 0
src/module/module.js

@@ -0,0 +1,64 @@
+import { forEachValue } from '../util'
+
+export default class Module {
+  constructor (rawModule, runtime) {
+    this.runtime = runtime
+    this._children = Object.create(null)
+    this._rawModule = rawModule
+  }
+
+  get state () {
+    return this._rawModule.state || {}
+  }
+
+  get namespace () {
+    return this._rawModule.namespace || ''
+  }
+
+  addChild (key, module) {
+    this._children[key] = module
+  }
+
+  removeChild (key) {
+    delete this._children[key]
+  }
+
+  getChild (key) {
+    return this._children[key]
+  }
+
+  update (rawModule) {
+    this._rawModule.namespace = rawModule.namespace
+    if (rawModule.actions) {
+      this._rawModule.actions = rawModule.actions
+    }
+    if (rawModule.mutations) {
+      this._rawModule.mutations = rawModule.mutations
+    }
+    if (rawModule.getters) {
+      this._rawModule.getters = rawModule.getters
+    }
+  }
+
+  forEachChild (fn) {
+    forEachValue(this._children, fn)
+  }
+
+  forEachGetter (fn) {
+    if (this._rawModule.getters) {
+      forEachValue(this._rawModule.getters, fn)
+    }
+  }
+
+  forEachAction (fn) {
+    if (this._rawModule.actions) {
+      forEachValue(this._rawModule.actions, fn)
+    }
+  }
+
+  forEachMutation (fn) {
+    if (this._rawModule.mutations) {
+      forEachValue(this._rawModule.mutations, fn)
+    }
+  }
+}

+ 7 - 0
src/util.js

@@ -46,6 +46,13 @@ export function deepCopy (obj, cache = []) {
   return copy
   return copy
 }
 }
 
 
+/**
+ * forEach for object
+ */
+export function forEachValue (obj, fn) {
+  Object.keys(obj).forEach(key => fn(obj[key], key))
+}
+
 export function isObject (obj) {
 export function isObject (obj) {
   return obj !== null && typeof obj === 'object'
   return obj !== null && typeof obj === 'object'
 }
 }

+ 3 - 1
test/unit/jasmine.json

@@ -2,7 +2,9 @@
   "spec_dir": "test/unit",
   "spec_dir": "test/unit",
   "spec_files": [
   "spec_files": [
     "test.js",
     "test.js",
-    "util.js"
+    "util.js",
+    "module/module.js",
+    "module/module-collection.js"
   ],
   ],
   "helpers": [
   "helpers": [
     "../../node_modules/babel-register/lib/node.js"
     "../../node_modules/babel-register/lib/node.js"

+ 95 - 0
test/unit/module/module-collection.js

@@ -0,0 +1,95 @@
+import ModuleCollection from '../../../src/module/module-collection'
+
+describe('ModuleCollection', () => {
+  it('get', () => {
+    const collection = new ModuleCollection({
+      state: { value: 1 },
+      modules: {
+        a: {
+          state: { value: 2 }
+        },
+        b: {
+          state: { value: 3 },
+          modules: {
+            c: {
+              state: { value: 4 }
+            }
+          }
+        }
+      }
+    })
+    expect(collection.get([]).state.value).toBe(1)
+    expect(collection.get(['a']).state.value).toBe(2)
+    expect(collection.get(['b']).state.value).toBe(3)
+    expect(collection.get(['b', 'c']).state.value).toBe(4)
+  })
+
+  it('getNamespace', () => {
+    const module = (namespace, children) => {
+      return {
+        namespace,
+        modules: children
+      }
+    }
+    const collection = new ModuleCollection({
+      namespace: 'ignore/', // root module namespace should be ignored
+      modules: {
+        a: module('a/', {
+          b: module(null, {
+            c: module('c/')
+          }),
+          d: module('d/')
+        })
+      }
+    })
+    const check = (path, expected) => {
+      const type = 'test'
+      const namespace = collection.getNamespace(path)
+      expect(namespace + type).toBe(expected)
+    }
+    check(['a'], 'a/test')
+    check(['a', 'b'], 'a/test')
+    check(['a', 'b', 'c'], 'a/c/test')
+    check(['a', 'd'], 'a/d/test')
+  })
+
+  it('register', () => {
+    const collection = new ModuleCollection({})
+    collection.register(['a'], {
+      state: { value: 1 }
+    })
+    collection.register(['b'], {
+      state: { value: 2 }
+    })
+    collection.register(['a', 'b'], {
+      state: { value: 3 }
+    })
+
+    expect(collection.get(['a']).state.value).toBe(1)
+    expect(collection.get(['b']).state.value).toBe(2)
+    expect(collection.get(['a', 'b']).state.value).toBe(3)
+  })
+
+  it('unregister', () => {
+    const collection = new ModuleCollection({})
+    collection.register(['a'], {
+      state: { value: true }
+    })
+    expect(collection.get(['a']).state.value).toBe(true)
+
+    collection.unregister(['a'])
+    expect(collection.get(['a'])).toBe(undefined)
+  })
+
+  it('does not unregister initial modules', () => {
+    const collection = new ModuleCollection({
+      modules: {
+        a: {
+          state: { value: true }
+        }
+      }
+    })
+    collection.unregister(['a'])
+    expect(collection.get(['a']).state.value).toBe(true)
+  })
+})

+ 29 - 0
test/unit/module/module.js

@@ -0,0 +1,29 @@
+import Module from '../../../src/module/module'
+
+describe('Module', () => {
+  it('get state', () => {
+    const module = new Module({
+      state: {
+        value: true
+      }
+    })
+    expect(module.state).toEqual({ value: true })
+  })
+
+  it('get state: should return object if state option is empty', () => {
+    const module = new Module({})
+    expect(module.state).toEqual({})
+  })
+
+  it('get namespacer: no namespace option', () => {
+    const module = new Module({})
+    expect(module.namespace).toBe('')
+  })
+
+  it('get namespacer: namespace option is string value', () => {
+    const module = new Module({
+      namespace: 'prefix/'
+    })
+    expect(module.namespace).toBe('prefix/')
+  })
+})

+ 291 - 0
test/unit/test.js

@@ -248,6 +248,33 @@ describe('Vuex', () => {
     expect(store.getters.bar).toBe(3)
     expect(store.getters.bar).toBe(3)
   })
   })
 
 
+  it('dynamic module registration with namespace inheritance', () => {
+    const store = new Vuex.Store({
+      modules: {
+        a: {
+          namespace: 'prefix/'
+        }
+      }
+    })
+    const actionSpy = jasmine.createSpy()
+    const mutationSpy = jasmine.createSpy()
+    store.registerModule(['a', 'b'], {
+      state: { value: 1 },
+      getters: { foo: state => state.value },
+      actions: { foo: actionSpy },
+      mutations: { foo: mutationSpy }
+    })
+
+    expect(store.state.a.b.value).toBe(1)
+    expect(store.getters['prefix/foo']).toBe(1)
+
+    store.dispatch('prefix/foo')
+    expect(actionSpy).toHaveBeenCalled()
+
+    store.commit('prefix/foo')
+    expect(mutationSpy).toHaveBeenCalled()
+  })
+
   it('store injection', () => {
   it('store injection', () => {
     const store = new Vuex.Store()
     const store = new Vuex.Store()
     const vm = new Vue({
     const vm = new Vue({
@@ -581,6 +608,215 @@ describe('Vuex', () => {
     })
     })
   })
   })
 
 
+  it('module: namespace', () => {
+    const actionSpy = jasmine.createSpy()
+    const mutationSpy = jasmine.createSpy()
+
+    const store = new Vuex.Store({
+      modules: {
+        a: {
+          namespace: 'prefix/',
+          state: {
+            a: 1
+          },
+          getters: {
+            b: () => 2
+          },
+          actions: {
+            [TEST]: actionSpy
+          },
+          mutations: {
+            [TEST]: mutationSpy
+          }
+        }
+      }
+    })
+
+    expect(store.state.a.a).toBe(1)
+    expect(store.getters['prefix/b']).toBe(2)
+    store.dispatch('prefix/' + TEST)
+    expect(actionSpy).toHaveBeenCalled()
+    store.commit('prefix/' + TEST)
+    expect(mutationSpy).toHaveBeenCalled()
+  })
+
+  it('module: nested namespace', () => {
+    // mock module generator
+    const actionSpys = []
+    const mutationSpys = []
+    const createModule = (name, namespace, children) => {
+      const actionSpy = jasmine.createSpy()
+      const mutationSpy = jasmine.createSpy()
+
+      actionSpys.push(actionSpy)
+      mutationSpys.push(mutationSpy)
+
+      return {
+        namespace,
+        state: {
+          [name]: true
+        },
+        getters: {
+          [name]: state => state[name]
+        },
+        actions: {
+          [name]: actionSpy
+        },
+        mutations: {
+          [name]: mutationSpy
+        },
+        modules: children
+      }
+    }
+
+    // mock module
+    const modules = {
+      a: createModule('a', 'a/', { // a/a
+        b: createModule('b', null, { // a/b - does not add namespace
+          c: createModule('c', 'c/') // a/c/c
+        }),
+        d: createModule('d', 'd/'), // a/d/d
+      })
+    }
+
+    const store = new Vuex.Store({ modules })
+
+    const expectedTypes = [
+      'a/a', 'a/b', 'a/c/c', 'a/d/d'
+    ]
+
+    // getters
+    expectedTypes.forEach(type => {
+      expect(store.getters[type]).toBe(true)
+    })
+
+    // actions
+    expectedTypes.forEach(type => {
+      store.dispatch(type)
+    })
+    actionSpys.forEach(spy => {
+      expect(spy.calls.count()).toBe(1)
+    })
+
+    // mutations
+    expectedTypes.forEach(type => {
+      store.commit(type)
+    })
+    mutationSpys.forEach(spy => {
+      expect(spy.calls.count()).toBe(1)
+    })
+  })
+
+  it('module: getters are namespaced in namespaced module', () => {
+    const store = new Vuex.Store({
+      state: { value: 'root' },
+      getters: {
+        foo: state => state.value
+      },
+      modules: {
+        a: {
+          namespace: 'prefix/',
+          state: { value: 'module' },
+          getters: {
+            foo: state => state.value,
+            bar: (state, getters) => getters.foo,
+            baz: (state, getters, rootState, rootGetters) => rootGetters.foo
+          }
+        }
+      }
+    })
+
+    expect(store.getters['prefix/foo']).toBe('module')
+    expect(store.getters['prefix/bar']).toBe('module')
+    expect(store.getters['prefix/baz']).toBe('root')
+  })
+
+  it('module: action context is namespaced in namespaced module', done => {
+    const rootActionSpy = jasmine.createSpy()
+    const rootMutationSpy = jasmine.createSpy()
+    const moduleActionSpy = jasmine.createSpy()
+    const moduleMutationSpy = jasmine.createSpy()
+
+    const store = new Vuex.Store({
+      state: { value: 'root' },
+      getters: { foo: state => state.value },
+      actions: { foo: rootActionSpy },
+      mutations: { foo: rootMutationSpy },
+      modules: {
+        a: {
+          namespace: 'prefix/',
+          state: { value: 'module' },
+          getters: { foo: state => state.value },
+          actions: {
+            foo: moduleActionSpy,
+            test ({ dispatch, commit, getters, rootGetters }) {
+              expect(getters.foo).toBe('module')
+              expect(rootGetters.foo).toBe('root')
+
+              dispatch('foo')
+              expect(moduleActionSpy.calls.count()).toBe(1)
+              dispatch('foo', null, { root: true })
+              expect(rootActionSpy.calls.count()).toBe(1)
+
+              commit('foo')
+              expect(moduleMutationSpy.calls.count()).toBe(1)
+              commit('foo', null, { root: true })
+              expect(rootMutationSpy.calls.count()).toBe(1)
+
+              done()
+            }
+          },
+          mutations: { foo: moduleMutationSpy }
+        }
+      }
+    })
+
+    store.dispatch('prefix/test')
+  })
+
+  it('module: use other module that has same namespace', done => {
+    const actionSpy = jasmine.createSpy()
+    const mutationSpy = jasmine.createSpy()
+
+    const store = new Vuex.Store({
+      modules: {
+        parent: {
+          namespace: 'prefix/',
+
+          modules: {
+            a: {
+              state: { value: 'a' },
+              getters: { foo: state => state.value },
+              actions: { foo: actionSpy },
+              mutations: { foo: mutationSpy }
+            },
+
+            b: {
+              state: { value: 'b' },
+              getters: { bar: (state, getters) => getters.foo },
+              actions: {
+                test ({ dispatch, commit, getters }) {
+                  expect(getters.foo).toBe('a')
+                  expect(getters.bar).toBe('a')
+
+                  dispatch('foo')
+                  expect(actionSpy).toHaveBeenCalled()
+
+                  commit('foo')
+                  expect(mutationSpy).toHaveBeenCalled()
+
+                  done()
+                }
+              }
+            }
+          }
+        }
+      }
+    })
+
+    store.dispatch('prefix/test')
+  })
+
   it('dispatching multiple actions in different modules', done => {
   it('dispatching multiple actions in different modules', done => {
     const store = new Vuex.Store({
     const store = new Vuex.Store({
       modules: {
       modules: {
@@ -991,6 +1227,61 @@ describe('Vuex', () => {
     )
     )
   })
   })
 
 
+  it('hot reload: update namespace', () => {
+    // prevent to print notification of unknown action/mutation
+    spyOn(console, 'error')
+
+    const actionSpy = jasmine.createSpy()
+    const mutationSpy = jasmine.createSpy()
+
+    const store = new Vuex.Store({
+      modules: {
+        a: {
+          namespace: 'prefix/',
+          state: { value: 1 },
+          getters: { foo: state => state.value },
+          actions: { foo: actionSpy },
+          mutations: { foo: mutationSpy }
+        }
+      }
+    })
+
+    expect(store.state.a.value).toBe(1)
+    expect(store.getters['prefix/foo']).toBe(1)
+    store.dispatch('prefix/foo')
+    expect(actionSpy.calls.count()).toBe(1)
+    store.commit('prefix/foo')
+    expect(actionSpy.calls.count()).toBe(1)
+
+    store.hotUpdate({
+      modules: {
+        a: {
+          namespace: 'prefix-changed/'
+        }
+      }
+    })
+
+    expect(store.state.a.value).toBe(1)
+    expect(store.getters['prefix/foo']).toBe(undefined) // removed
+    expect(store.getters['prefix-changed/foo']).toBe(1) // renamed
+
+    // should not be called
+    store.dispatch('prefix/foo')
+    expect(actionSpy.calls.count()).toBe(1)
+
+    // should be called
+    store.dispatch('prefix-changed/foo')
+    expect(actionSpy.calls.count()).toBe(2)
+
+    // should not be called
+    store.commit('prefix/foo')
+    expect(mutationSpy.calls.count()).toBe(1)
+
+    // should be called
+    store.commit('prefix-changed/foo')
+    expect(mutationSpy.calls.count()).toBe(2)
+  })
+
   it('watch: with resetting vm', done => {
   it('watch: with resetting vm', done => {
     const store = new Vuex.Store({
     const store = new Vuex.Store({
       state: {
       state: {

+ 10 - 3
types/index.d.ts

@@ -37,8 +37,8 @@ export declare class Store<S> {
 export declare function install(Vue: typeof _Vue): void;
 export declare function install(Vue: typeof _Vue): void;
 
 
 export interface Dispatch {
 export interface Dispatch {
-  (type: string, payload?: any): Promise<any[]>;
-  <P extends Payload>(payloadWithType: P): Promise<any[]>;
+  (type: string, payload?: any, options?: DispatchOptions): Promise<any[]>;
+  <P extends Payload>(payloadWithType: P, options?: DispatchOptions): Promise<any[]>;
 }
 }
 
 
 export interface Commit {
 export interface Commit {
@@ -52,14 +52,20 @@ export interface ActionContext<S, R> {
   state: S;
   state: S;
   getters: any;
   getters: any;
   rootState: R;
   rootState: R;
+  rootGetters: any;
 }
 }
 
 
 export interface Payload {
 export interface Payload {
   type: string;
   type: string;
 }
 }
 
 
+export interface DispatchOptions {
+  root?: boolean;
+}
+
 export interface CommitOptions {
 export interface CommitOptions {
   silent?: boolean;
   silent?: boolean;
+  root?: boolean;
 }
 }
 
 
 export interface StoreOptions<S> {
 export interface StoreOptions<S> {
@@ -72,12 +78,13 @@ export interface StoreOptions<S> {
   strict?: boolean;
   strict?: boolean;
 }
 }
 
 
-export type Getter<S, R> = (state: S, getters: any, rootState: R) => any;
+export type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any;
 export type Action<S, R> = (injectee: ActionContext<S, R>, payload: any) => any;
 export type Action<S, R> = (injectee: ActionContext<S, R>, payload: any) => any;
 export type Mutation<S> = (state: S, payload: any) => any;
 export type Mutation<S> = (state: S, payload: any) => any;
 export type Plugin<S> = (store: Store<S>) => any;
 export type Plugin<S> = (store: Store<S>) => any;
 
 
 export interface Module<S, R> {
 export interface Module<S, R> {
+  namespace?: string;
   state?: S;
   state?: S;
   getters?: GetterTree<S, R>;
   getters?: GetterTree<S, R>;
   actions?: ActionTree<S, R>;
   actions?: ActionTree<S, R>;

+ 53 - 0
types/test/index.ts

@@ -121,6 +121,59 @@ namespace NestedModules {
   });
   });
 }
 }
 
 
+namespace NamespacedModule {
+  const store = new Vuex.Store({
+    state: { value: 0 },
+    getters: {
+      rootValue: state => state.value
+    },
+    actions: {
+      foo () {}
+    },
+    mutations: {
+      foo () {}
+    },
+    modules: {
+      a: {
+        namespace: "prefix/",
+        state: { value: 1 },
+        modules: {
+          b: {
+            state: { value: 2 }
+          },
+          c: {
+            namespace: "nested/",
+            state: { value: 3 },
+            getters: {
+              constant: () => 10,
+              count (state, getters, rootState, rootGetters) {
+                getters.constant;
+                rootGetters.rootValue;
+              }
+            },
+            actions: {
+              test ({ dispatch, commit, getters, rootGetters }) {
+                getters.constant;
+                rootGetters.rootValue;
+
+                dispatch("foo");
+                dispatch("foo", null, { root: true });
+
+                commit("foo");
+                commit("foo", null, { root: true });
+              },
+              foo () {}
+            },
+            mutations: {
+              foo () {}
+            }
+          }
+        }
+      }
+    }
+  });
+}
+
 namespace RegisterModule {
 namespace RegisterModule {
   interface RootState {
   interface RootState {
     value: number;
     value: number;