Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
feat: add initial version
  • Loading branch information
posva committed Nov 18, 2019
1 parent ca52775 commit 06aeef5
Show file tree
Hide file tree
Showing 9 changed files with 325 additions and 10 deletions.
2 changes: 2 additions & 0 deletions .eslintrc.js
Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions __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' },
},
})
})
})
7 changes: 0 additions & 7 deletions __tests__/index.spec.ts

This file was deleted.

6 changes: 6 additions & 0 deletions __tests__/setup.ts
@@ -0,0 +1,6 @@
import Vue from 'vue'
import VueCompositionAPI from '@vue/composition-api'

beforeAll(() => {
Vue.use(VueCompositionAPI)
})
1 change: 1 addition & 0 deletions jest.config.js
Expand Up @@ -3,6 +3,7 @@ module.exports = {
collectCoverage: true,
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
testMatch: ['<rootDir>/__tests__/**/*.spec.ts'],
setupFilesAfterEnv: ['./__tests__/setup.ts'],
globals: {
'ts-jest': {
diagnostics: {
Expand Down
4 changes: 3 additions & 1 deletion package.json
Expand Up @@ -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",
Expand All @@ -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",
Expand Down
89 changes: 89 additions & 0 deletions 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<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
)
})
}
128 changes: 126 additions & 2 deletions 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<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',
// },
// },
// },
// })
79 changes: 79 additions & 0 deletions 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<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
}
}
}

0 comments on commit 06aeef5

Please sign in to comment.