]> git.ipfire.org Git - thirdparty/vuejs/pinia.git/commitdiff
feat: add initial version
authorEduardo San Martin Morote <posva13@gmail.com>
Mon, 18 Nov 2019 21:03:18 +0000 (22:03 +0100)
committerEduardo San Martin Morote <posva13@gmail.com>
Mon, 18 Nov 2019 21:03:18 +0000 (22:03 +0100)
.eslintrc.js
__tests__/createStore.spec.ts [new file with mode: 0644]
__tests__/index.spec.ts [deleted file]
__tests__/setup.ts [new file with mode: 0644]
jest.config.js
package.json
src/devtools.ts [new file with mode: 0644]
src/index.ts
src/types.ts [new file with mode: 0644]

index 43e0fcaa823b7c9997f4ad6a57f4d06b0961ffc9..f57fd6cea23fe41c54abd79d99ec2f64c67d0fe8 100644 (file)
@@ -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 (file)
index 0000000..cc416bc
--- /dev/null
@@ -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 (file)
index 240f142..0000000
+++ /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 (file)
index 0000000..906908a
--- /dev/null
@@ -0,0 +1,6 @@
+import Vue from 'vue'
+import VueCompositionAPI from '@vue/composition-api'
+
+beforeAll(() => {
+  Vue.use(VueCompositionAPI)
+})
index 7ff71123db1c4c57bb8b64083af0c79a2ca5df18..eb5cc4804bb34a231e5b14d6b6ba8b108ae80df6 100644 (file)
@@ -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: {
index 01e450ac1e51616267e8e9dac05930d7f6cf5d80..84edd64e3342d65ed261dcfd1ef4374a30935c7b 100644 (file)
@@ -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 (file)
index 0000000..926239d
--- /dev/null
@@ -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
+    )
+  })
+}
index c1ac933c63a67334a3b081a248f2ce2fa8b1b59e..a23671fea6828445195ae4418eea6d642cd111f7 100644 (file)
@@ -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',
+//       },
+//     },
+//   },
+// })
diff --git a/src/types.ts b/src/types.ts
new file mode 100644 (file)
index 0000000..ff71736
--- /dev/null
@@ -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
+    }
+  }
+}