├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── LICENSE ├── README.md ├── dist ├── index.d.ts ├── index.js └── index.js.map ├── examples └── single-file.ts ├── jest-setup.ts ├── jest.config.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── index.ts ├── tests ├── anon-handler.spec.ts ├── complete.spec.ts ├── dynamic-register-module.ts ├── main.ts ├── nested.spec.ts ├── plugin.spec.ts ├── root.spec.ts └── store │ ├── auth │ ├── auth.ts │ └── state.ts │ ├── birthday │ ├── actions │ │ └── removeFirstAfter.ts │ ├── birthday.ts │ └── state.ts │ └── index.ts ├── tsconfig-src.json ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/**/* 2 | coverage/**/* 3 | .nyc_output/**/* 4 | npm-debug.log 5 | src/**/*.js 6 | src/**/*.js.map 7 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "protocol": "inspector", 12 | "program": "${workspaceRoot}/dist/tests/main.js", 13 | "outFiles": [ 14 | "${workspaceRoot}/dist/**/*.js" 15 | ] 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.format.placeOpenBraceOnNewLineForFunctions": true, 4 | "typescript.format.placeOpenBraceOnNewLineForControlBlocks": true, 5 | "tsimporter.emitSemicolon": false, 6 | "tsimporter.noStatusBar": false, 7 | "tsimporter.filesToExclude": [ 8 | "./dist/**" 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "npm", 4 | "isShellCommand": true, 5 | "args": [ 6 | "run" 7 | ], 8 | "showOutput": "always", 9 | "tasks": [ 10 | { 11 | "taskName": "build", 12 | "isBuildCommand": true, 13 | "isBackground": false, 14 | "problemMatcher": "$tsc" 15 | }, 16 | // { 17 | // "taskName": "clean" 18 | // }, 19 | // { 20 | // "taskName": "lint" 21 | // }, 22 | // { 23 | // "taskName": "coverage" 24 | // }, 25 | { 26 | "taskName": "test", 27 | "isTestCommand": true 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuex-Typex 2 | 3 | ### A TypeScript pattern for strongly-typed access to Vuex Store modules 4 | 5 | I really like [vuex-typescript](https://github.com/istrib/vuex-typescript/) by [@istrib](https://github.com/istrib) 6 | 7 | I just wanted to take the ideas _bit_ further. 8 | 9 | My main changes are: 10 | - Avoid passing $store/context to the accessor methods: we can encapsulate these within the accessors by providing the store later: 11 | i.e. `basket.commitAppendItem(newItem)` should be sufficient. 12 | - No need to distinguish between payload / payload-less versions of commit + dispatch. 13 | Typescript overloads solve this problem. 14 | - Promises returned from dispatch should be strongly-typed. 15 | - Assumes namespaced modules 16 | 17 | I also took the point of view that we don't need to start with a vuex-store options object. If we treat the accessor-creator as a builder, then the store can be generated: 18 | 19 | ```typescript 20 | import { getStoreBuilder, BareActionContext } from "vuex-typex" 21 | import Vuex, { Store } from "vuex" 22 | import Vue from "vue" 23 | const delay = (duration: number) => new Promise((c, e) => setTimeout(c, duration)) 24 | 25 | Vue.use(Vuex) 26 | 27 | export interface RootState { basket: BasketState } 28 | export interface BasketState { items: Item[] } 29 | export interface Item { id: string, name: string } 30 | 31 | const storeBuilder = getStoreBuilder() 32 | const moduleBuilder = storeBuilder.module("basket", { items: [] }) 33 | 34 | namespace basket 35 | { 36 | const appendItemMutation = (state: BasketState, payload: { item: Item }) => state.items.push(payload.item) 37 | const delayedAppendAction = async (context: BareActionContext) => 38 | { 39 | await delay(1000) 40 | basket.commitAppendItem({ item: { id: "abc123", name: "ABC Item" } }) 41 | } 42 | 43 | export const commitAppendItem = moduleBuilder.commit(appendItemMutation) 44 | export const dispatchDelayedAppend = moduleBuilder.dispatch(delayedAppendAction) 45 | } 46 | export default basket 47 | 48 | /// in the main app file 49 | const storeBuilder = getStoreBuilder() 50 | new Vue({ 51 | el: '#app', 52 | template: "....", 53 | store: storeBuilder.vuexStore() 54 | }) 55 | ``` 56 | 57 | - A [more complete example as a gist](https://gist.github.com/mrcrowl/d7fd8d0369759a9fe315dbf27dc1bced) 58 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | import { Module, Plugin, Store, StoreOptions } from "vuex"; 2 | export declare type MutationHandler = (state: S, payload: P) => void; 3 | export declare type ActionHandler = (context: BareActionContext, payload: P) => Promise | T; 4 | export declare type GetterHandler = (state: S, getters: G, rootState: R) => T; 5 | declare type Promisify = T extends PromiseLike ? T : Promise; 6 | export interface BareActionContext { 7 | state: S; 8 | rootState: R; 9 | getters: G; 10 | } 11 | export interface ModuleBuilder { 12 | /** The namespace of this ModuleBuilder */ 13 | readonly namespace: string; 14 | /** Creates a strongly-typed nested module within this module */ 15 | module(namespace: string, initialState: M): ModuleBuilder; 16 | /** Gets an existing nested module within this module */ 17 | module(namespace: string): ModuleBuilder; 18 | /** Set the initial state for an existing module */ 19 | setInitialState(initialState: S): void; 20 | /** Creates a strongly-typed commit function for the provided mutation handler */ 21 | commit

(handler: MutationHandler): () => void; 22 | commit

(handler: MutationHandler): (payload: P) => void; 23 | commit

(handler: MutationHandler, name: string): () => void; 24 | commit

(handler: MutationHandler, name: string): (payload: P) => void; 25 | /** Creates a strongly-typed dispatch function for the provided action handler */ 26 | dispatch(handler: ActionHandler): () => Promise; 27 | dispatch(handler: ActionHandler): (payload: P) => Promise; 28 | dispatch(handler: ActionHandler): () => Promisify; 29 | dispatch(handler: ActionHandler): (payload: P) => Promisify; 30 | dispatch(handler: ActionHandler, name: string): () => Promise; 31 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promise; 32 | dispatch(handler: ActionHandler, name: string): () => Promisify; 33 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify; 34 | /** Creates a strongly-typed read function for the provided getter function */ 35 | read(handler: GetterHandler): () => T; 36 | read(handler: GetterHandler, name: string): () => T; 37 | /** Creates a method to return this module's state */ 38 | state(): () => S; 39 | /** Output a Vuex Module definition. Called after all strongly-typed functions have been obtained */ 40 | vuexModule(): Module; 41 | _provideStore(store: Store): void; 42 | } 43 | export interface VuexStoreOptions { 44 | plugins?: Plugin[]; 45 | } 46 | export interface StoreBuilder { 47 | /** Creates a ModuleBuilder for the namespace provided */ 48 | module(namespace: string, state: S): ModuleBuilder; 49 | /** Gets an existing ModuleBuilder for the namespace provided */ 50 | module(namespace: string): ModuleBuilder; 51 | /** Output a Vuex Store after all modules have been built */ 52 | vuexStore(): Store; 53 | /** Output a Vuex Store and provide options, e.g. plugins -- these take precedence over any auto-generated options */ 54 | vuexStore(overrideOptions: StoreOptions): Store; 55 | /** Creates a strongly-typed commit function for the provided mutation handler */ 56 | commit

(handler: MutationHandler): () => void; 57 | commit

(handler: MutationHandler): (payload: P) => void; 58 | commit

(handler: MutationHandler, name: string): () => void; 59 | commit

(handler: MutationHandler, name: string): (payload: P) => void; 60 | /** Creates a strongly-typed dispatch function for the provided action handler */ 61 | dispatch(handler: ActionHandler): () => Promise; 62 | dispatch(handler: ActionHandler): (payload: P) => Promise; 63 | dispatch(handler: ActionHandler): () => Promisify; 64 | dispatch(handler: ActionHandler): (payload: P) => Promisify; 65 | dispatch(handler: ActionHandler, name: string): () => Promise; 66 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promise; 67 | dispatch(handler: ActionHandler, name: string): () => Promisify; 68 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify; 69 | /** Creates a strongly-typed read function for the provided getter function */ 70 | read(handler: GetterHandler): () => T; 71 | read(handler: GetterHandler, name: string): () => T; 72 | /** Creates a method to return the root state */ 73 | state(): () => R; 74 | /** Dynamically register module */ 75 | registerModule(namespace: string): void; 76 | /** WARNING: Discards vuex store and reset modules (non intended for end-user use) */ 77 | reset(): void; 78 | } 79 | /** Get a reference to the default store builder */ 80 | export declare function getStoreBuilder(): StoreBuilder; 81 | /** Get a reference to a named store builder */ 82 | export declare function getStoreBuilder(name: string): StoreBuilder; 83 | export {}; 84 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { value: true }); 4 | 5 | var vuex = require('vuex'); 6 | 7 | /*! ***************************************************************************** 8 | Copyright (c) Microsoft Corporation. 9 | 10 | Permission to use, copy, modify, and/or distribute this software for any 11 | purpose with or without fee is hereby granted. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 14 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 15 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 17 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 18 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | PERFORMANCE OF THIS SOFTWARE. 20 | ***************************************************************************** */ 21 | /* global Reflect, Promise */ 22 | 23 | var extendStatics = function(d, b) { 24 | extendStatics = Object.setPrototypeOf || 25 | ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || 26 | function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; 27 | return extendStatics(d, b); 28 | }; 29 | 30 | function __extends(d, b) { 31 | extendStatics(d, b); 32 | function __() { this.constructor = d; } 33 | d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); 34 | } 35 | 36 | var __assign = function() { 37 | __assign = Object.assign || function __assign(t) { 38 | for (var s, i = 1, n = arguments.length; i < n; i++) { 39 | s = arguments[i]; 40 | for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; 41 | } 42 | return t; 43 | }; 44 | return __assign.apply(this, arguments); 45 | }; 46 | 47 | var useRootNamespace = { root: true }; 48 | var ModuleBuilderImpl = /** @class */ (function () { 49 | function ModuleBuilderImpl(namespace, _initialState) { 50 | this.namespace = namespace; 51 | this._initialState = _initialState; 52 | this._getters = {}; 53 | this._mutations = {}; 54 | this._actions = {}; 55 | this._moduleBuilders = {}; 56 | } 57 | ModuleBuilderImpl.prototype.state = function () { 58 | var _this = this; 59 | if (!this.namespace) { 60 | return function () { return _this._store.state; }; 61 | } 62 | else if (this.namespace.indexOf("/") < 0) { 63 | return function () { return _this._store.state[_this.namespace]; }; 64 | } 65 | else { 66 | var namespaces_1 = this.namespace.split("/"); 67 | return function () { 68 | var accessor = _this._store.state; 69 | for (var _i = 0, namespaces_2 = namespaces_1; _i < namespaces_2.length; _i++) { 70 | var name_1 = namespaces_2[_i]; 71 | accessor = accessor[name_1]; 72 | } 73 | return accessor; 74 | }; 75 | } 76 | }; 77 | ModuleBuilderImpl.prototype.setInitialState = function (initialState) { 78 | this._initialState = initialState; 79 | }; 80 | ModuleBuilderImpl.prototype.module = function (namespace, initialState) { 81 | var existingModule = this._moduleBuilders[namespace]; 82 | var qualifiedNamespace = qualifyNamespace(this.namespace, namespace); 83 | if (!initialState && existingModule) { 84 | return existingModule; 85 | } 86 | // both arguments: create a module 87 | if (existingModule && initialState) { 88 | existingModule.setInitialState(initialState); 89 | return existingModule; 90 | } 91 | var nestedBuilder = new ModuleBuilderImpl(qualifiedNamespace, initialState || null); 92 | this._moduleBuilders[namespace] = nestedBuilder; 93 | return nestedBuilder; 94 | }; 95 | ModuleBuilderImpl.prototype.commit = function (handler, name) { 96 | var _this = this; 97 | var _a = qualifyKey(handler, this.namespace, name), key = _a.key, namespacedKey = _a.namespacedKey; 98 | if (this._mutations[key]) { 99 | throw new Error("There is already a mutation named " + key + "."); 100 | } 101 | this._mutations[key] = handler; 102 | return (function (payload) { return _this._store.commit(namespacedKey, payload, useRootNamespace); }); 103 | }; 104 | ModuleBuilderImpl.prototype.dispatch = function (handler, name) { 105 | var _this = this; 106 | var _a = qualifyKey(handler, this.namespace, name), key = _a.key, namespacedKey = _a.namespacedKey; 107 | if (this._actions[key]) { 108 | throw new Error("There is already an action named " + key + "."); 109 | } 110 | this._actions[key] = handler; 111 | return function (payload) { return _this._store.dispatch(namespacedKey, payload, useRootNamespace); }; 112 | }; 113 | ModuleBuilderImpl.prototype.read = function (handler, name) { 114 | var _this = this; 115 | var _a = qualifyKey(handler, this.namespace, name), key = _a.key, namespacedKey = _a.namespacedKey; 116 | if (this._getters[key]) { 117 | throw new Error("There is already a getter named " + key + "."); 118 | } 119 | this._getters[key] = handler; 120 | return function () { 121 | if (_this._store.rootGetters) { 122 | return _this._store.rootGetters[namespacedKey]; 123 | } 124 | return _this._store.getters[namespacedKey]; 125 | }; 126 | }; 127 | ModuleBuilderImpl.prototype.vuexModule = function () { 128 | if (!this._vuexModule) { 129 | // build nested modules recursively, if any 130 | var modules = {}; 131 | for (var _i = 0, _a = Object.keys(this._moduleBuilders); _i < _a.length; _i++) { 132 | var namespace = _a[_i]; 133 | modules[namespace] = this._moduleBuilders[namespace].vuexModule(); 134 | } 135 | this._vuexModule = { 136 | namespaced: true, 137 | state: this._initialState || {}, 138 | getters: this._getters, 139 | mutations: this._mutations, 140 | actions: this._actions, 141 | modules: modules 142 | }; 143 | } 144 | return this._vuexModule; 145 | }; 146 | ModuleBuilderImpl.prototype._provideStore = function (store) { 147 | this._store = store; 148 | forEachValue(this._moduleBuilders, function (m) { return m._provideStore(store); }); 149 | }; 150 | return ModuleBuilderImpl; 151 | }()); 152 | function qualifyKey(handler, namespace, name) { 153 | var key = name || handler.name; 154 | if (!key) { 155 | throw new Error("Vuex handler functions must not be anonymous. Possible causes: fat-arrow functions, uglify. To fix, pass a unique name as a second parameter after your callback."); 156 | } 157 | return { key: key, namespacedKey: qualifyNamespace(namespace, key) }; 158 | } 159 | function qualifyNamespace(namespace, key) { 160 | return namespace ? namespace + "/" + key : key; 161 | } 162 | var StoreBuilderImpl = /** @class */ (function (_super) { 163 | __extends(StoreBuilderImpl, _super); 164 | function StoreBuilderImpl() { 165 | return _super.call(this, "", {}) || this; 166 | } 167 | StoreBuilderImpl.prototype.module = function (namespace, initialState) { 168 | return _super.prototype.module.call(this, namespace, initialState); 169 | }; 170 | StoreBuilderImpl.prototype.vuexStore = function (overrideOptions) { 171 | if (overrideOptions === void 0) { overrideOptions = {}; } 172 | if (!this._store) { 173 | var options = __assign(__assign({}, this.vuexModule()), overrideOptions); 174 | var store_1 = new vuex.Store(options); 175 | forEachValue(this._moduleBuilders, function (m) { return m._provideStore(store_1); }); 176 | this._store = store_1; 177 | } 178 | return this._store; 179 | }; 180 | StoreBuilderImpl.prototype.registerModule = function (namespace) { 181 | if (this._store && this._vuexModule) { 182 | var mBuilder = this._moduleBuilders[namespace]; 183 | if (!mBuilder) 184 | throw 'fail to register module: ' + namespace; 185 | mBuilder._provideStore(this._store); 186 | var vModule = mBuilder.vuexModule(); 187 | this._store.registerModule(namespace, vModule); 188 | this._vuexModule.modules[namespace] = vModule; 189 | } 190 | else { 191 | throw 'vuexStore hasn\'t been called yet, use module() instead.'; 192 | } 193 | }; 194 | StoreBuilderImpl.prototype.reset = function () { 195 | this._store = undefined; 196 | this._moduleBuilders = {}; 197 | }; 198 | return StoreBuilderImpl; 199 | }(ModuleBuilderImpl)); 200 | var forEachValue = function (dict, loop) { 201 | Object.keys(dict).forEach(function (key) { return loop(dict[key]); }); 202 | }; 203 | var storeBuilderSingleton = new StoreBuilderImpl(); 204 | var namedStoreBuilderMap = Object.create(null); 205 | function getStoreBuilder(name) { 206 | // the default store builder 207 | if (!name) { 208 | return storeBuilderSingleton; 209 | } 210 | // a named store builder 211 | var builder = namedStoreBuilderMap[name] || (namedStoreBuilderMap[name] = new StoreBuilderImpl()); 212 | return builder; 213 | } 214 | 215 | exports.getStoreBuilder = getStoreBuilder; 216 | //# sourceMappingURL=index.js.map 217 | -------------------------------------------------------------------------------- /dist/index.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"index.js","sources":["../src/index.ts"],"sourcesContent":["import { ActionTree, GetterTree, Module, ModuleTree, MutationTree, Plugin, Store, StoreOptions } from \"vuex\";\n\nconst useRootNamespace = { root: true }\n\nexport type MutationHandler = (state: S, payload: P) => void\nexport type ActionHandler = (context: BareActionContext, payload: P) => Promise | T\nexport type GetterHandler = (state: S, getters: G, rootState: R) => T\ntype Promisify = T extends PromiseLike ? T : Promise;\n\n\ninterface Dictionary { [key: string]: T }\ninterface RootStore extends Store { rootGetters?: any }\n\nexport interface BareActionContext\n{\n state: S\n rootState: R\n getters: G\n}\n\nclass ModuleBuilderImpl implements ModuleBuilder {\n protected _store: RootStore | undefined\n\n protected _getters: GetterTree = {}\n protected _mutations: MutationTree = {}\n protected _actions: ActionTree = {}\n protected _moduleBuilders: Dictionary> = {}\n\n protected _vuexModule: Module | undefined\n\n constructor(public readonly namespace: string, private _initialState: S | null) { }\n\n state(): () => S\n {\n if (!this.namespace)\n {\n return () => this._store!.state as S\n }\n else if (this.namespace.indexOf(\"/\") < 0)\n {\n return () => (this._store!.state)[this.namespace] as S\n }\n else\n {\n const namespaces = this.namespace.split(\"/\")\n return () =>\n {\n let accessor: any = this._store!.state\n for (const name of namespaces)\n {\n accessor = accessor[name]\n }\n return (accessor) as S\n }\n }\n }\n\n setInitialState(initialState: S): void\n {\n this._initialState = initialState\n }\n\n module(namespace: string, initialState: M): ModuleBuilder\n module(namespace: string): ModuleBuilder\n module(namespace: string, initialState?: M): ModuleBuilder\n {\n const existingModule = this._moduleBuilders[namespace]\n const qualifiedNamespace = qualifyNamespace(this.namespace, namespace)\n if (!initialState && existingModule)\n {\n return existingModule\n }\n\n // both arguments: create a module\n if (existingModule && initialState)\n {\n existingModule.setInitialState(initialState)\n return existingModule\n }\n\n const nestedBuilder = new ModuleBuilderImpl(qualifiedNamespace, initialState || null)\n this._moduleBuilders[namespace] = nestedBuilder\n return nestedBuilder\n }\n\n commit

(handler: MutationHandler): () => void\n commit

(handler: MutationHandler): (payload: P) => void\n commit

(handler: MutationHandler, name: string): () => void\n commit

(handler: MutationHandler, name: string): (payload: P) => void\n commit

(handler: MutationHandler, name?: string)\n {\n const { key, namespacedKey } = qualifyKey(handler, this.namespace, name)\n if (this._mutations[key])\n {\n throw new Error(`There is already a mutation named ${key}.`)\n }\n this._mutations[key] = handler\n return ((payload: P) => this._store!.commit(namespacedKey, payload, useRootNamespace)) as any\n }\n\n dispatch(handler: ActionHandler): () => Promise\n dispatch(handler: ActionHandler): (payload: P) => Promise\n dispatch(handler: ActionHandler): () => Promisify\n dispatch(handler: ActionHandler): (payload: P) => Promisify\n dispatch(handler: ActionHandler, name: string): () => Promise\n dispatch(handler: ActionHandler, name: string): (payload: P) => Promise\n dispatch(handler: ActionHandler, name: string): () => Promisify\n dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify\n dispatch(handler: any, name?: string): any\n {\n const { key, namespacedKey } = qualifyKey(handler, this.namespace, name)\n if (this._actions[key])\n {\n throw new Error(`There is already an action named ${key}.`)\n }\n this._actions[key] = handler\n return (payload: P) => this._store!.dispatch(namespacedKey, payload, useRootNamespace)\n }\n\n read(handler: GetterHandler): () => T\n read(handler: GetterHandler, name: string): () => T\n read(handler: GetterHandler, name?: string): () => T\n {\n const { key, namespacedKey } = qualifyKey(handler, this.namespace, name)\n if (this._getters[key])\n {\n throw new Error(`There is already a getter named ${key}.`)\n }\n this._getters[key] = handler\n return () =>\n {\n if (this._store!.rootGetters)\n {\n return this._store!.rootGetters[namespacedKey] as T\n }\n return this._store!.getters[namespacedKey] as T\n }\n }\n\n vuexModule(): Module\n {\n if (!this._vuexModule)\n {\n // build nested modules recursively, if any\n const modules: ModuleTree = {}\n for (const namespace of Object.keys(this._moduleBuilders))\n {\n modules[namespace] = this._moduleBuilders[namespace].vuexModule()\n }\n\n this._vuexModule = {\n namespaced: true,\n state: this._initialState || {},\n getters: this._getters,\n mutations: this._mutations,\n actions: this._actions,\n modules\n }\n }\n return this._vuexModule\n }\n\n _provideStore(store: Store)\n {\n this._store = store\n\n forEachValue(this._moduleBuilders, m => m._provideStore(store))\n }\n}\n\nfunction qualifyKey(handler: Function, namespace: string | undefined, name?: string): { key: string, namespacedKey: string }\n{\n const key: string = name || handler.name\n if (!key)\n {\n throw new Error(`Vuex handler functions must not be anonymous. Possible causes: fat-arrow functions, uglify. To fix, pass a unique name as a second parameter after your callback.`)\n }\n return { key, namespacedKey: qualifyNamespace(namespace, key) }\n}\n\nfunction qualifyNamespace(namespace: string | undefined, key: string)\n{\n return namespace ? `${namespace}/${key}` : key\n}\n\nexport interface ModuleBuilder\n{\n /** The namespace of this ModuleBuilder */\n readonly namespace: string\n\n /** Creates a strongly-typed nested module within this module */\n module(namespace: string, initialState: M): ModuleBuilder\n\n /** Gets an existing nested module within this module */\n module(namespace: string): ModuleBuilder\n\n /** Set the initial state for an existing module */\n setInitialState(initialState: S): void\n\n /** Creates a strongly-typed commit function for the provided mutation handler */\n commit

(handler: MutationHandler): () => void\n commit

(handler: MutationHandler): (payload: P) => void\n commit

(handler: MutationHandler, name: string): () => void\n commit

(handler: MutationHandler, name: string): (payload: P) => void\n\n /** Creates a strongly-typed dispatch function for the provided action handler */\n dispatch(handler: ActionHandler): () => Promise\n dispatch(handler: ActionHandler): (payload: P) => Promise\n dispatch(handler: ActionHandler): () => Promisify\n dispatch(handler: ActionHandler): (payload: P) => Promisify\n dispatch(handler: ActionHandler, name: string): () => Promise\n dispatch(handler: ActionHandler, name: string): (payload: P) => Promise\n dispatch(handler: ActionHandler, name: string): () => Promisify\n dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify\n\n /** Creates a strongly-typed read function for the provided getter function */\n read(handler: GetterHandler): () => T\n read(handler: GetterHandler, name: string): () => T\n\n /** Creates a method to return this module's state */\n state(): () => S\n\n /** Output a Vuex Module definition. Called after all strongly-typed functions have been obtained */\n vuexModule(): Module\n\n _provideStore(store: Store): void\n}\n\nclass StoreBuilderImpl extends ModuleBuilderImpl {\n constructor()\n {\n super(\"\", {})\n }\n\n module(namespace: string, initialState: S): ModuleBuilder\n module(namespace: string): ModuleBuilder\n module(namespace: string, initialState?: S): ModuleBuilder\n {\n return super.module(namespace, initialState) as ModuleBuilder\n }\n\n vuexStore(): Store\n vuexStore(overrideOptions: StoreOptions): Store\n vuexStore(overrideOptions: StoreOptions = {}): Store\n {\n if (!this._store)\n {\n const options: StoreOptions & { namespaced?: boolean } = {\n ...this.vuexModule(),\n ...overrideOptions\n }\n const store = new Store(options)\n forEachValue(this._moduleBuilders, m => m._provideStore(store))\n this._store = store\n }\n return this._store\n }\n\n registerModule(namespace: string): void\n {\n if (this._store && this._vuexModule) {\n const mBuilder = this._moduleBuilders[namespace]\n if (!mBuilder) throw 'fail to register module: ' + namespace\n mBuilder._provideStore(this._store)\n\n const vModule = mBuilder.vuexModule()\n this._store.registerModule(namespace, vModule)\n\n this._vuexModule.modules![namespace] = vModule\n } else {\n throw 'vuexStore hasn\\'t been called yet, use module() instead.'\n }\n }\n\n reset()\n {\n this._store = undefined\n this._moduleBuilders = {}\n }\n}\n\nconst forEachValue = (dict: Dictionary, loop: (value: T) => any) =>\n{\n Object.keys(dict).forEach(key => loop(dict[key]))\n}\n\nexport interface VuexStoreOptions\n{\n plugins?: Plugin[]\n}\n\nexport interface StoreBuilder\n{\n /** Creates a ModuleBuilder for the namespace provided */\n module(namespace: string, state: S): ModuleBuilder\n\n /** Gets an existing ModuleBuilder for the namespace provided */\n module(namespace: string): ModuleBuilder\n\n /** Output a Vuex Store after all modules have been built */\n vuexStore(): Store\n\n /** Output a Vuex Store and provide options, e.g. plugins -- these take precedence over any auto-generated options */\n vuexStore(overrideOptions: StoreOptions): Store\n\n /** Creates a strongly-typed commit function for the provided mutation handler */\n commit

(handler: MutationHandler): () => void\n commit

(handler: MutationHandler): (payload: P) => void\n commit

(handler: MutationHandler, name: string): () => void\n commit

(handler: MutationHandler, name: string): (payload: P) => void\n\n /** Creates a strongly-typed dispatch function for the provided action handler */\n dispatch(handler: ActionHandler): () => Promise\n dispatch(handler: ActionHandler): (payload: P) => Promise\n dispatch(handler: ActionHandler): () => Promisify\n dispatch(handler: ActionHandler): (payload: P) => Promisify\n dispatch(handler: ActionHandler, name: string): () => Promise\n dispatch(handler: ActionHandler, name: string): (payload: P) => Promise\n dispatch(handler: ActionHandler, name: string): () => Promisify\n dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify\n\n /** Creates a strongly-typed read function for the provided getter function */\n read(handler: GetterHandler): () => T\n read(handler: GetterHandler, name: string): () => T\n\n /** Creates a method to return the root state */\n state(): () => R\n\n /** Dynamically register module */\n registerModule(namespace: string): void\n\n /** WARNING: Discards vuex store and reset modules (non intended for end-user use) */\n reset(): void\n}\n\nconst storeBuilderSingleton = new StoreBuilderImpl()\nconst namedStoreBuilderMap: { [name: string]: StoreBuilderImpl } = Object.create(null)\n\n/** Get a reference to the default store builder */\nexport function getStoreBuilder(): StoreBuilder\n/** Get a reference to a named store builder */\nexport function getStoreBuilder(name: string): StoreBuilder\nexport function getStoreBuilder(name?: string): StoreBuilder\n{\n // the default store builder\n if (!name)\n {\n return storeBuilderSingleton\n }\n\n // a named store builder\n const builder = namedStoreBuilderMap[name] || (namedStoreBuilderMap[name] = new StoreBuilderImpl())\n return builder\n}\n"],"names":["Store"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,IAAM,gBAAgB,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,CAAA;AAkBvC;IAUI,2BAA4B,SAAiB,EAAU,aAAuB;QAAlD,cAAS,GAAT,SAAS,CAAQ;QAAU,kBAAa,GAAb,aAAa,CAAU;QAPpE,aAAQ,GAAqB,EAAE,CAAA;QAC/B,eAAU,GAAoB,EAAE,CAAA;QAChC,aAAQ,GAAqB,EAAE,CAAA;QAC/B,oBAAe,GAAsC,EAAE,CAAA;KAIkB;IAEnF,iCAAK,GAAL;QAAA,iBAuBC;QArBG,IAAI,CAAC,IAAI,CAAC,SAAS,EACnB;YACI,OAAO,cAAM,OAAK,KAAI,CAAC,MAAO,CAAC,KAAU,GAAA,CAAA;SAC5C;aACI,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,EACxC;YACI,OAAO,cAAM,OAAM,KAAI,CAAC,MAAO,CAAC,KAAM,CAAC,KAAI,CAAC,SAAS,CAAM,GAAA,CAAA;SAC9D;aAED;YACI,IAAM,YAAU,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC5C,OAAO;gBAEH,IAAI,QAAQ,GAAQ,KAAI,CAAC,MAAO,CAAC,KAAK,CAAA;gBACtC,KAAmB,UAAU,EAAV,eAAA,YAAU,EAAV,wBAAU,EAAV,IAAU,EAC7B;oBADK,IAAM,MAAI,mBAAA;oBAEX,QAAQ,GAAG,QAAQ,CAAC,MAAI,CAAC,CAAA;iBAC5B;gBACD,OAAa,QAAc,CAAA;aAC9B,CAAA;SACJ;KACJ;IAED,2CAAe,GAAf,UAAgB,YAAe;QAE3B,IAAI,CAAC,aAAa,GAAG,YAAY,CAAA;KACpC;IAID,kCAAM,GAAN,UAAU,SAAiB,EAAE,YAAgB;QAEzC,IAAM,cAAc,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;QACtD,IAAM,kBAAkB,GAAG,gBAAgB,CAAC,IAAI,CAAC,SAAS,EAAE,SAAS,CAAC,CAAA;QACtE,IAAI,CAAC,YAAY,IAAI,cAAc,EACnC;YACI,OAAO,cAAc,CAAA;SACxB;;QAGD,IAAI,cAAc,IAAI,YAAY,EAClC;YACI,cAAc,CAAC,eAAe,CAAC,YAAY,CAAC,CAAA;YAC5C,OAAO,cAAc,CAAA;SACxB;QAED,IAAM,aAAa,GAAG,IAAI,iBAAiB,CAAO,kBAAkB,EAAE,YAAY,IAAI,IAAI,CAAC,CAAA;QAC3F,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,GAAG,aAAa,CAAA;QAC/C,OAAO,aAAa,CAAA;KACvB;IAMD,kCAAM,GAAN,UAAU,OAA8B,EAAE,IAAa;QAAvD,iBASC;QAPS,IAAA,KAAyB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAhE,GAAG,SAAA,EAAE,aAAa,mBAA8C,CAAA;QACxE,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EACxB;YACI,MAAM,IAAI,KAAK,CAAC,uCAAqC,GAAG,MAAG,CAAC,CAAA;SAC/D;QACD,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,GAAG,OAAO,CAAA;QAC9B,QAAQ,UAAC,OAAU,IAAK,OAAA,KAAI,CAAC,MAAO,CAAC,MAAM,CAAC,aAAa,EAAE,OAAO,EAAE,gBAAgB,CAAC,GAAA,EAAQ;KAChG;IAUD,oCAAQ,GAAR,UAAe,OAAY,EAAE,IAAa;QAA1C,iBASC;QAPS,IAAA,KAAyB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAhE,GAAG,SAAA,EAAE,aAAa,mBAA8C,CAAA;QACxE,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EACtB;YACI,MAAM,IAAI,KAAK,CAAC,sCAAoC,GAAG,MAAG,CAAC,CAAA;SAC9D;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO,CAAA;QAC5B,OAAO,UAAC,OAAU,IAAK,OAAA,KAAI,CAAC,MAAO,CAAC,QAAQ,CAAC,aAAa,EAAE,OAAO,EAAE,gBAAgB,CAAC,GAAA,CAAA;KACzF;IAID,gCAAI,GAAJ,UAAQ,OAAkC,EAAE,IAAa;QAAzD,iBAgBC;QAdS,IAAA,KAAyB,UAAU,CAAC,OAAO,EAAE,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,EAAhE,GAAG,SAAA,EAAE,aAAa,mBAA8C,CAAA;QACxE,IAAI,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,EACtB;YACI,MAAM,IAAI,KAAK,CAAC,qCAAmC,GAAG,MAAG,CAAC,CAAA;SAC7D;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,OAAO,CAAA;QAC5B,OAAO;YAEH,IAAI,KAAI,CAAC,MAAO,CAAC,WAAW,EAC5B;gBACI,OAAO,KAAI,CAAC,MAAO,CAAC,WAAW,CAAC,aAAa,CAAM,CAAA;aACtD;YACD,OAAO,KAAI,CAAC,MAAO,CAAC,OAAO,CAAC,aAAa,CAAM,CAAA;SAClD,CAAA;KACJ;IAED,sCAAU,GAAV;QAEI,IAAI,CAAC,IAAI,CAAC,WAAW,EACrB;;YAEI,IAAM,OAAO,GAAkB,EAAE,CAAA;YACjC,KAAwB,UAAiC,EAAjC,KAAA,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,EAAjC,cAAiC,EAAjC,IAAiC,EACzD;gBADK,IAAM,SAAS,SAAA;gBAEhB,OAAO,CAAC,SAAS,CAAC,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,UAAU,EAAE,CAAA;aACpE;YAED,IAAI,CAAC,WAAW,GAAG;gBACf,UAAU,EAAE,IAAI;gBAChB,KAAK,EAAE,IAAI,CAAC,aAAa,IAAO,EAAE;gBAClC,OAAO,EAAE,IAAI,CAAC,QAAQ;gBACtB,SAAS,EAAE,IAAI,CAAC,UAAU;gBAC1B,OAAO,EAAE,IAAI,CAAC,QAAQ;gBACtB,OAAO,SAAA;aACV,CAAA;SACJ;QACD,OAAO,IAAI,CAAC,WAAW,CAAA;KAC1B;IAED,yCAAa,GAAb,UAAc,KAAe;QAEzB,IAAI,CAAC,MAAM,GAAG,KAAK,CAAA;QAEnB,YAAY,CAAC,IAAI,CAAC,eAAe,EAAE,UAAA,CAAC,IAAI,OAAA,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,GAAA,CAAC,CAAA;KAClE;IACL,wBAAC;AAAD,CAAC,IAAA;AAED,SAAS,UAAU,CAAC,OAAiB,EAAE,SAA6B,EAAE,IAAa;IAE/E,IAAM,GAAG,GAAW,IAAI,IAAI,OAAO,CAAC,IAAI,CAAA;IACxC,IAAI,CAAC,GAAG,EACR;QACI,MAAM,IAAI,KAAK,CAAC,oKAAoK,CAAC,CAAA;KACxL;IACD,OAAO,EAAE,GAAG,KAAA,EAAE,aAAa,EAAE,gBAAgB,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAA;AACnE,CAAC;AAED,SAAS,gBAAgB,CAAC,SAA6B,EAAE,GAAW;IAEhE,OAAO,SAAS,GAAM,SAAS,SAAI,GAAK,GAAG,GAAG,CAAA;AAClD,CAAC;AA6CD;IAAkC,oCAAyB;IACvD;eAEI,kBAAM,EAAE,EAAE,EAAE,CAAC;KAChB;IAID,iCAAM,GAAN,UAAU,SAAiB,EAAE,YAAgB;QAEzC,OAAO,iBAAM,MAAM,YAAC,SAAS,EAAE,YAAY,CAAwB,CAAA;KACtE;IAID,oCAAS,GAAT,UAAU,eAAqC;QAArC,gCAAA,EAAA,oBAAqC;QAE3C,IAAI,CAAC,IAAI,CAAC,MAAM,EAChB;YACI,IAAM,OAAO,yBACN,IAAI,CAAC,UAAU,EAAE,GACjB,eAAe,CACrB,CAAA;YACD,IAAM,OAAK,GAAG,IAAIA,UAAK,CAAI,OAAO,CAAC,CAAA;YACnC,YAAY,CAAC,IAAI,CAAC,eAAe,EAAE,UAAA,CAAC,IAAI,OAAA,CAAC,CAAC,aAAa,CAAC,OAAK,CAAC,GAAA,CAAC,CAAA;YAC/D,IAAI,CAAC,MAAM,GAAG,OAAK,CAAA;SACtB;QACD,OAAO,IAAI,CAAC,MAAM,CAAA;KACrB;IAED,yCAAc,GAAd,UAAe,SAAiB;QAE5B,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,WAAW,EAAE;YACjC,IAAM,QAAQ,GAAG,IAAI,CAAC,eAAe,CAAC,SAAS,CAAC,CAAA;YAChD,IAAI,CAAC,QAAQ;gBAAE,MAAM,2BAA2B,GAAG,SAAS,CAAA;YAC5D,QAAQ,CAAC,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;YAEnC,IAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,EAAE,CAAA;YACrC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,SAAS,EAAE,OAAO,CAAC,CAAA;YAE9C,IAAI,CAAC,WAAW,CAAC,OAAQ,CAAC,SAAS,CAAC,GAAG,OAAO,CAAA;SACjD;aAAM;YACH,MAAM,0DAA0D,CAAA;SACnE;KACJ;IAED,gCAAK,GAAL;QAEI,IAAI,CAAC,MAAM,GAAG,SAAS,CAAA;QACvB,IAAI,CAAC,eAAe,GAAG,EAAE,CAAA;KAC5B;IACL,uBAAC;AAAD,CAnDA,CAAkC,iBAAiB,GAmDlD;AAED,IAAM,YAAY,GAAG,UAAI,IAAmB,EAAE,IAAuB;IAEjE,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,UAAA,GAAG,IAAI,OAAA,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,GAAA,CAAC,CAAA;AACrD,CAAC,CAAA;AAmDD,IAAM,qBAAqB,GAAG,IAAI,gBAAgB,EAAO,CAAA;AACzD,IAAM,oBAAoB,GAA8C,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAAA;SAM3E,eAAe,CAAI,IAAa;;IAG5C,IAAI,CAAC,IAAI,EACT;QACI,OAAO,qBAAqB,CAAA;KAC/B;;IAGD,IAAM,OAAO,GAAG,oBAAoB,CAAC,IAAI,CAAC,KAAK,oBAAoB,CAAC,IAAI,CAAC,GAAG,IAAI,gBAAgB,EAAK,CAAC,CAAA;IACtG,OAAO,OAAO,CAAA;AAClB;;;;"} -------------------------------------------------------------------------------- /examples/single-file.ts: -------------------------------------------------------------------------------- 1 | import { getStoreBuilder } from "../src/index" 2 | import Vuex, { Store, ActionContext } from "vuex" 3 | import Vue from "vue" 4 | const delay = (duration: number) => new Promise((c, e) => setTimeout(c, duration)) 5 | 6 | Vue.use(Vuex) 7 | 8 | export interface RootState { basket: BasketState } 9 | export interface BasketState { items: Item[] } 10 | export interface Item { id: string, name: string } 11 | 12 | const storeBuilder = getStoreBuilder() 13 | const moduleBuilder = storeBuilder.module("basket", { items: [] }) 14 | 15 | namespace basket 16 | { 17 | const appendItemMutation = (state: BasketState, payload: { item: Item }) => state.items.push(payload.item) 18 | const delayedAppendAction = async (context: ActionContext) => 19 | { 20 | await delay(1000) 21 | basket.commitAppendItem({ item: { id: "abc123", name: "ABC Item" } }) 22 | } 23 | 24 | export const commitAppendItem = moduleBuilder.commit(appendItemMutation) 25 | export const dispatchDelayedAppend = moduleBuilder.dispatch(delayedAppendAction) 26 | } 27 | export default basket -------------------------------------------------------------------------------- /jest-setup.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | 3 | Vue.config.devtools = false; 4 | Vue.config.productionTip = false; 5 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: "ts-jest", 3 | testRegex: "tests/.*\\.spec.ts$", 4 | moduleNameMapper: { 5 | }, 6 | globals: { 7 | "ts-jest": { 8 | diagnostics: false, 9 | }, 10 | }, 11 | setupFiles: ["/jest-setup.ts"], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-typex", 3 | "version": "3.1.9", 4 | "description": "A TypeScript pattern for strongly-typed access to Vuex Store modules", 5 | "files": [ 6 | "dist/index.d.ts", 7 | "dist/index.js", 8 | "dist/index.js.map" 9 | ], 10 | "main": "dist/index.js", 11 | "typings": "dist/index.d.ts", 12 | "peerDependencies": { 13 | "vuex": "^3.0.1" 14 | }, 15 | "dependencies": { 16 | "vuex": "^3.0.1" 17 | }, 18 | "devDependencies": { 19 | "@types/jest": "^26.0.20", 20 | "jest": "^26.6.3", 21 | "rollup": "^2.38.0", 22 | "rollup-plugin-cleaner": "^1.0.0", 23 | "rollup-plugin-typescript2": "^0.29.0", 24 | "ts-jest": "^26.4.4", 25 | "tslib": "^2.1.0", 26 | "typescript": "^4.1.3", 27 | "vue": "^2.5.22" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/mrcrowl/vuex-typex.git" 32 | }, 33 | "keywords": [ 34 | "vuex", 35 | "typescript", 36 | "builder" 37 | ], 38 | "author": "mrcrowl", 39 | "license": "MIT", 40 | "bugs": { 41 | "url": "https://github.com/mrcrowl/vuex-typex/issues" 42 | }, 43 | "homepage": "https://github.com/mrcrowl/vuex-typex#readme", 44 | "scripts": { 45 | "typecheck": "tsc --noEmit", 46 | "build": "rollup --config -m", 47 | "test:unit": "jest", 48 | "test": "npm run test:unit" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from 'rollup-plugin-typescript2'; 2 | import cleaner from 'rollup-plugin-cleaner'; 3 | 4 | export default { 5 | input: 'src/index.ts', 6 | output: { 7 | dir: 'dist', 8 | format: 'cjs', 9 | }, 10 | plugins: [ 11 | typescript({ 12 | tsconfig: "tsconfig-src.json" 13 | }), 14 | cleaner({ 15 | targets: [ 16 | './dist/' 17 | ] 18 | }) 19 | ], 20 | external: [ 'vuex' ] // <-- suppresses the warning 21 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { ActionTree, GetterTree, Module, ModuleTree, MutationTree, Plugin, Store, StoreOptions } from "vuex"; 2 | 3 | const useRootNamespace = { root: true } 4 | 5 | export type MutationHandler = (state: S, payload: P) => void 6 | export type ActionHandler = (context: BareActionContext, payload: P) => Promise | T 7 | export type GetterHandler = (state: S, getters: G, rootState: R, rootGetters: any) => T 8 | type Promisify = T extends PromiseLike ? T : Promise; 9 | 10 | 11 | interface Dictionary { [key: string]: T } 12 | interface RootStore extends Store { rootGetters?: any } 13 | 14 | export interface BareActionContext 15 | { 16 | state: S 17 | rootState: R 18 | getters: G 19 | } 20 | 21 | class ModuleBuilderImpl implements ModuleBuilder { 22 | protected _store: RootStore | undefined 23 | 24 | protected _getters: GetterTree = {} 25 | protected _mutations: MutationTree = {} 26 | protected _actions: ActionTree = {} 27 | protected _moduleBuilders: Dictionary> = {} 28 | 29 | protected _vuexModule: Module | undefined 30 | 31 | constructor(public readonly namespace: string, private _initialState: S | null) { } 32 | 33 | state(): () => S 34 | { 35 | if (!this.namespace) 36 | { 37 | return () => this._store!.state as S 38 | } 39 | else if (this.namespace.indexOf("/") < 0) 40 | { 41 | return () => (this._store!.state)[this.namespace] as S 42 | } 43 | else 44 | { 45 | const namespaces = this.namespace.split("/") 46 | return () => 47 | { 48 | let accessor: any = this._store!.state 49 | for (const name of namespaces) 50 | { 51 | accessor = accessor[name] 52 | } 53 | return (accessor) as S 54 | } 55 | } 56 | } 57 | 58 | setInitialState(initialState: S): void 59 | { 60 | this._initialState = initialState 61 | } 62 | 63 | module(namespace: string, initialState: M): ModuleBuilder 64 | module(namespace: string): ModuleBuilder 65 | module(namespace: string, initialState?: M): ModuleBuilder 66 | { 67 | const existingModule = this._moduleBuilders[namespace] 68 | const qualifiedNamespace = qualifyNamespace(this.namespace, namespace) 69 | if (!initialState && existingModule) 70 | { 71 | return existingModule 72 | } 73 | 74 | // both arguments: create a module 75 | if (existingModule && initialState) 76 | { 77 | existingModule.setInitialState(initialState) 78 | return existingModule 79 | } 80 | 81 | const nestedBuilder = new ModuleBuilderImpl(qualifiedNamespace, initialState || null) 82 | this._moduleBuilders[namespace] = nestedBuilder 83 | return nestedBuilder 84 | } 85 | 86 | commit

(handler: MutationHandler): () => void 87 | commit

(handler: MutationHandler): (payload: P) => void 88 | commit

(handler: MutationHandler, name: string): () => void 89 | commit

(handler: MutationHandler, name: string): (payload: P) => void 90 | commit

(handler: MutationHandler, name?: string) 91 | { 92 | const { key, namespacedKey } = qualifyKey(handler, this.namespace, name) 93 | if (this._mutations[key]) 94 | { 95 | throw new Error(`There is already a mutation named ${key}.`) 96 | } 97 | this._mutations[key] = handler 98 | return ((payload: P) => this._store!.commit(namespacedKey, payload, useRootNamespace)) as any 99 | } 100 | 101 | dispatch(handler: ActionHandler): () => Promise 102 | dispatch(handler: ActionHandler): (payload: P) => Promise 103 | dispatch(handler: ActionHandler): () => Promisify 104 | dispatch(handler: ActionHandler): (payload: P) => Promisify 105 | dispatch(handler: ActionHandler, name: string): () => Promise 106 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promise 107 | dispatch(handler: ActionHandler, name: string): () => Promisify 108 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify 109 | dispatch(handler: any, name?: string): any 110 | { 111 | const { key, namespacedKey } = qualifyKey(handler, this.namespace, name) 112 | if (this._actions[key]) 113 | { 114 | throw new Error(`There is already an action named ${key}.`) 115 | } 116 | this._actions[key] = handler 117 | return (payload: P) => this._store!.dispatch(namespacedKey, payload, useRootNamespace) 118 | } 119 | 120 | read(handler: GetterHandler): () => T 121 | read(handler: GetterHandler, name: string): () => T 122 | read(handler: GetterHandler, name?: string): () => T 123 | { 124 | const { key, namespacedKey } = qualifyKey(handler, this.namespace, name) 125 | if (this._getters[key]) 126 | { 127 | throw new Error(`There is already a getter named ${key}.`) 128 | } 129 | this._getters[key] = handler 130 | return () => 131 | { 132 | if (this._store!.rootGetters) 133 | { 134 | return this._store!.rootGetters[namespacedKey] as T 135 | } 136 | return this._store!.getters[namespacedKey] as T 137 | } 138 | } 139 | 140 | vuexModule(): Module 141 | { 142 | if (!this._vuexModule) 143 | { 144 | // build nested modules recursively, if any 145 | const modules: ModuleTree = {} 146 | for (const namespace of Object.keys(this._moduleBuilders)) 147 | { 148 | modules[namespace] = this._moduleBuilders[namespace].vuexModule() 149 | } 150 | 151 | this._vuexModule = { 152 | namespaced: true, 153 | state: this._initialState || {}, 154 | getters: this._getters, 155 | mutations: this._mutations, 156 | actions: this._actions, 157 | modules 158 | } 159 | } 160 | return this._vuexModule 161 | } 162 | 163 | _provideStore(store: Store) 164 | { 165 | this._store = store 166 | 167 | forEachValue(this._moduleBuilders, m => m._provideStore(store)) 168 | } 169 | } 170 | 171 | function qualifyKey(handler: Function, namespace: string | undefined, name?: string): { key: string, namespacedKey: string } 172 | { 173 | const key: string = name || handler.name 174 | if (!key) 175 | { 176 | throw new Error(`Vuex handler functions must not be anonymous. Possible causes: fat-arrow functions, uglify. To fix, pass a unique name as a second parameter after your callback.`) 177 | } 178 | return { key, namespacedKey: qualifyNamespace(namespace, key) } 179 | } 180 | 181 | function qualifyNamespace(namespace: string | undefined, key: string) 182 | { 183 | return namespace ? `${namespace}/${key}` : key 184 | } 185 | 186 | export interface ModuleBuilder 187 | { 188 | /** The namespace of this ModuleBuilder */ 189 | readonly namespace: string 190 | 191 | /** Creates a strongly-typed nested module within this module */ 192 | module(namespace: string, initialState: M): ModuleBuilder 193 | 194 | /** Gets an existing nested module within this module */ 195 | module(namespace: string): ModuleBuilder 196 | 197 | /** Set the initial state for an existing module */ 198 | setInitialState(initialState: S): void 199 | 200 | /** Creates a strongly-typed commit function for the provided mutation handler */ 201 | commit

(handler: MutationHandler): () => void 202 | commit

(handler: MutationHandler): (payload: P) => void 203 | commit

(handler: MutationHandler, name: string): () => void 204 | commit

(handler: MutationHandler, name: string): (payload: P) => void 205 | 206 | /** Creates a strongly-typed dispatch function for the provided action handler */ 207 | dispatch(handler: ActionHandler): () => Promise 208 | dispatch(handler: ActionHandler): (payload: P) => Promise 209 | dispatch(handler: ActionHandler): () => Promisify 210 | dispatch(handler: ActionHandler): (payload: P) => Promisify 211 | dispatch(handler: ActionHandler, name: string): () => Promise 212 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promise 213 | dispatch(handler: ActionHandler, name: string): () => Promisify 214 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify 215 | 216 | /** Creates a strongly-typed read function for the provided getter function */ 217 | read(handler: GetterHandler): () => T 218 | read(handler: GetterHandler, name: string): () => T 219 | 220 | /** Creates a method to return this module's state */ 221 | state(): () => S 222 | 223 | /** Output a Vuex Module definition. Called after all strongly-typed functions have been obtained */ 224 | vuexModule(): Module 225 | 226 | _provideStore(store: Store): void 227 | } 228 | 229 | class StoreBuilderImpl extends ModuleBuilderImpl { 230 | constructor() 231 | { 232 | super("", {}) 233 | } 234 | 235 | module(namespace: string, initialState: S): ModuleBuilder 236 | module(namespace: string): ModuleBuilder 237 | module(namespace: string, initialState?: S): ModuleBuilder 238 | { 239 | return super.module(namespace, initialState) as ModuleBuilder 240 | } 241 | 242 | vuexStore(): Store 243 | vuexStore(overrideOptions: StoreOptions): Store 244 | vuexStore(overrideOptions: StoreOptions = {}): Store 245 | { 246 | if (!this._store) 247 | { 248 | const options: StoreOptions & { namespaced?: boolean } = { 249 | ...this.vuexModule(), 250 | ...overrideOptions 251 | } 252 | const store = new Store(options) 253 | forEachValue(this._moduleBuilders, m => m._provideStore(store)) 254 | this._store = store 255 | } 256 | return this._store 257 | } 258 | 259 | registerModule(namespace: string): void 260 | { 261 | if (this._store && this._vuexModule) { 262 | const mBuilder = this._moduleBuilders[namespace] 263 | if (!mBuilder) throw 'fail to register module: ' + namespace 264 | mBuilder._provideStore(this._store) 265 | 266 | const vModule = mBuilder.vuexModule() 267 | this._store.registerModule(namespace, vModule) 268 | 269 | this._vuexModule.modules![namespace] = vModule 270 | } else { 271 | throw 'vuexStore hasn\'t been called yet, use module() instead.' 272 | } 273 | } 274 | 275 | reset() 276 | { 277 | this._store = undefined 278 | this._moduleBuilders = {} 279 | } 280 | } 281 | 282 | const forEachValue = (dict: Dictionary, loop: (value: T) => any) => 283 | { 284 | Object.keys(dict).forEach(key => loop(dict[key])) 285 | } 286 | 287 | export interface VuexStoreOptions 288 | { 289 | plugins?: Plugin[] 290 | } 291 | 292 | export interface StoreBuilder 293 | { 294 | /** Creates a ModuleBuilder for the namespace provided */ 295 | module(namespace: string, state: S): ModuleBuilder 296 | 297 | /** Gets an existing ModuleBuilder for the namespace provided */ 298 | module(namespace: string): ModuleBuilder 299 | 300 | /** Output a Vuex Store after all modules have been built */ 301 | vuexStore(): Store 302 | 303 | /** Output a Vuex Store and provide options, e.g. plugins -- these take precedence over any auto-generated options */ 304 | vuexStore(overrideOptions: StoreOptions): Store 305 | 306 | /** Creates a strongly-typed commit function for the provided mutation handler */ 307 | commit

(handler: MutationHandler): () => void 308 | commit

(handler: MutationHandler): (payload: P) => void 309 | commit

(handler: MutationHandler, name: string): () => void 310 | commit

(handler: MutationHandler, name: string): (payload: P) => void 311 | 312 | /** Creates a strongly-typed dispatch function for the provided action handler */ 313 | dispatch(handler: ActionHandler): () => Promise 314 | dispatch(handler: ActionHandler): (payload: P) => Promise 315 | dispatch(handler: ActionHandler): () => Promisify 316 | dispatch(handler: ActionHandler): (payload: P) => Promisify 317 | dispatch(handler: ActionHandler, name: string): () => Promise 318 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promise 319 | dispatch(handler: ActionHandler, name: string): () => Promisify 320 | dispatch(handler: ActionHandler, name: string): (payload: P) => Promisify 321 | 322 | /** Creates a strongly-typed read function for the provided getter function */ 323 | read(handler: GetterHandler): () => T 324 | read(handler: GetterHandler, name: string): () => T 325 | 326 | /** Creates a method to return the root state */ 327 | state(): () => R 328 | 329 | /** Dynamically register module */ 330 | registerModule(namespace: string): void 331 | 332 | /** WARNING: Discards vuex store and reset modules (non intended for end-user use) */ 333 | reset(): void 334 | } 335 | 336 | const storeBuilderSingleton = new StoreBuilderImpl() 337 | const namedStoreBuilderMap: { [name: string]: StoreBuilderImpl } = Object.create(null) 338 | 339 | /** Get a reference to the default store builder */ 340 | export function getStoreBuilder(): StoreBuilder 341 | /** Get a reference to a named store builder */ 342 | export function getStoreBuilder(name: string): StoreBuilder 343 | export function getStoreBuilder(name?: string): StoreBuilder 344 | { 345 | // the default store builder 346 | if (!name) 347 | { 348 | return storeBuilderSingleton 349 | } 350 | 351 | // a named store builder 352 | const builder = namedStoreBuilderMap[name] || (namedStoreBuilderMap[name] = new StoreBuilderImpl()) 353 | return builder 354 | } 355 | -------------------------------------------------------------------------------- /tests/anon-handler.spec.ts: -------------------------------------------------------------------------------- 1 | import { getStoreBuilder, StoreBuilder } from "../src/index" 2 | import { ModuleBuilder } from "../src/index" 3 | 4 | interface AnonState { age: number } 5 | 6 | describe("Create an anon store", () => 7 | { 8 | let moduleBuilder: ModuleBuilder 9 | 10 | beforeEach(() => 11 | { 12 | const anonStore: StoreBuilder<{}> = getStoreBuilder("anon") 13 | anonStore.reset() 14 | moduleBuilder = anonStore.module("anon", { age: 36 }) 15 | }) 16 | 17 | describe("try to create a getter with anon function", () => 18 | { 19 | it("should fail", () => 20 | { 21 | 22 | expect(() => 23 | { 24 | const readApproxDaysAlive = moduleBuilder.read((state: AnonState) => Math.round(state.age * 365.25)) 25 | }).toThrow() 26 | }) 27 | }) 28 | 29 | describe("try to create a getter with explicit name", () => 30 | { 31 | it("should succeed", () => 32 | { 33 | expect(() => 34 | { 35 | const readApproxDaysAlive = moduleBuilder.read((state: AnonState) => Math.round(state.age * 365.25), "daysAlive") 36 | }).not.toThrow() 37 | }) 38 | }) 39 | 40 | const daysAliveGetter = (state: AnonState) => Math.round(state.age * 365.25) // <-- named function 41 | describe("try to create a getter with named function", () => 42 | { 43 | it("should succeed", () => 44 | { 45 | expect(() => 46 | { 47 | const readApproxDaysAlive = moduleBuilder.read(daysAliveGetter) 48 | }).not.toThrow() 49 | }) 50 | }) 51 | }) -------------------------------------------------------------------------------- /tests/complete.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import Vuex, { Store } from "vuex"; 3 | import birthday from "./store/birthday/birthday"; 4 | import { buildStore, RootState } from "./store/index"; 5 | 6 | describe("Run an action", () => 7 | { 8 | let store: Store 9 | 10 | beforeEach(() => 11 | { 12 | Vue.use(Vuex) 13 | store = buildStore() 14 | store.replaceState({ 15 | birthday: { 16 | birthdays: [ 17 | { name: "Richard", dob: new Date(1995, 10, 11) }, 18 | { name: "Erlich", dob: new Date(1983, 1, 17) }, 19 | { name: "Nelson", dob: new Date(1996, 3, 28) }, 20 | { name: "Dinesh", dob: new Date(1989, 1, 7) }, 21 | { name: "Bertram", dob: new Date(1985, 7, 14) }, 22 | { name: "Donald", dob: new Date(1994, 5, 31) }, 23 | { name: "Monica", dob: new Date(1996, 8, 26) }, 24 | ] 25 | }, 26 | auth: { isLoggedIn: false, userID: "" } 27 | }) 28 | }) 29 | 30 | describe("that removes first 2 birthdays with delays", () => 31 | { 32 | it("should show Bertram after removing first two birthdays", async () => 33 | { 34 | expect(birthday.oldestName).toEqual("Erlich") 35 | await birthday.dispatchRemoveFirstAfterDelay(5) 36 | await birthday.dispatchRemoveFirstAfterDelay(5) 37 | expect(birthday.oldestName).toEqual("Bertram") 38 | }) 39 | 40 | it("DOB for Betram should be defined", async () => 41 | { 42 | expect(birthday.dateOfBirthFor("Bertram")).toBeDefined 43 | }) 44 | 45 | it("DOB for Betram should be 14-Aug-1985", async () => 46 | { 47 | expect(birthday.dateOfBirthFor("Bertram")!.getTime()).toEqual(new Date(1985, 7, 14).getTime()) 48 | }) 49 | 50 | it("DOB for Joe Bloggs should be undefined", async () => 51 | { 52 | expect(birthday.dateOfBirthFor("Joe Bloggs")).toBeUndefined 53 | }) 54 | 55 | it("oldestName should be undefined when no birthdays", async () => 56 | { 57 | birthday.commitClearBirthdays() 58 | expect(birthday.oldestName).toBeUndefined 59 | }) 60 | 61 | it("oldestName should be Nancy when birthday added from empty", async () => 62 | { 63 | birthday.commitClearBirthdays() 64 | birthday.commitAddBirthday({ birthday: { dob: new Date(2017, 5, 15), name: "Nancy" } }) 65 | expect(birthday.oldestName).toEqual("Nancy") 66 | }) 67 | }) 68 | }) -------------------------------------------------------------------------------- /tests/dynamic-register-module.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue" 2 | import Vuex, { Store } from "vuex" 3 | import { buildStore } from "./store" 4 | import { RootState } from "./store/index" 5 | import { getStoreBuilder } from "../src/index" 6 | 7 | describe("Output the store", () => 8 | { 9 | let store: Store 10 | 11 | beforeEach(() => 12 | { 13 | Vue.use(Vuex) 14 | }) 15 | 16 | describe("register module before calling vuexStore()", () => 17 | { 18 | it("should fail", async () => 19 | { 20 | expect(() => { 21 | getStoreBuilder().registerModule('aModule') 22 | }).toThrow() 23 | }) 24 | }) 25 | 26 | describe("call an unregistered module", () => 27 | { 28 | it("should only work after registered", async () => 29 | { 30 | store = buildStore() 31 | const birthday = (await import("./store/birthday/birthday")).default 32 | 33 | expect(() => { 34 | birthday.commitClearBirthdays() 35 | }).toThrow() 36 | 37 | getStoreBuilder().registerModule('birthday') 38 | 39 | expect(() => { 40 | birthday.commitClearBirthdays() 41 | }).not.toThrow() 42 | }) 43 | }) 44 | }) -------------------------------------------------------------------------------- /tests/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import * as Vuex from "vuex"; 3 | import { buildStore } from "./store"; 4 | import birthday from "./store/birthday/birthday"; 5 | import { RootState } from "./store/index"; 6 | 7 | let store: Vuex.Store 8 | 9 | async function test() 10 | { 11 | Vue.use(Vuex) 12 | store = buildStore() 13 | store.replaceState({ 14 | birthday: { 15 | birthdays: [ 16 | { name: "Richard", dob: new Date(1995, 10, 11) }, 17 | { name: "Erlich", dob: new Date(1983, 1, 17) }, 18 | { name: "Nelson", dob: new Date(1996, 3, 28) }, 19 | { name: "Dinesh", dob: new Date(1989, 1, 7) }, 20 | { name: "Bertram", dob: new Date(1985, 7, 14) }, 21 | { name: "Donald", dob: new Date(1994, 5, 31) }, 22 | { name: "Monica", dob: new Date(1996, 8, 26) }, 23 | ] 24 | }, 25 | auth: { isLoggedIn: false, userID: "" } 26 | }) 27 | expect(birthday.oldestName).toEqual("Erlich") 28 | await birthday.dispatchRemoveFirstAfterDelay(20) 29 | await birthday.dispatchRemoveFirstAfterDelay(20) 30 | expect(birthday.oldestName).toEqual("Bertram") 31 | } 32 | 33 | test(); -------------------------------------------------------------------------------- /tests/nested.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import * as Vuex from "vuex"; 3 | import { getStoreBuilder, ModuleBuilder, StoreBuilder } from "../src/index"; 4 | 5 | interface OuterState { str: string, inner: InnerState } 6 | interface InnerState { int: number } 7 | 8 | describe("Create a store", () => 9 | { 10 | let outerBuilder: ModuleBuilder 11 | let innerBuilder: ModuleBuilder 12 | let storeBuilder: StoreBuilder<{}> 13 | beforeEach(() => 14 | { 15 | Vue.use(Vuex) 16 | storeBuilder = getStoreBuilder("nested-store") 17 | outerBuilder = storeBuilder.module("outer", { str: "hello, world." }) 18 | innerBuilder = outerBuilder.module("inner", { int: 42 }) 19 | // innerBuilder = storeBuilder.module("outer/inner", { int: 42 }) 20 | }) 21 | 22 | describe("that includes nested modules", () => 23 | { 24 | it("should access nested value", () => 25 | { 26 | const store = storeBuilder.vuexStore() 27 | const readState = outerBuilder.state() 28 | 29 | expect(readState().inner.int).toBe(42) 30 | }) 31 | }) 32 | }) -------------------------------------------------------------------------------- /tests/plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import * as Vuex from "vuex"; 3 | import { Store } from "vuex"; 4 | import { getStoreBuilder, ModuleBuilder, StoreBuilder } from "../src/index"; 5 | 6 | interface PluginState { age: number } 7 | 8 | describe("Create a store", () => 9 | { 10 | let moduleBuilder: ModuleBuilder 11 | let storeBuilder: StoreBuilder<{}> 12 | let log: string[] = [] 13 | let commitIncrease: () => void 14 | let commitDecrease: () => void 15 | beforeEach(() => 16 | { 17 | Vue.use(Vuex) 18 | storeBuilder = getStoreBuilder("plugin-store") 19 | moduleBuilder = storeBuilder.module("pluggy", { age: 36 }) 20 | commitIncrease = moduleBuilder.commit((state, payload) => { state.age++ }, "increase") 21 | commitDecrease = moduleBuilder.commit((state, payload) => { state.age-- }, "decrease") 22 | }) 23 | 24 | describe("that includes a logger plugin", () => 25 | { 26 | it("should log each mutation", () => 27 | { 28 | const loggerPlugin = (store: Store<{}>) => 29 | { 30 | store.subscribe((mutation, state) => 31 | { 32 | log.push(mutation.type) 33 | }) 34 | } 35 | 36 | storeBuilder.vuexStore({ plugins: [loggerPlugin] }) 37 | commitIncrease() 38 | commitDecrease() 39 | 40 | expect(log.length).toBe(2) 41 | expect(log).toEqual(["pluggy/increase", "pluggy/decrease"]) 42 | }) 43 | }) 44 | }) -------------------------------------------------------------------------------- /tests/root.spec.ts: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import * as Vuex from "vuex"; 3 | import { getStoreBuilder, StoreBuilder } from "../src/index"; 4 | 5 | interface RootState { name: string } 6 | 7 | describe("Create a store", () => 8 | { 9 | let storeBuilder: StoreBuilder 10 | beforeEach(() => 11 | { 12 | Vue.use(Vuex) 13 | storeBuilder = getStoreBuilder("root") 14 | storeBuilder.reset() 15 | }) 16 | 17 | describe("that has no modules (root-only)", () => 18 | { 19 | it("should access root state value", () => 20 | { 21 | const stateReader = storeBuilder.state() 22 | const store = storeBuilder.vuexStore({ 23 | state: { name: "david" } 24 | }) 25 | expect(stateReader().name).toBe("david") 26 | }) 27 | 28 | it("should support getters", () => 29 | { 30 | const uppercaseName = (state: RootState) => state.name.toUpperCase() 31 | const uppercaseNameGetter = storeBuilder.read(uppercaseName) 32 | const store = storeBuilder.vuexStore({ 33 | state: { name: "david" } 34 | }) 35 | expect(uppercaseNameGetter()).toBe("DAVID") 36 | }) 37 | }) 38 | }) -------------------------------------------------------------------------------- /tests/store/auth/auth.ts: -------------------------------------------------------------------------------- 1 | 2 | import { AuthState } from "./state" 3 | import { RootState } from "../index" 4 | import { getStoreBuilder } from "../../../src" 5 | 6 | const initialState: AuthState = { 7 | userID: "b6c8185c6d0af2f5d968", 8 | isLoggedIn: true 9 | } 10 | 11 | const storeBuilder = getStoreBuilder() 12 | const moduleBuilder = storeBuilder.module("auth", initialState) 13 | 14 | const auth = { 15 | commitSetUserID: moduleBuilder.commit((state, payload: { userID: string }) => state.userID = payload.userID, "setUserID"), 16 | commitSetIsLoggedIn: moduleBuilder.commit((state, payload: { isLoggedIn: boolean }) => state.isLoggedIn = payload.isLoggedIn, "isLoggedIn"), 17 | dispatchLogin: moduleBuilder.dispatch((context) => 18 | { 19 | return 20 | }, "login") 21 | } 22 | 23 | export default auth -------------------------------------------------------------------------------- /tests/store/auth/state.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AuthState 3 | { 4 | userID: string 5 | isLoggedIn: boolean 6 | } -------------------------------------------------------------------------------- /tests/store/birthday/actions/removeFirstAfter.ts: -------------------------------------------------------------------------------- 1 | import birthday from "../birthday" 2 | import { BareActionContext } from "../../../../src/index" 3 | import { BirthdayState } from "../state" 4 | import { RootState } from "../../index" 5 | 6 | export default async function removeFirstAfterDelay(context: BareActionContext, delay: number) 7 | { 8 | if (context.state.birthdays.length > 2) 9 | { 10 | await new Promise((resolve, _) => setTimeout(resolve, delay)); // second delay 11 | birthday.commitRemoveFirstBirthday() 12 | } 13 | 14 | return 15 | } -------------------------------------------------------------------------------- /tests/store/birthday/birthday.ts: -------------------------------------------------------------------------------- 1 | 2 | import Vuex from "vuex" 3 | import { getStoreBuilder } from "../../../src" 4 | import { BirthdayState, Birthday } from "./state" 5 | import { Module } from "vuex" 6 | import { RootState } from "../index" 7 | import removeFirstAfterDelay from "./actions/removeFirstAfter"; 8 | import { ModuleBuilder } from "../../../src" 9 | 10 | const initialState: BirthdayState = { 11 | birthdays: [] 12 | } 13 | 14 | const mb = getStoreBuilder().module("birthday", initialState) 15 | 16 | const addBirthdayMut = (state: BirthdayState, payload: { birthday: Birthday }) => state.birthdays.push(payload.birthday) 17 | const removeFirstBirthdayMut = (state: BirthdayState) => state.birthdays.shift() 18 | const clearBirthdaysMut = (state:BirthdayState) => state.birthdays = [] 19 | 20 | const oldestNameGetter = mb.read((state): string | undefined => 21 | { 22 | const oldestBirthday = (state.birthdays).slice().sort((a, b) => a.dob.getTime() - b.dob.getTime())[0] 23 | return oldestBirthday && oldestBirthday.name 24 | }, "oldestName") 25 | 26 | const dateOfBirthForMethod = mb.read((state) => (name: string) => 27 | { 28 | const matches = state.birthdays.filter(b => b.name === name) 29 | if (matches.length) 30 | { 31 | return matches[0].dob 32 | } 33 | 34 | return 35 | }, "dob") 36 | 37 | const stateReader = mb.state(); 38 | 39 | const birthday = { 40 | // getters + methods 41 | get state() { return stateReader() }, 42 | get oldestName() { return oldestNameGetter() }, 43 | dateOfBirthFor(name: string) { return dateOfBirthForMethod()(name) }, 44 | 45 | // mutations 46 | commitAddBirthday: mb.commit(addBirthdayMut), 47 | commitRemoveFirstBirthday: mb.commit(removeFirstBirthdayMut), 48 | commitClearBirthdays: mb.commit(clearBirthdaysMut), 49 | 50 | // actions 51 | dispatchRemoveFirstAfterDelay: mb.dispatch(removeFirstAfterDelay), 52 | } 53 | 54 | // birthday.commitAddBirthday({ birthday: { dob: new Date(1980, 2, 3), name: "Louise" } }) 55 | // birthday.commitRemoveFirstBirthday() 56 | // birthday.dateOfBirthFor("Louise") 57 | // birthday.dispatchRemoveFirstAfter(1000) 58 | 59 | export default birthday 60 | export { mb as birthdayModuleBuilder } -------------------------------------------------------------------------------- /tests/store/birthday/state.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Birthday 3 | { 4 | name: string 5 | dob: Date 6 | } 7 | 8 | export interface BirthdayState 9 | { 10 | birthdays: Birthday[] 11 | } -------------------------------------------------------------------------------- /tests/store/index.ts: -------------------------------------------------------------------------------- 1 | 2 | import { getStoreBuilder } from "../../src" 3 | import { AuthState } from "./auth/state"; 4 | import { BirthdayState } from "./birthday/state" 5 | import { Store } from "vuex" 6 | import birthday from "./birthday/birthday" 7 | 8 | export interface RootState 9 | { 10 | auth: AuthState 11 | birthday: BirthdayState 12 | } 13 | 14 | export const buildStore = () => getStoreBuilder().vuexStore() -------------------------------------------------------------------------------- /tsconfig-src.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "include": [ 4 | "src" 5 | ] 6 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "module": "ES2015", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "dom", 8 | "es5", 9 | "es2015" 10 | ], 11 | // "strict": true, 12 | "allowSyntheticDefaultImports": true, 13 | "experimentalDecorators": true, 14 | "noImplicitAny": true, 15 | "noImplicitReturns": true, 16 | "noImplicitThis": true, 17 | "strictNullChecks": true, 18 | "esModuleInterop": true, 19 | "target": "es5", 20 | "sourceMap": true, 21 | "declaration": true, 22 | "declarationDir": "./dist", 23 | "removeComments": false, 24 | "skipLibCheck": true, 25 | "types" : [ 26 | "jest", 27 | "vuex" 28 | ] 29 | }, 30 | "include": [ 31 | "src", 32 | "tests", 33 | "examples" 34 | ], 35 | "exclude": [ 36 | "node_modules" 37 | ] 38 | } --------------------------------------------------------------------------------