<|start_filename|>spec/vuex-helpers.spec.js<|end_filename|>
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';

import {
    mapInstanceState,
    mapInstanceGetters,
    mapInstanceMutations,
    mapInstanceActions,
} from '../src/vuex-helpers';

const localVue = createLocalVue();
localVue.use(Vuex);

describe('Vuex Store Utils', () => {

    describe('instance-aware map* functions', () => {

        describe('mapInstanceState', () => {

            const testModule = {
                namespaced: true,
                state: () => ({
                    value: 1,
                }),
            };

            const getModuleName = cmp => cmp.vuexNamespace;

            function getComponent() {
                return localVue.component('test-component', {
                    props: ['vuexNamespace'],
                    computed: {
                        ...mapInstanceState(getModuleName, ['value']),
                        ...mapInstanceState(getModuleName, {
                            value2: state => state.value,
                        }),
                    },
                    render(h) {
                        return h('div');
                    },
                });
            }

            it('should provide mapState based on an instance-aware function', () => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        myNamespace: testModule,
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'myNamespace',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(wrapper.vm.value).toBe(store.state.myNamespace.value);
                expect(wrapper.vm.value).toBe(1);
                expect(wrapper.vm.value2).toBe(store.state.myNamespace.value);
                expect(wrapper.vm.value2).toBe(1);
            });

            it('should work for nested modules', () => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        parent: {
                            namespaced: true,
                            modules: {
                                child: {
                                    namespaced: true,
                                    modules: {
                                        grandchild: testModule,
                                    },
                                },
                            },
                        },
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'parent/child/grandchild',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(wrapper.vm.value).toBe(store.state.parent.child.grandchild.value);
                expect(wrapper.vm.value).toBe(1);
                expect(wrapper.vm.value2).toBe(store.state.parent.child.grandchild.value);
                expect(wrapper.vm.value2).toBe(1);
            });

        });

        describe('mapInstanceGetters', () => {
            const testModule = {
                namespaced: true,
                state: () => ({
                    value: 1,
                }),
                getters: {
                    valueGetter(state) {
                        return state.value;
                    },
                },
            };

            const getModuleName = cmp => cmp.vuexNamespace;

            function getComponent() {
                return localVue.component('test-component', {
                    props: ['vuexNamespace'],
                    computed: {
                        ...mapInstanceGetters(getModuleName, ['valueGetter']),
                        ...mapInstanceGetters(getModuleName, {
                            valueGetter2: 'valueGetter',
                        }),
                    },
                    render(h) {
                        return h('div');
                    },
                });
            }

            it('should provide mapGetters based on an instance-aware function', () => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        myNamespace: testModule,
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'myNamespace',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(wrapper.vm.valueGetter).toBe(store.state.myNamespace.value);
                expect(wrapper.vm.valueGetter).toBe(1);
                expect(wrapper.vm.valueGetter2).toBe(store.state.myNamespace.value);
                expect(wrapper.vm.valueGetter2).toBe(1);
            });

            it('should work for nested modules', () => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        parent: {
                            namespaced: true,
                            modules: {
                                child: {
                                    namespaced: true,
                                    modules: {
                                        grandchild: testModule,
                                    },
                                },
                            },
                        },
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'parent/child/grandchild',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(wrapper.vm.valueGetter).toBe(store.state.parent.child.grandchild.value);
                expect(wrapper.vm.valueGetter).toBe(1);
                expect(wrapper.vm.valueGetter2).toBe(store.state.parent.child.grandchild.value);
                expect(wrapper.vm.valueGetter2).toBe(1);
            });

        });

        describe('mapInstanceMutations', () => {

            const testModule = {
                namespaced: true,
                state: () => ({
                    value: 1,
                }),
                mutations: {
                    SET_VALUE(state, payload) {
                        // eslint-disable-next-line no-param-reassign
                        state.value = payload;
                    },
                },
            };

            const getModuleName = cmp => cmp.vuexNamespace;

            function getComponent() {
                return localVue.component('test-component', {
                    props: ['vuexNamespace'],
                    methods: {
                        ...mapInstanceMutations(getModuleName, ['SET_VALUE']),
                        ...mapInstanceMutations(getModuleName, {
                            setValue: 'SET_VALUE',
                        }),
                    },
                    render(h) {
                        return h('div');
                    },
                });
            }

            it('should provide mapMutations based on an instance-aware function', () => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        myNamespace: testModule,
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'myNamespace',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(store.state.myNamespace.value).toBe(1);
                wrapper.vm.SET_VALUE(2);
                expect(store.state.myNamespace.value).toBe(2);
                wrapper.vm.setValue(3);
                expect(store.state.myNamespace.value).toBe(3);
            });

            it('should work for nested modules', () => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        parent: {
                            namespaced: true,
                            modules: {
                                child: {
                                    namespaced: true,
                                    modules: {
                                        grandchild: testModule,
                                    },
                                },
                            },
                        },
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'parent/child/grandchild',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(store.state.parent.child.grandchild.value).toBe(1);
                wrapper.vm.SET_VALUE(2);
                expect(store.state.parent.child.grandchild.value).toBe(2);
                wrapper.vm.setValue(3);
                expect(store.state.parent.child.grandchild.value).toBe(3);
            });

        });

        describe('mapInstanceActions', () => {

            const testModule = {
                namespaced: true,
                state: () => ({
                    value: 1,
                }),
                mutations: {
                    SET_VALUE(state, payload) {
                        // eslint-disable-next-line no-param-reassign
                        state.value = payload;
                    },
                },
                actions: {
                    SET_VALUE_ASYNC({ commit }, payload) {
                        return new Promise(r => setTimeout(() => {
                            commit('SET_VALUE', payload);
                            r();
                        }, 1));
                    },
                },
            };

            const getModuleName = cmp => cmp.vuexNamespace;

            function getComponent() {
                return localVue.component('test-component', {
                    props: ['vuexNamespace'],
                    methods: {
                        ...mapInstanceActions(getModuleName, ['SET_VALUE_ASYNC']),
                        ...mapInstanceActions(getModuleName, {
                            setValueAsync: 'SET_VALUE_ASYNC',
                        }),
                    },
                    render(h) {
                        return h('div');
                    },
                });
            }

            it('should provide mapActions based on an instance-aware function', (done) => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        myNamespace: testModule,
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'myNamespace',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(store.state.myNamespace.value).toBe(1);
                wrapper.vm.SET_VALUE_ASYNC(2)
                    .then(() => {
                        expect(store.state.myNamespace.value).toBe(2);
                        return wrapper.vm.setValueAsync(3);
                    })
                    .then(() => {
                        expect(store.state.myNamespace.value).toBe(3);
                        done();
                    });
            });

            it('should work for nested modules', (done) => {
                const store = new Vuex.Store({
                    state: {},
                    modules: {
                        parent: {
                            namespaced: true,
                            modules: {
                                child: {
                                    namespaced: true,
                                    modules: {
                                        grandchild: testModule,
                                    },
                                },
                            },
                        },
                    },
                });

                const wrapper = mount(getComponent(), {
                    propsData: {
                        vuexNamespace: 'parent/child/grandchild',
                    },
                    mocks: {
                        $store: store,
                    },
                });

                expect(store.state.parent.child.grandchild.value).toBe(1);
                wrapper.vm.SET_VALUE_ASYNC(2)
                    .then(() => {
                        expect(store.state.parent.child.grandchild.value).toBe(2);
                        return wrapper.vm.setValueAsync(3);
                    })
                    .then(() => {
                        expect(store.state.parent.child.grandchild.value).toBe(3);
                        done();
                    });
            });

        });

    });

});


<|start_filename|>src/vuex-helpers.js<|end_filename|>
function isPlainObject(obj) {
    return Object.prototype.toString.call(obj) === '[object Object]';
}

function getModuleState(state, ns) {
    // "Walk" the state tree step by step to support nested namespaces
    return ns.split('/').reduce((s, p) => s[p], state);
}

/**
 * @description
 * mapState implementation that supports a dynamic module namespaced based on
 * the component instance
 *
 * @param   {Function}        getNamespace Function accepting a component instance
 *                                         and returning a slash-separated Vuex
 *                                         module namespace
 * @param   {Object|String[]} mappers      Object of mapper functions or arrays of
 *                                         state field strings
 * @returns {Object}          instance-aware state computed property methods
 */
export function mapInstanceState(getNamespace, mappers) {
    if (Array.isArray(mappers)) {
        return mappers.reduce((acc, name) => Object.assign(acc, {
            // Note: Do not use an arrow function as we need to capture `this` at runtime
            [name]() {
                const moduleState = getModuleState(this.$store.state, getNamespace(this));
                return moduleState[name];
            },
        }), {});
    }

    // istanbul ignore else
    if (isPlainObject(mappers)) {
        return Object.entries(mappers).reduce((acc, [name, mapper]) => Object.assign(acc, {
            // Note: Do not use an arrow function as we need to capture `this` at runtime
            [name]() {
                const moduleState = getModuleState(this.$store.state, getNamespace(this));
                return mapper.call(this, moduleState);
            },
        }), {});
    }

    // istanbul ignore next
    console.warn('mapInstanceState requires an array or object as the second argument');
    // istanbul ignore next
    return {};
}

/**
 * @description
 * mapGetters implementation that supports a dynamic module namespaced based on
 * the component instance
 *
 * @param   {Function}        getNamespace Function accepting a component instance
 *                                         and returning a slash-separated Vuex
 *                                         module namespace
 * @param   {String[]|Object} mappers      Array of getter names or object of
 *                                         getter aliases
 * @returns {Object}          instance-aware state computed property methods
 */
export function mapInstanceGetters(getNamespace, mappers) {
    if (Array.isArray(mappers)) {
        return mappers.reduce((acc, name) => Object.assign(acc, {
            // Note: Do not use an arrow function as we need to capture `this` at runtime
            [name]() {
                return this.$store.getters[`${getNamespace(this)}/${name}`];
            },
        }), {});
    }

    // istanbul ignore else
    if (isPlainObject(mappers)) {
        return Object.entries(mappers).reduce((acc, [alias, name]) => Object.assign(acc, {
            // Note: Do not use an arrow function as we need to capture `this` at runtime
            [alias]() {
                return this.$store.getters[`${getNamespace(this)}/${name}`];
            },
        }), {});
    }

    // istanbul ignore next
    console.warn('mapInstanceGetters requires an array or object as the second argument');
    // istanbul ignore next
    return {};
}

/**
 * @description
 * mapMutations implementation that supports a dynamic module namespaced based on
 * the component instance
 *
 * @param   {Function}        getNamespace Function accepting a component instance
 *                                         and returning a slash-separated Vuex
 *                                         module namespace
 * @param   {String[]|Object} mutations    Array of mutation names or object of
 *                                         mutation aliases
 * @returns {Object}          instance-aware state computed property methods
 */
export function mapInstanceMutations(getNamespace, mutations) {
    if (Array.isArray(mutations)) {
        return mutations.reduce((acc, name) => Object.assign(acc, {
            [name](payload) {
                return this.$store.commit(`${getNamespace(this)}/${name}`, payload);
            },
        }), {});
    }

    // istanbul ignore else
    if (isPlainObject(mutations)) {
        return Object.entries(mutations).reduce((acc, [alias, name]) => Object.assign(acc, {
            [alias](payload) {
                return this.$store.commit(`${getNamespace(this)}/${name}`, payload);
            },
        }), {});
    }

    // istanbul ignore next
    console.warn('mapInstanceMutations requires an array or object as the second argument');
    // istanbul ignore next
    return {};
}

/**
 * @description
 * mapMutations implementation that supports a dynamic module namespaced based on
 * the component instance
 *
 * @param   {Function}        getNamespace Function accepting a component instance
 *                                         and returning a slash-separated Vuex
 *                                         module namespace
 * @param   {String[]|Object} actions      Array of action names or object of
 *                                         action aliases
 * @returns {Object}          instance-aware state computed property methods
 */
export function mapInstanceActions(getNamespace, actions) {
    if (Array.isArray(actions)) {
        return actions.reduce((acc, name) => Object.assign(acc, {
            [name](payload) {
                return this.$store.dispatch(`${getNamespace(this)}/${name}`, payload);
            },
        }), {});
    }

    // istanbul ignore else
    if (isPlainObject(actions)) {
        return Object.entries(actions).reduce((acc, [alias, name]) => Object.assign(acc, {
            [alias](payload) {
                return this.$store.dispatch(`${getNamespace(this)}/${name}`, payload);
            },
        }), {});
    }

    // istanbul ignore next
    console.warn('mapInstanceActions requires an array or object as the second argument');
    // istanbul ignore next
    return {};
}


<|start_filename|>jest.config.js<|end_filename|>
module.exports = {
    clearMocks: true,
    collectCoverage: true,
    collectCoverageFrom: ['src/**/*.js'],
    coverageReporters: ['html', 'text'],
    coverageThreshold: {
        global: {
            branches: 95,
            functions: 95,
            lines: 95,
            statements: 95,
        },
    },
    silent: false,
    transform: {
        '^.+\\.js?$': 'babel-jest',
    },
    verbose: true,
};


<|start_filename|>package.json<|end_filename|>
{
  "name": "@urbn/vuex-helpers",
  "version": "0.1.0",
  "description": "A collection of vuex-helper methods",
  "author": "<EMAIL>",
  "license": "MIT",
  "main": "dist/vuex-helpers.cjs.js",
  "module": "src/vuex-helpers.js",
  "browser": "dist/vuex-helpers.umd.min.js",
  "files": [
    "dist",
    "src",
    "LICENSE.md",
    "package.json",
    "package-lock.json",
    "README.md"
  ],
  "sideEffects": false,
  "homepage": "https://github.com/brophdawg11/vuex-helpers#readme",
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com/brophdawg11/vuex-helpers.git"
  },
  "bugs": {
    "url": "https://github.com/brophdawg11/vuex-helpers/issues"
  },
  "keywords": [
    "js",
    "vue",
    "vuex"
  ],
  "scripts": {
    "build": "rollup -c",
    "clean": "rm -rf dist/",
    "lint": "eslint .",
    "prepublishOnly": "npm run validate && npm run build",
    "test": "jest",
    "test:debug": "node --inspect-brk ./node_modules/.bin/jest --runInBand --collectCoverage false",
    "test:watch": "jest --watch",
    "validate": "npm run lint && npm run test"
  },
  "devDependencies": {
    "@babel/preset-env": "7.8.4",
    "@rollup/plugin-commonjs": "11.0.2",
    "@rollup/plugin-node-resolve": "7.1.1",
    "@vue/test-utils": "1.0.0-beta.31",
    "babel-jest": "25.1.0",
    "eslint": "6.8.0",
    "eslint-config-airbnb-base": "14.0.0",
    "eslint-plugin-import": "2.20.1",
    "eslint-plugin-jest": "23.7.0",
    "husky": "4.2.1",
    "jest": "25.1.0",
    "rollup": "1.31.0",
    "rollup-plugin-terser": "5.2.0",
    "vue": "2.6.11",
    "vue-template-compiler": "2.6.11",
    "vuex": "3.1.2"
  },
  "peerDependencies": {
    "vuex": "3.x"
  }
}


<|start_filename|>.eslintrc.js<|end_filename|>
module.exports = {
    root: true,
    extends: 'airbnb-base',
    rules: {
        // Kept from airbnb-base@13, that switched to 'always' in 14
        'arrow-parens': ['error', 'as-needed', { requireForBlockBody: true }],
        indent: ['error', 4],
        'no-console': 'off',
        'no-plusplus': 'off',
        'no-underscore-dangle': 'off',
        'operator-linebreak': 'off',
        'padded-blocks': 'off',
        'valid-jsdoc': ['error', {
            prefer: {
                'arg': 'param',
                'return': 'return',
            },
            preferType: {
                'boolean': 'Boolean',
                'function': 'Function',
                'number': 'Number',
                'object': 'Object',
                'string': 'String',
            },
        }],
    },
    overrides: [{
        files: ['spec/**'],
        plugins: [
            'jest',
        ],
        env: {
            jest: true,
        },
        rules: {
            'no-console': 'off',
        },
    }],
};
