From: Eduardo San Martin Morote Date: Mon, 18 Nov 2019 21:03:18 +0000 (+0100) Subject: feat: add initial version X-Git-Tag: v0.0.1~22 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=06aeef54e2cad66696063c62829dac74e15fd19e;p=thirdparty%2Fvuejs%2Fpinia.git feat: add initial version --- diff --git a/.eslintrc.js b/.eslintrc.js index 43e0fcaa..f57fd6ce 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -13,6 +13,8 @@ module.exports = { }, rules: { '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/ban-ts-ignore': 'off' }, // "env": { // "jest": true diff --git a/__tests__/createStore.spec.ts b/__tests__/createStore.spec.ts new file mode 100644 index 00000000..cc416bc3 --- /dev/null +++ b/__tests__/createStore.spec.ts @@ -0,0 +1,19 @@ +import { createStore } from '../src' + +describe('createStore', () => { + it('sets the initial state', () => { + const state = { + a: true, + nested: { + a: { b: 'string' }, + }, + } + const store = createStore('main', state) + expect(store.state).toEqual({ + a: true, + nested: { + a: { b: 'string' }, + }, + }) + }) +}) diff --git a/__tests__/index.spec.ts b/__tests__/index.spec.ts deleted file mode 100644 index 240f1429..00000000 --- a/__tests__/index.spec.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { mylib } from '../src' - -describe('mylib', () => { - it('works', () => { - expect(mylib()).toBe(true) - }) -}) diff --git a/__tests__/setup.ts b/__tests__/setup.ts new file mode 100644 index 00000000..906908aa --- /dev/null +++ b/__tests__/setup.ts @@ -0,0 +1,6 @@ +import Vue from 'vue' +import VueCompositionAPI from '@vue/composition-api' + +beforeAll(() => { + Vue.use(VueCompositionAPI) +}) diff --git a/jest.config.js b/jest.config.js index 7ff71123..eb5cc480 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,6 +3,7 @@ module.exports = { collectCoverage: true, collectCoverageFrom: ['/src/**/*.ts'], testMatch: ['/__tests__/**/*.spec.ts'], + setupFilesAfterEnv: ['./__tests__/setup.ts'], globals: { 'ts-jest': { diagnostics: { diff --git a/package.json b/package.json index 01e450ac..84edd64e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@types/jest": "^24.0.18", "@typescript-eslint/eslint-plugin": "^2.3.1", "@typescript-eslint/parser": "^2.3.1", + "@vue/composition-api": "^0.3.2", "codecov": "^3.6.1", "eslint": "^6.4.0", "eslint-config-prettier": "^6.3.0", @@ -49,7 +50,8 @@ "rollup-plugin-terser": "^5.1.2", "rollup-plugin-typescript2": "^0.25.2", "ts-jest": "^24.1.0", - "typescript": "^3.6.3" + "typescript": "^3.6.3", + "vue": "^2.6.10" }, "repository": { "type": "git", diff --git a/src/devtools.ts b/src/devtools.ts new file mode 100644 index 00000000..926239d7 --- /dev/null +++ b/src/devtools.ts @@ -0,0 +1,89 @@ +import { DevtoolHook, StateTree, Store } from './types' + +const target = + typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : { __VUE_DEVTOOLS_GLOBAL_HOOK__: undefined } + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const devtoolHook: DevtoolHook | undefined = target.__VUE_DEVTOOLS_GLOBAL_HOOK__ + +interface RootState { + _devtoolHook: DevtoolHook + _vm: { $options: { computed: {} } } + _mutations: {} + // we neeed to store modules names + _modulesNamespaceMap: Record + _modules: { + // we only need this specific method to let devtools retrieve the module name + get(name: string): boolean + } + state: Record + + replaceState: Function + registerModule: Function + unregisterModule: Function +} + +let rootStore: RootState + +export function devtoolPlugin(store: Store) { + if (!devtoolHook) return + + if (!rootStore) { + rootStore = { + _devtoolHook: devtoolHook, + _vm: { $options: { computed: {} } }, + _mutations: {}, + // we neeed to store modules names + _modulesNamespaceMap: {}, + _modules: { + // we only need this specific method to let devtools retrieve the module name + get(name: string) { + return name in rootStore._modulesNamespaceMap + }, + }, + state: {}, + + replaceState: () => { + // we handle replacing per store so we do nothing here + }, + // these are used by the devtools + registerModule: () => {}, + unregisterModule: () => {}, + } + devtoolHook.emit('vuex:init', rootStore) + } + + rootStore.state[store.name] = store.state + + // tell the devtools we added a module + rootStore.registerModule(store.name, store) + + Object.defineProperty(rootStore.state, store.name, { + get: () => store.state, + set: state => store.replaceState(state), + }) + + // Vue.set(rootStore.state, store.name, store.state) + // the trailing slash is removed by the devtools + rootStore._modulesNamespaceMap[store.name + '/'] = true + + devtoolHook.on('vuex:travel-to-state', targetState => { + store.replaceState(targetState[store.name] as S) + }) + + store.subscribe((mutation, state) => { + rootStore.state[store.name] = state + devtoolHook.emit( + 'vuex:mutation', + { + ...mutation, + type: `[${mutation.storeName}] ${mutation.type}`, + }, + rootStore.state + ) + }) +} diff --git a/src/index.ts b/src/index.ts index c1ac933c..a23671fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,127 @@ -export function mylib() { - return true +import { ref, watch } from '@vue/composition-api' +import { Ref } from '@vue/composition-api/dist/reactivity' +import { + StateTree, + Store, + SubscriptionCallback, + DeepPartial, + isPlainObject, +} from './types' +import { devtoolPlugin } from './devtools' + +function createState(initialState: S) { + const state: Ref = ref(initialState) + + // type State = UnwrapRef + + function replaceState(newState: S) { + state.value = newState + } + + return { + state, + replaceState, + } } + +function innerPatch( + target: T, + patchToApply: DeepPartial +): T { + // TODO: get all keys + for (const key in patchToApply) { + const subPatch = patchToApply[key] + const targetValue = target[key] + if (isPlainObject(targetValue) && isPlainObject(subPatch)) { + target[key] = innerPatch(targetValue, subPatch) + } else { + // @ts-ignore + target[key] = subPatch + } + } + + return target +} + +export function createStore( + name: string, + initialState: S + // methods: Record +): Store { + const { state, replaceState } = createState(initialState) + + let isListening = true + const subscriptions: SubscriptionCallback[] = [] + + watch( + () => state.value, + state => { + if (isListening) { + subscriptions.forEach(callback => { + callback({ storeName: name, type: '🧩 in place', payload: {} }, state) + }) + } + }, + { + deep: true, + flush: 'sync', + } + ) + + function patch(partialState: DeepPartial): void { + isListening = false + innerPatch(state.value, partialState) + isListening = true + subscriptions.forEach(callback => { + callback( + { storeName: name, type: '⤵️ patch', payload: partialState }, + state.value + ) + }) + } + + function subscribe(callback: SubscriptionCallback): void { + subscriptions.push(callback) + // TODO: return function to remove subscription + } + + const store: Store = { + name, + // it is replaced below by a getter + state: state.value, + + patch, + subscribe, + replaceState: (newState: S) => { + isListening = false + replaceState(newState) + isListening = true + }, + } + + // make state access invisible + Object.defineProperty(store, 'state', { + get: () => state.value, + }) + + // Devtools injection hue hue + devtoolPlugin(store) + + return store +} + +// export const store = createStore('main', initialState) +// export const cartStore = createStore('cart', { +// items: ['thing 1'], +// }) + +// store.patch({ +// toggle: 'off', +// nested: { +// a: { +// b: { +// c: 'one', +// }, +// }, +// }, +// }) diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..ff717369 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,79 @@ +interface JSONSerializable { + toJSON(): string +} + +export type StateTreeValue = + | string + | symbol + | number + | boolean + | null + | void + | Function + | StateTree + | StateTreeArray + | JSONSerializable + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface StateTree + extends Record {} + +export function isPlainObject( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + o: any +): o is StateTree { + return ( + o && + typeof o === 'object' && + Object.prototype.toString.call(o) === '[object Object]' && + typeof o.toJSON !== 'function' + ) +} + +// symbol is not allowed yet https://github.com/Microsoft/TypeScript/issues/1863 +// export interface StateTree { +// [x: number]: StateTreeValue +// [x: symbol]: StateTreeValue +// [x: string]: StateTreeValue +// } + +// eslint-disable-next-line @typescript-eslint/no-empty-interface +interface StateTreeArray extends Array {} + +// type TODO = any +// type StoreMethod = TODO +export type DeepPartial = { [K in keyof T]?: DeepPartial } +// type DeepReadonly = { readonly [P in keyof T]: DeepReadonly } + +export type SubscriptionCallback = ( + mutation: { storeName: string; type: string; payload: DeepPartial }, + state: S +) => void + +export interface Store { + name: string + + state: S + patch(partialState: DeepPartial): void + + replaceState(newState: S): void + subscribe(callback: SubscriptionCallback): void +} + +export interface DevtoolHook { + on(event: string, callback: (targetState: StateTree) => void): void + // eslint-disable-next-line @typescript-eslint/no-explicit-any + emit(event: string, ...payload: any[]): void +} + +// add the __VUE_DEVTOOLS_GLOBAL_HOOK__ variable to the global namespace +declare global { + interface Window { + __VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook + } + namespace NodeJS { + interface Global { + __VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook + } + } +}