├── .circleci └── config.yml ├── .eslintrc.js ├── .gitignore ├── .prettierrc.js ├── LICENSE ├── README.md ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── composition.ts └── index.ts ├── tests └── unit │ └── install.spec.ts └── tsconfig.json /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | defaults: &defaults 3 | working_directory: ~/repo 4 | docker: 5 | - image: circleci/node:12.13 6 | jobs: 7 | test: 8 | <<: *defaults 9 | steps: 10 | - attach_workspace: 11 | at: ~/repo 12 | - run: npm run test 13 | codecov: 14 | <<: *defaults 15 | steps: 16 | - attach_workspace: 17 | at: ~/repo 18 | - run: npm run codecov 19 | coverage: 20 | <<: *defaults 21 | steps: 22 | - attach_workspace: 23 | at: ~/repo 24 | - run: npm run coverage 25 | - persist_to_workspace: 26 | root: ~/repo 27 | paths: . 28 | - store_artifacts: 29 | path: /coverage 30 | build: 31 | <<: *defaults 32 | steps: 33 | - attach_workspace: 34 | at: ~/repo 35 | - run: npm run build 36 | - persist_to_workspace: 37 | root: ~/repo 38 | paths: . 39 | - store_artifacts: 40 | path: /pkg 41 | deploy: 42 | <<: *defaults 43 | steps: 44 | - attach_workspace: 45 | at: ~/repo 46 | - run: echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/repo/pkg/.npmrc 47 | - run: cd pkg && npm publish 48 | 49 | install: 50 | <<: *defaults 51 | steps: 52 | - checkout 53 | - restore_cache: 54 | keys: 55 | - v1-dependencies-{{ checksum "package.json" }} 56 | - v1-dependencies- 57 | - run: npm ci 58 | - save_cache: 59 | paths: 60 | - node_modules 61 | key: v1-dependencies-{{ checksum "package.json" }} 62 | - persist_to_workspace: 63 | root: ~/repo 64 | paths: . 65 | workflows: 66 | version: 2 67 | main: 68 | jobs: 69 | - install: 70 | filters: 71 | tags: 72 | only: /.*/ 73 | - build: 74 | filters: 75 | tags: 76 | only: /.*/ 77 | requires: 78 | - install 79 | - test: 80 | filters: 81 | tags: 82 | only: /.*/ 83 | requires: 84 | - install 85 | - coverage: 86 | filters: 87 | tags: 88 | only: /.*/ 89 | requires: 90 | - install 91 | - codecov: 92 | filters: 93 | tags: 94 | only: /.*/ 95 | requires: 96 | - coverage 97 | - deploy: 98 | requires: 99 | - test 100 | - build 101 | filters: 102 | tags: 103 | only: /^v.*/ 104 | branches: 105 | ignore: /.*/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | }, 6 | extends: [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/typescript/recommended', 10 | '@vue/prettier', 11 | '@vue/prettier/@typescript-eslint', 12 | ], 13 | rules: { 14 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 15 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 16 | // '@typescript-eslint/explicit-function-return-type': 'off', 17 | // '@typescript-eslint/no-use-before-define': 'off', 18 | // '@typescript-eslint/no-explicit-any': 'off', 19 | }, 20 | parserOptions: { 21 | ecmaVersion: 2020, 22 | }, 23 | overrides: [ 24 | { 25 | files: ['**/__tests__/*.{j,t}s?(x)'], 26 | env: { 27 | jest: true, 28 | }, 29 | }, 30 | ], 31 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | pkg 3 | coverage 4 | .gitpod.yml -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | endOfLine: 'auto', 6 | } 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Patryk Wałach 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: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PatrykWalach/vuex-composition-api/78d44d42f7fe87cb8e378e979f12d46b657182b5/README.md -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | testEnvironment: 'jsdom', 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-composition-api", 3 | "version": "3.0.0", 4 | "description": "Make Vuex modules with syntax inspired by @vue/composition-api", 5 | "@pika/pack": { 6 | "pipeline": [ 7 | [ 8 | "@pika/plugin-ts-standard-pkg" 9 | ], 10 | [ 11 | "@pika/plugin-build-node" 12 | ], 13 | [ 14 | "@pika/plugin-build-web" 15 | ], 16 | [ 17 | "@pika/plugin-build-deno" 18 | ] 19 | ] 20 | }, 21 | "scripts": { 22 | "build": "pika build", 23 | "coverage": "jest --collect-coverage", 24 | "codecov": "codecov", 25 | "lint": "eslint --ext .js,.jsx,.vue,.ts,.tsx --fix .", 26 | "test": "jest", 27 | "watch": "jest --watch" 28 | }, 29 | "keywords": [ 30 | "vue", 31 | "composition-api", 32 | "router", 33 | "hooks", 34 | "typescript" 35 | ], 36 | "author": "Patryk Wałach", 37 | "license": "MIT", 38 | "peerDependencies": { 39 | "vue": ">=3.0.0-alpha.0" 40 | }, 41 | "repository": { 42 | "type": "git", 43 | "url": "git+https://github.com/PatrykWalach/vuex-composition-api.git" 44 | }, 45 | "devDependencies": { 46 | "@pika/pack": "^0.5.0", 47 | "@pika/plugin-build-deno": "^0.9.2", 48 | "@pika/plugin-build-node": "^0.9.2", 49 | "@pika/plugin-build-web": "^0.9.2", 50 | "@pika/plugin-ts-standard-pkg": "^0.9.2", 51 | "@types/jest": "^26.0.14", 52 | "@types/node": "^14.11.2", 53 | "@typescript-eslint/eslint-plugin": "^2.33.0", 54 | "@typescript-eslint/parser": "^2.33.0", 55 | "@vue/eslint-config-prettier": "^6.0.0", 56 | "@vue/eslint-config-typescript": "^5.1.0", 57 | "@vue/test-utils": "^2.0.0-beta.5", 58 | "codecov": "^3.7.2", 59 | "eslint": "^6.8.0", 60 | "eslint-plugin-prettier": "^3.1.4", 61 | "eslint-plugin-vue": "^7.0.0-beta.3", 62 | "husky": "^4.3.0", 63 | "jest": "^26.4.2", 64 | "lint-staged": "^9.5.0", 65 | "prettier": "^2.1.2", 66 | "ts-jest": "^26.4.0", 67 | "typescript": "^3.9.7", 68 | "vue": "^3.0.0" 69 | }, 70 | "husky": { 71 | "hooks": { 72 | "pre-commit": "lint-staged" 73 | } 74 | }, 75 | "lint-staged": { 76 | "*.{js,jsx,vue,ts,tsx}": "eslint --cache --fix" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/composition.ts: -------------------------------------------------------------------------------- 1 | import { App, InjectionKey, inject, reactive, UnwrapRef, Ref } from 'vue' 2 | 3 | export const VUEX: InjectionKey = Symbol('vuex') 4 | 5 | export interface Context { 6 | use: VuexStore 7 | } 8 | 9 | export type StoreSetup = (context: Context) => T 10 | 11 | interface Store { 12 | name: string 13 | setup: StoreSetup 14 | } 15 | 16 | export function defineStore( 17 | name: string, 18 | setup: StoreSetup, 19 | ): Store { 20 | return { name, setup } 21 | } 22 | 23 | export type VuexStore = ( 24 | options: Store, 25 | ) => UnwrapNestedRefs 26 | 27 | export class Vuex { 28 | private _stores = new Map() 29 | private _setupContext: Record = {} 30 | 31 | public install(app: T) { 32 | app.provide(VUEX, this) 33 | return app 34 | } 35 | public store({ 36 | name, 37 | setup, 38 | }: Store): UnwrapNestedRefs { 39 | const store = this._stores.get(name) 40 | if (store) { 41 | return store as UnwrapNestedRefs 42 | } 43 | 44 | const storeInstance = reactive( 45 | setup((this._setupContext as unknown) as Context), 46 | ) 47 | 48 | this._stores.set(name, storeInstance) 49 | return storeInstance 50 | } 51 | 52 | constructor(plugins: Plugin[]) { 53 | plugins 54 | .concat((provide) => provide('use', this.store.bind(this))) 55 | .forEach((plugin) => 56 | plugin((key, value) => { 57 | this._setupContext[key] = value 58 | }), 59 | ) 60 | } 61 | } 62 | 63 | export type Plugin = (provide: (key: string, value: T) => void) => any 64 | 65 | export type UnwrapNestedRefs = T extends Ref ? T : UnwrapRef 66 | 67 | export const createVuex = ( 68 | { plugins }: { plugins: Plugin[] } = { plugins: [] }, 69 | ): Vuex => new Vuex(plugins) 70 | 71 | export const useStore = (storeOptions: Store) => { 72 | const vuex = inject(VUEX) 73 | if (!vuex) { 74 | throw new Error('no vuex provided') 75 | } 76 | return vuex.store(storeOptions) 77 | } 78 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './composition' 2 | -------------------------------------------------------------------------------- /tests/unit/install.spec.ts: -------------------------------------------------------------------------------- 1 | import { createVuex, defineStore, useStore } from '../../src' 2 | import { h, ref, defineComponent } from 'vue' 3 | import { mount } from '@vue/test-utils' 4 | 5 | declare module '../../src' { 6 | interface Context { 7 | fn: jest.Mock 8 | } 9 | } 10 | 11 | 12 | describe('vuex', () => { 13 | const counterOptions = defineStore('counter', () => { 14 | const value = ref(0) 15 | 16 | function increment() { 17 | value.value++ 18 | } 19 | 20 | return { 21 | value, 22 | increment, 23 | } 24 | }) 25 | 26 | it('works composition', async () => { 27 | // const counterOptions = defineStore({ 28 | // state: () => ({ value: 0 }), 29 | 30 | // getters: { 31 | // isIncremented() { 32 | // return this.state > 0 33 | // }, 34 | // }, 35 | 36 | // actions: { 37 | // increment() { 38 | // this.value++ 39 | // }, 40 | // }, 41 | // }) 42 | 43 | const vuex = createVuex() 44 | 45 | const wrapper = mount( 46 | defineComponent({ 47 | render() { 48 | return [ 49 | h('button', { onClick: this.counter.increment }), 50 | h('div', this.counter.value), 51 | ] 52 | }, 53 | setup() { 54 | const counter = useStore(counterOptions) 55 | return { counter } 56 | }, 57 | // use: () => ({ 58 | // counter: counterOptions, 59 | // }), 60 | }), 61 | { global: { plugins: [vuex] } }, 62 | ) 63 | 64 | expect(wrapper.find('div').text()).toStrictEqual('0') 65 | 66 | await wrapper.find('button').trigger('click') 67 | 68 | expect(wrapper.find('div').text()).toStrictEqual('1') 69 | }) 70 | it('works setup nested', async () => { 71 | const rootCounterOptions = defineStore('rootCounter', ({ use }) => { 72 | const counter = use(counterOptions) 73 | 74 | function incrementTwice() { 75 | counter.increment() 76 | counter.increment() 77 | } 78 | 79 | return { 80 | incrementTwice, 81 | } 82 | }) 83 | 84 | const vuex = createVuex() 85 | 86 | const wrapper = mount( 87 | defineComponent({ 88 | render() { 89 | return [ 90 | h('button', { onClick: this.rootCounter.incrementTwice }), 91 | h('div', this.counter.value), 92 | ] 93 | }, 94 | setup() { 95 | const counter = useStore(counterOptions) 96 | const rootCounter = useStore(rootCounterOptions) 97 | return { counter, rootCounter } 98 | }, 99 | }), 100 | { global: { plugins: [vuex] } }, 101 | ) 102 | 103 | expect(wrapper.find('div').text()).toStrictEqual('0') 104 | 105 | await wrapper.find('button').trigger('click') 106 | 107 | expect(wrapper.find('div').text()).toStrictEqual('2') 108 | }) 109 | it('works plugin', async () => { 110 | const withPluginOptions = defineStore('withPlugin', ({ fn }) => { 111 | function callFn() { 112 | fn() 113 | } 114 | 115 | return { 116 | callFn, 117 | } 118 | }) 119 | 120 | const fn = jest.fn() 121 | 122 | const vuex = createVuex({ 123 | plugins: [(provide) => provide('fn', fn)], 124 | }) 125 | 126 | const wrapper = mount( 127 | defineComponent({ 128 | render() { 129 | return [h('button', { onClick: this.withPlugin.callFn })] 130 | }, 131 | setup() { 132 | const withPlugin = useStore(withPluginOptions) 133 | return { withPlugin } 134 | }, 135 | }), 136 | { global: { plugins: [vuex] } }, 137 | ) 138 | 139 | expect(fn).not.toBeCalled() 140 | 141 | await wrapper.find('button').trigger('click') 142 | 143 | expect(fn).toBeCalled() 144 | }) 145 | }) 146 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "esnext", 5 | "esModuleInterop": true, 6 | "moduleResolution": "node", 7 | "target": "es2020", 8 | "types": ["node", "jest"] 9 | }, 10 | "include": ["src/**/*"], 11 | "exclude": ["node_modules", "**/__tests__/*"] 12 | } 13 | --------------------------------------------------------------------------------