},
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/no-namespace': 'off',
+ '@typescript-eslint/ban-ts-ignore': 'off'
},
// "env": {
// "jest": true
--- /dev/null
+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' },
+ },
+ })
+ })
+})
+++ /dev/null
-import { mylib } from '../src'
-
-describe('mylib', () => {
- it('works', () => {
- expect(mylib()).toBe(true)
- })
-})
--- /dev/null
+import Vue from 'vue'
+import VueCompositionAPI from '@vue/composition-api'
+
+beforeAll(() => {
+ Vue.use(VueCompositionAPI)
+})
collectCoverage: true,
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
testMatch: ['<rootDir>/__tests__/**/*.spec.ts'],
+ setupFilesAfterEnv: ['./__tests__/setup.ts'],
globals: {
'ts-jest': {
diagnostics: {
"@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",
"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",
--- /dev/null
+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<string, boolean>
+ _modules: {
+ // we only need this specific method to let devtools retrieve the module name
+ get(name: string): boolean
+ }
+ state: Record<string, StateTree>
+
+ replaceState: Function
+ registerModule: Function
+ unregisterModule: Function
+}
+
+let rootStore: RootState
+
+export function devtoolPlugin<S extends StateTree>(store: Store<S>) {
+ 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
+ )
+ })
+}
-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<S extends StateTree>(initialState: S) {
+ const state: Ref<S> = ref(initialState)
+
+ // type State = UnwrapRef<typeof state>
+
+ function replaceState(newState: S) {
+ state.value = newState
+ }
+
+ return {
+ state,
+ replaceState,
+ }
}
+
+function innerPatch<T extends StateTree>(
+ target: T,
+ patchToApply: DeepPartial<T>
+): 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<S extends StateTree>(
+ name: string,
+ initialState: S
+ // methods: Record<string | symbol, StoreMethod>
+): Store<S> {
+ const { state, replaceState } = createState(initialState)
+
+ let isListening = true
+ const subscriptions: SubscriptionCallback<S>[] = []
+
+ 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<S>): 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<S>): void {
+ subscriptions.push(callback)
+ // TODO: return function to remove subscription
+ }
+
+ const store: Store<S> = {
+ 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',
+// },
+// },
+// },
+// })
--- /dev/null
+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<string | number | symbol, StateTreeValue> {}
+
+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<StateTreeValue> {}
+
+// type TODO = any
+// type StoreMethod = TODO
+export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> }
+// type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
+
+export type SubscriptionCallback<S> = (
+ mutation: { storeName: string; type: string; payload: DeepPartial<S> },
+ state: S
+) => void
+
+export interface Store<S extends StateTree> {
+ name: string
+
+ state: S
+ patch(partialState: DeepPartial<S>): void
+
+ replaceState(newState: S): void
+ subscribe(callback: SubscriptionCallback<S>): 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
+ }
+ }
+}