From 3dd210b569fb5e95828c044ab921fe97f8e3369e Mon Sep 17 00:00:00 2001 From: Yugo Ogura Date: Fri, 5 Feb 2021 09:16:06 +0900 Subject: [PATCH] feat: make it compatible with vue3 (vuex 4 and router 4) (#100) Co-authored-by: Kia King Ishii --- package.json | 10 +-- src/index.ts | 45 +++++------ test/index.spec.ts | 181 ++++++++++++++++++++++++++++++++------------- yarn.lock | 110 ++++++++++++++++++++++++--- 4 files changed, 253 insertions(+), 93 deletions(-) diff --git a/package.json b/package.json index 344359e..fe037b1 100644 --- a/package.json +++ b/package.json @@ -36,8 +36,8 @@ }, "homepage": "https://github.com/vuejs/vuex-router-sync#readme", "peerDependencies": { - "vue-router": "^3.0.0", - "vuex": "^3.0.0" + "vue-router": "^4.0.2", + "vuex": "^4.0.0-rc.2" }, "devDependencies": { "@rollup/plugin-commonjs": "^11.1.0", @@ -59,8 +59,8 @@ "ts-jest": "^25.4.0", "tslib": "^1.11.1", "typescript": "^3.8.3", - "vue": "^2.5.0", - "vue-router": "^3.0.0", - "vuex": "^3.0.0" + "vue": "^3.0.5", + "vue-router": "^4.0.2", + "vuex": "^4.0.0-rc.2" } } diff --git a/src/index.ts b/src/index.ts index 0919bc7..d6d7ff2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,36 +1,30 @@ import { Store } from 'vuex' -import VueRouter, { Route } from 'vue-router' +import { Router, RouteLocationNormalized } from 'vue-router' export interface SyncOptions { moduleName: string } -export interface State { - name?: string | null - path: string - hash: string - query: Record - params: Record - fullPath: string - meta?: any +export interface State + extends Omit { from?: Omit } export interface Transition { - to: Route - from: Route + to: RouteLocationNormalized + from: RouteLocationNormalized } export function sync( store: Store, - router: VueRouter, + router: Router, options?: SyncOptions ): () => void { const moduleName = (options || {}).moduleName || 'route' store.registerModule(moduleName, { namespaced: true, - state: cloneRoute(router.currentRoute), + state: cloneRoute(router.currentRoute.value), mutations: { ROUTE_CHANGED(_state: State, transition: Transition): void { store.state[moduleName] = cloneRoute(transition.to, transition.from) @@ -44,18 +38,18 @@ export function sync( // sync router on store change const storeUnwatch = store.watch( (state) => state[moduleName], - (route: Route) => { + (route: RouteLocationNormalized) => { const { fullPath } = route if (fullPath === currentPath) { return } if (currentPath != null) { isTimeTraveling = true - router.push(route as any) + router.push(route) } currentPath = fullPath }, - { sync: true } as any + { flush: 'sync' } ) // sync store on router navigation @@ -69,22 +63,21 @@ export function sync( }) return function unsync(): void { - // On unsync, remove router hook - if (afterEachUnHook != null) { - afterEachUnHook() - } + // remove router hook + afterEachUnHook() - // On unsync, remove store watch - if (storeUnwatch != null) { - storeUnwatch() - } + // remove store watch + storeUnwatch() - // On unsync, unregister Module with store + // unregister Module with store store.unregisterModule(moduleName) } } -function cloneRoute(to: Route, from?: Route): State { +function cloneRoute( + to: RouteLocationNormalized, + from?: RouteLocationNormalized +): State { const clone: State = { name: to.name, path: to.path, diff --git a/test/index.spec.ts b/test/index.spec.ts index 2836f67..9199d1a 100644 --- a/test/index.spec.ts +++ b/test/index.spec.ts @@ -1,54 +1,64 @@ -import Vue from 'vue' -import Vuex, { mapState } from 'vuex' -import VueRouter from 'vue-router' +import { createApp, defineComponent, h, computed, nextTick } from 'vue' +import { createStore, useStore } from 'vuex' +import { createRouter, createMemoryHistory, RouterView } from 'vue-router' import { sync } from '@/index' -Vue.use(Vuex) -Vue.use(VueRouter) +async function run(originalModuleName: string, done: Function): Promise { + const moduleName = originalModuleName || 'route' -function run(originalModuleName: string, done: Function): void { - const moduleName: string = originalModuleName || 'route' - - const store = new Vuex.Store({ - state: { msg: 'foo' } + const store = createStore({ + state() { + return { msg: 'foo' } + } }) - const Home = Vue.extend({ - computed: mapState(moduleName, { - path: (state: any) => state.fullPath, - foo: (state: any) => state.params.foo, - bar: (state: any) => state.params.bar - }), - render(h) { - return h('div', [this.path, ' ', this.foo, ' ', this.bar]) + const Home = defineComponent({ + setup() { + const store = useStore() + const path = computed(() => store.state[moduleName].fullPath) + const foo = computed(() => store.state[moduleName].params.foo) + const bar = computed(() => store.state[moduleName].params.bar) + return () => h('div', [path.value, ' ', foo.value, ' ', bar.value]) } }) - const router = new VueRouter({ - mode: 'abstract', - routes: [{ path: '/:foo/:bar', component: Home }] + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/', + component: { + template: 'root' + } + }, + { path: '/:foo/:bar', component: Home } + ] }) - sync(store, router, { - moduleName: originalModuleName - }) + originalModuleName + ? sync(store, router, { moduleName: originalModuleName }) + : sync(store, router) router.push('/a/b') + await router.isReady() expect((store.state as any)[moduleName].fullPath).toBe('/a/b') expect((store.state as any)[moduleName].params).toEqual({ foo: 'a', bar: 'b' }) - const app = new Vue({ - store, - router, - render: (h) => h('router-view') - }).$mount() + const rootEl = document.createElement('div') + document.body.appendChild(rootEl) - expect(app.$el.textContent).toBe('/a/b a b') + const app = createApp({ + render: () => h(RouterView) + }) + app.use(store) + app.use(router) + app.mount(rootEl) - router.push('/c/d?n=1#hello') + expect(rootEl.textContent).toBe('/a/b a b') + await router.push('/c/d?n=1#hello') expect((store.state as any)[moduleName].fullPath).toBe('/c/d?n=1#hello') expect((store.state as any)[moduleName].params).toEqual({ foo: 'c', @@ -57,25 +67,40 @@ function run(originalModuleName: string, done: Function): void { expect((store.state as any)[moduleName].query).toEqual({ n: '1' }) expect((store.state as any)[moduleName].hash).toEqual('#hello') - Vue.nextTick(() => { - expect(app.$el.textContent).toBe('/c/d?n=1#hello c d') + nextTick(() => { + expect(rootEl.textContent).toBe('/c/d?n=1#hello c d') done() }) } -test('default usage', (done) => { - run('', done) +test('default usage', async (done) => { + await run('', done) }) -test('with custom moduleName', (done) => { - run('moduleName', done) +test('with custom moduleName', async (done) => { + await run('moduleName', done) }) -test('unsync', (done) => { - const store = new Vuex.Store({}) +test('unsync', async (done) => { + const store = createStore({ + state() { + return { msg: 'foo' } + } + }) + spyOn(store, 'watch').and.callThrough() - const router = new VueRouter() + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/', + component: { + template: 'root' + } + } + ] + }) const moduleName = 'testDesync' const unsync = sync(store, router, { @@ -83,23 +108,79 @@ test('unsync', (done) => { }) expect(unsync).toBeInstanceOf(Function) - // Test module registered, store watched, router hooked expect((store as any).state[moduleName]).toBeDefined() expect((store as any).watch).toHaveBeenCalled() - expect((store as any)._watcherVM).toBeDefined() - expect((store as any)._watcherVM._watchers).toBeDefined() - expect((store as any)._watcherVM._watchers.length).toBe(1) - expect((router as any).afterHooks).toBeDefined() - expect((router as any).afterHooks.length).toBe(1) // Now unsync vuex-router-sync unsync() - // Ensure router unhooked, store-unwatched, module unregistered - expect((router as any).afterHooks.length).toBe(0) - expect((store as any)._watcherVm).toBeUndefined() + // Ensure module unregistered, no store change + router.push('/') + await router.isReady() expect((store as any).state[moduleName]).toBeUndefined() - + expect((store as any).state).toEqual({ msg: 'foo' }) done() }) + +test('time traveling', async () => { + const store = createStore({ + state() { + return { msg: 'foo' } + } + }) + + const router = createRouter({ + history: createMemoryHistory(), + routes: [ + { + path: '/', + component: { + template: 'root' + } + }, + { + path: '/a', + component: { + template: 'a' + } + } + ] + }) + + sync(store, router) + + const state1 = clone(store.state) + + // time travel before any route change so that we can test `currentPath` + // being `undefined` + store.replaceState(state1) + + expect((store.state as any).route.path).toBe('/') + + // change route, save new state to time travel later on + await router.push('/a') + + expect((store.state as any).route.path).toBe('/a') + + const state2 = clone(store.state) + + // change route again so that we're on different route than `state2` + await router.push('/') + + expect((store.state as any).route.path).toBe('/') + + // time travel to check we go back to the old route + store.replaceState(state2) + + expect((store.state as any).route.path).toBe('/a') + + // final push to the route to fire `afterEach` hook on router + await router.push('/a') + + expect((store.state as any).route.path).toBe('/a') +}) + +function clone(state: any) { + return JSON.parse(JSON.stringify(state)) +} diff --git a/yarn.lock b/yarn.lock index 57eb6e7..8d1fb13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -121,6 +121,11 @@ dependencies: "@babel/types" "^7.8.3" +"@babel/helper-validator-identifier@^7.10.4": + version "7.10.4" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2" + integrity sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw== + "@babel/helper-validator-identifier@^7.9.0", "@babel/helper-validator-identifier@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.9.5.tgz#90977a8e6fbf6b431a7dc31752eee233bf052d80" @@ -149,6 +154,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/parser@^7.12.0": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.7.tgz#fee7b39fe809d0e73e5b25eecaf5780ef3d73056" + integrity sha512-oWR02Ubp4xTLCAqPRiNIuMVgNO5Aif/xpXtabhzW2HWUD47XJsAB4Zd/Rg30+XeQA3juXigV7hlquOTmwqLiwg== + "@babel/plugin-syntax-async-generators@^7.8.4": version "7.8.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" @@ -252,6 +262,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.12.0": + version "7.12.7" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.7.tgz#6039ff1e242640a29452c9ae572162ec9a8f5d13" + integrity sha512-MNyI92qZq6jrQkXvtIiykvl4WtoRrVV9MPn+ZfsoEENjiWcBQ3ZSHrkxnJWgWtLX3XXqX5hrSQ+X69wkmesXuQ== + dependencies: + "@babel/helper-validator-identifier" "^7.10.4" + lodash "^4.17.19" + to-fast-properties "^2.0.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -587,6 +606,54 @@ dependencies: "@types/yargs-parser" "*" +"@vue/compiler-core@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.0.5.tgz#a6e54cabe9536e74c6513acd2649f311af1d43ac" + integrity sha512-iFXwk2gmU/GGwN4hpBwDWWMLvpkIejf/AybcFtlQ5V1ur+5jwfBaV0Y1RXoR6ePfBPJixtKZ3PmN+M+HgMAtfQ== + dependencies: + "@babel/parser" "^7.12.0" + "@babel/types" "^7.12.0" + "@vue/shared" "3.0.5" + estree-walker "^2.0.1" + source-map "^0.6.1" + +"@vue/compiler-dom@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.0.5.tgz#7885a13e6d18f64dde8ebceec052ed2c102696c2" + integrity sha512-HSOSe2XSPuCkp20h4+HXSiPH9qkhz6YbW9z9ZtL5vef2T2PMugH7/osIFVSrRZP/Ul5twFZ7MIRlp8tPX6e4/g== + dependencies: + "@vue/compiler-core" "3.0.5" + "@vue/shared" "3.0.5" + +"@vue/reactivity@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.0.5.tgz#e3789e4d523d845f9ae0b4d770e2b45594742fd2" + integrity sha512-3xodUE3sEIJgS7ntwUbopIpzzvi7vDAOjVamfb2l+v1FUg0jpd3gf62N2wggJw3fxBMr+QvyxpD+dBoxLsmAjw== + dependencies: + "@vue/shared" "3.0.5" + +"@vue/runtime-core@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.0.5.tgz#da6331d5f300d5794e9e0ebdc8a8bd72a9e19962" + integrity sha512-Cnyi2NqREwOLcTEsIi1DQX1hHtkVj4eGm4hBG7HhokS05DqpK4/80jG6PCCnCH9rIJDB2FqtaODX397210plXg== + dependencies: + "@vue/reactivity" "3.0.5" + "@vue/shared" "3.0.5" + +"@vue/runtime-dom@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.0.5.tgz#1ce2c9c449e26ab06963da0064096e882a7a8935" + integrity sha512-iilX1KySeIzHHtErT6Y44db1rhWK5tAI0CiJIPr+SJoZ2jbjoOSE6ff/jfIQakchbm1d6jq6VtRVnp5xYdOXKA== + dependencies: + "@vue/runtime-core" "3.0.5" + "@vue/shared" "3.0.5" + csstype "^2.6.8" + +"@vue/shared@3.0.5": + version "3.0.5" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.0.5.tgz#c131d88bd6713cc4d93b3bb1372edb1983225ff0" + integrity sha512-gYsNoGkWejBxNO6SNRjOh/xKeZ0H0V+TFzaPzODfBjkAIb0aQgBuixC1brandC/CDJy1wYPwSoYrXpvul7m6yw== + JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.yarnpkg.com/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" @@ -1399,6 +1466,11 @@ cssstyle@^2.0.0: dependencies: cssom "~0.3.6" +csstype@^2.6.8: + version "2.6.14" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.14.tgz#004822a4050345b55ad4dcc00be1d9cf2f4296de" + integrity sha512-2mSc+VEpGPblzAxyeR+vZhJKgYg0Og0nnRi7pmRXFYYxSfnOnW8A5wwQb4n4cE2nIOzqKOAzLCaEX6aBmNEv8A== + currently-unhandled@^0.4.1: version "0.4.1" resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" @@ -1598,6 +1670,11 @@ estree-walker@^1.0.1: resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== +estree-walker@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.1.tgz#f8e030fb21cefa183b44b7ad516b747434e7a3e0" + integrity sha512-tF0hv+Yi2Ot1cwj9eYHtxC0jB9bmjacjQs6ZBTj82H8JwUywFuc+7E83NWfNMwHXZc11mjfFcVXPe9gEP4B8dg== + esutils@^2.0.2: version "2.0.3" resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" @@ -2961,6 +3038,11 @@ lodash@^4.17.13, lodash@^4.17.15: resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== +lodash@^4.17.19: + version "4.17.20" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" + integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== + lolex@^5.0.0: version "5.1.2" resolved "https://registry.yarnpkg.com/lolex/-/lolex-5.1.2.tgz#953694d098ce7c07bc5ed6d0e42bc6c0c6d5a367" @@ -4605,20 +4687,24 @@ vlq@^0.2.1, vlq@^0.2.2: resolved "https://registry.yarnpkg.com/vlq/-/vlq-0.2.3.tgz#8f3e4328cf63b1540c0d67e1b2778386f8975b26" integrity sha512-DRibZL6DsNhIgYQ+wNdWDL2SL3bKPlVrRiBqV5yuMm++op8W4kGFtaQfCs4KEJn0wBZcHVHJ3eoywX8983k1ow== -vue-router@^3.0.0: - version "3.1.6" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.1.6.tgz#45f5a3a3843e31702c061dd829393554e4328f89" - integrity sha512-GYhn2ynaZlysZMkFE5oCHRUTqE8BWs/a9YbKpNLi0i7xD6KG1EzDqpHQmv1F5gXjr8kL5iIVS8EOtRaVUEXTqA== +vue-router@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.0.2.tgz#5702bf8fa14535b80142fde226bf41a84917b1f4" + integrity sha512-LCsTSb5H25dZCxjsLasM9UED1BTg9vyTnp0Z9UhwC6QoqgLuHr/ySf7hjI/V0j2+xCKqJtecfmpghk6U8I2e4w== -vue@^2.5.0: - version "2.6.11" - resolved "https://registry.yarnpkg.com/vue/-/vue-2.6.11.tgz#76594d877d4b12234406e84e35275c6d514125c5" - integrity sha512-VfPwgcGABbGAue9+sfrD4PuwFar7gPb1yl1UK1MwXoQPAw0BKSqWfoYCT/ThFrdEVWoI51dBuyCoiNU9bZDZxQ== +vue@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.0.5.tgz#de1b82eba24abfe71e0970fc9b8d4b2babdc3fe1" + integrity sha512-TfaprOmtsAfhQau7WsomXZ8d9op/dkQLNIq8qPV3A0Vxs6GR5E+c1rfJS1SDkXRQj+dFyfnec7+U0Be1huiScg== + dependencies: + "@vue/compiler-dom" "3.0.5" + "@vue/runtime-dom" "3.0.5" + "@vue/shared" "3.0.5" -vuex@^3.0.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/vuex/-/vuex-3.3.0.tgz#665b4630ea1347317139fcc5cb495aab3ec5e513" - integrity sha512-1MfcBt+YFd20DPwKe0ThhYm1UEXZya4gVKUvCy7AtS11YAOUR+9a6u4fsv1Rr6ePZCDNxW/M1zuIaswp6nNv8Q== +vuex@^4.0.0-rc.2: + version "4.0.0-rc.2" + resolved "https://registry.yarnpkg.com/vuex/-/vuex-4.0.0-rc.2.tgz#3681c84eb6f5171b039edaa17cc78105e20724f3" + integrity sha512-HCPzYGea1xL7fMpDoMiHKujC1Bi/HM9LS5ML0Kv55zQtZJvOl0Lq7eWvJoen+SI4Lf7p9V5AqcVsoLPXNBywjg== w3c-hr-time@^1.0.1: version "1.0.2"